blob: ec25607d906f785a26f4ab5cdce0e0fed9cf119f [file] [log] [blame]
James Kuszmauldac091f2022-03-22 09:35:06 -07001import {ByteBuffer} from 'flatbuffers';
James Kuszmaulb84c0912022-04-13 19:44:52 -07002import {Connection} from '../../aos/network/www/proxy';
3import {IntakeState, Status as SuperstructureStatus, SuperstructureState} from '../control_loops/superstructure/superstructure_status_generated'
4import {LocalizerOutput} from '../localizer/localizer_output_generated';
5import {RejectionReason} from '../localizer/localizer_status_generated';
6import {Status as DrivetrainStatus} from '../../frc971/control_loops/drivetrain/drivetrain_status_generated';
7import {LocalizerVisualization, TargetEstimateDebug} from '../localizer/localizer_visualization_generated';
James Kuszmaulf3ef9e12022-03-05 17:13:00 -08008
9import {FIELD_LENGTH, FIELD_WIDTH, FT_TO_M, IN_TO_M} from './constants';
10
11// (0,0) is field center, +X is toward red DS
12const FIELD_SIDE_Y = FIELD_WIDTH / 2;
13const FIELD_EDGE_X = FIELD_LENGTH / 2;
14
15const ROBOT_WIDTH = 34 * IN_TO_M;
16const ROBOT_LENGTH = 36 * IN_TO_M;
17
18const PI_COLORS = ['#ff00ff', '#ffff00', '#00ffff', '#ffa500'];
19
20export class FieldHandler {
21 private canvas = document.createElement('canvas');
22 private localizerOutput: LocalizerOutput|null = null;
James Kuszmaulb84c0912022-04-13 19:44:52 -070023 private drivetrainStatus: DrivetrainStatus|null = null;
James Kuszmaulf3ef9e12022-03-05 17:13:00 -080024 private superstructureStatus: SuperstructureStatus|null = null;
25
26 // Image information indexed by timestamp (seconds since the epoch), so that
27 // we can stop displaying images after a certain amount of time.
28 private localizerImageMatches = new Map<number, LocalizerVisualization>();
29 private outerTarget: HTMLElement =
30 (document.getElementById('outer_target') as HTMLElement);
31 private innerTarget: HTMLElement =
32 (document.getElementById('inner_target') as HTMLElement);
33 private x: HTMLElement = (document.getElementById('x') as HTMLElement);
34 private y: HTMLElement = (document.getElementById('y') as HTMLElement);
35 private theta: HTMLElement =
36 (document.getElementById('theta') as HTMLElement);
37 private shotDistance: HTMLElement =
38 (document.getElementById('shot_distance') as HTMLElement);
39 private turret: HTMLElement =
40 (document.getElementById('turret') as HTMLElement);
James Kuszmaula0871872022-03-05 22:12:39 -080041 private fire: HTMLElement =
42 (document.getElementById('fire') as HTMLElement);
43 private mpcSolveTime: HTMLElement =
44 (document.getElementById('mpc_solve_time') as HTMLElement);
Austin Schuh41472552022-03-13 18:09:41 -070045 private mpcHorizon: HTMLElement =
46 (document.getElementById('mpc_horizon') as HTMLElement);
James Kuszmaula0871872022-03-05 22:12:39 -080047 private shotCount: HTMLElement =
48 (document.getElementById('shot_count') as HTMLElement);
49 private catapult: HTMLElement =
50 (document.getElementById('catapult') as HTMLElement);
51 private superstructureState: HTMLElement =
52 (document.getElementById('superstructure_state') as HTMLElement);
53 private intakeState: HTMLElement =
54 (document.getElementById('intake_state') as HTMLElement);
55 private reseatingInCatapult: HTMLElement =
56 (document.getElementById('reseating_in_catapult') as HTMLElement);
57 private flippersOpen: HTMLElement =
58 (document.getElementById('flippers_open') as HTMLElement);
59 private climber: HTMLElement =
60 (document.getElementById('climber') as HTMLElement);
James Kuszmaulf3ef9e12022-03-05 17:13:00 -080061 private frontIntake: HTMLElement =
62 (document.getElementById('front_intake') as HTMLElement);
63 private backIntake: HTMLElement =
64 (document.getElementById('back_intake') as HTMLElement);
65 private imagesAcceptedCounter: HTMLElement =
66 (document.getElementById('images_accepted') as HTMLElement);
67 private imagesRejectedCounter: HTMLElement =
68 (document.getElementById('images_rejected') as HTMLElement);
69 private rejectionReasonCells: HTMLElement[] = [];
70 private fieldImage: HTMLImageElement = new Image();
71
72 constructor(private readonly connection: Connection) {
73 (document.getElementById('field') as HTMLElement).appendChild(this.canvas);
74
75 this.fieldImage.src = "2022.png";
76
77 for (const value in RejectionReason) {
78 // Typescript generates an iterator that produces both numbers and
79 // strings... don't do anything on the string iterations.
80 if (isNaN(Number(value))) {
81 continue;
82 }
83 const row = document.createElement('div');
84 const nameCell = document.createElement('div');
85 nameCell.innerHTML = RejectionReason[value];
86 row.appendChild(nameCell);
87 const valueCell = document.createElement('div');
88 valueCell.innerHTML = 'NA';
89 this.rejectionReasonCells.push(valueCell);
90 row.appendChild(valueCell);
91 document.getElementById('vision_readouts').appendChild(row);
92 }
93
94 for (let ii = 0; ii < PI_COLORS.length; ++ii) {
95 const legendEntry = document.createElement('div');
96 legendEntry.style.color = PI_COLORS[ii];
97 legendEntry.innerHTML = 'PI' + (ii + 1).toString()
98 document.getElementById('legend').appendChild(legendEntry);
99 }
100
101 this.connection.addConfigHandler(() => {
James Kuszmaul43c0f2c2022-04-03 16:17:26 -0700102 // Visualization message is reliable so that we can see *all* the vision
103 // matches.
104 this.connection.addReliableHandler(
Milind Upadhyay47481ca2022-09-24 20:46:29 -0700105 '/localizer', "frc971.controls.LocalizerVisualization",
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800106 (data) => {
107 this.handleLocalizerDebug(data);
108 });
109 this.connection.addHandler(
Milind Upadhyay47481ca2022-09-24 20:46:29 -0700110 '/drivetrain', "frc971.control_loops.drivetrain.Status", (data) => {
James Kuszmaulb84c0912022-04-13 19:44:52 -0700111 this.handleDrivetrainStatus(data);
112 });
113 this.connection.addHandler(
Milind Upadhyay47481ca2022-09-24 20:46:29 -0700114 '/localizer', "frc971.controls.LocalizerOutput", (data) => {
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800115 this.handleLocalizerOutput(data);
116 });
117 this.connection.addHandler(
Milind Upadhyay47481ca2022-09-24 20:46:29 -0700118 '/superstructure', "y2022.control_loops.superstructure.Status",
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800119 (data) => {
120 this.handleSuperstructureStatus(data);
121 });
122 });
123 }
124
125 private handleLocalizerDebug(data: Uint8Array): void {
126 const now = Date.now() / 1000.0;
127
128 const fbBuffer = new ByteBuffer(data);
129 this.localizerImageMatches.set(
James Kuszmauldac091f2022-03-22 09:35:06 -0700130 now, LocalizerVisualization.getRootAsLocalizerVisualization(fbBuffer));
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800131
132 const debug = this.localizerImageMatches.get(now);
133
134 if (debug.statistics()) {
135 this.imagesAcceptedCounter.innerHTML =
136 debug.statistics().totalAccepted().toString();
137 this.imagesRejectedCounter.innerHTML =
138 (debug.statistics().totalCandidates() -
139 debug.statistics().totalAccepted())
140 .toString();
141 if (debug.statistics().rejectionReasonCountLength() ==
142 this.rejectionReasonCells.length) {
143 for (let ii = 0; ii < debug.statistics().rejectionReasonCountLength();
144 ++ii) {
145 this.rejectionReasonCells[ii].innerHTML =
146 debug.statistics().rejectionReasonCount(ii).toString();
147 }
148 } else {
149 console.error('Unexpected number of rejection reasons in counter.');
150 }
151 this.imagesRejectedCounter.innerHTML =
152 (debug.statistics().totalCandidates() -
153 debug.statistics().totalAccepted())
154 .toString();
155 }
156 }
157
158 private handleLocalizerOutput(data: Uint8Array): void {
159 const fbBuffer = new ByteBuffer(data);
James Kuszmauldac091f2022-03-22 09:35:06 -0700160 this.localizerOutput = LocalizerOutput.getRootAsLocalizerOutput(fbBuffer);
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800161 }
162
James Kuszmaulb84c0912022-04-13 19:44:52 -0700163 private handleDrivetrainStatus(data: Uint8Array): void {
164 const fbBuffer = new ByteBuffer(data);
165 this.drivetrainStatus = DrivetrainStatus.getRootAsStatus(fbBuffer);
166 }
167
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800168 private handleSuperstructureStatus(data: Uint8Array): void {
169 const fbBuffer = new ByteBuffer(data);
James Kuszmauldac091f2022-03-22 09:35:06 -0700170 this.superstructureStatus = SuperstructureStatus.getRootAsStatus(fbBuffer);
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800171 }
172
173 drawField(): void {
174 const ctx = this.canvas.getContext('2d');
James Kuszmauldee532c2022-04-16 14:43:06 -0700175 ctx.save();
176 ctx.scale(-1.0, 1.0);
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800177 ctx.drawImage(
178 this.fieldImage, 0, 0, this.fieldImage.width, this.fieldImage.height,
179 -FIELD_EDGE_X, -FIELD_SIDE_Y, FIELD_LENGTH, FIELD_WIDTH);
James Kuszmauldee532c2022-04-16 14:43:06 -0700180 ctx.restore();
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800181 }
182
183 drawCamera(
184 x: number, y: number, theta: number, color: string = 'blue',
185 extendLines: boolean = true): void {
186 const ctx = this.canvas.getContext('2d');
187 ctx.save();
188 ctx.translate(x, y);
189 ctx.rotate(theta);
190 ctx.strokeStyle = color;
191 ctx.beginPath();
192 ctx.moveTo(0.5, 0.5);
193 ctx.lineTo(0, 0);
194 if (extendLines) {
195 ctx.lineTo(100.0, 0);
196 ctx.lineTo(0, 0);
197 }
198 ctx.lineTo(0.5, -0.5);
199 ctx.stroke();
200 ctx.beginPath();
201 ctx.arc(0, 0, 0.25, -Math.PI / 4, Math.PI / 4);
202 ctx.stroke();
203 ctx.restore();
204 }
205
206 drawRobot(
207 x: number, y: number, theta: number, turret: number|null,
208 color: string = 'blue', dashed: boolean = false,
209 extendLines: boolean = true): void {
210 const ctx = this.canvas.getContext('2d');
211 ctx.save();
212 ctx.translate(x, y);
213 ctx.rotate(theta);
214 ctx.strokeStyle = color;
215 ctx.lineWidth = ROBOT_WIDTH / 10.0;
216 if (dashed) {
217 ctx.setLineDash([0.05, 0.05]);
218 } else {
219 // Empty array = solid line.
220 ctx.setLineDash([]);
221 }
222 ctx.rect(-ROBOT_LENGTH / 2, -ROBOT_WIDTH / 2, ROBOT_LENGTH, ROBOT_WIDTH);
223 ctx.stroke();
224
225 // Draw line indicating which direction is forwards on the robot.
226 ctx.beginPath();
227 ctx.moveTo(0, 0);
228 if (extendLines) {
229 ctx.lineTo(1000.0, 0);
230 } else {
231 ctx.lineTo(ROBOT_LENGTH / 2.0, 0);
232 }
233 ctx.stroke();
234
235 if (turret !== null) {
236 ctx.save();
237 ctx.rotate(turret);
238 const turretRadius = ROBOT_WIDTH / 3.0;
239 ctx.strokeStyle = 'red';
240 // Draw circle for turret.
241 ctx.beginPath();
242 ctx.arc(0, 0, turretRadius, 0, 2.0 * Math.PI);
243 ctx.stroke();
244 // Draw line in circle to show forwards.
245 ctx.beginPath();
246 ctx.moveTo(0, 0);
247 if (extendLines) {
248 ctx.lineTo(1000.0, 0);
249 } else {
250 ctx.lineTo(turretRadius, 0);
251 }
252 ctx.stroke();
253 ctx.restore();
254 }
255 ctx.restore();
256 }
257
258 setZeroing(div: HTMLElement): void {
259 div.innerHTML = 'zeroing';
260 div.classList.remove('faulted');
261 div.classList.add('zeroing');
262 div.classList.remove('near');
263 }
264
265 setEstopped(div: HTMLElement): void {
266 div.innerHTML = 'estopped';
267 div.classList.add('faulted');
268 div.classList.remove('zeroing');
269 div.classList.remove('near');
270 }
271
272 setTargetValue(
273 div: HTMLElement, target: number, val: number, tolerance: number): void {
274 div.innerHTML = val.toFixed(4);
275 div.classList.remove('faulted');
276 div.classList.remove('zeroing');
277 if (Math.abs(target - val) < tolerance) {
278 div.classList.add('near');
279 } else {
280 div.classList.remove('near');
281 }
282 }
283
284 setValue(div: HTMLElement, val: number): void {
285 div.innerHTML = val.toFixed(4);
286 div.classList.remove('faulted');
287 div.classList.remove('zeroing');
288 div.classList.remove('near');
289 }
290
291 draw(): void {
292 this.reset();
293 this.drawField();
294
295 // Draw the matches with debugging information from the localizer.
296 const now = Date.now() / 1000.0;
James Kuszmaula0871872022-03-05 22:12:39 -0800297 if (this.superstructureStatus) {
298 this.shotDistance.innerHTML = this.superstructureStatus.aimer() ?
Austin Schuh0a732cd2022-03-29 21:19:59 -0700299 (this.superstructureStatus.aimer().shotDistance() /
300 0.0254).toFixed(2) +
301 'in, ' +
302 this.superstructureStatus.aimer().shotDistance().toFixed(2) +
303 'm' :
James Kuszmaula0871872022-03-05 22:12:39 -0800304 'NA';
305
306 this.fire.innerHTML = this.superstructureStatus.fire() ? 'true' : 'false';
307
Austin Schuh41472552022-03-13 18:09:41 -0700308 this.mpcHorizon.innerHTML =
309 this.superstructureStatus.mpcHorizon().toFixed(2);
James Kuszmaula0871872022-03-05 22:12:39 -0800310
311 this.setValue(this.mpcSolveTime, this.superstructureStatus.solveTime());
312
313 this.shotCount.innerHTML =
314 this.superstructureStatus.shotCount().toFixed(0);
315
316 this.superstructureState.innerHTML =
317 SuperstructureState[this.superstructureStatus.state()];
318
319 this.intakeState.innerHTML =
320 IntakeState[this.superstructureStatus.intakeState()];
321
322 this.reseatingInCatapult.innerHTML =
323 this.superstructureStatus.reseatingInCatapult() ? 'true' : 'false';
324
325 this.flippersOpen.innerHTML =
326 this.superstructureStatus.flippersOpen() ? 'true' : 'false';
327
328 if (!this.superstructureStatus.catapult() ||
329 !this.superstructureStatus.catapult().zeroed()) {
330 this.setZeroing(this.catapult);
331 } else if (this.superstructureStatus.catapult().estopped()) {
332 this.setEstopped(this.catapult);
333 } else {
334 this.setTargetValue(
335 this.catapult,
336 this.superstructureStatus.catapult().unprofiledGoalPosition(),
337 this.superstructureStatus.catapult().estimatorState().position(),
338 1e-3);
339 }
340
341 if (!this.superstructureStatus.climber() ||
342 !this.superstructureStatus.climber().zeroed()) {
343 this.setZeroing(this.climber);
344 } else if (this.superstructureStatus.climber().estopped()) {
345 this.setEstopped(this.climber);
346 } else {
347 this.setTargetValue(
348 this.climber,
349 this.superstructureStatus.climber().unprofiledGoalPosition(),
350 this.superstructureStatus.climber().estimatorState().position(),
351 1e-3);
352 }
353
354
355
356 if (!this.superstructureStatus.turret() ||
357 !this.superstructureStatus.turret().zeroed()) {
358 this.setZeroing(this.turret);
359 } else if (this.superstructureStatus.turret().estopped()) {
360 this.setEstopped(this.turret);
361 } else {
362 this.setTargetValue(
363 this.turret,
364 this.superstructureStatus.turret().unprofiledGoalPosition(),
365 this.superstructureStatus.turret().estimatorState().position(),
366 1e-3);
367 }
368
369 if (!this.superstructureStatus.intakeBack() ||
370 !this.superstructureStatus.intakeBack().zeroed()) {
371 this.setZeroing(this.backIntake);
372 } else if (this.superstructureStatus.intakeBack().estopped()) {
373 this.setEstopped(this.backIntake);
374 } else {
375 this.setValue(
376 this.backIntake,
377 this.superstructureStatus.intakeBack().estimatorState().position());
378 }
379
380 if (!this.superstructureStatus.intakeFront() ||
381 !this.superstructureStatus.intakeFront().zeroed()) {
382 this.setZeroing(this.frontIntake);
383 } else if (this.superstructureStatus.intakeFront().estopped()) {
384 this.setEstopped(this.frontIntake);
385 } else {
386 this.setValue(
387 this.frontIntake,
388 this.superstructureStatus.intakeFront()
389 .estimatorState()
390 .position());
391 }
392 }
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800393
James Kuszmaulb84c0912022-04-13 19:44:52 -0700394 if (this.drivetrainStatus && this.drivetrainStatus.trajectoryLogging()) {
395 this.drawRobot(
396 this.drivetrainStatus.trajectoryLogging().x(),
397 this.drivetrainStatus.trajectoryLogging().y(),
398 this.drivetrainStatus.trajectoryLogging().theta(), null, "#000000FF",
399 false);
400 }
401
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800402 if (this.localizerOutput) {
403 if (!this.localizerOutput.zeroed()) {
404 this.setZeroing(this.x);
405 this.setZeroing(this.y);
406 this.setZeroing(this.theta);
407 } else {
408 this.setValue(this.x, this.localizerOutput.x());
409 this.setValue(this.y, this.localizerOutput.y());
410 this.setValue(this.theta, this.localizerOutput.theta());
411 }
412
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800413 this.drawRobot(
414 this.localizerOutput.x(), this.localizerOutput.y(),
415 this.localizerOutput.theta(),
416 this.superstructureStatus ?
417 this.superstructureStatus.turret().position() :
418 null);
419 }
420
James Kuszmaul1bd41f72022-04-03 13:18:16 -0700421 for (const [time, value] of this.localizerImageMatches) {
422 const age = now - time;
423 const kRemovalAge = 1.0;
424 if (age > kRemovalAge) {
425 this.localizerImageMatches.delete(time);
426 continue;
427 }
428 const kMaxImageAlpha = 0.5;
429 const ageAlpha = kMaxImageAlpha * (kRemovalAge - age) / kRemovalAge
430 for (let i = 0; i < value.targetsLength(); i++) {
431 const imageDebug = value.targets(i);
432 const x = imageDebug.impliedRobotX();
433 const y = imageDebug.impliedRobotY();
434 const theta = imageDebug.impliedRobotTheta();
435 const cameraX = imageDebug.cameraX();
436 const cameraY = imageDebug.cameraY();
437 const cameraTheta = imageDebug.cameraTheta();
438 const accepted = imageDebug.accepted();
439 // Make camera readings fade over time.
440 const alpha = Math.round(255 * ageAlpha).toString(16).padStart(2, '0');
441 const dashed = false;
442 const acceptedRgb = accepted ? '#00FF00' : '#FF0000';
443 const acceptedRgba = acceptedRgb + alpha;
444 const cameraRgb = PI_COLORS[imageDebug.camera()];
445 const cameraRgba = cameraRgb + alpha;
446 this.drawRobot(x, y, theta, null, acceptedRgba, dashed, false);
447 this.drawCamera(cameraX, cameraY, cameraTheta, cameraRgba, false);
448 }
449 }
450
James Kuszmaulf3ef9e12022-03-05 17:13:00 -0800451 window.requestAnimationFrame(() => this.draw());
452 }
453
454 reset(): void {
455 const ctx = this.canvas.getContext('2d');
456 ctx.setTransform(1, 0, 0, 1, 0, 0);
457 const size = window.innerHeight * 0.9;
458 ctx.canvas.height = size;
459 const width = size / 2 + 20;
460 ctx.canvas.width = width;
461 ctx.clearRect(0, 0, size, width);
462
463 // Translate to center of display.
464 ctx.translate(width / 2, size / 2);
465 // Coordinate system is:
466 // x -> forward.
467 // y -> to the left.
468 ctx.rotate(-Math.PI / 2);
469 ctx.scale(1, -1);
470
471 const M_TO_PX = (size - 10) / FIELD_LENGTH;
472 ctx.scale(M_TO_PX, M_TO_PX);
473 ctx.lineWidth = 1 / M_TO_PX;
474 }
475}