Add climbing level to database
I spent a lot of time trying to make this use enums for the entire
data path. Unfortunately, I ran into a few issues. Firstly, I couldn't
figure out how make our Go SQL code happy with postgresql enums. I
kept getting errors about `unknown oid`. Secondly, I couldn't figure
out how to de-duplicate the enum between `submit_data_scouting.fbs`
and `request_data_scouting_response.fbs`. The generated Go code
doesn't import the dependency properly.
All this turned into an enum at the flatbuffer and TypeScript level,
but just an integer at the Go/postgres level.
A future patch can deal with this. Perhaps it'd be better to ignore
this altogether and just switch to a library like Gorm.
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: Id6cbb5502fd77f3107514b8d7cb9df2923a9d5f9
diff --git a/scouting/db/db.go b/scouting/db/db.go
index d9bebc9..4b40dbc 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -27,7 +27,15 @@
// TODO(phil): Re-order auto and teleop fields so auto comes first.
ShotsMissed, UpperGoalShots, LowerGoalShots int32
ShotsMissedAuto, UpperGoalAuto, LowerGoalAuto, PlayedDefense int32
- Climbing int32
+ // Climbing level:
+ // 0 -> "NoAttempt"
+ // 1 -> "Failed"
+ // 2 -> "FailedWithPlentyOfTime"
+ // 3 -> "Low"
+ // 4 -> "Medium"
+ // 5 -> "High"
+ // 6 -> "Transversal"
+ Climbing int32
// The username of the person who collected these statistics.
// "unknown" if submitted without logging in.
// Empty if the stats have not yet been collected.
@@ -50,6 +58,7 @@
if err != nil {
return nil, errors.New(fmt.Sprint("Failed to connect to postgres: ", err))
}
+
statement, err := database.Prepare("CREATE TABLE IF NOT EXISTS matches (" +
"id SERIAL PRIMARY KEY, " +
"MatchNumber INTEGER, " +
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index 8bebc69..718b2ce 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -75,7 +75,7 @@
"upper_goal_tele": 14,
"lower_goal_tele": 15,
"defense_rating": 3,
- "climbing": 1,
+ "climb_level": "Medium",
})
exit_code, _, stderr = run_debug_cli(["-submitDataScouting", json_path])
self.assertEqual(exit_code, 0, stderr)
@@ -97,14 +97,14 @@
UpperGoalTele: (int32) 14,
LowerGoalTele: (int32) 15,
DefenseRating: (int32) 3,
- Climbing: (int32) 1,
CollectedBy: (string) (len=9) "debug_cli",
AutoBall1: (bool) true,
AutoBall2: (bool) false,
AutoBall3: (bool) false,
AutoBall4: (bool) false,
AutoBall5: (bool) true,
- StartingQuadrant: (int32) 3
+ StartingQuadrant: (int32) 3,
+ ClimbLevel: (request_data_scouting_response.ClimbLevel) Medium
}"""), stdout)
def test_request_all_matches(self):
diff --git a/scouting/webserver/requests/messages/request_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
index 7d91cb7..d85dfbb 100644
--- a/scouting/webserver/requests/messages/request_data_scouting_response.fbs
+++ b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
@@ -1,5 +1,18 @@
namespace scouting.webserver.requests;
+// TODO(phil): Deduplicate with submit_data_scouting.
+// At the moment, our Go setup doesn't handle includes.
+enum ClimbLevel : byte {
+ NoAttempt = 0,
+ Failed,
+ // Tried for more than 10 seconds and failed.
+ FailedWithPlentyOfTime,
+ Low,
+ Medium,
+ High,
+ Transversal,
+}
+
table Stats {
team:int (id: 0);
match:int (id: 1);
@@ -11,7 +24,10 @@
upper_goal_tele:int (id:6);
lower_goal_tele:int (id:7);
defense_rating:int (id:8);
- climbing:int (id:9);
+
+ climbing:int (id:9, deprecated);
+ climb_level:ClimbLevel (id:17);
+
collected_by:string (id:10);
auto_ball_1:bool (id:11);
diff --git a/scouting/webserver/requests/messages/submit_data_scouting.fbs b/scouting/webserver/requests/messages/submit_data_scouting.fbs
index d3d87e2..a9c44a2 100644
--- a/scouting/webserver/requests/messages/submit_data_scouting.fbs
+++ b/scouting/webserver/requests/messages/submit_data_scouting.fbs
@@ -1,5 +1,18 @@
namespace scouting.webserver.requests;
+// TODO(phil): Deduplicate with request_scouting_data_response.
+// At the moment, our Go setup doesn't handle includes.
+enum ClimbLevel : byte {
+ NoAttempt = 0,
+ Failed,
+ // Tried for more than 10 seconds and failed.
+ FailedWithPlentyOfTime,
+ Low,
+ Medium,
+ High,
+ Transversal,
+}
+
table SubmitDataScouting {
team:int (id: 0);
match:int (id: 1);
@@ -18,9 +31,10 @@
// TODO: Document what the different values mean. E.g. 0 means no defense
// played against this robot?
defense_received_rating:int (id:10);
- // The rating that this robot gets for its climbing.
- // TODO: Change into an enum to make the different values self-documenting.
- climbing:int (id:9);
+
+ climbing:int (id:9, deprecated);
+ climb_level:ClimbLevel (id:17);
+
auto_ball_1:bool (id:11);
auto_ball_2:bool (id:12);
auto_ball_3:bool (id:13);
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 13d7396..118fafb 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -156,7 +156,7 @@
UpperGoalShots: request.UpperGoalTele(),
LowerGoalShots: request.LowerGoalTele(),
PlayedDefense: request.DefenseRating(),
- Climbing: request.Climbing(),
+ Climbing: int32(request.ClimbLevel()),
CollectedBy: username,
}
@@ -344,7 +344,7 @@
UpperGoalTele: stat.UpperGoalShots,
LowerGoalTele: stat.LowerGoalShots,
DefenseRating: stat.PlayedDefense,
- Climbing: stat.Climbing,
+ ClimbLevel: request_data_scouting_response.ClimbLevel(stat.Climbing),
CollectedBy: stat.CollectedBy,
})
}
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 12d918b..8165c7d 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -97,7 +97,7 @@
UpperGoalTele: 9971,
LowerGoalTele: 9971,
DefenseRating: 9971,
- Climbing: 9971,
+ ClimbLevel: submit_data_scouting.ClimbLevelLow,
}).Pack(builder))
response, err := debug.SubmitDataScouting("http://localhost:8080", builder.FinishedBytes())
@@ -231,7 +231,7 @@
AutoBallPickedUp: [5]bool{true, false, false, false, true},
ShotsMissed: 1, UpperGoalShots: 2, LowerGoalShots: 3,
ShotsMissedAuto: 4, UpperGoalAuto: 5, LowerGoalAuto: 6,
- PlayedDefense: 7, Climbing: 8,
+ PlayedDefense: 7, Climbing: 2,
CollectedBy: "john",
},
{
@@ -240,7 +240,7 @@
AutoBallPickedUp: [5]bool{false, false, true, false, false},
ShotsMissed: 2, UpperGoalShots: 3, LowerGoalShots: 4,
ShotsMissedAuto: 5, UpperGoalAuto: 6, LowerGoalAuto: 7,
- PlayedDefense: 8, Climbing: 9,
+ PlayedDefense: 8, Climbing: 4,
CollectedBy: "andrea",
},
},
@@ -260,33 +260,27 @@
expected := request_data_scouting_response.RequestDataScoutingResponseT{
StatsList: []*request_data_scouting_response.StatsT{
- // Team, Match,
- // MissedShotsAuto, UpperGoalAuto, LowerGoalAuto,
- // MissedShotsTele, UpperGoalTele, LowerGoalTele,
- // DefenseRating, Climbing,
- // CollectedBy,
- // AutoBall1, AutoBall2, AutoBall3,
- // AutoBall4, AutoBall5,
- // StartingQuadrant,
{
- 971, 1,
- 4, 5, 6,
- 1, 2, 3,
- 7, 8,
- "john",
- true, false, false,
- false, true,
- 1,
+ Team: 971, Match: 1,
+ MissedShotsAuto: 4, UpperGoalAuto: 5, LowerGoalAuto: 6,
+ MissedShotsTele: 1, UpperGoalTele: 2, LowerGoalTele: 3,
+ DefenseRating: 7,
+ CollectedBy: "john",
+ AutoBall1: true, AutoBall2: false, AutoBall3: false,
+ AutoBall4: false, AutoBall5: true,
+ StartingQuadrant: 1,
+ ClimbLevel: request_data_scouting_response.ClimbLevelFailedWithPlentyOfTime,
},
{
- 972, 1,
- 5, 6, 7,
- 2, 3, 4,
- 8, 9,
- "andrea",
- false, false, true,
- false, false,
- 2,
+ Team: 972, Match: 1,
+ MissedShotsAuto: 5, UpperGoalAuto: 6, LowerGoalAuto: 7,
+ MissedShotsTele: 2, UpperGoalTele: 3, LowerGoalTele: 4,
+ DefenseRating: 8,
+ CollectedBy: "andrea",
+ AutoBall1: false, AutoBall2: false, AutoBall3: true,
+ AutoBall4: false, AutoBall5: false,
+ StartingQuadrant: 2,
+ ClimbLevel: request_data_scouting_response.ClimbLevelMedium,
},
},
}
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index d067c3e..357d6b7 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -2,13 +2,11 @@
import {FormsModule} from '@angular/forms';
import {Builder, ByteBuffer} from 'flatbuffers';
import {ErrorResponse} from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
-import {SubmitDataScouting} from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_generated';
+import {ClimbLevel, SubmitDataScouting} from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_generated';
import {SubmitDataScoutingResponse} from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_response_generated';
type Section = 'Team Selection'|'Auto'|'TeleOp'|'Climb'|'Other'|
'Review and Submit'|'Success';
-type Level = 'NoAttempt'|'Failed'|'FailedWithPlentyOfTime'|'Low'|'Medium'|
- 'High'|'Transversal';
@Component({
selector: 'app-entry',
@@ -16,6 +14,10 @@
styleUrls: ['../common.css', './entry.component.css']
})
export class EntryComponent {
+ // Re-export the type here so that we can use it in the `[value]` attribute
+ // of radio buttons.
+ readonly ClimbLevel = ClimbLevel;
+
section: Section = 'Team Selection';
@Output() switchTabsEvent = new EventEmitter<string>();
@Input() matchNumber: number = 1;
@@ -28,7 +30,7 @@
teleShotsMissed: number = 0;
defensePlayedOnScore: number = 0;
defensePlayedScore: number = 0;
- level: Level = 'NoAttempt';
+ level: ClimbLevel = ClimbLevel.NoAttempt;
ball1: boolean = false;
ball2: boolean = false;
ball3: boolean = false;
@@ -56,6 +58,7 @@
} else if (this.section === 'Other') {
this.section = 'Review and Submit';
} else if (this.section === 'Review and Submit') {
+
this.submitDataScouting();
return;
} else if (this.section === 'Success') {
@@ -109,10 +112,8 @@
SubmitDataScouting.addAutoBall4(builder, this.ball4);
SubmitDataScouting.addAutoBall5(builder, this.ball5);
SubmitDataScouting.addStartingQuadrant(builder, this.quadrant);
-
// TODO(phil): Add support for defensePlayedOnScore.
- // TODO(phil): Fix the Climbing score.
- SubmitDataScouting.addClimbing(builder, 1);
+ SubmitDataScouting.addClimbLevel(builder, this.level);
builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
const buffer = builder.asUint8Array();
diff --git a/scouting/www/entry/entry.module.ts b/scouting/www/entry/entry.module.ts
index b4d81c0..45a8a62 100644
--- a/scouting/www/entry/entry.module.ts
+++ b/scouting/www/entry/entry.module.ts
@@ -1,12 +1,21 @@
-import {NgModule} from '@angular/core';
+import {NgModule, Pipe, PipeTransform} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {CounterButtonModule} from '../counter_button/counter_button.module';
import {EntryComponent} from './entry.component';
+import {ClimbLevel} from 'org_frc971/scouting/webserver/requests/messages/submit_data_scouting_generated';
+
+@Pipe({name: 'levelToString'})
+export class LevelToStringPipe implements PipeTransform {
+ transform(level: ClimbLevel): string {
+ return ClimbLevel[level];
+ }
+}
+
@NgModule({
- declarations: [EntryComponent],
+ declarations: [EntryComponent, LevelToStringPipe],
exports: [EntryComponent],
imports: [CommonModule, FormsModule, CounterButtonModule],
})
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index 5849f3b..ead9f83 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -69,19 +69,19 @@
<div *ngSwitchCase="'Climb'" id="climb" class="container-fluid">
<form>
- <input [(ngModel)]="level" type="radio" name="level" id="no_attempt" value="NoAttempt">
+ <input [(ngModel)]="level" type="radio" name="level" id="no_attempt" [value]="ClimbLevel.NoAttempt">
<label for="no_attempt">No climbing attempt</label><br>
- <input [(ngModel)]="level" type="radio" name="level" id="low" value="Low">
+ <input [(ngModel)]="level" type="radio" name="level" id="low" [value]="ClimbLevel.Low">
<label for="low">Low</label><br>
- <input [(ngModel)]="level" type="radio" name="level" id="medium" value="Medium">
+ <input [(ngModel)]="level" type="radio" name="level" id="medium" [value]="ClimbLevel.Medium">
<label for="medium">Medium</label><br>
- <input [(ngModel)]="level" type="radio" name="level" id="high" value="High">
+ <input [(ngModel)]="level" type="radio" name="level" id="high" [value]="ClimbLevel.High">
<label for="high">High</label><br>
- <input [(ngModel)]="level" type="radio" name="level" id="transversal" value="Transversal">
+ <input [(ngModel)]="level" type="radio" name="level" id="transversal" [value]="ClimbLevel.Transversal">
<label for="transversal">Transversal</label><br>
- <input [(ngModel)]="level" type="radio" name="level" id="failed" value="Failed">
+ <input [(ngModel)]="level" type="radio" name="level" id="failed" [value]="ClimbLevel.Failed">
<label for="failed">Failed</label><br>
- <input [(ngModel)]="level" type="radio" name="level" id="failed_with_plenty_of_time" value="FailedWithPlentyOfTime">
+ <input [(ngModel)]="level" type="radio" name="level" id="failed_with_plenty_of_time" [value]="ClimbLevel.FailedWithPlentyOfTime">
<label for="failed_with_plenty_of_time">Failed (attempted with more than 10 seconds left)</label><br>
</form>
<div class="row">
@@ -177,7 +177,7 @@
<h4>Climb</h4>
<ul>
- <li>Level: {{level}}</li>
+ <li>Level: {{level | levelToString}}</li>
</ul>
<h4>Other</h4>