summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rwxr-xr-xdocker/scripts/run-unit-tests.sh5
-rw-r--r--test/codes/LinearTest.mc146
-rw-r--r--test/data/CodeVectors.mc230
-rw-r--r--test/golden/fenix7/code.pngbin0 -> 3294 bytes
-rw-r--r--test/golden/fr255s/code.pngbin0 -> 2267 bytes
-rw-r--r--test/golden/fr70/code.pngbin0 -> 13889 bytes
-rw-r--r--test/golden/fr965/code.pngbin0 -> 12507 bytes
-rw-r--r--test/golden/instinct3amoled45mm/code.pngbin0 -> 11749 bytes
-rw-r--r--test/golden/venusq2/code.pngbin0 -> 3037 bytes
-rw-r--r--test/golden/venux1/code.pngbin0 -> 11473 bytes
-rw-r--r--tools/barcode_ref.py97
-rw-r--r--tools/check_codes.py60
-rw-r--r--tools/gen_code_vectors.py124
-rw-r--r--tools/tests/test_barcode_tools.py65
-rw-r--r--tools/ui_flow.py29
16 files changed, 757 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index 1f2fdba..61a2b46 100644
--- a/Makefile
+++ b/Makefile
@@ -72,7 +72,7 @@ DOCS_RUN = $(DOCKER) run --rm --init --label project=$(PROJECT) \
--read-only --tmpfs /tmp:rw,exec,nosuid,size=1g --shm-size 512m \
-v "$(ROOT)":/work -w /work --network none $(DOCS_IMAGE)
-.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons vectors corpus spike spike-sim \
+.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons vectors corpus codes spike spike-sim \
lint package docs ci shell clean nuke lock products \
check-docker dirs need-image need-docs-image
@@ -139,6 +139,9 @@ vectors: dirs need-image ## Regenerate test/data/Vectors.mc (cross-checked RFC/N
corpus: dirs need-image ## Regenerate test/data/FuzzCorpus.mc (seeded fuzz inputs for the import parsers)
$(DEV_OFFLINE) python3 tools/gen_fuzz_corpus.py
+codes: dirs need-image ## Regenerate test/data/CodeVectors.mc (zxing-cpp reference barcodes)
+ $(DEV_OFFLINE) python3 tools/gen_code_vectors.py
+
spike: dirs need-image ## Build the M1 settings spike (debug .prg + beta .iq) into build/spike
$(DEV_OFFLINE) build-spike.sh build
diff --git a/docker/scripts/run-unit-tests.sh b/docker/scripts/run-unit-tests.sh
index 60a8955..a935db3 100755
--- a/docker/scripts/run-unit-tests.sh
+++ b/docker/scripts/run-unit-tests.sh
@@ -8,7 +8,8 @@
# Every test build is compiled first, so a compile error stops the run before
# the simulator starts. One simulator then runs every device in turn.
# monkeydo exits with 1 even when all tests pass, so the verdict comes from
-# tools/ciq_test_report.py; the script fails if any device fails.
+# tools/ciq_test_report.py, and the barcodes the tests dumped are decoded
+# with zxing-cpp (tools/check_codes.py); the script fails if any device fails.
set -euo pipefail
# shellcheck source=ciq-env.sh
source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh"
@@ -38,6 +39,8 @@ for device in "$@"; do
>"${results}/${device}.log" 2>&1 || true
python3 /work/tools/ciq_test_report.py "${results}/${device}.log" \
--device "${device}" --junit "${results}/${device}.xml" || failed=1
+ # README 12.4 check 2: the barcodes the encoders produced must decode.
+ python3 /work/tools/check_codes.py "${results}/${device}.log" || failed=1
done
if ((failed)); then
diff --git a/test/codes/LinearTest.mc b/test/codes/LinearTest.mc
new file mode 100644
index 0000000..ed42ccf
--- /dev/null
+++ b/test/codes/LinearTest.mc
@@ -0,0 +1,146 @@
+// Author: cirakg & claude ai
+// Summary: Tests for the linear barcode encoders: zxing-cpp reference rows, validation messages, and a dump for decoding.
+
+import Toybox.Lang;
+import Toybox.Test;
+
+(:test)
+module LinearFixtures {
+
+ function moduleText(m as ByteArray) as String {
+ var s = "";
+ for (var i = 0; i < m.size(); i++) {
+ s += m[i] == 1 ? "1" : "0";
+ }
+ return s;
+ }
+
+ // The error message for payload, or "" when it encodes.
+ function errorOf(symbology as Number, payload as String, options as Number) as String {
+ var r = Codes.encode(symbology, payload, options);
+ return r instanceof Codes.CodeError ? r.message : "";
+ }
+
+ function opts(check as Number) as Number {
+ return Codes.OPT_SHOW_TEXT | (check << Codes.OPT_CHECK_SHIFT) | 1;
+ }
+}
+
+// README 12.4 check 1: identical modules to zxing-cpp for every row.
+(:test)
+function linearCodesMatchTheReference(logger as Test.Logger) as Boolean {
+ var rows = CodeVectors.LINEAR;
+ for (var i = 0; i < rows.size(); i++) {
+ var row = rows[i] as Array;
+ var symbology = row[0] as Number;
+ var payload = row[1] as String;
+ var result = Codes.encode(symbology, payload, row[2] as Number);
+ if (result instanceof Codes.CodeError) {
+ logger.error("row " + i + " (" + Codes.name(symbology) + " '" + payload + "'): " + result.message);
+ return false;
+ }
+ var got = LinearFixtures.moduleText(result.modules);
+ var expected = row[3] as String;
+ if (symbology == Codes.CODE128) {
+ // zxing-cpp 3 writes Code 128 through zint, whose optimal encoder
+ // breaks ties between equally short encodings differently (for
+ // example set C for a final digit pair). Both are optimal: the
+ // length must match, and the code is decoded by check_codes.py.
+ logger.debug("CODEDUMP " + symbology + " " + Bytes.toHex(Bytes.fromUtf8(result.text)) + " " + got);
+ if (got.length() != expected.length()) {
+ logger.error("row " + i + " (Code 128 '" + payload + "') has " + got.length() + " modules, zxing-cpp "
+ + expected.length());
+ return false;
+ }
+ continue;
+ }
+ if (!got.equals(expected)) {
+ logger.error("row " + i + " (" + Codes.name(symbology) + " '" + payload + "') differs:\n got " + got + "\n exp " + expected);
+ return false;
+ }
+ }
+ logger.debug("CODES reference rows=" + rows.size());
+ return true;
+}
+
+(:test)
+function linearCheckDigitModes(logger as Test.Logger) as Boolean {
+ var add = LinearFixtures.opts(Codes.CHECK_ADD_VERIFY);
+ var fix = LinearFixtures.opts(Codes.CHECK_FIX);
+ var off = LinearFixtures.opts(Codes.CHECK_OFF);
+ var added = Codes.encode(Codes.EAN13, "590123412345", add) as Codes.LinearCode;
+ var fixed = Codes.encode(Codes.EAN13, "5901234123450", fix) as Codes.LinearCode;
+ var kept = Codes.encode(Codes.EAN13, "5901234123450", off) as Codes.LinearCode;
+ var wrong = LinearFixtures.errorOf(Codes.EAN13, "5901234123450", add);
+ var offShort = LinearFixtures.errorOf(Codes.EAN13, "590123412345", off);
+ return added.text.equals("5901234123457") && added.note != null
+ && fixed.text.equals("5901234123457") && (fixed.note as String).find("was 0") != null
+ && kept.text.equals("5901234123450")
+ && wrong.find("check digit should be 7, got 0") != null && wrong.find("Fix if wrong") != null
+ && offShort.find("needs all 13 digits") != null;
+}
+
+(:test)
+function linearValidationMessages(logger as Test.Logger) as Boolean {
+ var o = LinearFixtures.opts(Codes.CHECK_ADD_VERIFY);
+ var off = LinearFixtures.opts(Codes.CHECK_OFF);
+ var cases = [
+ [Codes.EAN13, "59012341234a", o, "digits only"],
+ [Codes.EAN13, "123", o, "needs 12 or 13 digits"],
+ [Codes.UPCA, "1234", o, "UPC-A needs 11 or 12"],
+ [Codes.EAN8, "12345678", o, "check digit should be"],
+ [Codes.UPCE, "2123456", o, "number system must be 0 or 1"],
+ [Codes.UPCE, "0502553", o, "not a valid zero suppression"],
+ [Codes.UPCE, "12345", o, "6, 7 or 8 digits"],
+ [Codes.ITF, "123", off, "even number of digits"],
+ [Codes.ITF, "1234", o, "odd number of digits"],
+ [Codes.CODE39, "abc", o, "isn't allowed"],
+ [Codes.CODE128, "Ünï", o, "printable ASCII"],
+ [Codes.CODE128, "", o, "needs some text"],
+ [Codes.CODABAR, "A123", o, "both ends or at neither"],
+ [Codes.CODABAR, "A1B2A", o, "isn't allowed there"],
+ [Codes.QR, "hello", o, "not supported yet"]
+ ];
+ for (var i = 0; i < cases.size(); i++) {
+ var c = cases[i] as Array;
+ var message = LinearFixtures.errorOf(c[0] as Number, c[1] as String, c[2] as Number);
+ if (message.find(c[3] as String) == null) {
+ logger.error("case " + i + ": got '" + message + "'");
+ return false;
+ }
+ }
+ var codabar = Codes.encode(Codes.CODABAR, "40156", o) as Codes.LinearCode;
+ return codabar.text.equals("A40156A") && codabar.note != null;
+}
+
+// README 12.4 check 2: dumps encoder outputs for seeded random payloads;
+// tools/check_codes.py decodes them with zxing-cpp after the test run.
+(:test)
+function linearDumpForDecoding(logger as Test.Logger) as Boolean {
+ var seed = 12345;
+ var count = 0;
+ var symbologies = [Codes.EAN13, Codes.EAN8, Codes.UPCA, Codes.UPCE, Codes.CODE128, Codes.CODE39, Codes.ITF, Codes.CODABAR];
+ var o = LinearFixtures.opts(Codes.CHECK_ADD_VERIFY);
+ for (var i = 0; i < 160; i++) {
+ var symbology = symbologies[i % symbologies.size()] as Number;
+ var length = [12, 7, 11, 7, 10, 8, 10, 8][i % symbologies.size()] as Number;
+ var alphabet = ["0123456789", "0123456789", "0123456789", "0123456789",
+ "0123456789 AZaz!#$%&()*+,-./:;<=>?@", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%", "0123456789", "0123456789-$:/.+"][i % symbologies.size()] as String;
+ var text = "";
+ for (var k = 0; k < length; k++) {
+ seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF;
+ var at = (seed >> 8) % alphabet.length();
+ text += alphabet.substring(at, at + 1);
+ }
+ if (symbology == Codes.UPCE) {
+ text = "0" + text.substring(1, 7);
+ }
+ var result = Codes.encode(symbology, text, o);
+ if (result instanceof Codes.CodeError) {
+ continue; // invalid random UPC-E suppressions are expected
+ }
+ logger.debug("CODEDUMP " + symbology + " " + Bytes.toHex(Bytes.fromUtf8(result.text)) + " " + LinearFixtures.moduleText(result.modules));
+ count++;
+ }
+ return count > 120;
+}
diff --git a/test/data/CodeVectors.mc b/test/data/CodeVectors.mc
new file mode 100644
index 0000000..64f8644
--- /dev/null
+++ b/test/data/CodeVectors.mc
@@ -0,0 +1,230 @@
+// Author: cirakg & claude ai
+// Summary: zxing-cpp reference module rows for the linear barcode encoders (generated; do not edit).
+//
+// Generated by tools/gen_code_vectors.py, seed 1212. Rows: [symbology, payload, options,
+// expected modules without quiet zones]. Fake payloads only.
+
+import Toybox.Lang;
+
+(:test)
+module CodeVectors {
+ const LINEAR = [
+ [3, "590123412345", 17, "10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101"],
+ [3, "5901234123457", 17, "10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101"],
+ [4, "1234567", 17, "1010011001001001101111010100011010101001110101000010001001110010101"],
+ [5, "01234567890", 17, "10100011010011001001001101111010100011011000101010101000010001001001000111010011100101001110101"],
+ [6, "0425261", 17, "101001110100100110111001001101101011110011001010101"],
+ [6, "1234565", 17, "101001001101000010011101011100101011110110001010101"],
+ [1, "1234", 17, "110100111001011001110010001011000100100111101100011101011"],
+ [1, "123456789", 17, "11010011100101100111001000101100011100010110110000101001011110111011100101100100011110101100011101011"],
+ [1, "AB12345678", 17, "1101001000010100011000100010110001011101111010110011100100010110001110001011011000010100111011010001100011101011"],
+ [1, "A1234B12345", 17, "1101001000010100011000101110111101011001110010001011000101111011101000101100010011100110101110111101110110111010111011000111010111101100011101011"],
+ [1, " ~", 17, "110100100001101100110010001011110111101001001100011101011"],
+ [2, "CODE 39", 25, "10010110110101101101001010110101101001010101100101101101011001010100110101101011011001010101011001011010100101101101"],
+ [2, "CODE-39", 17, "100101101101011011010010101101011010010101011001011011010110010101001010110110110110010101010110010110101011011010010100101101101"],
+ [7, "1234", 25, "101011101000101011100011101110100010100011101"],
+ [7, "123", 17, "101011101000101011100011101110001000101011101"],
+ [8, "A40156B", 17, "101100100101011010010101010011010101100101101010010100101011010010010110"],
+ [8, "31117013206375", 17, "101100100101100101010101011001010101100101010110010100101101010101001101010110010110010101010100101101010100110100101011011001010101001011010110101001010110010010"],
+ [3, "449628825640", 17, "10101000110010111010111100100110001001000100101010110110010011101010000101110011100101110010101"],
+ [4, "0695689", 17, "1010001101010111100010110110001010101010000100100011101001110100101"],
+ [5, "16055073910", 17, "10100110010101111000110101100010110001000110101010100010010000101110100110011011100101110100101"],
+ [6, "0786278", 17, "101001000101101110101111001001100100010001001010101"],
+ [1, "c3wd.2k069S5cU/86_th", 17, "110100100001000010110011001011100111100101001000010011010011001110110011100101100001001010011101100110011101001110010110011011101000110111001001000010110011011101110101110011001110100110011001110100101001100001001111010010011000010110111001001100011101011"],
+ [2, "9H.4KR-IMOC", 25, "100101101101010110010110101101010011010110010101101010100110101101101010100110110101011001010010101101101011010011010110110101001011010110100101101101001010100101101101"],
+ [7, "61702865119723", 25, "101010001110111010100010101000111000111010001110101000111010001110111000101011100010101011100010111010111000100010001110001010111011101"],
+ [8, "C/412A", 17, "101001001101101101011010110100101010110010101001011010110010010"],
+ [3, "035872054051", 17, "10101111010110001011011101110110010011000110101010100111010111001110010100111011001101101100101"],
+ [4, "9625582", 17, "1010001011010111100100110110001010101001110100100011011001000100101"],
+ [5, "49416692285", 17, "10101000110001011010001100110010101111010111101010111010011011001101100100100010011101011100101"],
+ [6, "1722686", 17, "101011101100110110010011000010100010010101111010101"],
+ [1, "9E S98s@[Z(r49w", 17, "11010010000111001011001000110100011011001100110111010001110010110011101001100101111001001100011011011100011010111011000101000110010010010011110110010011101110010110011110010100110100011101100011101011"],
+ [2, "TRO6", 17, "100101101101010101101100101101010110010110101101001010110011010101010011011010100101101101"],
+ [7, "6797684058", 25, "101010111011101000100010111010111000100010001110111010001010101110001000111011100010111010001011101"],
+ [8, "A24825B", 17, "101100100101010010110101101001010011010101010010110110101001010010010110"],
+ [3, "590759572895", 17, "10100010110100111001000101100010001011011100101010100010011011001001000111010010011101110100101"],
+ [4, "1019853", 17, "1010011001000110100110010001011010101001000100111010000101000100101"],
+ [5, "94874085816", 17, "10100010110100011011011101110110100011000110101010100100010011101001000110011010100001011100101"],
+ [6, "0484250", 17, "101001110100010010100011001001101100010100111010101"],
+ [1, "721P9675", 17, "1101001110010011000010101111011101001110011011101110110101110111101011110001011000010010110111011101100011101011"],
+ [2, "M/ZG9-P", 25, "10010110110101101101010010100100101001010011011010101010100110110101100101101010010101101101011011010010100101101101"],
+ [7, "213161", 25, "101010001110101011100011100011101010100010001110111010100011101"],
+ [8, "A:62C", 17, "10110010010110101101101001010110101001011010100100110"],
+ [3, "124018767336", 17, "10100100110100011010011100110010001001001000101010101000010001001000010100001010100001101100101"],
+ [4, "8370591", 17, "1010110111011110101110110001101010101001110111010011001101001110101"],
+ [5, "86048184512", 17, "10101101110101111000110101000110110111001100101010100100010111001001110110011011011001100110101"],
+ [6, "1432594", 17, "101010001101111010010011011100100101110011101010101"],
+ [1, "/8S/~60;95396", 17, "11010010000101110011001110100110011011101000101110011001000101111011001110100100111011001110110010011100101100101110111101101110111010111100010100011010001100011101011"],
+ [2, " .$2", 25, "10010110110101001101011010110010101101010010010010101011001010110100101101101"],
+ [7, "8603556071098627", 25, "101011101000100011101010001000111011101011100010111000101010111011100010001010001010111011100010100011101110001011101000100011101010111010100011100011101"],
+ [8, "A3/03-.589::.B", 17, "101100100101100101010110110101101010100110110010101010100110101101101101011010100101001101010110100101011010110110110101101101101101101010010010110"],
+ [3, "745070459602", 17, "10101000110111001000110100100010001101001110101010100111011101001010000111001011011001000100101"],
+ [4, "1035585", 17, "1010011001000110101111010110001010101001110100100010011101001110101"],
+ [5, "39401147040", 17, "10101111010001011010001100011010011001001100101010101110010001001110010101110011100101000010101"],
+ [6, "1939274", 17, "101000101101111010010111001101100100010100011010101"],
+ [1, "8b95FA7584q|1", 17, "1101001000011101001100100100001101110010110011011100100100011000101010001100010111011110110000100101001111010010111101110100101111001010111100010011100110111101000101100011101011"],
+ [2, "LWK5+", 25, "100101101101010110101001101100110101010110101010011011010011010101001010010010100101101101"],
+ [7, "4724634851241021", 25, "101010101110100011100010111010001011100010001110001110101010001011101000111011100010111010100010111010001011100011101010001000111010001110101011100011101"],
+ [8, "D/842$43A", 17, "101001100101101101011010011010101011010010101001011010110010101011010010110010101010110010010"],
+ [3, "631279390553", 17, "10101111010110011001101100100010001011011110101010111010011100101001110100111010000101001110101"],
+ [4, "4640106", 17, "1010100011010111101000110001101010101100110111001010100001110100101"],
+ [5, "36194005869", 17, "10101111010101111001100100010110100011000110101010111001010011101001000101000011101001110100101"],
+ [6, "1702344", 17, "101011101101001110010011010000101000110011101010101"],
+ [1, "9", 17, "1101001000011100101100111001001101100011101011"],
+ [2, "E", 17, "100101101101011010110010101101011001010100101101101"],
+ [7, "34286455759703", 25, "101011101110100010100010001110101000111010111011100010100011100010111000101010001010001110111010111010111000100010001000111011101011101"],
+ [8, "C6D", 17, "10100100110100101011010100110010"],
+ [3, "391662728089", 17, "10100010110011001000010100001010011011011101101010110110010010001110010100100011101001000010101"],
+ [4, "5936211", 17, "1010110001000101101111010101111010101101100110011011001101100110101"],
+ [5, "76968977434", 17, "10101110110101111000101101011110110111000101101010100010010001001011100100001010111001101100101"],
+ [6, "0934703", 17, "101001011101000010100011001000100011010111101010101"],
+ [1, "0U*85c11", 17, "110100100001001110110011011101110110010001001110100110011011100100100001011001011101111011000100100101111010001100011101011"],
+ [2, "QK.D/", 17, "1001011011010101010110011011010101001101100101011010101011001011010010010100101010010110110100101101101"],
+ [7, "501097", 25, "101011101011100010001011101010001000111010111010111000100011101"],
+ [8, "B51$9:7531787C", 17, "10010010110110101001010101100101011001010110100101011010110110100101101011010100101100101010101011001010010110101001101010100101101010100100110"],
+ [3, "856746516120", 17, "10101100010000101011101100111010000101011000101010110011010100001100110110110011100101110100101"],
+ [4, "8578033", 17, "1010110111011000101110110110111010101110010100001010000101110010101"],
+ [5, "67915446798", 17, "10101011110111011000101100110010110001010001101010101110010100001000100111010010010001010000101"],
+ [6, "0780039", 17, "101001000101101110001101010011101111010010111010101"],
+ [1, "686932&712633MB7*1B", 17, "1101001110010000100110101100100001100011011010111101110100110010001011101111010011010000111001001101010001100010111101110101110110001000101100011101101110110010001001001110011010001011000101100111001100011101011"],
+ [2, "ZECCK+0T", 17, "1001011011010100110110101011010110010101101101001010110110100101011010101001101001010010010101001101101010101101100101100101101010100101101101"],
+ [7, "108342", 25, "101011101010001000111011100010001011101010100011101011100011101"],
+ [8, "B-5+746+17A", 17, "100100101101010011010110101001010110110110100101101010110100101001010110101101101101010110010100101101010110010010"],
+ [3, "434270355934", 17, "10101111010011101001001101110110100111010000101010100111010011101110100100001010111001001110101"],
+ [4, "6164715", 17, "1010101111001100101011110100011010101000100110011010011101101100101"],
+ [5, "11306583166", 17, "10100110010011001011110100011010101111011000101010100100010000101100110101000010100001110010101"],
+ [6, "1042301", 17, "101000110100111010011011011110100011010110011010101"],
+ [1, "0|[}Kh0158Df;9", 17, "110100100001001110110010101111000111000110101010001111010110001110100110000101011101111011001101100111011000101011110111010110001000101100001001110110010011100101100111101000101100011101011"],
+ [2, "V8RE. ", 25, "1001011011010100110101011011010010110101101010110010110101100101011001010110101001101011010100101101101"],
+ [7, "0995829616", 25, "101010100011101110001010001110100011101011101000101110100010111000100011101011101000100010111011101"],
+ [8, "C7B", 17, "10100100110100101101010010010110"],
+ [3, "013645629332", 17, "10100110010111101010111101000110110001010111101010110110011101001000010100001011011001001000101"],
+ [4, "3321111", 17, "1010111101011110100100110011001010101100110110011011001101011100101"],
+ [5, "72563954209", 17, "10101110110010011011000101011110111101000101101010100111010111001101100111001011101001010000101"],
+ [6, "0248435", 17, "101001101101000110001001010001101111010111001010101"],
+ [1, "70i461!C6rX8", 17, "11010011100101100001001011110111010000110100110010011101100111010010011100110110011011001000100011011001110100100100111101110001011011101001100100010011001100011101011"],
+ [2, "M 3EF", 25, "100101101101011011010100101001101011010110110010101011010110010101011011001010100101101101"],
+ [7, "952022523820", 25, "101010001110100011101010111010001000111010111000101011100011101000111010100011100011101010001010111010001000111011101"],
+ [8, "D2/D", 17, "1010011001010100101101101101011010100110010"],
+ [3, "883500588448", 17, "10101101110100001011000101001110100111011000101010100100010010001011100101110010010001000010101"],
+ [4, "1230084", 17, "1010011001001001101111010001101010101110010100100010111001010000101"],
+ [5, "36281639231", 17, "10101111010101111001001101101110011001010111101010100001011101001101100100001011001101101100101"],
+ [6, "1280395", 17, "101001001100010010001101010000100101110110001010101"],
+ [1, "y62.r,>40034(64[x86", 17, "1101001000011011011110110011101001100111001010011001110100100111101011001110011011011000101110111101100010100010010011000101111011101100100111010001100100110011101001100100111011100011010111100100101011101111011110100100100010001101100011101011"],
+ [2, "G", 17, "100101101101010101001101101010100110110100101101101"],
+ [7, "6355553723020067", 25, "101010001110001110101011100010111000101011100010111000101011101110101000100010001110001010111010100011101110100010101110001110001010111011101000100011101"],
+ [8, "A4$9$-84+B", 17, "1011001001010110100101011001010110100101010110010101010011010100110101010110100101011011011010010010110"],
+ [3, "380795213128", 17, "10101101110001101001000100101110111001001001101010110011010000101100110110110010010001100110101"],
+ [4, "6764839", 17, "1010101111011101101011110100011010101001000100001011101001110100101"],
+ [5, "57862780506", 17, "10101100010111011011011101011110010011011101101010100100011100101001110111001010100001001000101"],
+ [6, "1284836", 17, "101001001101101110011101000100101111010000101010101"],
+ [1, "5BP41M1Q.3", 17, "1101001000011011100100100010110001110111011011001001110100111001101011101100010011100110110100011101001100111011001011100111010110001100011101011"],
+ [2, "/NF2", 17, "100101101101010010010100101010110100110101101100101010110010101101100101011010100101101101"],
+ [7, "845963608089", 25, "101011101010001110100011101000111010001010001110001110101010111011100010001011101010001110001011101000101110001011101"],
+ [8, "D6$88//41C", 17, "10100110010100101011010110010101001101010100110101011011010110110110101101011010010101011001010100100110"],
+ [3, "762561922265", 17, "10101011110011011011000100001010011001001011101010110110011011001101100101000010011101001110101"],
+ [4, "9615253", 17, "1010001011010111100110010110001010101101100100111010000101110100101"],
+ [5, "48264198556", 17, "10101000110110111001001101011110100011001100101010111010010010001001110100111010100001101100101"],
+ [6, "0286351", 17, "101001101101101110000101011110101110010011001010101"],
+ [1, "/80nYM", 17, "11010010000101110011001110100110010011101100110000101001110110100010111011000110111000101100011101011"],
+ [2, "NTZ", 17, "10010110110101010110100110101011011001010011011010101101001010110100101101101"],
+ [7, "0611286273", 25, "101010100011100011101011100010101011100010001110101000111010111000111010100010001000101110111011101"],
+ [8, "D24:4++3$:6:7B", 17, "101001100101010010110101101001011010110110101101001010110110110101101101101100101010101100101011010110110100101011011010110110100101101010010010110"],
+ [3, "867786950060", 17, "10101011110010001011101100010010000101000101101010100111011100101110010101000011100101110010101"],
+ [4, "4001759", 17, "1010100011000110100011010011001010101000100100111011101001011100101"],
+ [5, "02736375670", 17, "10100011010010011011101101111010101111011110101010100010010011101010000100010011100101101100101"],
+ [6, "0945268", 17, "101001011101000110111001001001101011110001001010101"],
+ [1, "80", 17, "1101001110010100111100100100111101100011101011"],
+ [2, "B22W-2", 25, "1001011011010101101001011010110010101101011001010110110011010101010010101101101011001010110100101101101"],
+ [7, "553661", 25, "101011100010111000101011101110001000101010001110111010100011101"],
+ [8, "B58429+-1$D", 17, "10010010110110101001010011010101011010010101001011011010010101011011011010100110101010110010101100101010100110010"],
+ [3, "078529030738", 17, "10101110110110111011000100100110001011000110101010100001011100101000100100001010010001110010101"],
+ [4, "7854460", 17, "1010111011011011101100010100011010101011100101000011100101011100101"],
+ [5, "04596111625", 17, "10100011010100011011000100010110101111001100101010110011011001101010000110110010011101011100101"],
+ [6, "1162246", 17, "101001100100001010010011001101100111010101111010101"],
+ [1, "DEE_55sR4P653910", 17, "110100100001011000100010001101000100011010001010011000011011100100110111001001011110010011000101110110010011101110111011010111011110100101100001101000100011001000100110111001001100011101011"],
+ [2, "MCQIMZ72VOB", 17, "1001011011010110110101001011011010010101010101100110101101001101011011010100101001101101010101001011011010110010101101001101010110110101101001010110100101101001101011010100101101101"],
+ [7, "63460187555009", 25, "101010001110001110101010100011100010111010001011101110100011101010111000100011100010111000101011101011100010001010100011101110001011101"],
+ [8, "C821D", 17, "1010010011010011010101010010110101011001010100110010"],
+ [3, "690596854636", 17, "10100010110100111011100100101110101111011011101010100111010111001010000100001010100001110100101"],
+ [4, "2575091", 17, "1010010011011000101110110110001010101110010111010011001101100110101"],
+ [5, "54909634786", 17, "10101100010100011000101100011010001011010111101010100001010111001000100100100010100001100110101"],
+ [6, "1301838", 17, "101011110100011010110011000100101111010001001010101"],
+ [1, "49(824(2_:){-9", 17, "110100111001101000111010111101110100011001001110100110011001110010110010011101000110010011001110010101001100001110010011011001001000111101101101001101110011100101100110011101001100011101011"],
+ [2, "7S%Z294C", 25, "100101101101010100101101101011010110010101001001001010011011010101011001010110101100101101010100110101101101101001010100101101101"],
+ [7, "70", 25, "101010101000111000111011101"],
+ [8, "B:99:9549//51C", 17, "10010010110110101101101101001010110100101011010110110110100101011010100101011010010110100101011011010110110110101101101010010101011001010100100110"],
+ [3, "789302830511", 17, "10101101110010111011110101001110010011000100101010100001011100101001110110011011001101110100101"],
+ [4, "4693088", 17, "1010100011010111100010110111101010101110010100100010010001110010101"],
+ [5, "42174970532", 17, "10101000110010011001100101110110100011000101101010100010011100101001110100001011011001110010101"],
+ [6, "1704761", 17, "101011101100011010011101001000101011110110011010101"],
+ [1, "9GE26f^44039V!0&", 17, "1101001000011100101100110100010001000110100011001110010110011101001011000010011110001010101110111101000110111010010011000101111011101110010110011101011000110011011001001110110010011001000101001111001100011101011"],
+ [2, "M", 25, "10010110110101101101010010100101101101"],
+ [7, "865176", 25, "101011101000100011101011100010111010100010100010001110111011101"],
+ [8, "A4D", 17, "10110010010101101001010100110010"],
+ [3, "548097674005", 17, "10101000110001001010011100010110111011000010101010100010010111001110010111001010011101110100101"],
+ [4, "6103085", 17, "1010101111001100100011010111101010101110010100100010011101001110101"],
+ [5, "79872221118", 17, "10101110110001011011011101110110010011001001101010110110011001101100110110011010010001010000101"],
+ [6, "0299335", 17, "101001101100010110001011010000101000010110001010101"],
+ [1, "0#~;35y7", 17, "110100100001001110110010010011000100010111101110110010011001011100110111001001101101111011101101110111101011101100011101011"],
+ [2, "4", 25, "10010110110101010011010110100101101101"],
+ [7, "331983305458", 25, "101011100011100010101011101000101000111011100010001011101011101110100010001011101011100010100011100010111010001011101"],
+ [8, "D+8+:3:3A", 17, "101001100101011011011010011010101011011011011010110110110010101011010110110110010101010110010010"],
+ [3, "557988048975", 17, "10101100010010001001011101101110110111010011101010101110010010001110100100010010011101001110101"],
+ [4, "3447860", 17, "1010111101010001101000110111011010101001000101000011100101001000101"],
+ [5, "62094742584", 17, "10101011110010011000110100010110100011011101101010101110011011001001110100100010111001000010101"],
+ [6, "0186399", 17, "101011001101101110000101010000100010110001011010101"],
+ [1, "a6298SMx@40I724", 17, "11010010000100101100001011101111011110001010111101000101011110111011011101000101110110001111001001011000110110110010011101001110110011000100010111011011101011101111011101001100101111011101100011101011"],
+ [2, "V4CZFET", 25, "10010110110101001101010110101001101011011011010010101001101101010101101100101011010110010101010110110010100101101101"],
+ [7, "79", 25, "101010100010111000111011101"],
+ [8, "A19A", 17, "101100100101010110010110100101010110010010"],
+ [3, "597417989799", 17, "10100010110010001001110100110010111011001011101010100100011101001000100111010011101001001000101"],
+ [4, "9196015", 17, "1010001011001100100010110101111010101110010110011010011101000010101"],
+ [5, "14477515835", 17, "10100110010100011010001101110110111011011000101010110011010011101001000100001010011101001000101"],
+ [6, "0826390", 17, "101000100100100110000101011110100010110100111010101"],
+ [1, ":J_65", 17, "110100100001110010011010110111000101001100001011101111010010110000100010111101100011101011"],
+ [2, "2V20LVP5U0", 17, "100101101101010110010101101001101010110101100101011010100110110101011010100110100110101011010110110100101101001101010110010101011010100110110101011010011010100101101101"],
+ [7, "8501", 25, "101011100010100011101010001011101110100011101"],
+ [8, "B1880419.5-4D", 17, "1001001011010101100101001101010100110101010101001101011010010101011001011010010101101101101011010100101010011010101101001010100110010"],
+ [3, "544278148264", 17, "10101000110011101001101101110110110111011001101010101110010010001101100101000010111001000100101"],
+ [4, "1872609", 17, "1010011001011011101110110010011010101010000111001011101001100110101"],
+ [5, "51082469544", 17, "10101100010011001000110101101110010011010001101010101000011101001001110101110010111001001000101"],
+ [6, "0120625", 17, "101011001100100110100111010111100100110111001010101"],
+ [1, "o30127I[LV83902GD", 17, "110100100001000111101010111011110110110110001011001110010111101110111011011101100010001011100011010100011011101110101100010111011110101111001001101111011010111101110110011100101101000100010110001000110111000101100011101011"],
+ [2, "D210 S", 17, "10010110110101010110010110101100101011011010010101101010011011010100110101101010110101100101001001001010100101101101"],
+ [7, "997053", 25, "101010111000101110001010101000111000111011100010001110101011101"],
+ [8, "B.$$-/8//5B", 17, "10010010110110110110101011001010101100101010100110101101101011010011010101101101011011011010110110101001010010010110"],
+ [3, "826391546219", 17, "10100100110000101011110100101110110011011000101010101110010100001101100110011011101001101100101"],
+ [4, "3897820", 17, "1010111101011011100010110111011010101001000110110011100101000010101"],
+ [5, "09462863478", 17, "10100011010001011010001101011110010011011011101010101000010000101011100100010010010001001110101"],
+ [6, "1937632", 17, "101000101101111010010001010111101000010011011010101"],
+ [1, "D5HJ4<295l5wi", 17, "1101001000010110001000110111001001100010100010110111000110010011101110011010011001110010111001011001101110010011001010000110111001001111001010010000110100100110011101100011101011"],
+ [2, "LFWD5QYI", 17, "1001011011010101101010011010110110010101100110101010101011001011011010011010101010101100110110010110101010110100110101001101101010100101101101"],
+ [7, "823127", 25, "101011101000101110100011100011101010100010111010100011100011101"],
+ [8, "A6-+5C", 17, "101100100101001010110101001101010110110110110101001010100100110"],
+ [3, "922372309656", 17, "10100100110011011010000101110110011011011110101010111001011101001010000100111010100001001000101"],
+ [4, "1455795", 17, "1010011001010001101100010110001010101000100111010010011101001000101"],
+ [5, "42634366629", 17, "10101000110010011010111101111010100011011110101010101000010100001010000110110011101001110100101"],
+ [6, "1258894", 17, "101001001101100010001001011011100101110011101010101"],
+ [1, "165672LWG9_Z380z6", 17, "11010011100100111011001110001011010011000010101111011101000110111011101000110110100010001110010110010100110000111011000101100101110011101001100100111011001101111011011001110100110010000101100011101011"],
+ [2, "/YYZ ", 17, "1001011011010100100101001011001011010101100101101010100110110101010011010110101011001011010100101101101"],
+ [7, "6716132785926531", 25, "101010111011101000100011101000100010111011100010001010111010111010100011100011100010100011101010111000101110100010001110111000101011100011101010100011101"],
+ [8, "B88$+603A", 17, "100100101101001101010100110101010110010101011011011010010101101010100110110010101010110010010"],
+ [3, "295961215368", 17, "10100010110110001001011100001010011001001101101010110011010011101000010101000010010001100110101"],
+ [4, "0767811", 17, "1010001101011101101011110111011010101001000110011011001101110010101"],
+ [5, "57010821457", 17, "10101100010111011000110100110010001101011011101010110110011001101011100100111010001001011100101"],
+ [6, "1172939", 17, "101001100100100010011011001011101111010001011010101"],
+ [1, "re584", 17, "110100100001001001111010110010000110111001001011101111010011110100101100001001100011101011"],
+ [2, "T9", 25, "100101101101010101101100101011001011010100101101101"],
+ [7, "399687558022", 25, "101011101110001010001010111000100011101011101010111000100011100010111000101011101010001110001010111000101011100011101"],
+ [8, "A4/D", 17, "1011001001010110100101101101011010100110010"],
+ [3, "267135345194", 17, "10101011110111011011001101000010110001010000101010101110010011101100110111010010111001001000101"],
+ [4, "5254450", 17, "1010110001001001101100010100011010101011100100111011100101000100101"],
+ [5, "02991573147", 17, "10100011010010011000101100010110011001011000101010100010010000101100110101110010001001101100101"],
+ [6, "0719345", 17, "101001000101100110001011011110101000110111001010101"],
+ [1, "DG014R3a>72z%7d&F4H", 17, "1101001000010110001000110100010001001110110010011100110110010011101100010111011001011100100101100001101101100011101101110110011100101101111011010001001100111011011101000010011010011001000100011000101100100111011000101000111011101101100011101011"],
+ [2, "3244BKWIS", 17, "10010110110101101100101010101100101011010100110101101010011010110101101001011011010101001101100110101010101101001101010110101100101001010110110100101101101"],
+ [7, "5620356163", 25, "101011101000111000101010111010001000111011100011101000101010001110111010100010001110001110101011101"],
+ [8, "B+2C", 17, "1001001011010110110110101001011010100100110"]
+ ];
+}
diff --git a/test/golden/fenix7/code.png b/test/golden/fenix7/code.png
new file mode 100644
index 0000000..d42686b
--- /dev/null
+++ b/test/golden/fenix7/code.png
Binary files differ
diff --git a/test/golden/fr255s/code.png b/test/golden/fr255s/code.png
new file mode 100644
index 0000000..fc6989a
--- /dev/null
+++ b/test/golden/fr255s/code.png
Binary files differ
diff --git a/test/golden/fr70/code.png b/test/golden/fr70/code.png
new file mode 100644
index 0000000..4353f5b
--- /dev/null
+++ b/test/golden/fr70/code.png
Binary files differ
diff --git a/test/golden/fr965/code.png b/test/golden/fr965/code.png
new file mode 100644
index 0000000..35d817e
--- /dev/null
+++ b/test/golden/fr965/code.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/code.png b/test/golden/instinct3amoled45mm/code.png
new file mode 100644
index 0000000..6cabbac
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/code.png
Binary files differ
diff --git a/test/golden/venusq2/code.png b/test/golden/venusq2/code.png
new file mode 100644
index 0000000..42c16a7
--- /dev/null
+++ b/test/golden/venusq2/code.png
Binary files differ
diff --git a/test/golden/venux1/code.png b/test/golden/venux1/code.png
new file mode 100644
index 0000000..bcfd63e
--- /dev/null
+++ b/test/golden/venux1/code.png
Binary files differ
diff --git a/tools/barcode_ref.py b/tools/barcode_ref.py
new file mode 100644
index 0000000..2e58b5e
--- /dev/null
+++ b/tools/barcode_ref.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# Author: cirakg & claude ai
+# Summary: Shared barcode helpers for the reference tools: zxing-cpp module rows, rendering, decoding, check digits.
+"""Helpers shared by gen_code_vectors.py and check_codes.py.
+
+Symbology numbers match source/codes/Codes.mc. zxing-cpp is both the
+reference encoder (module rows) and the decoder (README 12.4).
+"""
+
+from __future__ import annotations
+
+from typing import Any, cast
+
+import zxingcpp
+from PIL import Image
+
+QR, CODE128, CODE39, EAN13, EAN8, UPCA, UPCE, ITF, CODABAR = range(9)
+FORMATS = {
+ CODE128: zxingcpp.BarcodeFormat.Code128,
+ CODE39: zxingcpp.BarcodeFormat.Code39,
+ EAN13: zxingcpp.BarcodeFormat.EAN13,
+ EAN8: zxingcpp.BarcodeFormat.EAN8,
+ UPCA: zxingcpp.BarcodeFormat.UPCA,
+ UPCE: zxingcpp.BarcodeFormat.UPCE,
+ ITF: zxingcpp.BarcodeFormat.ITF,
+ CODABAR: zxingcpp.BarcodeFormat.Codabar,
+}
+CODE39_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%"
+LINEAR_QUIET_MODULES = 10 # README 12.2
+
+
+def reference_modules(symbology: int, text: str) -> str:
+ """zxing-cpp's module row for text, without quiet zones, as '0'/'1'."""
+ barcode = zxingcpp.create_barcode(text, FORMATS[symbology])
+ image = barcode.to_image(scale=1, add_hrt=False, add_quiet_zones=False)
+ # zxing-cpp images support the buffer protocol; its type stubs don't say so.
+ row = bytes(memoryview(cast(Any, image)))[: image.shape[1]]
+ return "".join("1" if v < 128 else "0" for v in row)
+
+
+def render_linear(modules: str, scale: int, height: int = 40) -> Image.Image:
+ """Black bars on white with the quiet zone, `scale` pixels per module."""
+ quiet = LINEAR_QUIET_MODULES * scale
+ image = Image.new("L", (len(modules) * scale + 2 * quiet, height), 255)
+ for i, module in enumerate(modules):
+ if module == "1":
+ image.paste(0, (quiet + i * scale, 0, quiet + (i + 1) * scale, height))
+ return image
+
+
+# What to read each symbology as. Code 39 is read as standard Code 39: by
+# default zxing-cpp also tries full-ASCII (extended) Code 39, which turns
+# pairs like "+S" into "s" (the app doesn't encode extended Code 39).
+READ_FORMATS = {**FORMATS, CODE39: zxingcpp.BarcodeFormat.Code39Std, UPCA: zxingcpp.BarcodeFormat.EAN13}
+
+
+def decode(image: Image.Image, symbology: int | None = None) -> list[str]:
+ """Texts zxing-cpp reads in image (only as `symbology` when given)."""
+ if symbology is None:
+ return [result.text for result in zxingcpp.read_barcodes(image)]
+ # One format works as a format set at runtime; the stubs only allow the set type.
+ formats = cast(zxingcpp.BarcodeFormats, READ_FORMATS[symbology])
+ return [result.text for result in zxingcpp.read_barcodes(image, formats=formats)]
+
+
+def gs1_check(digits: str) -> int:
+ """GS1 mod-10 check digit of the data digits."""
+ total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(digits)))
+ return (10 - total % 10) % 10
+
+
+def code39_check(text: str) -> str:
+ return CODE39_ALPHABET[sum(CODE39_ALPHABET.index(c) for c in text) % 43]
+
+
+def upce_expand(code: str) -> str:
+ """UPC-E 'NS dddddd C' (8 digits) as the 13-digit EAN form zxing reads."""
+ ns, d, check = code[0], code[1:7], code[7]
+ last = d[5]
+ if last in "012":
+ body = d[0:2] + last + "0000" + d[2:5]
+ elif last == "3":
+ body = d[0:3] + "00000" + d[3:5]
+ elif last == "4":
+ body = d[0:4] + "00000" + d[4]
+ else:
+ body = d[0:5] + "0000" + last
+ return "0" + ns + body + check
+
+
+def expected_reading(symbology: int, readable: str) -> str:
+ """What zxing-cpp reports when it decodes a code whose human text is `readable`."""
+ if symbology == UPCA:
+ return "0" + readable
+ if symbology == UPCE:
+ return upce_expand(readable)
+ return readable
diff --git a/tools/check_codes.py b/tools/check_codes.py
new file mode 100644
index 0000000..22d3033
--- /dev/null
+++ b/tools/check_codes.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+# Author: cirakg & claude ai
+# Summary: Decode the barcodes the watch's encoders produced in a unit-test run with zxing-cpp (README 12.4 check 2).
+"""Decode-check the CODEDUMP lines of a Monkey C test log.
+
+Each line is "CODEDUMP <symbology> <readable text as hex> <modules>". Every
+code is rendered with its quiet zone at 1, 2 and 3 pixels per module and
+must decode to the expected text at each size.
+
+Usage: check_codes.py LOG [LOG ...] Exit: 0 all decoded, 1 a failure, 2 no dump found.
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+import barcode_ref as ref
+
+DUMP = re.compile(r"CODEDUMP (\d+) ([0-9a-f]*) ([01]+)")
+SCALES = (1, 2, 3)
+
+
+def check_log(path: Path) -> tuple[int, list[str]]:
+ """(codes checked, failure messages) for one log."""
+ checked = 0
+ failures: list[str] = []
+ for match in DUMP.finditer(path.read_text(encoding="utf-8", errors="replace")):
+ symbology = int(match.group(1))
+ text = bytes.fromhex(match.group(2)).decode("utf-8")
+ modules = match.group(3)
+ expected = ref.expected_reading(symbology, text)
+ for scale in SCALES:
+ got = ref.decode(ref.render_linear(modules, scale), symbology)
+ if expected not in got:
+ failures.append(f"symbology {symbology} '{text}' at {scale} px/module: read {got}")
+ checked += 1
+ return checked, failures
+
+
+def main(argv: list[str] | None = None) -> int:
+ paths = [Path(p) for p in (argv if argv is not None else sys.argv[1:])]
+ total = 0
+ failed = False
+ for path in paths:
+ checked, failures = check_log(path)
+ total += checked
+ for failure in failures[:10]:
+ print(f"[{path.stem}] {failure}")
+ failed = failed or bool(failures)
+ print(f"[{path.stem}] decoded {checked} barcodes at {len(SCALES)} sizes: {'FAILED' if failures else 'ok'}")
+ if total == 0:
+ print("no CODEDUMP lines found")
+ return 2
+ return 1 if failed else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tools/gen_code_vectors.py b/tools/gen_code_vectors.py
new file mode 100644
index 0000000..970ca06
--- /dev/null
+++ b/tools/gen_code_vectors.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# Author: cirakg & claude ai
+# Summary: Generate test/data/CodeVectors.mc: zxing-cpp reference module rows for the linear barcode encoders.
+"""Reference module rows for README 12.4 check 1 (all fake payloads).
+
+For every linear symbology: fixed edge cases plus seeded random payloads,
+each with the options the watch uses and zxing-cpp's module row. Where the
+watch adds something zxing doesn't (a Code 39 or ITF check character, Codabar
+start/stop characters), the reference is made from the completed text.
+
+ gen_code_vectors.py [--seed 1212] [--count 25] [--out test/data/CodeVectors.mc]
+"""
+
+from __future__ import annotations
+
+import argparse
+import random
+import string
+from pathlib import Path
+
+import barcode_ref as ref
+
+CHECK_ADD, CHECK_FIX, CHECK_OFF = 0, 1, 2
+TEXT_ON = 0x10
+
+
+def options(check: int) -> int:
+ return TEXT_ON | (check << 2) | 1
+
+
+def digits(rng: random.Random, n: int) -> str:
+ return "".join(rng.choice(string.digits) for _ in range(n))
+
+
+def valid_upce(rng: random.Random) -> str:
+ """Six UPC-E digits that are a canonical zero suppression."""
+ while True:
+ d = digits(rng, 6)
+ last = d[5]
+ if (last == "3" and d[2] in "012") or (last == "4" and d[3] == "0") or (last in "56789" and d[4] == "0"):
+ continue
+ return d
+
+
+def cases(rng: random.Random, count: int) -> list[tuple[int, str, int, str]]:
+ """(symbology, payload, options, zxing text) tuples."""
+ out: list[tuple[int, str, int, str]] = []
+ fixed = [
+ (ref.EAN13, "590123412345", options(CHECK_ADD), "590123412345"),
+ (ref.EAN13, "5901234123457", options(CHECK_ADD), "5901234123457"),
+ (ref.EAN8, "1234567", options(CHECK_ADD), "1234567"),
+ (ref.UPCA, "01234567890", options(CHECK_ADD), "01234567890"),
+ (ref.UPCE, "0425261", options(CHECK_ADD), "0425261"),
+ (ref.UPCE, "1234565", options(CHECK_ADD), "1234565"),
+ (ref.CODE128, "1234", options(CHECK_ADD), "1234"),
+ (ref.CODE128, "123456789", options(CHECK_ADD), "123456789"),
+ (ref.CODE128, "AB12345678", options(CHECK_ADD), "AB12345678"),
+ (ref.CODE128, "A1234B12345", options(CHECK_ADD), "A1234B12345"),
+ (ref.CODE128, " ~", options(CHECK_ADD), " ~"),
+ (ref.CODE39, "CODE 39", options(CHECK_OFF), "CODE 39"),
+ (ref.CODE39, "CODE-39", options(CHECK_ADD), "CODE-39" + ref.code39_check("CODE-39")),
+ (ref.ITF, "1234", options(CHECK_OFF), "1234"),
+ (ref.ITF, "123", options(CHECK_ADD), "123" + str(ref.gs1_check("123"))),
+ (ref.CODABAR, "A40156B", options(CHECK_ADD), "A40156B"),
+ (ref.CODABAR, "31117013206375", options(CHECK_ADD), "A31117013206375A"),
+ ]
+ out.extend(fixed)
+ printable = string.ascii_letters + string.digits + " !#$%&()*+,-./:;<=>?@[]^_{|}~"
+ for _ in range(count):
+ out.append((ref.EAN13, digits(rng, 12), options(CHECK_ADD), ""))
+ out.append((ref.EAN8, digits(rng, 7), options(CHECK_ADD), ""))
+ out.append((ref.UPCA, digits(rng, 11), options(CHECK_ADD), ""))
+ out.append((ref.UPCE, rng.choice("01") + valid_upce(rng), options(CHECK_ADD), ""))
+ n = rng.randint(1, 20)
+ mixed = "".join(rng.choice(string.digits) if rng.random() < 0.5 else rng.choice(printable) for _ in range(n))
+ out.append((ref.CODE128, mixed, options(CHECK_ADD), mixed))
+ c39 = "".join(rng.choice(ref.CODE39_ALPHABET) for _ in range(rng.randint(1, 12)))
+ with_check = rng.random() < 0.5
+ out.append(
+ (
+ ref.CODE39,
+ c39,
+ options(CHECK_ADD if with_check else CHECK_OFF),
+ c39 + ref.code39_check(c39) if with_check else c39,
+ )
+ )
+ itf = digits(rng, 2 * rng.randint(1, 8))
+ out.append((ref.ITF, itf, options(CHECK_OFF), itf))
+ inner = "".join(rng.choice("0123456789-$:/.+") for _ in range(rng.randint(1, 12)))
+ guard = rng.choice("ABCD") + inner + rng.choice("ABCD")
+ out.append((ref.CODABAR, guard, options(CHECK_ADD), guard))
+ return out
+
+
+def render(rows: list[tuple[int, str, int, str]], seed: int) -> str:
+ lines = []
+ for symbology, payload, opts, text in rows:
+ modules = ref.reference_modules(symbology, text or payload)
+ escaped = payload.replace("\\", "\\\\").replace('"', '\\"')
+ lines.append(f' [{symbology}, "{escaped}", {opts}, "{modules}"]')
+ body = ",\n".join(lines)
+ return (
+ "// Author: cirakg & claude ai\n"
+ "// Summary: zxing-cpp reference module rows for the linear barcode encoders (generated; do not edit).\n"
+ f"//\n// Generated by tools/gen_code_vectors.py, seed {seed}. Rows: [symbology, payload, options,\n"
+ "// expected modules without quiet zones]. Fake payloads only.\n\n"
+ "import Toybox.Lang;\n\n(:test)\nmodule CodeVectors {\n const LINEAR = [\n" + body + "\n ];\n}\n"
+ )
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
+ parser.add_argument("--seed", type=int, default=1212)
+ parser.add_argument("--count", type=int, default=25)
+ parser.add_argument("--out", type=Path, default=Path("test/data/CodeVectors.mc"))
+ args = parser.parse_args(argv)
+ rows = cases(random.Random(args.seed), args.count)
+ args.out.write_text(render(rows, args.seed), encoding="utf-8")
+ print(f"wrote {len(rows)} reference rows to {args.out}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tools/tests/test_barcode_tools.py b/tools/tests/test_barcode_tools.py
new file mode 100644
index 0000000..7be1f18
--- /dev/null
+++ b/tools/tests/test_barcode_tools.py
@@ -0,0 +1,65 @@
+# Author: cirakg & claude ai
+# Summary: Tests for the barcode reference tools: zxing-cpp rows, rendering, decoding, check digits, generator, checker.
+from __future__ import annotations
+
+import random
+from pathlib import Path
+
+import barcode_ref as ref
+import check_codes
+import gen_code_vectors
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def test_reference_rows_have_the_standard_lengths() -> None:
+ assert ref.reference_modules(ref.EAN13, "590123412345") == (
+ "10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101"
+ )
+ assert len(ref.reference_modules(ref.EAN8, "1234567")) == 67
+ assert len(ref.reference_modules(ref.UPCE, "0425261")) == 51
+
+
+def test_rendered_codes_decode() -> None:
+ modules = ref.reference_modules(ref.CODE39, "ABC")
+ for scale in (1, 2, 3):
+ assert ref.decode(ref.render_linear(modules, scale), ref.CODE39) == ["ABC"]
+
+
+def test_check_digits() -> None:
+ assert ref.gs1_check("590123412345") == 7
+ assert ref.gs1_check("1234567") == 0
+ assert ref.code39_check("CODE 39") == "R" # (12+24+13+14+38+3+9) % 43 = 27
+
+
+def test_expected_reading_normalises_upc() -> None:
+ assert ref.expected_reading(ref.UPCA, "012345678905") == "0012345678905"
+ assert ref.upce_expand("01234565") == "0012345000065"
+
+
+def test_generator_only_makes_canonical_upce() -> None:
+ rng = random.Random(5)
+ for _ in range(200):
+ d = gen_code_vectors.valid_upce(rng)
+ ref.reference_modules(ref.UPCE, "0" + d) # zxing-cpp rejects non-canonical ones
+
+
+def test_committed_vectors_match_the_generator() -> None:
+ rows = gen_code_vectors.cases(random.Random(1212), 25)
+ committed = (ROOT / "test" / "data" / "CodeVectors.mc").read_text(encoding="utf-8")
+ assert committed == gen_code_vectors.render(rows, 1212)
+
+
+def test_check_codes_reads_dump_lines(tmp_path: Path) -> None:
+ good = ref.reference_modules(ref.EAN8, "1234567")
+ log = tmp_path / "dev.log"
+ log.write_text(
+ f"CODEDUMP {ref.EAN8} {b'12345670'.hex()} {good}\nCODEDUMP {ref.EAN8} {b'12345670'.hex()} {good[:-5]}00000\n",
+ encoding="utf-8",
+ )
+ checked, failures = check_codes.check_log(log)
+ assert checked == 2 and len(failures) == 3 # the damaged code fails at all three sizes
+ assert check_codes.main([str(log)]) == 1
+ empty = tmp_path / "empty.log"
+ empty.write_text("nothing\n", encoding="utf-8")
+ assert check_codes.main([str(empty)]) == 2
diff --git a/tools/ui_flow.py b/tools/ui_flow.py
index 3e82231..82e5d65 100644
--- a/tools/ui_flow.py
+++ b/tools/ui_flow.py
@@ -23,7 +23,8 @@ The flow covers: the results of an Aegis import sent through the settings
form (the fixture sets the form, as the phone would), the list, a TOTP entry, a HOTP entry and its next code, the
PIN screen, a wrong PIN, a correct PIN opening a locked entry, auto-lock
closing it, the menu, the About screen (watches with buttons; touch-only
-menus have no focus to move) and the lockout after 5 wrong PINs.
+menus have no focus to move), the lockout after 5 wrong PINs, and an EAN-13
+code whose screenshot must decode with zxing-cpp (README 12.4).
Each screenshot is compared with test/golden/<device>/<screen>.png like
sim_screenshot.py does (or saved there with --approve).
@@ -43,6 +44,7 @@ from dataclasses import dataclass
from pathlib import Path
import sim_screenshot as shots
+import zxingcpp
EXIT_OK, EXIT_DIFFERENT, EXIT_FLOW = 0, 1, 2
SCREEN_TIMEOUT_S = 20.0
@@ -57,6 +59,7 @@ FIXTURE_PIN = "123456" # source/app/Fixtures.mc
WRONG_PIN = "000000"
FAILURES_UNTIL_WAIT = 5 # source/storage/Lockout.mc: the 5th failure starts a delay
SELECTOR_OK = 10 # source/ui/PinView.mc: selector positions 0-9, then OK
+GYM_CARD = "5901234123457" # source/app/Fixtures.mc: EAN-13 with its check digit
class FlowError(Exception):
@@ -295,7 +298,7 @@ class Flow:
"""Call before an input whose result `expect` should wait for."""
self.log.set_mark()
- def shot(self, name: str) -> None:
+ def shot(self, name: str, expect_code: str | None = None) -> None:
raw = self.sim.capture(name)
out = self.out / f"{name}.png"
label = f"{self.sim.device_dir.name}/{name}"
@@ -305,6 +308,14 @@ class Flow:
print(f"[{label}] {err}")
self.results[name] = EXIT_DIFFERENT
return
+ if expect_code is not None:
+ # README 12.4 check 3: the code on the screenshot must decode.
+ read = [r.text for r in zxingcpp.read_barcodes(screen)]
+ if expect_code not in read:
+ print(f"[{label}] the barcode didn't decode to {expect_code!r}: read {read}")
+ self.results[name] = EXIT_DIFFERENT
+ return
+ print(f"[{label}] barcode decodes to {expect_code!r}")
golden = self.golden / f"{name}.png"
if self.approve:
shots.save_png(screen, golden, f"Approved simulator screen '{name}' of {self.sim.device_dir.name}")
@@ -414,6 +425,19 @@ def flow_lockout(f: Flow) -> None:
f.shot("pin_wait")
+def flow_code(f: Flow) -> None:
+ """The EAN-13 gym card (index 6): its screenshot must decode."""
+ f.act()
+ f.back() # leave the waiting PIN screen
+ f.expect("list")
+ for _ in range(4): # Bank (2) to the gym card (6)
+ f.next()
+ f.act()
+ f.open_focused_row()
+ f.expect("code")
+ f.shot("code", expect_code=GYM_CARD)
+
+
def run_flow(f: Flow) -> None:
"""The scripted session; see the module docstring."""
flow_import(f)
@@ -421,6 +445,7 @@ def run_flow(f: Flow) -> None:
flow_locked_entry(f)
flow_menu(f)
flow_lockout(f)
+ flow_code(f)
def find_key(data: object, key: str) -> object: