Scouting App: Add ability to submit data with QR Codes
This patch adds the option for a scout to generate a QR Code
storing the collected data instead of submitting it. Next,
another user can submit the data by scanning the QR Code using the
new 'Scan' tab.
Since flatbuffers are pretty inefficient in terms of space usage, we
have trouble fitting all the data into a single QR code. The app
allows the user to split the data into multiple QR codes which have to
be scanned.
I tried a bunch of upstream QR code scanning libraries, but they're
all either unmaintained, not open source, or just don't work well. I
ended up settling on OpenCV because it was the most reliable library I
could find.
Co-Authored-By: Filip Kujawa <filip.j.kujawa@gmail.com>
Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
Signed-off-by: Philipp Schrader <philipp.schrader+971@gmail.com>
Change-Id: I794b54bf7e8389200aa2abe8d05f622a987bca9c
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 6da8be8..09b1a8b 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -26,6 +26,7 @@
} from '../../webserver/requests/messages/submit_2024_actions_generated';
import {Match} from '../../webserver/requests/messages/request_all_matches_response_generated';
import {MatchListRequestor} from '../rpc';
+import * as pako from 'pako';
type Section =
| 'Team Selection'
@@ -35,6 +36,7 @@
| 'Endgame'
| 'Dead'
| 'Review and Submit'
+ | 'QR Code'
| 'Success';
// TODO(phil): Deduplicate with match_list.component.ts.
@@ -50,6 +52,13 @@
f: 'Finals',
};
+// The maximum number of bytes per QR code. The user can adjust this value to
+// make the QR code contain less information, but easier to scan.
+const QR_CODE_PIECE_SIZES = [150, 300, 450, 600, 750, 900];
+
+// 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';
@@ -112,6 +121,7 @@
// of radio buttons.
readonly COMP_LEVELS = COMP_LEVELS;
readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
+ readonly QR_CODE_PIECE_SIZES = QR_CODE_PIECE_SIZES;
readonly ScoreType = ScoreType;
readonly StageType = StageType;
@@ -136,6 +146,16 @@
teamSelectionIsValid = false;
+ // When the user chooses to generate QR codes, we convert the flatbuffer into
+ // a long string. Since we frequently have more data than we can display in a
+ // single QR code, we break the data into multiple QR codes. The data for
+ // each QR code ("pieces") is stored in the `qrCodeValuePieces` list below.
+ // The `qrCodeValueIndex` keeps track of which QR code we're currently
+ // displaying.
+ qrCodeValuePieceSize = QR_CODE_PIECE_SIZES[DEFAULT_QR_CODE_PIECE_SIZE_INDEX];
+ qrCodeValuePieces: string[] = [];
+ qrCodeValueIndex: number = 0;
+
constructor(private readonly matchListRequestor: MatchListRequestor) {}
ngOnInit() {
@@ -219,7 +239,8 @@
}
if (action.type == 'endMatchAction') {
- // endMatchAction occurs at the same time as penaltyAction so add to its timestamp to make it unique.
+ // endMatchAction occurs at the same time as penaltyAction so add to its
+ // timestamp to make it unique.
action.timestamp += 1;
}
@@ -282,6 +303,11 @@
this.errorMessage = '';
this.progressMessage = '';
+ // For the QR code screen, we need to make the value to encode available.
+ if (target == 'QR Code') {
+ this.updateQrCodeValuePieceSize();
+ }
+
this.section = target;
}
@@ -291,7 +317,7 @@
this.header.nativeElement.scrollIntoView();
}
- async submit2024Actions() {
+ createActionsBuffer() {
const builder = new Builder();
const actionOffsets: number[] = [];
@@ -419,10 +445,44 @@
Submit2024Actions.addPreScouting(builder, this.preScouting);
builder.finish(Submit2024Actions.endSubmit2024Actions(builder));
- const buffer = builder.asUint8Array();
+ return builder.asUint8Array();
+ }
+
+ // Same as createActionsBuffer, but encoded as Base64. It's also split into
+ // a number of pieces so that each piece is roughly limited to
+ // `qrCodeValuePieceSize` bytes.
+ createBase64ActionsBuffers(): string[] {
+ const originalBuffer = this.createActionsBuffer();
+ const deflatedData = pako.deflate(originalBuffer, {level: 9});
+
+ const pieceSize = this.qrCodeValuePieceSize;
+ const fullValue = btoa(String.fromCharCode(...deflatedData));
+ const numPieces = Math.ceil(fullValue.length / pieceSize);
+
+ let splitData: string[] = [];
+ for (let i = 0; i < numPieces; i++) {
+ const splitPiece = fullValue.slice(i * pieceSize, (i + 1) * pieceSize);
+ splitData.push(`${i}_${numPieces}_${pieceSize}_${splitPiece}`);
+ }
+ return splitData;
+ }
+
+ setQrCodeValueIndex(index: number) {
+ this.qrCodeValueIndex = Math.max(
+ 0,
+ Math.min(index, this.qrCodeValuePieces.length - 1)
+ );
+ }
+
+ updateQrCodeValuePieceSize() {
+ this.qrCodeValuePieces = this.createBase64ActionsBuffers();
+ this.qrCodeValueIndex = 0;
+ }
+
+ async submit2024Actions() {
const res = await fetch('/requests/submit/submit_2024_actions', {
method: 'POST',
- body: buffer,
+ body: this.createActionsBuffer(),
});
if (res.ok) {