blob: 4b60ac1362e2b3102e7523db398cc913ef543e80 [file] [log] [blame]
Philipp Schrader817cce32022-03-26 15:00:00 -07001import {
2 Component,
3 ElementRef,
4 EventEmitter,
5 Input,
6 OnInit,
7 Output,
8 ViewChild,
9} from '@angular/core';
Ravago Jones2813c032022-03-16 23:44:11 -070010import {FormsModule} from '@angular/forms';
James Kuszmauldac091f2022-03-22 09:35:06 -070011import {Builder, ByteBuffer} from 'flatbuffers';
Philipp Schraderd7efa2b2023-02-17 21:15:13 -080012import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
Philipp Schrader817cce32022-03-26 15:00:00 -070013import {
Filip Kujawa0ef334c2023-02-20 19:42:45 -080014 ObjectType,
15 ScoreLevel,
16 SubmitActions,
17 StartMatchAction,
Filip Kujawa0b4b1e52023-04-15 14:05:40 -070018 MobilityAction,
Filip Kujawa4413a592023-03-01 10:54:34 -080019 AutoBalanceAction,
Filip Kujawa0ef334c2023-02-20 19:42:45 -080020 PickupObjectAction,
21 PlaceObjectAction,
22 RobotDeathAction,
23 EndMatchAction,
24 ActionType,
25 Action,
26} from '../../webserver/requests/messages/submit_actions_generated';
Philipp Schrader8702b782023-04-15 17:33:37 -070027import {Match} from '../../webserver/requests/messages/request_all_matches_response_generated';
28import {MatchListRequestor} from '@org_frc971/scouting/www/rpc';
Philipp Schrader8b8ed672022-03-05 18:08:50 -080029
Philipp Schrader817cce32022-03-26 15:00:00 -070030type Section =
31 | 'Team Selection'
Filip Kujawa0ef334c2023-02-20 19:42:45 -080032 | 'Init'
33 | 'Pickup'
34 | 'Place'
35 | 'Endgame'
36 | 'Dead'
Philipp Schrader817cce32022-03-26 15:00:00 -070037 | 'Review and Submit'
38 | 'Success';
Philipp Schrader80587432022-03-05 15:41:22 -080039
Philipp Schrader8aeb14f2022-04-08 21:23:18 -070040// TODO(phil): Deduplicate with match_list.component.ts.
41const COMP_LEVELS = ['qm', 'ef', 'qf', 'sf', 'f'] as const;
42type CompLevel = typeof COMP_LEVELS[number];
43
44// TODO(phil): Deduplicate with match_list.component.ts.
45const COMP_LEVEL_LABELS: Record<CompLevel, string> = {
46 qm: 'Qualifications',
47 ef: 'Eighth Finals',
48 qf: 'Quarter Finals',
49 sf: 'Semi Finals',
50 f: 'Finals',
51};
52
Filip Kujawa0ef334c2023-02-20 19:42:45 -080053type ActionT =
54 | {
55 type: 'startMatchAction';
56 timestamp?: number;
57 position: number;
58 }
59 | {
Filip Kujawa0b4b1e52023-04-15 14:05:40 -070060 type: 'mobilityAction';
61 timestamp?: number;
62 mobility: boolean;
63 }
64 | {
Filip Kujawa4413a592023-03-01 10:54:34 -080065 type: 'autoBalanceAction';
66 timestamp?: number;
67 docked: boolean;
68 engaged: boolean;
Emily Markova63c63f62023-03-29 20:57:35 -070069 balanceAttempt: boolean;
Filip Kujawa4413a592023-03-01 10:54:34 -080070 }
71 | {
Filip Kujawa0ef334c2023-02-20 19:42:45 -080072 type: 'pickupObjectAction';
73 timestamp?: number;
74 objectType: ObjectType;
75 auto?: boolean;
76 }
77 | {
78 type: 'placeObjectAction';
79 timestamp?: number;
80 objectType?: ObjectType;
81 scoreLevel: ScoreLevel;
82 auto?: boolean;
83 }
84 | {
85 type: 'robotDeathAction';
86 timestamp?: number;
87 robotOn: boolean;
88 }
89 | {
90 type: 'endMatchAction';
91 docked: boolean;
92 engaged: boolean;
Emily Markova63c63f62023-03-29 20:57:35 -070093 balanceAttempt: boolean;
Filip Kujawa0ef334c2023-02-20 19:42:45 -080094 timestamp?: number;
95 }
96 | {
97 // This is not a action that is submitted,
98 // It is used for undoing purposes.
99 type: 'endAutoPhase';
100 timestamp?: number;
101 };
emilym38d08ba2022-10-22 15:25:01 -0700102
Philipp Schrader23993e82022-03-18 18:54:00 -0700103@Component({
104 selector: 'app-entry',
105 templateUrl: './entry.ng.html',
Philipp Schrader175a93c2023-02-19 13:13:40 -0800106 styleUrls: ['../app/common.css', './entry.component.css'],
Philipp Schrader23993e82022-03-18 18:54:00 -0700107})
Philipp Schrader75021f52023-04-09 21:14:13 -0700108export class EntryComponent implements OnInit {
Philipp Schrader36df73a2022-03-17 23:27:24 -0700109 // Re-export the type here so that we can use it in the `[value]` attribute
110 // of radio buttons.
Philipp Schrader8aeb14f2022-04-08 21:23:18 -0700111 readonly COMP_LEVELS = COMP_LEVELS;
112 readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800113 readonly ObjectType = ObjectType;
114 readonly ScoreLevel = ScoreLevel;
Philipp Schrader36df73a2022-03-17 23:27:24 -0700115
Ravago Jones2813c032022-03-16 23:44:11 -0700116 section: Section = 'Team Selection';
Ravago Jones2813c032022-03-16 23:44:11 -0700117 @Input() matchNumber: number = 1;
Philipp Schrader8702b782023-04-15 17:33:37 -0700118 // TODO(phil): Change the type of teamNumber to a string.
Ravago Jones2813c032022-03-16 23:44:11 -0700119 @Input() teamNumber: number = 1;
Philipp Schrader30b4a682022-04-16 14:36:17 -0700120 @Input() setNumber: number = 1;
Philipp Schrader8aeb14f2022-04-08 21:23:18 -0700121 @Input() compLevel: CompLevel = 'qm';
Philipp Schrader75021f52023-04-09 21:14:13 -0700122 @Input() skipTeamSelection = false;
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800123
Philipp Schrader8702b782023-04-15 17:33:37 -0700124 matchList: Match[] = [];
125
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800126 actionList: ActionT[] = [];
Philipp Schrader8702b782023-04-15 17:33:37 -0700127 progressMessage: string = '';
Ravago Jones2813c032022-03-16 23:44:11 -0700128 errorMessage: string = '';
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800129 autoPhase: boolean = true;
Filip Kujawab73e94c2023-04-19 09:33:14 -0700130 mobilityCompleted: boolean = false;
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800131 lastObject: ObjectType = null;
132
Philipp Schradere1498852023-04-15 18:06:45 -0700133 preScouting: boolean = false;
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800134 matchStartTimestamp: number = 0;
135
Philipp Schrader8702b782023-04-15 17:33:37 -0700136 teamSelectionIsValid = false;
137
138 constructor(private readonly matchListRequestor: MatchListRequestor) {}
139
Philipp Schrader75021f52023-04-09 21:14:13 -0700140 ngOnInit() {
141 // When the user navigated from the match list, we can skip the team
142 // selection. I.e. we trust that the user clicked the correct button.
143 this.section = this.skipTeamSelection ? 'Init' : 'Team Selection';
Philipp Schrader8702b782023-04-15 17:33:37 -0700144
145 if (this.section == 'Team Selection') {
146 this.fetchMatchList();
147 }
148 }
149
150 async fetchMatchList() {
151 this.progressMessage = 'Fetching match list. Please be patient.';
152 this.errorMessage = '';
153
154 try {
155 this.matchList = await this.matchListRequestor.fetchMatchList();
156 this.progressMessage = 'Successfully fetched match list.';
157 } catch (e) {
158 this.errorMessage = e;
159 this.progressMessage = '';
160 }
161 }
162
163 // This gets called when the user changes something on the Init screen.
164 // It makes sure that the user can't click "Next" until the information is
Philipp Schradere1498852023-04-15 18:06:45 -0700165 // valid, or this is for pre-scouting.
Philipp Schrader8702b782023-04-15 17:33:37 -0700166 updateTeamSelectionValidity(): void {
Philipp Schradere1498852023-04-15 18:06:45 -0700167 this.teamSelectionIsValid = this.preScouting || this.matchIsInMatchList();
Philipp Schrader8702b782023-04-15 17:33:37 -0700168 }
169
170 matchIsInMatchList(): boolean {
171 // If the user deletes the content of the teamNumber field, the value here
172 // is undefined. Guard against that.
173 if (this.teamNumber == null) {
174 return false;
175 }
176 const teamNumber = this.teamNumber.toString();
177
178 for (const match of this.matchList) {
179 if (
180 this.matchNumber == match.matchNumber() &&
181 this.setNumber == match.setNumber() &&
182 this.compLevel == match.compLevel() &&
183 (teamNumber === match.r1() ||
184 teamNumber === match.r2() ||
185 teamNumber === match.r3() ||
186 teamNumber === match.b1() ||
187 teamNumber === match.b2() ||
188 teamNumber === match.b3())
189 ) {
190 return true;
191 }
192 }
193 return false;
Philipp Schrader75021f52023-04-09 21:14:13 -0700194 }
195
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800196 addAction(action: ActionT): void {
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800197 if (action.type == 'startMatchAction') {
198 // Unix nanosecond timestamp.
199 this.matchStartTimestamp = Date.now() * 1e6;
200 action.timestamp = 0;
201 } else {
202 // Unix nanosecond timestamp relative to match start.
203 action.timestamp = Date.now() * 1e6 - this.matchStartTimestamp;
204 }
205
Filip Kujawab73e94c2023-04-19 09:33:14 -0700206 if (action.type == 'mobilityAction') {
207 this.mobilityCompleted = true;
208 }
209
Filip Kujawae4402712023-04-19 09:45:10 -0700210 if (action.type == 'autoBalanceAction') {
211 // Timestamp is a unique index in the database so
212 // adding one makes sure it dosen't overlap with the
213 // start teleop action that is added at the same time.
214 action.timestamp += 1;
215 }
216
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800217 if (
218 action.type == 'pickupObjectAction' ||
219 action.type == 'placeObjectAction'
220 ) {
221 action.auto = this.autoPhase;
222 if (action.type == 'pickupObjectAction') {
223 this.lastObject = action.objectType;
224 } else if (action.type == 'placeObjectAction') {
225 action.objectType = this.lastObject;
226 }
227 }
228 this.actionList.push(action);
229 }
230
231 undoLastAction() {
232 if (this.actionList.length > 0) {
233 let lastAction = this.actionList.pop();
234 switch (lastAction?.type) {
235 case 'endAutoPhase':
236 this.autoPhase = true;
237 case 'pickupObjectAction':
238 this.section = 'Pickup';
239 break;
240 case 'placeObjectAction':
241 this.section = 'Place';
242 break;
243 case 'endMatchAction':
244 this.section = 'Pickup';
245 break;
Filip Kujawa9f56d0e2023-03-03 19:44:43 -0800246 case 'robotDeathAction':
247 // TODO(FILIP): Return user to the screen they
248 // clicked dead robot on. Pickup is fine for now but
249 // might cause confusion.
250 this.section = 'Pickup';
251 break;
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800252 default:
253 break;
254 }
255 }
256 }
257
Emily Markovaf4b06a22023-05-10 17:44:09 -0700258 stringifyObjectType(objectType: ObjectType): String {
259 return ObjectType[objectType];
260 }
261
262 stringifyScoreLevel(scoreLevel: ScoreLevel): String {
263 return ScoreLevel[scoreLevel];
264 }
265
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800266 changeSectionTo(target: Section) {
Philipp Schrader8702b782023-04-15 17:33:37 -0700267 // Clear the messages since they won't be relevant in the next section.
268 this.errorMessage = '';
269 this.progressMessage = '';
270
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800271 this.section = target;
272 }
Philipp Schrader80587432022-03-05 15:41:22 -0800273
Ravago Jones2813c032022-03-16 23:44:11 -0700274 @ViewChild('header') header: ElementRef;
Philipp Schrader6b2e9502022-03-15 23:42:56 -0700275
Ravago Jones2813c032022-03-16 23:44:11 -0700276 private scrollToTop() {
277 this.header.nativeElement.scrollIntoView();
278 }
279
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800280 async submitActions() {
James Kuszmauldac091f2022-03-22 09:35:06 -0700281 const builder = new Builder();
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800282 const actionOffsets: number[] = [];
283
284 for (const action of this.actionList) {
285 let actionOffset: number | undefined;
286 console.log(action.type);
287
288 switch (action.type) {
289 case 'startMatchAction':
290 const startMatchActionOffset =
291 StartMatchAction.createStartMatchAction(builder, action.position);
292 actionOffset = Action.createAction(
293 builder,
Philipp Schrader8c878a22023-03-20 22:36:38 -0700294 BigInt(action.timestamp || 0),
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800295 ActionType.StartMatchAction,
296 startMatchActionOffset
297 );
298 break;
Filip Kujawa0b4b1e52023-04-15 14:05:40 -0700299 case 'mobilityAction':
300 const mobilityActionOffset = MobilityAction.createMobilityAction(
301 builder,
302 action.mobility
303 );
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800304 actionOffset = Action.createAction(
305 builder,
Philipp Schrader8c878a22023-03-20 22:36:38 -0700306 BigInt(action.timestamp || 0),
Filip Kujawa0b4b1e52023-04-15 14:05:40 -0700307 ActionType.MobilityAction,
308 mobilityActionOffset
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800309 );
310 break;
Filip Kujawa4c286442023-03-03 10:41:22 -0800311 case 'autoBalanceAction':
312 const autoBalanceActionOffset =
313 AutoBalanceAction.createAutoBalanceAction(
314 builder,
315 action.docked,
Emily Markova63c63f62023-03-29 20:57:35 -0700316 action.engaged,
317 action.balanceAttempt
Filip Kujawa4c286442023-03-03 10:41:22 -0800318 );
319 actionOffset = Action.createAction(
320 builder,
Philipp Schrader8c878a22023-03-20 22:36:38 -0700321 BigInt(action.timestamp || 0),
Filip Kujawa4c286442023-03-03 10:41:22 -0800322 ActionType.AutoBalanceAction,
323 autoBalanceActionOffset
324 );
325 break;
326
Filip Kujawa0b4b1e52023-04-15 14:05:40 -0700327 case 'pickupObjectAction':
328 const pickupObjectActionOffset =
329 PickupObjectAction.createPickupObjectAction(
330 builder,
331 action.objectType,
332 action.auto || false
333 );
334 actionOffset = Action.createAction(
335 builder,
336 BigInt(action.timestamp || 0),
337 ActionType.PickupObjectAction,
338 pickupObjectActionOffset
339 );
340 break;
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800341 case 'placeObjectAction':
342 const placeObjectActionOffset =
343 PlaceObjectAction.createPlaceObjectAction(
344 builder,
345 action.objectType,
346 action.scoreLevel,
347 action.auto || false
348 );
349 actionOffset = Action.createAction(
350 builder,
Philipp Schrader8c878a22023-03-20 22:36:38 -0700351 BigInt(action.timestamp || 0),
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800352 ActionType.PlaceObjectAction,
353 placeObjectActionOffset
354 );
355 break;
356
357 case 'robotDeathAction':
358 const robotDeathActionOffset =
359 RobotDeathAction.createRobotDeathAction(builder, action.robotOn);
360 actionOffset = Action.createAction(
361 builder,
Philipp Schrader8c878a22023-03-20 22:36:38 -0700362 BigInt(action.timestamp || 0),
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800363 ActionType.RobotDeathAction,
364 robotDeathActionOffset
365 );
366 break;
367
368 case 'endMatchAction':
369 const endMatchActionOffset = EndMatchAction.createEndMatchAction(
370 builder,
371 action.docked,
Emily Markova63c63f62023-03-29 20:57:35 -0700372 action.engaged,
373 action.balanceAttempt
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800374 );
375 actionOffset = Action.createAction(
376 builder,
Philipp Schrader8c878a22023-03-20 22:36:38 -0700377 BigInt(action.timestamp || 0),
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800378 ActionType.EndMatchAction,
379 endMatchActionOffset
380 );
381 break;
382
383 case 'endAutoPhase':
384 // Not important action.
385 break;
386
387 default:
388 throw new Error(`Unknown action type`);
389 }
390
391 if (actionOffset !== undefined) {
392 actionOffsets.push(actionOffset);
393 }
394 }
Philipp Schradere859e6e2023-03-22 19:59:51 -0700395 const teamNumberFb = builder.createString(this.teamNumber.toString());
396 const compLevelFb = builder.createString(this.compLevel);
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800397
398 const actionsVector = SubmitActions.createActionsListVector(
Philipp Schrader817cce32022-03-26 15:00:00 -0700399 builder,
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800400 actionOffsets
Philipp Schrader817cce32022-03-26 15:00:00 -0700401 );
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800402 SubmitActions.startSubmitActions(builder);
Philipp Schradere859e6e2023-03-22 19:59:51 -0700403 SubmitActions.addTeamNumber(builder, teamNumberFb);
404 SubmitActions.addMatchNumber(builder, this.matchNumber);
405 SubmitActions.addSetNumber(builder, this.setNumber);
406 SubmitActions.addCompLevel(builder, compLevelFb);
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800407 SubmitActions.addActionsList(builder, actionsVector);
Philipp Schradere1498852023-04-15 18:06:45 -0700408 SubmitActions.addPreScouting(builder, this.preScouting);
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800409 builder.finish(SubmitActions.endSubmitActions(builder));
Ravago Jones2813c032022-03-16 23:44:11 -0700410
411 const buffer = builder.asUint8Array();
Sabina Leaver9b4eb312023-02-20 19:58:17 -0800412 const res = await fetch('/requests/submit/submit_actions', {
Philipp Schrader817cce32022-03-26 15:00:00 -0700413 method: 'POST',
414 body: buffer,
415 });
Ravago Jones2813c032022-03-16 23:44:11 -0700416
417 if (res.ok) {
418 // We successfully submitted the data. Report success.
419 this.section = 'Success';
Filip Kujawa0ef334c2023-02-20 19:42:45 -0800420 this.actionList = [];
Ravago Jones2813c032022-03-16 23:44:11 -0700421 } else {
422 const resBuffer = await res.arrayBuffer();
423 const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
James Kuszmauldac091f2022-03-22 09:35:06 -0700424 const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
Ravago Jones2813c032022-03-16 23:44:11 -0700425
426 const errorMessage = parsedResponse.errorMessage();
Philipp Schrader817cce32022-03-26 15:00:00 -0700427 this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
Alex Perrybb3d2062022-03-05 18:14:33 -0800428 }
Ravago Jones2813c032022-03-16 23:44:11 -0700429 }
Philipp Schrader80587432022-03-05 15:41:22 -0800430}