Merge "Let second robot work without being fully complete"
diff --git a/.bazelignore b/.bazelignore
index 145e9e2..9166728 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -6,6 +6,7 @@
scouting/www/entry/node_modules
scouting/www/match_list/node_modules
scouting/www/notes/node_modules
+scouting/www/pipes/node_modules
scouting/www/rpc/node_modules
scouting/www/shift_schedule/node_modules
scouting/www/view/node_modules
diff --git a/WORKSPACE b/WORKSPACE
index a345ff5..f68d1b0 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -918,6 +918,7 @@
"@//scouting/www/entry:package.json",
"@//scouting/www/match_list:package.json",
"@//scouting/www/notes:package.json",
+ "@//scouting/www/pipes:package.json",
"@//scouting/www/rpc:package.json",
"@//scouting/www/scan:package.json",
"@//scouting/www/shift_schedule:package.json",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2ddfb24..f718ebc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -123,6 +123,9 @@
'@org_frc971/scouting/www/notes':
specifier: workspace:*
version: link:notes
+ '@org_frc971/scouting/www/pipes':
+ specifier: workspace:*
+ version: link:pipes
'@org_frc971/scouting/www/pit_scouting':
specifier: workspace:*
version: link:pit_scouting
@@ -156,6 +159,9 @@
'@org_frc971/scouting/webserver/requests/messages':
specifier: workspace:*
version: link:../../webserver/requests/messages
+ '@org_frc971/scouting/www/pipes':
+ specifier: workspace:*
+ version: link:../pipes
'@org_frc971/scouting/www/rpc':
specifier: workspace:*
version: link:../rpc
@@ -190,6 +196,8 @@
specifier: workspace:*
version: link:../../webserver/requests/messages
+ scouting/www/pipes: {}
+
scouting/www/pit_scouting:
dependencies:
'@angular/forms':
diff --git a/scouting/scouting_qrcode_test.cy.js b/scouting/scouting_qrcode_test.cy.js
index 668cba8..559481e 100644
--- a/scouting/scouting_qrcode_test.cy.js
+++ b/scouting/scouting_qrcode_test.cy.js
@@ -86,7 +86,7 @@
cy.get('#review_data li')
.eq(0)
.should('have.text', ' Started match at position 1 ');
- cy.get('#review_data li').eq(1).should('have.text', 'Picked up Note');
+ cy.get('#review_data li').eq(1).should('have.text', ' Picked up Note ');
cy.get('#review_data li')
.last()
.should(
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
index d15cdfc..2de6879 100644
--- a/scouting/scouting_test.cy.js
+++ b/scouting/scouting_test.cy.js
@@ -106,7 +106,7 @@
cy.get('#review_data li')
.eq(0)
.should('have.text', ' Started match at position 1 ');
- cy.get('#review_data li').eq(1).should('have.text', 'Picked up Note');
+ cy.get('#review_data li').eq(1).should('have.text', ' Picked up Note ');
cy.get('#review_data li')
.last()
.should(
diff --git a/scouting/webserver/requests/messages/submit_2024_actions.fbs b/scouting/webserver/requests/messages/submit_2024_actions.fbs
index 9462fbe..e927b67 100644
--- a/scouting/webserver/requests/messages/submit_2024_actions.fbs
+++ b/scouting/webserver/requests/messages/submit_2024_actions.fbs
@@ -46,13 +46,21 @@
spotlight:bool (id:2);
}
+table EndAutoPhaseAction {
+}
+
+table EndTeleopPhaseAction {
+}
+
union ActionType {
MobilityAction,
StartMatchAction,
+ EndAutoPhaseAction,
PickupNoteAction,
PlaceNoteAction,
PenaltyAction,
RobotDeathAction,
+ EndTeleopPhaseAction,
EndMatchAction
}
@@ -72,4 +80,4 @@
// submission. I.e. checking that the match information exists in the match
// list should be skipped.
pre_scouting:bool (id: 5);
-}
\ No newline at end of file
+}
diff --git a/scouting/www/app/app.module.ts b/scouting/www/app/app.module.ts
index fdbf6fa..e23c2f3 100644
--- a/scouting/www/app/app.module.ts
+++ b/scouting/www/app/app.module.ts
@@ -4,6 +4,7 @@
import {ServiceWorkerModule} from '@angular/service-worker';
import {App} from './app';
+import {PipeModule} from '@org_frc971/scouting/www/pipes';
import {EntryModule} from '@org_frc971/scouting/www/entry';
import {MatchListModule} from '@org_frc971/scouting/www/match_list';
import {NotesModule} from '@org_frc971/scouting/www/notes';
@@ -27,6 +28,7 @@
EntryModule,
NotesModule,
MatchListModule,
+ PipeModule,
ShiftScheduleModule,
DriverRankingModule,
ViewModule,
diff --git a/scouting/www/entry/BUILD b/scouting/www/entry/BUILD
index 6bffbf8..24da904 100644
--- a/scouting/www/entry/BUILD
+++ b/scouting/www/entry/BUILD
@@ -1,11 +1,13 @@
load("@npm//:defs.bzl", "npm_link_all_packages")
load("//tools/build_rules:js.bzl", "ng_pkg")
+load("//tools/build_rules:template.bzl", "jinja2_template")
npm_link_all_packages(name = "node_modules")
ng_pkg(
name = "entry",
extra_srcs = [
+ ":action_helper.ts",
"//scouting/www:app_common_css",
],
deps = [
@@ -13,3 +15,24 @@
"//:node_modules/flatbuffers",
],
)
+
+jinja2_template(
+ name = "action_helper.ts",
+ src = "action_helper.jinja2.ts",
+ list_parameters = {
+ # Is there a way to auto-generate the list of actions here? Would be
+ # nice not to have a duplicate list here when they're already known in
+ # the .fbs file.
+ "ACTIONS": [
+ "EndMatchAction",
+ "MobilityAction",
+ "PenaltyAction",
+ "PickupNoteAction",
+ "PlaceNoteAction",
+ "RobotDeathAction",
+ "StartMatchAction",
+ "EndAutoPhaseAction",
+ "EndTeleopPhaseAction",
+ ],
+ },
+)
diff --git a/scouting/www/entry/action_helper.jinja2.ts b/scouting/www/entry/action_helper.jinja2.ts
new file mode 100644
index 0000000..bdce4d3
--- /dev/null
+++ b/scouting/www/entry/action_helper.jinja2.ts
@@ -0,0 +1,32 @@
+import {
+ ActionT,
+ ActionType,
+{% for action in ACTIONS %}
+ {{ action }}T,
+{% endfor %}
+} from '@org_frc971/scouting/webserver/requests/messages/submit_2024_actions_generated';
+
+export type ConcreteAction =
+{% for action in ACTIONS %}
+ {{ action }}T {% if not loop.last %} | {% endif %}
+{% endfor %};
+
+export class ActionHelper {
+ constructor(
+ private addAction: (actionType: ActionType, action: ConcreteAction) => void
+ ){}
+
+ {% for action in ACTIONS %}
+ // Calls `addAction` in entry.component.ts with the proper arguments. This
+ // also forces users to specify all the attributes in the `action` object.
+ public add{{ action}}(action: NonFunctionProperties<{{ action }}T>): void {
+ this.addAction(ActionType.{{ action }}, Object.assign(new {{ action }}T(), action));
+ }
+ {% endfor %}
+}
+
+type NonFunctionPropertyNames<T> = {
+ [K in keyof T]: T[K] extends Function ? never : K
+}[keyof T];
+
+type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index d2c922e..cd59884 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -12,20 +12,29 @@
import {ErrorResponse} from '@org_frc971/scouting/webserver/requests/messages/error_response_generated';
import {
StartMatchAction,
+ StartMatchActionT,
ScoreType,
StageType,
Submit2024Actions,
MobilityAction,
+ MobilityActionT,
PenaltyAction,
+ PenaltyActionT,
PickupNoteAction,
+ PickupNoteActionT,
PlaceNoteAction,
+ PlaceNoteActionT,
RobotDeathAction,
+ RobotDeathActionT,
EndMatchAction,
+ EndMatchActionT,
ActionType,
Action,
+ ActionT,
} from '@org_frc971/scouting/webserver/requests/messages/submit_2024_actions_generated';
import {Match} from '@org_frc971/scouting/webserver/requests/messages/request_all_matches_response_generated';
import {MatchListRequestor} from '@org_frc971/scouting/www/rpc';
+import {ActionHelper, ConcreteAction} from './action_helper';
import * as pako from 'pako';
type Section =
@@ -59,57 +68,12 @@
// The default index into QR_CODE_PIECE_SIZES.
const DEFAULT_QR_CODE_PIECE_SIZE_INDEX = QR_CODE_PIECE_SIZES.indexOf(750);
-type ActionT =
- | {
- type: 'startMatchAction';
- timestamp?: number;
- position: number;
- }
- | {
- type: 'mobilityAction';
- timestamp?: number;
- mobility: boolean;
- }
- | {
- type: 'pickupNoteAction';
- timestamp?: number;
- auto?: boolean;
- }
- | {
- type: 'placeNoteAction';
- timestamp?: number;
- scoreType: ScoreType;
- auto?: boolean;
- }
- | {
- type: 'robotDeathAction';
- timestamp?: number;
- robotDead: boolean;
- }
- | {
- type: 'penaltyAction';
- timestamp?: number;
- penalties: number;
- }
- | {
- type: 'endMatchAction';
- stageType: StageType;
- trapNote: boolean;
- spotlight: boolean;
- timestamp?: number;
- }
- | {
- // This is not a action that is submitted,
- // It is used for undoing purposes.
- type: 'endAutoPhase';
- timestamp?: number;
- }
- | {
- // This is not a action that is submitted,
- // It is used for undoing purposes.
- type: 'endTeleopPhase';
- timestamp?: number;
- };
+// The actions that are purely used for tracking state. They don't actually
+// have any permanent meaning and will not be saved in the database.
+const STATE_ACTIONS: ActionType[] = [
+ ActionType.EndAutoPhaseAction,
+ ActionType.EndTeleopPhaseAction,
+];
@Component({
selector: 'app-entry',
@@ -124,6 +88,15 @@
readonly QR_CODE_PIECE_SIZES = QR_CODE_PIECE_SIZES;
readonly ScoreType = ScoreType;
readonly StageType = StageType;
+ readonly ActionT = ActionT;
+ readonly ActionType = ActionType;
+ readonly StartMatchActionT = StartMatchActionT;
+ readonly MobilityActionT = MobilityActionT;
+ readonly PickupNoteActionT = PickupNoteActionT;
+ readonly PlaceNoteActionT = PlaceNoteActionT;
+ readonly RobotDeathActionT = RobotDeathActionT;
+ readonly PenaltyActionT = PenaltyActionT;
+ readonly EndMatchActionT = EndMatchActionT;
section: Section = 'Team Selection';
@Input() matchNumber: number = 1;
@@ -136,6 +109,7 @@
matchList: Match[] = [];
+ actionHelper: ActionHelper;
actionList: ActionT[] = [];
progressMessage: string = '';
errorMessage: string = '';
@@ -168,6 +142,12 @@
constructor(private readonly matchListRequestor: MatchListRequestor) {}
ngOnInit() {
+ this.actionHelper = new ActionHelper(
+ (actionType: ActionType, action: ConcreteAction) => {
+ this.addAction(actionType, action);
+ }
+ );
+
// When the user navigated from the match list, we can skip the team
// selection. I.e. we trust that the user clicked the correct button.
this.section = this.skipTeamSelection ? 'Init' : 'Team Selection';
@@ -236,60 +216,58 @@
}
addPenalties(): void {
- this.addAction({type: 'penaltyAction', penalties: this.penalties});
+ this.actionHelper.addPenaltyAction({penalties: this.penalties});
}
- addAction(action: ActionT): void {
- if (action.type == 'startMatchAction') {
+ addAction(actionType: ActionType, action: ConcreteAction): void {
+ let timestamp: number = 0;
+
+ if (actionType == ActionType.StartMatchAction) {
// Unix nanosecond timestamp.
this.matchStartTimestamp = Date.now() * 1e6;
- action.timestamp = 0;
} else {
// Unix nanosecond timestamp relative to match start.
- action.timestamp = Date.now() * 1e6 - this.matchStartTimestamp;
+ timestamp = Date.now() * 1e6 - this.matchStartTimestamp;
}
- if (action.type == 'endMatchAction') {
+ if (actionType == ActionType.EndMatchAction) {
// endMatchAction occurs at the same time as penaltyAction so add to its
// timestamp to make it unique.
- action.timestamp += 1;
+ timestamp += 1;
}
- if (action.type == 'mobilityAction') {
+ if (actionType == ActionType.MobilityAction) {
this.mobilityCompleted = true;
}
- if (action.type == 'pickupNoteAction' || action.type == 'placeNoteAction') {
- action.auto = this.autoPhase;
- }
- this.actionList.push(action);
+ this.actionList.push(new ActionT(BigInt(timestamp), actionType, action));
}
undoLastAction() {
if (this.actionList.length > 0) {
let lastAction = this.actionList.pop();
- switch (lastAction?.type) {
- case 'endAutoPhase':
+ switch (lastAction?.actionTakenType) {
+ case ActionType.EndAutoPhaseAction:
this.autoPhase = true;
this.section = 'Pickup';
- case 'pickupNoteAction':
+ case ActionType.PickupNoteAction:
this.section = 'Pickup';
break;
- case 'endTeleopPhase':
+ case ActionType.EndTeleopPhaseAction:
this.section = 'Pickup';
break;
- case 'placeNoteAction':
+ case ActionType.PlaceNoteAction:
this.section = 'Place';
break;
- case 'endMatchAction':
+ case ActionType.EndMatchAction:
this.section = 'Endgame';
- case 'mobilityAction':
+ case ActionType.MobilityAction:
this.mobilityCompleted = false;
break;
- case 'startMatchAction':
+ case ActionType.StartMatchAction:
this.section = 'Init';
break;
- case 'robotDeathAction':
+ case ActionType.RobotDeathAction:
// TODO(FILIP): Return user to the screen they
// clicked dead robot on. Pickup is fine for now but
// might cause confusion.
@@ -331,111 +309,11 @@
const actionOffsets: number[] = [];
for (const action of this.actionList) {
- let actionOffset: number | undefined;
-
- switch (action.type) {
- case 'startMatchAction':
- const startMatchActionOffset =
- StartMatchAction.createStartMatchAction(builder, action.position);
- actionOffset = Action.createAction(
- builder,
- BigInt(action.timestamp || 0),
- ActionType.StartMatchAction,
- startMatchActionOffset
- );
- break;
- case 'mobilityAction':
- const mobilityActionOffset = MobilityAction.createMobilityAction(
- builder,
- action.mobility
- );
- actionOffset = Action.createAction(
- builder,
- BigInt(action.timestamp || 0),
- ActionType.MobilityAction,
- mobilityActionOffset
- );
- break;
- case 'penaltyAction':
- const penaltyActionOffset = PenaltyAction.createPenaltyAction(
- builder,
- action.penalties
- );
- actionOffset = Action.createAction(
- builder,
- BigInt(action.timestamp || 0),
- ActionType.PenaltyAction,
- penaltyActionOffset
- );
- break;
- case 'pickupNoteAction':
- const pickupNoteActionOffset =
- PickupNoteAction.createPickupNoteAction(
- builder,
- action.auto || false
- );
- actionOffset = Action.createAction(
- builder,
- BigInt(action.timestamp || 0),
- ActionType.PickupNoteAction,
- pickupNoteActionOffset
- );
- break;
- case 'placeNoteAction':
- const placeNoteActionOffset = PlaceNoteAction.createPlaceNoteAction(
- builder,
- action.scoreType,
- action.auto || false
- );
- actionOffset = Action.createAction(
- builder,
- BigInt(action.timestamp || 0),
- ActionType.PlaceNoteAction,
- placeNoteActionOffset
- );
- break;
-
- case 'robotDeathAction':
- const robotDeathActionOffset =
- RobotDeathAction.createRobotDeathAction(builder, action.robotDead);
- actionOffset = Action.createAction(
- builder,
- BigInt(action.timestamp || 0),
- ActionType.RobotDeathAction,
- robotDeathActionOffset
- );
- break;
-
- case 'endMatchAction':
- const endMatchActionOffset = EndMatchAction.createEndMatchAction(
- builder,
- action.stageType,
- action.trapNote,
- action.spotlight
- );
- actionOffset = Action.createAction(
- builder,
- BigInt(action.timestamp || 0),
- ActionType.EndMatchAction,
- endMatchActionOffset
- );
- break;
-
- case 'endAutoPhase':
- // Not important action.
- break;
-
- case 'endTeleopPhase':
- // Not important action.
- break;
-
- default:
- throw new Error(`Unknown action type`);
+ if (STATE_ACTIONS.includes(action.actionTakenType)) {
+ // Actions only used for undo purposes are not submitted.
+ continue;
}
-
- if (actionOffset !== undefined) {
- actionOffsets.push(actionOffset);
- }
+ actionOffsets.push(action.pack(builder));
}
const teamNumberFb = builder.createString(this.teamNumber);
const compLevelFb = builder.createString(this.compLevel);
diff --git a/scouting/www/entry/entry.module.ts b/scouting/www/entry/entry.module.ts
index df92042..8757ced 100644
--- a/scouting/www/entry/entry.module.ts
+++ b/scouting/www/entry/entry.module.ts
@@ -4,9 +4,11 @@
import {EntryComponent} from './entry.component';
import {QRCodeModule} from 'angularx-qrcode';
+import {PipeModule} from '@org_frc971/scouting/www/pipes';
+
@NgModule({
declarations: [EntryComponent],
exports: [EntryComponent],
- imports: [CommonModule, FormsModule, QRCodeModule],
+ imports: [PipeModule, CommonModule, FormsModule, QRCodeModule],
})
export class EntryModule {}
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index 233d63a..d95bcec 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -102,7 +102,7 @@
<button
class="btn btn-primary"
[disabled]="!selectedValue"
- (click)="changeSectionTo('Pickup'); addAction({type: 'startMatchAction', position: selectedValue});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addStartMatchAction({position: selectedValue});"
>
Start Match
</button>
@@ -111,7 +111,8 @@
</div>
<div *ngSwitchCase="'Pickup'" id="PickUp" class="container-fluid">
<h6 class="text-muted">
- Last Action: {{actionList[actionList.length - 1].type}}
+ Last Action: {{ActionType[actionList[actionList.length -
+ 1].actionTakenType]}}
</h6>
<!--
Decrease distance between buttons during auto to make space for auto balancing
@@ -123,20 +124,20 @@
<button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
<button
class="btn btn-danger"
- (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotDead: true});"
+ (click)="changeSectionTo('Dead'); actionHelper.addRobotDeathAction({robotDead: true});"
>
DEAD
</button>
<button
class="btn btn-warning"
- (click)="changeSectionTo('Place'); addAction({type: 'pickupNoteAction'});"
+ (click)="changeSectionTo('Place'); actionHelper.addPickupNoteAction({auto: autoPhase});"
>
NOTE
</button>
<button
*ngIf="autoPhase && !mobilityCompleted"
class="btn btn-light"
- (click)="addAction({type: 'mobilityAction', mobility: true});"
+ (click)="actionHelper.addMobilityAction({mobility: true});"
>
Mobility
</button>
@@ -161,14 +162,14 @@
<button
*ngIf="autoPhase"
class="btn btn-dark"
- (click)="autoPhase = false; addAction({type: 'endAutoPhase'});"
+ (click)="autoPhase = false; actionHelper.addEndAutoPhaseAction({});"
>
Start Teleop
</button>
<button
*ngIf="!autoPhase"
class="btn btn-info"
- (click)="changeSectionTo('Endgame'); addAction({type: 'endTeleopPhase'});"
+ (click)="changeSectionTo('Endgame'); actionHelper.addEndTeleopPhaseAction({});"
>
Endgame
</button>
@@ -176,7 +177,8 @@
</div>
<div *ngSwitchCase="'Place'" id="Place" class="container-fluid">
<h6 class="text-muted">
- Last Action: {{actionList[actionList.length - 1].type}}
+ Last Action: {{ActionType[actionList[actionList.length -
+ 1].actionTakenType]}}
</h6>
<!--
Decrease distance between buttons during auto to make space for auto balancing
@@ -188,13 +190,13 @@
<button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
<button
class="btn btn-danger"
- (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotDead: true});"
+ (click)="changeSectionTo('Dead'); actionHelper.addRobotDeathAction({robotDead: true});"
>
DEAD
</button>
<button
class="btn btn-info"
- (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kDROPPED});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addPlaceNoteAction({auto: autoPhase, scoreType: ScoreType.kDROPPED});"
>
Dropped
</button>
@@ -211,7 +213,7 @@
>
<button
class="btn btn-success"
- (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kAMP});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addPlaceNoteAction({auto: autoPhase, scoreType: ScoreType.kAMP});"
style="width: 48%; height: 12vh; margin: 0px 10px 10px 0px"
>
AMP
@@ -219,21 +221,21 @@
<button
class="btn btn-warning"
- (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kAMP_AMPLIFIED});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addPlaceNoteAction({auto: autoPhase, scoreType: ScoreType.kAMP_AMPLIFIED});"
style="width: 48%; height: 12vh; margin: 0px 0px 10px 0px"
>
AMP AMPLIFIED
</button>
<button
class="btn btn-success"
- (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kSPEAKER});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addPlaceNoteAction({auto: autoPhase, scoreType: ScoreType.kSPEAKER});"
style="width: 48%; height: 12vh; margin: 0px 10px 0px 0px"
>
SPEAKER
</button>
<button
class="btn btn-warning"
- (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kSPEAKER_AMPLIFIED});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addPlaceNoteAction({auto: autoPhase, scoreType: ScoreType.kSPEAKER_AMPLIFIED});"
style="width: 48%; height: 12vh; margin: 0px 0px 0px 0px"
>
SPEAKER AMPLIFIED
@@ -244,21 +246,21 @@
<button
*ngIf="autoPhase"
class="btn btn-success"
- (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kAMP});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addPlaceNoteAction({auto: autoPhase, scoreType: ScoreType.kAMP});"
>
AMP
</button>
<button
*ngIf="autoPhase"
class="btn btn-warning"
- (click)="changeSectionTo('Pickup'); addAction({type: 'placeNoteAction', scoreType: ScoreType.kSPEAKER});"
+ (click)="changeSectionTo('Pickup'); actionHelper.addPlaceNoteAction({auto: autoPhase, scoreType: ScoreType.kSPEAKER});"
>
SPEAKER
</button>
<button
*ngIf="autoPhase && !mobilityCompleted"
class="btn btn-light"
- (click)="addAction({type: 'mobilityAction', mobility: true});"
+ (click)="actionHelper.addMobilityAction({mobility: true});"
>
Mobility
</button>
@@ -283,14 +285,14 @@
<button
class="btn btn-dark"
*ngIf="autoPhase"
- (click)="autoPhase = false; addAction({type: 'endAutoPhase'});"
+ (click)="autoPhase = false; actionHelper.addEndAutoPhaseAction({});"
>
Start Teleop
</button>
<button
*ngIf="!autoPhase"
class="btn btn-info"
- (click)="changeSectionTo('Endgame'); addAction({type: 'endTeleopPhase'});"
+ (click)="changeSectionTo('Endgame'); actionHelper.addEndTeleopPhaseAction({});"
>
Endgame
</button>
@@ -298,13 +300,14 @@
</div>
<div *ngSwitchCase="'Endgame'" id="Endgame" class="container-fluid">
<h6 class="text-muted">
- Last Action: {{actionList[actionList.length - 1].type}}
+ Last Action: {{ActionType[actionList[actionList.length -
+ 1].actionTakenType]}}
</h6>
<div class="d-grid gap-2">
<button class="btn btn-secondary" (click)="undoLastAction()">UNDO</button>
<button
class="btn btn-danger"
- (click)="changeSectionTo('Dead'); addAction({type: 'robotDeathAction', robotDead: true});"
+ (click)="changeSectionTo('Dead'); actionHelper.addRobotDeathAction({robotDead: true});"
>
DEAD
</button>
@@ -382,7 +385,7 @@
<button
*ngIf="!autoPhase"
class="btn btn-info"
- (click)="changeSectionTo('Review and Submit'); addPenalties(); addAction({type: 'endMatchAction', stageType: endGameAction, trapNote: noteIsTrapped, spotlight: endGameSpotlight});"
+ (click)="changeSectionTo('Review and Submit'); addPenalties(); actionHelper.addEndMatchAction({stageType: endGameAction, trapNote: noteIsTrapped, spotlight: endGameSpotlight});"
>
End Match
</button>
@@ -412,13 +415,13 @@
</div>
<button
class="btn btn-success"
- (click)="changeSectionTo('Pickup'); addAction({type: 'robotDeathAction', robotDead: false}); "
+ (click)="changeSectionTo('Pickup'); actionHelper.addRobotDeathAction({robotDead: false}); "
>
Revive
</button>
<button
class="btn btn-info"
- (click)="changeSectionTo('Review and Submit'); addPenalties(); addAction({type: 'endMatchAction', stageType: endGameAction, trapNote: noteIsTrapped, spotlight: endGameSpotlight});"
+ (click)="changeSectionTo('Review and Submit'); addPenalties(); actionHelper.addEndMatchAction({stageType: endGameAction, trapNote: noteIsTrapped, spotlight: endGameSpotlight});"
>
End Match
</button>
@@ -428,29 +431,40 @@
<div class="row">
<ul id="review_data">
<li *ngFor="let action of actionList" style="display: flex">
- <div [ngSwitch]="action.type" style="padding: 0px">
- <span *ngSwitchCase="'startMatchAction'">
- Started match at position {{$any(action).position}}
+ <div [ngSwitch]="action.actionTakenType" style="padding: 0px">
+ <span *ngSwitchCase="ActionType.StartMatchAction">
+ Started match at position {{(action.actionTaken | cast:
+ StartMatchActionT).position}}
</span>
- <span *ngSwitchCase="'pickupNoteAction'">Picked up Note</span>
- <span *ngSwitchCase="'placeNoteAction'">
- Placed at {{stringifyScoreType($any(action).scoreType)}}
+ <span *ngSwitchCase="ActionType.PickupNoteAction">
+ Picked up Note
</span>
- <span *ngSwitchCase="'endAutoPhase'">Ended auto phase</span>
- <span *ngSwitchCase="'endMatchAction'">
- Ended Match; stageType:
- {{stringifyStageType($any(action).stageType)}}, trapNote:
- {{$any(action).trapNote}}, spotlight: {{$any(action).spotlight}}
+ <span *ngSwitchCase="ActionType.PlaceNoteAction">
+ Placed at {{stringifyScoreType((action.actionTaken | cast:
+ PlaceNoteActionT).scoreType)}}
</span>
- <span *ngSwitchCase="'robotDeathAction'">
- Robot dead: {{$any(action).robotDead}}
+ <span *ngSwitchCase="ActionType.EndAutoPhaseAction">
+ Ended auto phase
</span>
- <span *ngSwitchCase="'mobilityAction'">
- Mobility: {{$any(action).mobility}}
+ <span *ngSwitchCase="ActionType.EndMatchAction">
+ Ended Match; stageType: {{stringifyStageType((action.actionTaken |
+ cast: EndMatchActionT).stageType)}}, trapNote:
+ {{(action.actionTaken | cast: EndMatchActionT).trapNote}},
+ spotlight: {{(action.actionTaken | cast:
+ EndMatchActionT).spotlight}}
</span>
- <span *ngSwitchDefault>{{action.type}}</span>
- <span *ngSwitchCase="'penaltyAction'">
- Penalties: {{$any(action).penalties}}
+ <span *ngSwitchCase="ActionType.RobotDeathAction">
+ Robot dead: {{(action.actionTaken | cast:
+ RobotDeathActionT).robotDead}}
+ </span>
+ <span *ngSwitchCase="ActionType.MobilityAction">
+ Mobility: {{(action.actionTaken | cast:
+ MobilityActionT).mobility}}
+ </span>
+ <span *ngSwitchDefault>{{action.actionTakenType}}</span>
+ <span *ngSwitchCase="ActionType.PenaltyAction">
+ Penalties: {{(action.actionTaken | cast:
+ PenaltyActionT).penalties}}
</span>
</div>
</li>
diff --git a/scouting/www/entry/package.json b/scouting/www/entry/package.json
index 39479f4..e97d92c 100644
--- a/scouting/www/entry/package.json
+++ b/scouting/www/entry/package.json
@@ -8,6 +8,7 @@
"@angular/platform-browser": "v16-lts",
"@types/pako": "2.0.3",
"@org_frc971/scouting/webserver/requests/messages": "workspace:*",
- "@org_frc971/scouting/www/rpc": "workspace:*"
+ "@org_frc971/scouting/www/rpc": "workspace:*",
+ "@org_frc971/scouting/www/pipes": "workspace:*"
}
}
diff --git a/scouting/www/package.json b/scouting/www/package.json
index 80fa5c9..da5f279 100644
--- a/scouting/www/package.json
+++ b/scouting/www/package.json
@@ -7,6 +7,7 @@
"@org_frc971/scouting/www/entry": "workspace:*",
"@org_frc971/scouting/www/match_list": "workspace:*",
"@org_frc971/scouting/www/notes": "workspace:*",
+ "@org_frc971/scouting/www/pipes": "workspace:*",
"@org_frc971/scouting/www/pit_scouting": "workspace:*",
"@org_frc971/scouting/www/scan": "workspace:*",
"@org_frc971/scouting/www/shift_schedule": "workspace:*",
diff --git a/scouting/www/pipes/BUILD b/scouting/www/pipes/BUILD
new file mode 100644
index 0000000..680eb09
--- /dev/null
+++ b/scouting/www/pipes/BUILD
@@ -0,0 +1,12 @@
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
+
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
+ name = "pipes",
+ extra_srcs = [
+ "public-api.ts",
+ ],
+ generate_public_api = False,
+)
diff --git a/scouting/www/pipes/cast.ts b/scouting/www/pipes/cast.ts
new file mode 100644
index 0000000..1bb24e8
--- /dev/null
+++ b/scouting/www/pipes/cast.ts
@@ -0,0 +1,15 @@
+import {Pipe, PipeTransform} from '@angular/core';
+
+@Pipe({name: 'cast'})
+export class CastPipe implements PipeTransform {
+ /**
+ * Cast (S: SuperType) into (T: Type) using @Generics.
+ * @param value (S: SuperType) obtained from input type.
+ * @optional @param type (T CastingType)
+ * type?: { new (): T }
+ * type?: new () => T
+ */
+ transform<S, T extends S>(value: S, type?: new () => T): T {
+ return <T>value;
+ }
+}
diff --git a/scouting/www/pipes/package.json b/scouting/www/pipes/package.json
new file mode 100644
index 0000000..b4ce582
--- /dev/null
+++ b/scouting/www/pipes/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "@org_frc971/scouting/www/pipes",
+ "private": true
+}
diff --git a/scouting/www/pipes/pipes.module.ts b/scouting/www/pipes/pipes.module.ts
new file mode 100644
index 0000000..b7dd4c4
--- /dev/null
+++ b/scouting/www/pipes/pipes.module.ts
@@ -0,0 +1,12 @@
+import {NgModule} from '@angular/core';
+import {CastPipe} from './cast';
+
+// Export types needed for the public API.
+export {CastPipe};
+
+@NgModule({
+ declarations: [CastPipe],
+ exports: [CastPipe],
+ imports: [],
+})
+export class PipeModule {}
diff --git a/scouting/www/pipes/public-api.ts b/scouting/www/pipes/public-api.ts
new file mode 100644
index 0000000..77a5641
--- /dev/null
+++ b/scouting/www/pipes/public-api.ts
@@ -0,0 +1 @@
+export * from './pipes.module';
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index b735802..bb15c2c 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -252,6 +252,7 @@
srcs = native.glob(
["**/*.ts", "**/*.css", "**/*.html"],
exclude = test_spec_srcs + [
+ "**/*.jinja2.*",
"public-api.ts",
],
) + extra_srcs
diff --git a/tools/lint/prettier.sh b/tools/lint/prettier.sh
index 0198a71..ef4f5ca 100755
--- a/tools/lint/prettier.sh
+++ b/tools/lint/prettier.sh
@@ -28,6 +28,7 @@
# TODO(phil): Support more than just //scouting.
web_files=($(git ls-tree --name-only --full-tree -r @ \
| grep '^scouting/' \
+ | grep -v '\.jinja2\.' \
| (grep \
-e '\.ts$' \
-e '\.js$' \
diff --git a/y2024/control_loops/superstructure/superstructure.cc b/y2024/control_loops/superstructure/superstructure.cc
index 4ab5966..9edc626 100644
--- a/y2024/control_loops/superstructure/superstructure.cc
+++ b/y2024/control_loops/superstructure/superstructure.cc
@@ -51,8 +51,10 @@
robot_constants_->common()->extend(),
robot_constants_->robot()->extend_constants()->zeroing_constants()),
extend_debouncer_(std::chrono::milliseconds(30),
- std::chrono::milliseconds(8)) {
- event_loop->SetRuntimeRealtimePriority(37);
+ std::chrono::milliseconds(8)),
+ transfer_debouncer_(std::chrono::milliseconds(30),
+ std::chrono::milliseconds(8)) {
+ event_loop->SetRuntimeRealtimePriority(30);
}
bool PositionNear(double position, double goal, double threshold) {
@@ -79,6 +81,9 @@
extend_debouncer_.Update(position->extend_beambreak(), timestamp);
const bool extend_beambreak = extend_debouncer_.state();
+ transfer_debouncer_.Update(position->transfer_beambreak(), timestamp);
+ const bool transfer_beambreak = transfer_debouncer_.state();
+
// Handle Climber Goal separately from main superstructure state machine
double climber_position =
robot_constants_->common()->climber_set_points()->retract();
@@ -221,6 +226,7 @@
unsafe_goal->intake_goal() == IntakeGoal::INTAKE &&
extend_at_retracted) {
state_ = SuperstructureState::INTAKING;
+ note_in_transfer_ = false;
}
extend_goal_location = ExtendStatus::RETRACTED;
@@ -231,7 +237,18 @@
if (extend_beambreak) {
state_ = SuperstructureState::LOADED;
}
- intake_roller_state = IntakeRollerStatus::INTAKING;
+
+ if (transfer_beambreak) {
+ note_in_transfer_ = true;
+ }
+
+ // Once the note is in the transfer, stop the intake rollers
+ if (note_in_transfer_) {
+ intake_roller_state = IntakeRollerStatus::NONE;
+ } else {
+ intake_roller_state = IntakeRollerStatus::INTAKING;
+ }
+
transfer_roller_status = TransferRollerStatus::TRANSFERING_IN;
extend_roller_status = ExtendRollerStatus::TRANSFERING_TO_EXTEND;
extend_goal_location = ExtendStatus::RETRACTED;
diff --git a/y2024/control_loops/superstructure/superstructure.h b/y2024/control_loops/superstructure/superstructure.h
index ce279a6..a1bf841 100644
--- a/y2024/control_loops/superstructure/superstructure.h
+++ b/y2024/control_loops/superstructure/superstructure.h
@@ -73,6 +73,9 @@
NoteGoal requested_note_goal_ = NoteGoal::NONE;
+ // True if the transfer beambreak has been triggered since last intake request
+ bool note_in_transfer_ = false;
+
aos::monotonic_clock::time_point transfer_start_time_ =
aos::monotonic_clock::time_point::min();
@@ -91,6 +94,8 @@
Debouncer extend_debouncer_;
+ Debouncer transfer_debouncer_;
+
DISALLOW_COPY_AND_ASSIGN(Superstructure);
};
diff --git a/y2024/control_loops/superstructure/superstructure_lib_test.cc b/y2024/control_loops/superstructure/superstructure_lib_test.cc
index 52c9a6c..9e8b3a5 100644
--- a/y2024/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2024/control_loops/superstructure/superstructure_lib_test.cc
@@ -66,6 +66,7 @@
event_loop_->MakeFetcher<Output>("/superstructure")),
extend_beambreak_(false),
catapult_beambreak_(false),
+ transfer_beambreak_(false),
intake_pivot_(
new CappedTestPlant(intake_pivot::MakeIntakePivotPlant()),
PositionSensorSimulator(simulated_robot_constants->robot()
@@ -275,6 +276,7 @@
position_builder.add_extend_beambreak(extend_beambreak_);
position_builder.add_catapult_beambreak(catapult_beambreak_);
+ position_builder.add_transfer_beambreak(transfer_beambreak_);
position_builder.add_intake_pivot(intake_pivot_offset);
position_builder.add_catapult(catapult_offset);
position_builder.add_altitude(altitude_offset);
@@ -292,6 +294,10 @@
catapult_beambreak_ = triggered;
}
+ void set_transfer_beambreak(bool triggered) {
+ transfer_beambreak_ = triggered;
+ }
+
AbsoluteEncoderSimulator *intake_pivot() { return &intake_pivot_; }
PotAndAbsoluteEncoderSimulator *catapult() { return &catapult_; }
PotAndAbsoluteEncoderSimulator *altitude() { return &altitude_; }
@@ -312,6 +318,7 @@
bool extend_beambreak_;
bool catapult_beambreak_;
+ bool transfer_beambreak_;
AbsoluteEncoderSimulator intake_pivot_;
PotAndAbsoluteEncoderSimulator climber_;
@@ -945,6 +952,58 @@
EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
}
+// Make sure we stop intaking once transfer beambreak is triggered
+TEST_F(SuperstructureTest, TransferBeamBreakStopsIntake) {
+ SetEnabled(true);
+
+ WaitUntilZeroed();
+
+ superstructure_plant_.intake_pivot()->InitializePosition(
+ frc971::constants::Range::FromFlatbuffer(
+ simulated_robot_constants_->common()->intake_pivot()->range())
+ .middle());
+
+ WaitUntilZeroed();
+
+ {
+ auto builder = superstructure_goal_sender_.MakeBuilder();
+
+ Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+ goal_builder.add_intake_goal(IntakeGoal::INTAKE);
+ goal_builder.add_intake_pivot(IntakePivotGoal::DOWN);
+ goal_builder.add_note_goal(NoteGoal::NONE);
+
+ ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+ }
+
+ RunFor(chrono::seconds(5));
+
+ VerifyNearGoal();
+
+ EXPECT_EQ(superstructure_status_fetcher_->state(),
+ SuperstructureState::INTAKING);
+
+ EXPECT_EQ(superstructure_output_fetcher_->intake_roller_voltage(),
+ simulated_robot_constants_->common()
+ ->intake_roller_voltages()
+ ->intaking());
+
+ superstructure_plant_.set_transfer_beambreak(true);
+
+ RunFor(chrono::seconds(2));
+
+ VerifyNearGoal();
+
+ EXPECT_EQ(superstructure_status_fetcher_->state(),
+ SuperstructureState::INTAKING);
+
+ EXPECT_EQ(superstructure_status_fetcher_->intake_roller(),
+ IntakeRollerStatus::NONE);
+
+ EXPECT_EQ(superstructure_output_fetcher_->intake_roller_voltage(), 0.0);
+}
+
// Tests the full range of activities we need to be doing from loading ->
// shooting
TEST_F(SuperstructureTest, LoadingToShooting) {
diff --git a/y2024/vision/calibrate_multi_cameras.cc b/y2024/vision/calibrate_multi_cameras.cc
index dee6646..69b6fc0 100644
--- a/y2024/vision/calibrate_multi_cameras.cc
+++ b/y2024/vision/calibrate_multi_cameras.cc
@@ -111,10 +111,10 @@
std::vector<CameraNode> CreateNodeList() {
std::vector<CameraNode> list;
- list.push_back({.node_name = "orin1", .camera_number = 0});
- list.push_back({.node_name = "orin1", .camera_number = 1});
list.push_back({.node_name = "imu", .camera_number = 0});
list.push_back({.node_name = "imu", .camera_number = 1});
+ list.push_back({.node_name = "orin1", .camera_number = 1});
+ list.push_back({.node_name = "orin1", .camera_number = 0});
return list;
}