blob: 431fda3a26047440d55c1c3b59bafe70c1c6ef71 [file] [log] [blame]
import {Component, NgZone, OnInit, ViewChild, ElementRef} from '@angular/core';
import {ErrorResponse} from '@org_frc971/scouting/webserver/requests/messages/error_response_generated';
import {ActionsSubmitter} from '@org_frc971/scouting/www/rpc';
import {Builder, ByteBuffer} from 'flatbuffers';
import * as pako from 'pako';
declare var cv: any;
declare var Module: any;
// The number of milliseconds between QR code scans.
const SCAN_PERIOD = 500;
@Component({
selector: 'app-scan',
templateUrl: './scan.ng.html',
styleUrls: ['../app/common.css', './scan.component.css'],
})
export class ScanComponent implements OnInit {
@ViewChild('video')
public video: ElementRef;
@ViewChild('canvas')
public canvas: ElementRef;
errorMessage: string = '';
progressMessage: string = 'Waiting for QR code(s)';
scanComplete: boolean = false;
videoStartedSuccessfully = false;
qrCodeValuePieces: string[] = [];
qrCodeValuePieceSize = 0;
scanStream: MediaStream | null = null;
scanTimer: ReturnType<typeof setTimeout> | null = null;
constructor(
private ngZone: NgZone,
private actionsSubmitter: ActionsSubmitter
) {}
ngOnInit() {
// If the user switched away from this tab, then the onRuntimeInitialized
// attribute will already be set. No need to load OpenCV again. If it's not
// loaded, however, we need to load it.
if (!Module['onRuntimeInitialized']) {
Module['onRuntimeInitialized'] = () => {
// Since the WASM code doesn't know about the Angular zone, we force
// it into the correct zone so that the UI gets updated properly.
this.ngZone.run(() => {
this.startScanning();
});
};
// Now that we set up the hook, we can load OpenCV.
this.loadOpenCv();
} else {
this.startScanning();
}
}
ngOnDestroy() {
clearInterval(this.scanTimer);
// Explicitly stop the streams so that the camera isn't being locked
// unnecessarily. I.e. other processes can use it too.
if (this.scanStream) {
this.scanStream.getTracks().forEach((track) => {
track.stop();
});
}
}
public ngAfterViewInit() {
// Start the video playback.
// It would be nice to let the user select which camera gets used. For now,
// we give the "environment" hint so that it faces away from the user.
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({video: {facingMode: 'environment'}})
.then(
(stream) => {
this.scanStream = stream;
this.video.nativeElement.srcObject = stream;
this.video.nativeElement.play();
this.videoStartedSuccessfully = true;
},
(reason) => {
this.progressMessage = '';
this.errorMessage = `Failed to start video: ${reason}`;
}
);
}
}
async scan() {
if (!this.videoStartedSuccessfully) {
return;
}
// Take a capture of the video stream. That capture can then be used by
// OpenCV to perform the QR code detection. Due to my inexperience, I could
// only make this code work if I size the (invisible) canvas to match the
// video element. Otherwise, I'd get cropped images.
// Can we stream the video directly into the canvas?
const width = this.video.nativeElement.clientWidth;
const height = this.video.nativeElement.clientHeight;
this.canvas.nativeElement.width = width;
this.canvas.nativeElement.height = height;
this.canvas.nativeElement
.getContext('2d')
.drawImage(this.video.nativeElement, 0, 0, width, height);
// Perform the QR code detection. We use the Aruco-variant of the detector
// here because it appears to detect QR codes much more reliably than the
// standard detector.
let mat = cv.imread('canvas');
let qrDecoder = new cv.QRCodeDetectorAruco();
const result = qrDecoder.detectAndDecode(mat);
mat.delete();
// Handle the result.
if (result) {
await this.scanSuccessHandler(result);
} else {
await this.scanFailureHandler();
}
}
async scanSuccessHandler(scanResult: string) {
// Reverse the conversion and obtain the original Uint8Array. In other
// words, undo the work in `scouting/www/entry/entry.component.ts`.
const [indexStr, numPiecesStr, pieceSizeStr, splitPiece] = scanResult.split(
'_',
4
);
// If we didn't get enough data, then maybe we scanned some non-scouting
// related QR code? Try to give a hint to the user.
if (!indexStr || !numPiecesStr || !pieceSizeStr || !splitPiece) {
this.progressMessage = '';
this.errorMessage = `Couldn't find scouting data in the QR code.`;
return;
}
const index = Number(indexStr);
const numPieces = Number(numPiecesStr);
const pieceSize = Number(pieceSizeStr);
if (
numPieces != this.qrCodeValuePieces.length ||
pieceSize != this.qrCodeValuePieceSize
) {
// The number of pieces or the piece size changed. We need to reset our accounting.
this.qrCodeValuePieces = new Array<string>(numPieces);
this.qrCodeValuePieceSize = pieceSize;
}
this.qrCodeValuePieces[index] = splitPiece;
this.progressMessage = `Scanned QR code ${index + 1} out of ${
this.qrCodeValuePieces.length
}`;
// Count up the number of missing pieces so we can give a progress update.
let numMissingPieces = 0;
for (const piece of this.qrCodeValuePieces) {
if (!piece) {
numMissingPieces++;
}
}
if (numMissingPieces > 0) {
this.progressMessage = `Waiting for ${numMissingPieces} out of ${this.qrCodeValuePieces.length} QR codes.`;
this.errorMessage = '';
return;
}
// Stop scanning now that we have all the pieces.
this.progressMessage = 'Scanned all QR codes. Submitting.';
this.scanComplete = true;
clearInterval(this.scanTimer);
const encodedData = this.qrCodeValuePieces.join('');
const deflatedData = Uint8Array.from(atob(encodedData), (c) =>
c.charCodeAt(0)
);
const actionBuffer = pako.inflate(deflatedData);
const res = await this.actionsSubmitter.submit(actionBuffer);
if (res.ok) {
// We successfully submitted the data. Report success.
this.progressMessage = 'Success!';
this.errorMessage = '';
} else {
const resBuffer = await res.arrayBuffer();
const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
const errorMessage = parsedResponse.errorMessage();
this.progressMessage = '';
this.errorMessage = `Submission failed with ${res.status} ${res.statusText}: "${errorMessage}"`;
}
}
async scanFailureHandler() {
this.progressMessage = '';
this.errorMessage = 'Failed to scan!';
}
loadOpenCv() {
// Make the browser load OpenCV.
let body = <HTMLDivElement>document.body;
let script = document.createElement('script');
script.innerHTML = '';
script.src = 'assets/opencv_4.9.0/opencv.js';
script.async = false;
script.defer = true;
script.onerror = (error) => {
this.progressMessage = '';
if (typeof error === 'string') {
this.errorMessage = `OpenCV failed to load: ${error}`;
} else {
this.errorMessage = 'OpenCV failed to load.';
}
// Since we use the onRuntimeInitialized property as a flag to see if we
// need to perform loading, we need to delete the property. When the user
// switches away from this tab and then switches back, we want to attempt
// loading again.
delete Module['onRuntimeInitialized'];
};
body.appendChild(script);
}
startScanning() {
this.scanTimer = setInterval(() => {
this.scan();
}, SCAN_PERIOD);
}
}