summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--source/codes/Aztec.mc419
-rw-r--r--source/codes/Codes.mc279
-rw-r--r--source/codes/DataMatrix.mc298
-rw-r--r--source/codes/Pdf417.mc286
-rw-r--r--source/codes/Qr.mc971
-rw-r--r--source/codes/ReedSolomon.mc205
-rw-r--r--source/settings/CodeForm.mc20
-rw-r--r--source/ui/CodeView.mc301
-rw-r--r--source/util/Jobs.mc32
-rw-r--r--test/codes/CodeCacheTest.mc58
-rw-r--r--test/codes/QrTest.mc124
-rw-r--r--tools/gen_products.py23
-rw-r--r--tools/tests/test_gen_products.py23
13 files changed, 2262 insertions, 777 deletions
diff --git a/source/codes/Aztec.mc b/source/codes/Aztec.mc
index 29b6c95..495abbf 100644
--- a/source/codes/Aztec.mc
+++ b/source/codes/Aztec.mc
@@ -36,8 +36,17 @@ module Aztec {
// Error-correction percentage per option: Low, Medium (default), Quartile, High.
const EC_PERCENT = [10, 23, 36, 50];
const QUIET = 2;
- const RS_SYMBOLS_PER_STEP = 40;
- const PAIRS_PER_STEP = 600;
+ // Work per step: each was checked in the simulator by running two
+ // steps per event on the Forerunner 255S (120,000 bytecodes per event)
+ // with a 1,400-byte payload (32 layers, GF(4096), 830 EC codewords).
+ const BYTES_PER_STEP = 200; // high-level encoding
+ const STUFF_BITS_PER_STEP = 1000; // bit stuffing
+ const FIELD_ENTRIES_PER_STEP = 500; // GF(1024) / GF(4096) tables
+ const GENERATOR_WORK_PER_STEP = 500; // generator coefficient updates
+ const ECC_WORK_PER_STEP = 400; // data symbols x EC symbols
+ const GRID_LINES_PER_STEP = 4; // reference grid
+ const WORDS_PER_STEP = 50; // codewords into bits
+ const PAIRS_PER_STEP = 250; // spiral bit pairs
// Text modes.
const UPPER = 0;
@@ -88,57 +97,90 @@ module Aztec {
// The data bits of text (see step 1).
function encodeBits(data as ByteArray) as ByteArray {
- var out = new Bits();
- var mode = UPPER;
- var i = 0;
- var n = data.size();
- while (i < n) {
- var c = classOf(data[i]);
- if (c == -1) {
- // A space: in the current text mode if it has one.
- out.put(1, mode == DIGIT ? 4 : 5);
- i++;
- continue;
- }
- if (c == BINARY) {
- // Binary Shift needs Upper or Lower; from Digit latch to Upper.
- if (mode == DIGIT) {
- out.put(14, 4);
- mode = UPPER;
+ var encoder = new BitEncoder(data);
+ while (!encoder.step(data.size() + 1)) {
+ }
+ return encoder.out.bits;
+ }
+
+ // encodeBits() in steps of about `bytes` input bytes. A Binary Shift
+ // run's length is written first, then its bytes, across steps.
+ class BitEncoder {
+ var out as Bits = new Bits();
+
+ private var _data as ByteArray;
+ private var _i as Number = 0;
+ private var _mode as Number = UPPER;
+ private var _runEnd as Number = -1;
+
+ function initialize(data as ByteArray) {
+ _data = data;
+ }
+
+ // True when every byte is encoded.
+ function step(bytes as Number) as Boolean {
+ var data = _data;
+ var n = data.size();
+ var budget = bytes;
+ while (budget > 0 && _i < n) {
+ if (_runEnd >= 0) {
+ var stop = _i + budget < _runEnd ? _i + budget : _runEnd;
+ budget -= stop - _i;
+ for (var k = _i; k < stop; k++) {
+ out.put(data[k], 8);
+ }
+ _i = stop;
+ if (_i >= _runEnd) {
+ _runEnd = -1;
+ }
+ continue;
}
- var end = i;
- while (end < n && end - i < 2078 && classOf(data[end]) == BINARY) {
- end++;
+ budget--;
+ var c = classOf(data[_i]);
+ if (c == -1) {
+ // A space: in the current text mode if it has one.
+ out.put(1, _mode == DIGIT ? 4 : 5);
+ _i++;
+ continue;
}
- var length = end - i;
- out.put(31, 5);
- if (length <= 31) {
- out.put(length, 5);
- } else {
- out.put(0, 5);
- out.put(length - 31, 11);
+ if (c == BINARY) {
+ // Binary Shift needs Upper or Lower; from Digit latch to Upper.
+ if (_mode == DIGIT) {
+ out.put(14, 4);
+ _mode = UPPER;
+ }
+ var end = _i;
+ while (end < n && end - _i < 2078 && classOf(data[end]) == BINARY) {
+ end++;
+ }
+ var length = end - _i;
+ budget -= length / 4; // the scan
+ out.put(31, 5);
+ if (length <= 31) {
+ out.put(length, 5);
+ } else {
+ out.put(0, 5);
+ out.put(length - 31, 11);
+ }
+ _runEnd = end;
+ continue;
}
- for (var k = i; k < end; k++) {
- out.put(data[k], 8);
+ if (c != _mode) {
+ latch(out, _mode, c);
+ _mode = c;
}
- i = end;
- continue;
- }
- if (c != mode) {
- latch(out, mode, c);
- mode = c;
- }
- var b = data[i];
- if (mode == UPPER) {
- out.put(b - 0x41 + 2, 5);
- } else if (mode == LOWER) {
- out.put(b - 0x61 + 2, 5);
- } else {
- out.put(b - 0x30 + 2, 4);
+ var b = data[_i];
+ if (_mode == UPPER) {
+ out.put(b - 0x41 + 2, 5);
+ } else if (_mode == LOWER) {
+ out.put(b - 0x61 + 2, 5);
+ } else {
+ out.put(b - 0x30 + 2, 4);
+ }
+ _i++;
}
- i++;
+ return _i >= n && _runEnd < 0;
}
- return out.bits;
}
// Latch codes between the text modes (ISO 24778 table 2).
@@ -164,41 +206,72 @@ module Aztec {
// size - 1 bits are all equal gets the opposite last bit; the final
// partial codeword is padded with ones (its last bit 0 if all ones).
function stuff(bits as ByteArray, size as Number) as Array<Number> {
- var out = [] as Array<Number>;
- var value = 0;
- var count = 0;
- var i = 0;
- while (i < bits.size()) {
- if (count == size - 1) {
- var allZero = value == 0;
- var allOne = value == (1 << (size - 1)) - 1;
- if (allZero || allOne) {
- value = (value << 1) | (allZero ? 1 : 0);
+ var stuffer = new Stuffer(bits, size);
+ stuffer.step(bits.size());
+ return stuffer.out;
+ }
+
+ // stuff() in steps of `count` input bits.
+ class Stuffer {
+ var out as Array<Number> = [] as Array<Number>;
+ var size as Number;
+
+ private var _bits as ByteArray;
+ private var _value as Number = 0;
+ private var _count as Number = 0;
+ private var _i as Number = 0;
+
+ function initialize(bits as ByteArray, codewordBits as Number) {
+ _bits = bits;
+ size = codewordBits;
+ }
+
+ // True when every bit is in (and the last codeword padded).
+ function step(count as Number) as Boolean {
+ var bits = _bits;
+ var stop = _i + count < bits.size() ? _i + count : bits.size();
+ var value = _value;
+ var have = _count;
+ var i = _i;
+ while (i < stop) {
+ if (have == size - 1) {
+ var allZero = value == 0;
+ var allOne = value == (1 << (size - 1)) - 1;
+ if (allZero || allOne) {
+ value = (value << 1) | (allZero ? 1 : 0);
+ out.add(value);
+ value = 0;
+ have = 0;
+ continue;
+ }
+ }
+ value = (value << 1) | bits[i];
+ have++;
+ i++;
+ if (have == size) {
out.add(value);
value = 0;
- count = 0;
- continue;
+ have = 0;
}
}
- value = (value << 1) | bits[i];
- count++;
- i++;
- if (count == size) {
- out.add(value);
- value = 0;
- count = 0;
- }
- }
- if (count > 0) {
- for (var k = count; k < size; k++) {
- value = (value << 1) | 1;
+ _value = value;
+ _count = have;
+ _i = i;
+ if (i < bits.size()) {
+ return false;
}
- if (value == (1 << size) - 1) {
- value = value - 1;
+ if (have > 0) {
+ for (var k = have; k < size; k++) {
+ value = (value << 1) | 1;
+ }
+ if (value == (1 << size) - 1) {
+ value = value - 1;
+ }
+ out.add(value);
+ _count = 0;
}
- out.add(value);
+ return true;
}
- return out;
}
// Encode synchronously (tests).
@@ -217,8 +290,13 @@ module Aztec {
private var _phase as Number = 0;
private var _words as Array<Number> = [] as Array<Number>;
private var _dataCount as Number = 0;
- private var _rem as Array<Number> = [] as Array<Number>;
private var _field as ReedSolomon.Field? = null;
+ private var _encoder as ReedSolomon.Encoder? = null;
+ private var _bitEncoder as BitEncoder? = null;
+ private var _stuffer as Stuffer? = null;
+ private var _stuffed as Dictionary<Number, Array<Number>> = {} as Dictionary<Number, Array<Number>>;
+ private var _configIndex as Number = 0;
+ private var _gridAt as Number = 0;
private var _at as Number = 0;
private var _m as ByteArray = []b;
private var _result as Codes.Matrix? = null;
@@ -237,7 +315,7 @@ module Aztec {
}
function percent() as Number {
- return _phase * 25;
+ return _phase * 100 / 6;
}
function matrix() as Codes.Matrix? {
@@ -246,65 +324,109 @@ module Aztec {
function step(budget as Number) as Boolean {
if (_phase == 0) {
- prepare();
+ encodeStep();
} else if (_phase == 1) {
- eccStep();
+ chooseStep();
} else if (_phase == 2) {
+ fieldStep();
+ } else if (_phase == 3) {
+ eccStep();
+ } else if (_phase == 4) {
+ matrixStep();
+ } else if (_phase == 5) {
placeStep();
}
- return _phase >= 3 || error != null;
+ return _phase >= 6 || error != null;
}
- private function prepare() as Void {
+ // Phase 0: the data bits, BYTES_PER_STEP bytes per step.
+ private function encodeStep() as Void {
if (_data.size() == 0) {
error = "Aztec needs some text";
return;
}
- var bits = encodeBits(_data);
- for (var i = 0; i < CONFIGS.size() && config.size() == 0; i++) {
- var c = CONFIGS[i] as Array<Number>;
- var words = stuff(bits, c[4]);
+ var encoder = _bitEncoder;
+ if (encoder == null) {
+ encoder = new BitEncoder(_data);
+ _bitEncoder = encoder;
+ }
+ if (!encoder.step(BYTES_PER_STEP)) {
+ return;
+ }
+ _bits = encoder.out.bits;
+ _bitEncoder = null;
+ _phase = 1;
+ }
+
+ // Phase 1: the first configuration that holds the data. A size the
+ // bits can't fit even unstuffed is skipped without stuffing; the
+ // stuffed codewords are computed once per codeword size, in steps.
+ private function chooseStep() as Void {
+ while (_configIndex < CONFIGS.size()) {
+ var c = CONFIGS[_configIndex] as Array<Number>;
+ var lowerBound = (_bits.size() + c[4] - 1) / c[4];
+ if ((lowerBound + 3) * 100 >= c[3] * (100 - _ec)) {
+ _configIndex++;
+ continue;
+ }
+ var words = _stuffed[c[4]];
+ if (words == null) {
+ var stuffer = _stuffer;
+ if (stuffer == null || stuffer.size != c[4]) {
+ stuffer = new Stuffer(_bits, c[4]);
+ _stuffer = stuffer;
+ }
+ if (!stuffer.step(STUFF_BITS_PER_STEP)) {
+ return;
+ }
+ words = stuffer.out;
+ _stuffed[c[4]] = words;
+ _stuffer = null;
+ }
if ((words.size() + 3) * 100 < c[3] * (100 - _ec)) {
config = c;
_words = words;
+ break;
}
+ _configIndex++;
}
+ _stuffed = {} as Dictionary<Number, Array<Number>>;
if (config.size() == 0) {
error = "too long for an Aztec code";
return;
}
_dataCount = _words.size();
- var ecCount = config[3] - _dataCount;
_field = new ReedSolomon.Field(polynomial(config[4]), 1 << config[4], 1);
- _rem = new [ecCount] as Array<Number>;
- for (var k = 0; k < ecCount; k++) {
- _rem[k] = 0;
- }
- _at = 0;
- _phase = 1;
+ _phase = 2;
}
- private function eccStep() as Void {
+ // Phase 2: GF(1024) and GF(4096) tables and the generator, in steps.
+ private function fieldStep() as Void {
var f = _field as ReedSolomon.Field;
- var n = _rem.size();
- var g = f.generator(n);
- var end = _at + RS_SYMBOLS_PER_STEP < _dataCount ? _at + RS_SYMBOLS_PER_STEP : _dataCount;
- for (var i = _at; i < end; i++) {
- var factor = _words[i] ^ _rem[0];
- for (var k = 0; k < n - 1; k++) {
- _rem[k] = _rem[k + 1] ^ f.multiply(g[k], factor);
- }
- _rem[n - 1] = f.multiply(g[n - 1], factor);
+ if (!f.grow(FIELD_ENTRIES_PER_STEP)) {
+ return;
+ }
+ var n = config[3] - _dataCount;
+ if (!f.buildGenerator(n, GENERATOR_WORK_PER_STEP)) {
+ return;
}
- _at = end;
- if (_at < _dataCount) {
+ _encoder = new ReedSolomon.Encoder(f, _words, n);
+ _phase = 3;
+ }
+
+ // Phase 3: Reed-Solomon, ECC_WORK_PER_STEP data symbols times EC
+ // symbols per step (at least one data symbol).
+ private function eccStep() as Void {
+ var encoder = _encoder as ReedSolomon.Encoder;
+ var n = encoder.remainder.size();
+ if (!encoder.step(ECC_WORK_PER_STEP / n > 0 ? ECC_WORK_PER_STEP / n : 1)) {
return;
}
- _words.addAll(_rem);
- _rem = [] as Array<Number>;
+ _words.addAll(encoder.remainder);
+ _encoder = null;
_field = null;
- startMatrix();
- _phase = 2;
+ _at = -1;
+ _phase = 4;
}
private function setDark(x as Number, y as Number, dark as Number) as Void {
@@ -312,55 +434,86 @@ module Aztec {
_m[y * size + x] = dark;
}
- // Finder, orientation marks and reference grid; then the spiral state.
- private function startMatrix() as Void {
+ // Phase 4: finder and orientation marks (one step), the reference
+ // grid of full-range symbols (GRID_LINES_PER_STEP lines per step),
+ // the codeword bits (WORDS_PER_STEP codewords per step); then the
+ // spiral state.
+ private function matrixStep() as Void {
var size = config[0];
var compact = config[1] == 1;
var c = size / 2;
var r = compact ? 5 : 7;
- _m = new [size * size]b;
- for (var x = -r; x < r; x++) {
- for (var y = -r; y < r; y++) {
- var d = x.abs() > y.abs() ? x.abs() : y.abs();
- setDark(c + x, c + y, (d + 1) % 2);
- }
+ if (_at == -1) {
+ drawFinder(size, compact, c, r);
+ _gridAt = 0;
+ _at = compact ? -3 : -2;
+ return;
}
- setDark(c - r, c - r, 1);
- setDark(c - r, c - r + 1, 1);
- setDark(c - r + 1, c - r, 1);
- setDark(c + r, c - r, 1);
- setDark(c + r, c - r + 1, 1);
- setDark(c + r, c + r - 1, 1);
- if (!compact) {
- for (var x = -c; x <= c; x++) {
- for (var y = -c; y <= c; y++) {
+ if (_at == -2) {
+ // Lines at multiples of 16 from the centre, across and down.
+ var half = c / 16;
+ var lines = 2 * half + 1;
+ for (var n = 0; n < GRID_LINES_PER_STEP && _gridAt < 2 * lines; n++) {
+ var v = (_gridAt % lines - half) * 16;
+ var across = _gridAt < lines;
+ for (var t = -c; t <= c; t++) {
+ var x = across ? t : v;
+ var y = across ? v : t;
if (x >= -r && x <= r && y >= -r && y <= r) {
continue;
}
- if (x % 16 == 0 || y % 16 == 0) {
- setDark(c + x, c + y, ((x + y + 1) % 2 + 2) % 2);
- }
+ setDark(c + x, c + y, ((x + y + 1) % 2 + 2) % 2);
}
+ _gridAt++;
}
+ if (_gridAt >= 2 * lines) {
+ _at = -3;
+ }
+ return;
}
// All codeword bits, most significant first, then reversed: the
// spiral starts from the last bit (as the reference does).
var bits = config[4];
- var all = new [_words.size() * bits]b;
- var at = all.size() - 1;
- for (var i = 0; i < _words.size(); i++) {
+ if (_bits.size() != _words.size() * bits) {
+ _bits = new [_words.size() * bits]b;
+ _gridAt = 0;
+ }
+ var all = _bits;
+ var stop = _gridAt + WORDS_PER_STEP < _words.size() ? _gridAt + WORDS_PER_STEP : _words.size();
+ for (var i = _gridAt; i < stop; i++) {
+ var at = all.size() - 1 - i * bits;
for (var k = bits - 1; k >= 0; k--) {
all[at] = (_words[i] >> k) & 1;
at--;
}
}
- _bits = all;
+ _gridAt = stop;
+ if (_gridAt < _words.size()) {
+ return;
+ }
_at = 0;
_num = 2;
_side = 0;
_layer = 0;
_x = c - r;
_y = c - r - 1;
+ _phase = 5;
+ }
+
+ private function drawFinder(size as Number, compact as Boolean, c as Number, r as Number) as Void {
+ _m = new [size * size]b;
+ for (var x = -r; x < r; x++) {
+ for (var y = -r; y < r; y++) {
+ var d = x.abs() > y.abs() ? x.abs() : y.abs();
+ setDark(c + x, c + y, (d + 1) % 2);
+ }
+ }
+ setDark(c - r, c - r, 1);
+ setDark(c - r, c - r + 1, 1);
+ setDark(c - r + 1, c - r, 1);
+ setDark(c + r, c - r, 1);
+ setDark(c + r, c - r + 1, 1);
+ setDark(c + r, c + r - 1, 1);
}
// The data spiral, PAIRS_PER_STEP bit pairs per step (a port of
@@ -460,7 +613,7 @@ module Aztec {
addModeMessage();
_words = [] as Array<Number>;
_result = new Codes.Matrix(size, size, QUIET, _m);
- _phase = 3;
+ _phase = 6;
}
// True on a reference-grid line (Python's % 16 == 0 for any sign).
diff --git a/source/codes/Codes.mc b/source/codes/Codes.mc
index f157b37..247acfa 100644
--- a/source/codes/Codes.mc
+++ b/source/codes/Codes.mc
@@ -76,55 +76,262 @@ module Codes {
return height * rowHeight;
}
- // Compact form for the encrypted cache: width, height, quiet zone,
- // row height (one byte each), then the modules packed 8 per byte
- // (eight per loop round: this runs in one event, under the watchdog).
+ // The cache form (Packed), in one go: tests and small codes. Jobs
+ // use Packer.
function toBytes() as ByteArray {
- var m = modules;
+ var packer = new Packer(self);
+ Jobs.runToCompletion(packer, 1);
+ return packer.result;
+ }
+
+ // A matrix from its cache form, unpacked in one go (tests); the app
+ // draws from Packed through RunFinder instead.
+ static function fromBytes(data as ByteArray) as Matrix? {
+ var packed = Packed.parse(data);
+ if (packed == null) {
+ return null;
+ }
+ var n = packed.width * packed.height;
+ var bits = new [n]b;
+ for (var i = 0; i < n; i++) {
+ bits[i] = packed.moduleAt(i);
+ }
+ var matrix = new Matrix(packed.width, packed.height, packed.quiet, bits);
+ matrix.rowHeight = packed.rowHeight;
+ return matrix;
+ }
+ }
+
+ // A matrix packed 8 modules per byte, as cached (sealed) in the vault:
+ // FORMAT (2) | width (2 bytes) | height (2 bytes) | quiet | rowHeight
+ // | modules, row-major, most significant bit first
+ // Version 0.7 cached one byte of width, too few for wide PDF417
+ // symbols; such a cache doesn't parse, and the code is encoded again.
+ class Packed {
+ static const FORMAT = 2;
+ static const HEADER = 7;
+
+ var width as Number;
+ var height as Number;
+ var quiet as Number;
+ var rowHeight as Number;
+ var bytes as ByteArray;
+
+ function initialize(data as ByteArray) {
+ bytes = data;
+ width = (data[1] << 8) | data[2];
+ height = (data[3] << 8) | data[4];
+ quiet = data[5];
+ rowHeight = data[6];
+ }
+
+ // The cache bytes as a Packed, or null when they are not format 2 or
+ // the size doesn't match.
+ static function parse(data as ByteArray?) as Packed? {
+ if (data == null || data.size() < HEADER || data[0] != FORMAT) {
+ return null;
+ }
+ var p = new Packed(data);
+ var n = p.width * p.height;
+ if (p.width == 0 || p.height == 0 || p.rowHeight == 0 || data.size() != HEADER + (n + 7) / 8) {
+ return null;
+ }
+ return p;
+ }
+
+ function drawnHeight() as Number {
+ return height * rowHeight;
+ }
+
+ // Module i (row-major), 1 = dark.
+ function moduleAt(i as Number) as Number {
+ return (bytes[HEADER + (i >> 3)] >> (7 - (i & 7))) & 1;
+ }
+ }
+
+ // Matrix -> Packed bytes, BYTES_PER_STEP output bytes (8 modules each)
+ // per step.
+ class Packer extends Jobs.Job {
+ static const BYTES_PER_STEP = 300;
+
+ var result as ByteArray = []b;
+
+ private var _matrix as Matrix;
+ private var _i as Number = 0;
+
+ function initialize(matrix as Matrix) {
+ Job.initialize();
+ _matrix = matrix;
+ var w = matrix.width;
+ var h = matrix.height;
+ result = new [Packed.HEADER + (w * h + 7) / 8]b;
+ result[0] = Packed.FORMAT;
+ result[1] = (w >> 8) & 0xFF;
+ result[2] = w & 0xFF;
+ result[3] = (h >> 8) & 0xFF;
+ result[4] = h & 0xFF;
+ result[5] = matrix.quiet;
+ result[6] = matrix.rowHeight;
+ }
+
+ function step(budget as Number) as Boolean {
+ var m = _matrix.modules;
var n = m.size();
var full = n / 8;
- var out = new [4 + (n + 7) / 8]b;
- out[0] = width;
- out[1] = height;
- out[2] = quiet;
- out[3] = rowHeight;
- for (var i = 0; i < full; i++) {
+ var out = result;
+ var stop = _i + BYTES_PER_STEP < full ? _i + BYTES_PER_STEP : full;
+ for (var i = _i; i < stop; i++) {
var k = i * 8;
- out[4 + i] = (m[k] << 7) | (m[k + 1] << 6) | (m[k + 2] << 5) | (m[k + 3] << 4)
+ out[Packed.HEADER + i] = (m[k] << 7) | (m[k + 1] << 6) | (m[k + 2] << 5) | (m[k + 3] << 4)
| (m[k + 4] << 3) | (m[k + 5] << 2) | (m[k + 6] << 1) | m[k + 7];
}
+ _i = stop;
+ if (_i < full) {
+ return false;
+ }
for (var k = full * 8; k < n; k++) {
- out[4 + full] = out[4 + full] | (m[k] << (7 - k % 8));
+ out[Packed.HEADER + full] = out[Packed.HEADER + full] | (m[k] << (7 - k % 8));
}
- return out;
+ return true;
}
+ }
- static function fromBytes(data as ByteArray) as Matrix? {
- if (data.size() < 4) {
- return null;
+ // A code as the dark runs of each stored row, ready to draw with one
+ // fillRectangle per run (drawing module by module in onUpdate would be
+ // too much for the watchdog with large codes). Each run is
+ // (row << 20) | (first column << 10) | length.
+ class Runs {
+ var width as Number;
+ var height as Number;
+ var quiet as Number;
+ var rowHeight as Number;
+ var runs as Array<Number> = [] as Array<Number>;
+
+ function initialize(packed as Packed) {
+ width = packed.width;
+ height = packed.height;
+ quiet = packed.quiet;
+ rowHeight = packed.rowHeight;
+ }
+
+ function drawnHeight() as Number {
+ return height * rowHeight;
+ }
+ }
+
+ // Packed -> Runs, about MODULES_PER_STEP modules (whole rows) per step;
+ // all-light and all-dark bytes are taken eight modules at a time.
+ class RunFinder extends Jobs.Job {
+ static const MODULES_PER_STEP = 800;
+
+ var result as Runs;
+
+ private var _packed as Packed;
+ private var _row as Number = 0;
+
+ function initialize(packed as Packed) {
+ Job.initialize();
+ _packed = packed;
+ result = new Runs(packed);
+ }
+
+ function step(budget as Number) as Boolean {
+ var p = _packed;
+ var w = p.width;
+ var bytes = p.bytes;
+ var runs = result.runs;
+ var rows = MODULES_PER_STEP / w > 0 ? MODULES_PER_STEP / w : 1;
+ var last = _row + rows < p.height ? _row + rows : p.height;
+ for (var y = _row; y < last; y++) {
+ var bit = y * w;
+ var end = bit + w;
+ while (bit < end) {
+ var b = bytes[Packed.HEADER + (bit >> 3)];
+ if ((bit & 7) == 0 && bit + 8 <= end && b == 0) {
+ bit += 8;
+ continue;
+ }
+ if (((b >> (7 - (bit & 7))) & 1) == 0) {
+ bit++;
+ continue;
+ }
+ var start = bit;
+ while (bit < end) {
+ var c = bytes[Packed.HEADER + (bit >> 3)];
+ if ((bit & 7) == 0 && bit + 8 <= end && c == 0xFF) {
+ bit += 8;
+ } else if (((c >> (7 - (bit & 7))) & 1) == 1) {
+ bit++;
+ } else {
+ break;
+ }
+ }
+ runs.add((y << 20) | ((start - y * w) << 10) | (bit - start));
+ }
}
- var w = data[0];
- var h = data[1];
- var n = w * h;
- if (w == 0 || h == 0 || data[3] == 0 || data.size() != 4 + (n + 7) / 8) {
- return null;
+ _row = last;
+ return _row >= p.height;
+ }
+ }
+
+ // A 2D code from the cache or a fresh encoding to Runs: encode (unless
+ // cached), pack (`cache` then holds the bytes to store), find the runs.
+ class DisplayJob extends Jobs.Job {
+ var error as String? = null;
+ var cache as ByteArray? = null;
+ var runs as Runs? = null;
+
+ private var _encoder as MatrixJob?;
+ private var _packer as Packer? = null;
+ private var _finder as RunFinder? = null;
+
+ // Either an encoder (fresh) or the cached bytes.
+ function initialize(encoder as MatrixJob?, packed as Packed?) {
+ Job.initialize();
+ _encoder = encoder;
+ if (packed != null) {
+ _finder = new RunFinder(packed);
}
- var bits = new [(n + 7) / 8 * 8]b;
- for (var i = 0; i < (n + 7) / 8; i++) {
- var v = data[4 + i];
- var k = i * 8;
- bits[k] = (v >> 7) & 1;
- bits[k + 1] = (v >> 6) & 1;
- bits[k + 2] = (v >> 5) & 1;
- bits[k + 3] = (v >> 4) & 1;
- bits[k + 4] = (v >> 3) & 1;
- bits[k + 5] = (v >> 2) & 1;
- bits[k + 6] = (v >> 1) & 1;
- bits[k + 7] = v & 1;
+ }
+
+ // Progress 0-100: encoding is most of it.
+ function percent() as Number {
+ var e = _encoder;
+ return e != null ? e.percent() * 9 / 10 : 90;
+ }
+
+ function step(budget as Number) as Boolean {
+ var finder = _finder;
+ if (finder != null) {
+ if (!finder.step(budget)) {
+ return false;
+ }
+ runs = finder.result;
+ _finder = null;
+ return true;
}
- var matrix = new Matrix(w, h, data[2], bits.size() == n ? bits : bits.slice(0, n));
- matrix.rowHeight = data[3];
- return matrix;
+ var packer = _packer;
+ if (packer != null) {
+ if (packer.step(budget)) {
+ cache = packer.result;
+ _packer = null;
+ _finder = new RunFinder(new Packed(packer.result));
+ }
+ return false;
+ }
+ var encoder = _encoder as MatrixJob;
+ if (!encoder.step(budget)) {
+ return false;
+ }
+ var matrix = encoder.matrix();
+ var err = encoder.error;
+ if (err != null || matrix == null) {
+ error = err != null ? err : "could not encode";
+ return true;
+ }
+ _packer = new Packer(matrix);
+ _encoder = null;
+ return false;
}
}
diff --git a/source/codes/DataMatrix.mc b/source/codes/DataMatrix.mc
index 45d5ff0..76a29e8 100644
--- a/source/codes/DataMatrix.mc
+++ b/source/codes/DataMatrix.mc
@@ -9,8 +9,8 @@
// 2. Pick the smallest square size, pad (129, then randomised pads).
// 3. Reed-Solomon over GF(256) (x^8+x^5+x^3+x^2+1, roots a^1..a^n),
// per interleaved block, in chunks.
-// 4. Place the codewords with the standard "utah" placement, a few
-// diagonal sweeps per step, then add the finder and clock patterns of
+// 4. Place the codewords with the standard "utah" placement, one
+// diagonal pass per step, then add the finder and clock patterns of
// every data region.
// Verified against ppf.datamatrix (ASCII mode) and by decoding with zxing-cpp.
@@ -32,9 +32,14 @@ module DataMatrix {
const PAD = 129;
const UPPER_SHIFT = 235;
const BASE256_LATCH = 231;
- // Work per step, sized for the 120,000-bytecode watchdog.
- const RS_SYMBOLS_PER_STEP = 40;
- const SWEEPS_PER_STEP = 6;
+ // The placement grid's mark for the fixed dark corner modules.
+ const FIXED_DARK = 0xFFFF;
+ // Work per step: each was checked in the simulator by running two
+ // steps per event on the Forerunner 255S (120,000 bytecodes per event),
+ // with a 1,500-byte payload (144 x 144 modules).
+ const BYTES_PER_STEP = 250; // codeword encodation
+ const ECC_WORK_PER_STEP = 400; // data symbols x EC symbols
+ const BUILD_MODULES_PER_STEP = 300; // symbol modules
var field as ReedSolomon.Field? = null;
@@ -49,41 +54,83 @@ module DataMatrix {
// Codewords for data: ASCII, or Base256 when cheaper (non-ASCII text).
function codewords(data as ByteArray) as Array<Number> {
- var ascii = [] as Array<Number>;
- var highBytes = 0;
- var i = 0;
- while (i < data.size()) {
- var b = data[i];
- if (b >= 0x30 && b <= 0x39 && i + 1 < data.size() && data[i + 1] >= 0x30 && data[i + 1] <= 0x39) {
- ascii.add(130 + (b - 0x30) * 10 + data[i + 1] - 0x30);
- i += 2;
- continue;
- }
- if (b >= 128) {
- ascii.add(UPPER_SHIFT);
- ascii.add(b - 128 + 1);
- highBytes++;
- } else {
- ascii.add(b + 1);
- }
- i++;
+ var builder = new CodewordBuilder(data);
+ while (!builder.step(data.size() + 1)) {
}
- var n = data.size();
- var base256Cost = 1 + (n <= 249 ? 1 : 2) + n;
- if (highBytes == 0 || ascii.size() <= base256Cost) {
- return ascii;
- }
- var out = [BASE256_LATCH] as Array<Number>;
- if (n <= 249) {
- out.add(randomise255(n, 2));
- } else {
- out.add(randomise255(n / 250 + 249, 2));
- out.add(randomise255(n % 250, 3));
+ return builder.result;
+ }
+
+ // codewords() in bounded steps: the ASCII encodation, then (when
+ // Base256 is cheaper) the Base256 one, `bytes` input bytes per step.
+ class CodewordBuilder {
+ var result as Array<Number> = [] as Array<Number>;
+
+ private var _data as ByteArray;
+ private var _ascii as Array<Number> = [] as Array<Number>;
+ private var _highBytes as Number = 0;
+ private var _i as Number = 0;
+ private var _base256 as Array<Number>? = null;
+ private var _k as Number = 0;
+
+ function initialize(data as ByteArray) {
+ _data = data;
}
- for (var k = 0; k < n; k++) {
- out.add(randomise255(data[k], out.size() + 1));
+
+ // True when result is ready.
+ function step(bytes as Number) as Boolean {
+ var data = _data;
+ var n = data.size();
+ if (_i < n) {
+ var stop = _i + bytes < n ? _i + bytes : n;
+ var i = _i;
+ var ascii = _ascii;
+ while (i < stop) {
+ var b = data[i];
+ if (b >= 0x30 && b <= 0x39 && i + 1 < n && data[i + 1] >= 0x30 && data[i + 1] <= 0x39) {
+ ascii.add(130 + (b - 0x30) * 10 + data[i + 1] - 0x30);
+ i += 2;
+ continue;
+ }
+ if (b >= 128) {
+ ascii.add(UPPER_SHIFT);
+ ascii.add(b - 128 + 1);
+ _highBytes++;
+ } else {
+ ascii.add(b + 1);
+ }
+ i++;
+ }
+ _i = i;
+ return false;
+ }
+ var out = _base256;
+ if (out == null) {
+ var base256Cost = 1 + (n <= 249 ? 1 : 2) + n;
+ if (_highBytes == 0 || _ascii.size() <= base256Cost) {
+ result = _ascii;
+ return true;
+ }
+ out = [BASE256_LATCH] as Array<Number>;
+ if (n <= 249) {
+ out.add(randomise255(n, 2));
+ } else {
+ out.add(randomise255(n / 250 + 249, 2));
+ out.add(randomise255(n % 250, 3));
+ }
+ _base256 = out;
+ _ascii = [] as Array<Number>;
+ }
+ var stop = _k + bytes < n ? _k + bytes : n;
+ for (var k = _k; k < stop; k++) {
+ out.add(randomise255(data[k], out.size() + 1));
+ }
+ _k = stop;
+ if (_k < n) {
+ return false;
+ }
+ result = out;
+ return true;
}
- return out;
}
// The 255-state randomising of Base256 values at 1-based position pos.
@@ -113,15 +160,21 @@ module DataMatrix {
private var _size as Array<Number> = [] as Array<Number>;
private var _cw as Array<Number> = [] as Array<Number>;
private var _blocks as Array<Array<Number>> = [] as Array<Array<Number>>;
- private var _rem as Array<Number> = [] as Array<Number>;
+ private var _builder as CodewordBuilder? = null;
+ private var _encoder as ReedSolomon.Encoder? = null;
private var _block as Number = 0;
- private var _at as Number = 0;
+ private var _buildRow as Number = 0;
+ private var _modules as ByteArray = []b;
private var _ecTotal as Array<Number> = [] as Array<Number>;
// Placement state.
private var _nrow as Number = 0;
private var _ncol as Number = 0;
- private var _place as Array<Number> = [] as Array<Number>; // 10 * codeword + bit, 0 = empty, -1 = fixed dark
+ // Placement grid, two bytes per module (big-endian): 10 * codeword +
+ // bit, 0 = empty, FIXED_DARK. Bytes, not Numbers: a 132 x 132 grid of
+ // Numbers took about 90 KB of the Forerunner 255S's 512 KB.
+ private var _place as ByteArray = []b;
private var _chr as Number = 1;
+ private var _downPass as Boolean = false;
private var _row as Number = 4;
private var _col as Number = 0;
private var _result as Codes.Matrix? = null;
@@ -132,7 +185,7 @@ module DataMatrix {
}
function percent() as Number {
- return _phase * 25;
+ return _phase * 20;
}
function matrix() as Codes.Matrix? {
@@ -145,20 +198,31 @@ module DataMatrix {
} else if (_phase == 1) {
eccStep();
} else if (_phase == 2) {
- placeStep();
+ startPlacement();
} else if (_phase == 3) {
+ placeStep();
+ } else if (_phase == 4) {
build();
}
- return _phase >= 4 || error != null;
+ return _phase >= 5 || error != null;
}
- // Codewords, size, padding, blocks.
+ // Codewords (BYTES_PER_STEP per step), then size, padding, blocks.
private function prepare() as Void {
if (_data.size() == 0) {
error = "Data Matrix needs some text";
return;
}
- var cw = codewords(_data);
+ var builder = _builder;
+ if (builder == null) {
+ builder = new CodewordBuilder(_data);
+ _builder = builder;
+ }
+ if (!builder.step(BYTES_PER_STEP)) {
+ return;
+ }
+ _builder = null;
+ var cw = builder.result;
for (var i = 0; i < SIZES.size() && _size.size() == 0; i++) {
var s = SIZES[i] as Array<Number>;
if (cw.size() <= s[3]) {
@@ -186,63 +250,68 @@ module DataMatrix {
_blocks.add(block);
}
_ecTotal = new [_size[4]] as Array<Number>;
- startBlock();
_phase = 1;
}
- private function startBlock() as Void {
- var n = _size[4] / _size[5];
- _rem = new [n] as Array<Number>;
- for (var k = 0; k < n; k++) {
- _rem[k] = 0;
- }
- _at = 0;
- }
-
- // Reed-Solomon, RS_SYMBOLS_PER_STEP data symbols per step.
+ // Reed-Solomon (the shared encoder: the same remainder as ISO 16022
+ // annex E), ECC_WORK_PER_STEP data symbols times EC symbols per step.
private function eccStep() as Void {
- var f = theField();
- var data = _blocks[_block];
- var n = _rem.size();
- var g = f.generator(n);
- var end = _at + RS_SYMBOLS_PER_STEP < data.size() ? _at + RS_SYMBOLS_PER_STEP : data.size();
- for (var i = _at; i < end; i++) {
- var factor = data[i] ^ _rem[0];
- for (var k = 0; k < n - 1; k++) {
- _rem[k] = _rem[k + 1] ^ f.multiply(g[k], factor);
+ var blocks = _size[5];
+ var n = _size[4] / blocks;
+ var encoder = _encoder;
+ if (encoder == null) {
+ var fresh = !theField().hasGenerator(n);
+ encoder = new ReedSolomon.Encoder(theField(), _blocks[_block], n);
+ _encoder = encoder;
+ if (fresh) {
+ return; // the generator took this step
}
- _rem[n - 1] = f.multiply(g[n - 1], factor);
}
- _at = end;
- if (_at < data.size()) {
+ if (!encoder.step(ECC_WORK_PER_STEP / n > 0 ? ECC_WORK_PER_STEP / n : 1)) {
return;
}
+ _encoder = null;
// Interleave this block's EC codewords.
- var blocks = _size[5];
for (var k = 0; k < n; k++) {
- _ecTotal[_block + k * blocks] = _rem[k];
+ _ecTotal[_block + k * blocks] = encoder.remainder[k];
}
_block++;
if (_block < blocks) {
- startBlock();
return;
}
_cw.addAll(_ecTotal);
_blocks = [] as Array<Array<Number>>;
+ _phase = 2;
+ }
+
+ // The empty placement grid (a new ByteArray is zeroed natively).
+ private function startPlacement() as Void {
_nrow = _size[1] * _size[2];
_ncol = _nrow;
- _place = new [_nrow * _ncol] as Array<Number>;
- for (var k = 0; k < _place.size(); k++) {
- _place[k] = 0;
- }
- _phase = 2;
+ _place = new [2 * _nrow * _ncol]b;
+ _phase = 3;
+ }
+
+ private function placeAt(i as Number) as Number {
+ return (_place[2 * i] << 8) | _place[2 * i + 1];
}
- // ISO 16022 annex F placement, SWEEPS_PER_STEP diagonal sweeps per step.
+ private function setPlace(i as Number, v as Number) as Void {
+ _place[2 * i] = (v >> 8) & 0xFF;
+ _place[2 * i + 1] = v & 0xFF;
+ }
+
+ // ISO 16022 annex F placement, one diagonal pass per step: each
+ // sweep is a pass up and to the right (after the corner cases), then
+ // one down and to the left (_downPass).
private function placeStep() as Void {
var nrow = _nrow;
var ncol = _ncol;
- for (var s = 0; s < SWEEPS_PER_STEP && (_row < nrow || _col < ncol); s++) {
+ if (!_downPass) {
+ if (_row >= nrow && _col >= ncol) {
+ finishPlacement();
+ return;
+ }
if (_row == nrow && _col == 0) {
corner1();
}
@@ -256,7 +325,8 @@ module DataMatrix {
corner4();
}
do {
- if (_row < nrow && _col >= 0 && _place[_row * ncol + _col] == 0) {
+ var at = 2 * (_row * ncol + _col);
+ if (_row < nrow && _col >= 0 && _place[at] == 0 && _place[at + 1] == 0) {
utah(_row, _col);
}
_row -= 2;
@@ -264,24 +334,33 @@ module DataMatrix {
} while (_row >= 0 && _col < ncol);
_row += 1;
_col += 3;
- do {
- if (_row >= 0 && _col < ncol && _place[_row * ncol + _col] == 0) {
- utah(_row, _col);
- }
- _row += 2;
- _col -= 2;
- } while (_row < nrow && _col >= 0);
- _row += 3;
- _col += 1;
- }
- if (_row < nrow || _col < ncol) {
+ _downPass = true;
return;
}
- if (_place[nrow * ncol - 1] == 0) {
- _place[nrow * ncol - 1] = -1;
- _place[nrow * ncol - ncol - 2] = -1;
+ do {
+ var at = 2 * (_row * ncol + _col);
+ if (_row >= 0 && _col < ncol && _place[at] == 0 && _place[at + 1] == 0) {
+ utah(_row, _col);
+ }
+ _row += 2;
+ _col -= 2;
+ } while (_row < nrow && _col >= 0);
+ _row += 3;
+ _col += 1;
+ _downPass = false;
+ if (_row >= nrow && _col >= ncol) {
+ finishPlacement();
}
- _phase = 3;
+ }
+
+ // The fixed corner modules when the grid leaves them empty.
+ private function finishPlacement() as Void {
+ var n = _nrow * _ncol;
+ if (placeAt(n - 1) == 0) {
+ setPlace(n - 1, FIXED_DARK);
+ setPlace(n - _ncol - 2, FIXED_DARK);
+ }
+ _phase = 4;
}
private function setModule(row as Number, col as Number, bit as Number) as Void {
@@ -295,7 +374,10 @@ module DataMatrix {
c += _ncol;
r += 4 - ((_ncol + 4) % 8);
}
- _place[r * _ncol + c] = 10 * _chr + bit;
+ var v = 10 * _chr + bit;
+ var at = 2 * (r * _ncol + c);
+ _place[at] = v >> 8;
+ _place[at + 1] = v & 0xFF;
}
private function utah(row as Number, col as Number) as Void {
@@ -359,13 +441,19 @@ module DataMatrix {
}
// The symbol: data modules mapped into the regions, plus each
- // region's finder (solid left and bottom) and clock (top, right).
+ // region's finder (solid left and bottom) and clock (top, right);
+ // BUILD_MODULES_PER_STEP modules (whole rows) per step.
private function build() as Void {
var size = _size[0];
var region = _size[1];
var box = region + 2;
- var m = new [size * size]b;
- for (var y = 0; y < size; y++) {
+ if (_modules.size() == 0) {
+ _modules = new [size * size]b;
+ }
+ var m = _modules;
+ var rows = BUILD_MODULES_PER_STEP / size > 0 ? BUILD_MODULES_PER_STEP / size : 1;
+ var last = _buildRow + rows < size ? _buildRow + rows : size;
+ for (var y = _buildRow; y < last; y++) {
var ry = y % box;
for (var x = 0; x < size; x++) {
var rx = x % box;
@@ -379,8 +467,9 @@ module DataMatrix {
} else {
var mr = (y / box) * region + ry - 1;
var mc = (x / box) * region + rx - 1;
- var v = _place[mr * _ncol + mc];
- if (v == -1) {
+ var at = 2 * (mr * _ncol + mc);
+ var v = (_place[at] << 8) | _place[at + 1];
+ if (v == FIXED_DARK) {
dark = 1;
} else if (v > 0) {
var cw = _cw[v / 10 - 1];
@@ -390,9 +479,14 @@ module DataMatrix {
m[y * size + x] = dark;
}
}
- _place = [] as Array<Number>;
+ _buildRow = last;
+ if (_buildRow < size) {
+ return;
+ }
+ _place = []b;
+ _modules = []b;
_result = new Codes.Matrix(size, size, QUIET, m);
- _phase = 4;
+ _phase = 5;
}
}
}
diff --git a/source/codes/Pdf417.mc b/source/codes/Pdf417.mc
index b17b5df..a294aa7 100644
--- a/source/codes/Pdf417.mc
+++ b/source/codes/Pdf417.mc
@@ -53,7 +53,12 @@ module Pdf417 {
[[29], [29, 27], [29, 28], []]
];
const PREFERENCE = [1, 0, 2, 3]; // lower, upper, mixed, punctuation
- const EC_STEP_WORDS = 20;
+ // Work per step: each was checked in the simulator by running two
+ // steps per event on the Forerunner 255S (120,000 bytecodes per event)
+ // with the largest payload its screen takes.
+ const COMPACT_PER_STEP = 150; // bytes (or runs) compacted
+ const EC_WORK_PER_STEP = 300; // data codewords x EC codewords
+ const ROWS_PER_STEP = 2;
// One of the generated table resources ("v" holds the values).
function table(id as ResourceId) as Array {
@@ -62,61 +67,179 @@ module Pdf417 {
// Data codewords (without length descriptor and padding).
function dataCodewords(data as ByteArray, text as Array<Array<Number>>) as Array<Number> {
- // Runs of one kind: [kind, start, end) triples.
- var runs = [] as Array<Number>;
- for (var i = 0; i < data.size(); i++) {
- var kind = kindOf(data[i], text);
- var n = runs.size();
- if (n > 0 && runs[n - 3] == kind) {
- runs[n - 1] = i + 1;
+ var compactor = new Compactor(data, text);
+ while (!compactor.step(data.size() + 1)) {
+ }
+ return compactor.out;
+ }
+
+ // dataCodewords() in steps of about `budget` bytes (or runs): the runs
+ // of one kind, then the merged runs (short digit runs next to text
+ // become text, equal neighbours join), then the compaction of each run.
+ class Compactor {
+ var out as Array<Number> = [] as Array<Number>;
+
+ private var _data as ByteArray;
+ private var _text as Array<Array<Number>>;
+ private var _stage as Number = 0;
+ private var _i as Number = 0;
+ // [kind, start, end) triples, before and after merging.
+ private var _runs as Array<Number> = [] as Array<Number>;
+ private var _merged as Array<Number> = [] as Array<Number>;
+ // Compaction: the position in the current run (-1 before its latch),
+ // the text submode and a text value waiting for its pair.
+ private var _pos as Number = -1;
+ private var _submode as Number = UPPER;
+ private var _half as Number = -1;
+
+ function initialize(data as ByteArray, text as Array<Array<Number>>) {
+ _data = data;
+ _text = text;
+ }
+
+ // True when out is complete.
+ function step(budget as Number) as Boolean {
+ if (_stage == 0) {
+ findRuns(budget);
+ } else if (_stage == 1) {
+ mergeRuns(budget);
} else {
- runs.add(kind);
- runs.add(i);
- runs.add(i + 1);
+ compact(budget);
}
+ return _stage == 3;
}
- // Short digit runs next to text become text.
- for (var r = 0; r < runs.size(); r += 3) {
- if (runs[r] == KIND_NUMERIC && runs[r + 2] - runs[r + 1] < 13
- && ((r > 0 && runs[r - 3] == KIND_TEXT) || (r + 3 < runs.size() && runs[r + 3] == KIND_TEXT))) {
- runs[r] = -KIND_TEXT - 10; // marked; resolved below so later checks see the original kinds
+
+ private function findRuns(budget as Number) as Void {
+ var data = _data;
+ var runs = _runs;
+ var stop = _i + budget < data.size() ? _i + budget : data.size();
+ for (var i = _i; i < stop; i++) {
+ var kind = kindOf(data[i], _text);
+ var n = runs.size();
+ if (n > 0 && runs[n - 3] == kind) {
+ runs[n - 1] = i + 1;
+ } else {
+ runs.add(kind);
+ runs.add(i);
+ runs.add(i + 1);
+ }
+ }
+ _i = stop;
+ if (_i >= data.size()) {
+ _i = 0;
+ _stage = 1;
}
}
- for (var r = 0; r < runs.size(); r += 3) {
- if (runs[r] == -KIND_TEXT - 10) {
- runs[r] = KIND_TEXT;
+
+ // Short digit runs next to text become text (judged by the original
+ // neighbours), and equal neighbours merge, in one pass.
+ private function mergeRuns(budget as Number) as Void {
+ var runs = _runs;
+ var merged = _merged;
+ var stop = _i + 3 * budget < runs.size() ? _i + 3 * budget : runs.size();
+ for (var r = _i; r < stop; r += 3) {
+ var kind = runs[r];
+ if (kind == KIND_NUMERIC && runs[r + 2] - runs[r + 1] < 13
+ && ((r > 0 && runs[r - 3] == KIND_TEXT) || (r + 3 < runs.size() && runs[r + 3] == KIND_TEXT))) {
+ kind = KIND_TEXT;
+ }
+ var n = merged.size();
+ if (n > 0 && merged[n - 3] == kind) {
+ merged[n - 1] = runs[r + 2];
+ } else {
+ merged.add(kind);
+ merged.add(runs[r + 1]);
+ merged.add(runs[r + 2]);
+ }
+ }
+ _i = stop;
+ if (_i >= runs.size()) {
+ _runs = [] as Array<Number>;
+ _i = 0;
+ _stage = 2;
}
}
- // Merge equal neighbours.
- var merged = [] as Array<Number>;
- for (var r = 0; r < runs.size(); r += 3) {
- var n = merged.size();
- if (n > 0 && merged[n - 3] == runs[r]) {
- merged[n - 1] = runs[r + 2];
- } else {
- merged.add(runs[r]);
- merged.add(runs[r + 1]);
- merged.add(runs[r + 2]);
+
+ private function compact(budget as Number) as Void {
+ var left = budget;
+ var merged = _merged;
+ while (left > 0 && _i < merged.size()) {
+ var kind = merged[_i];
+ var start = merged[_i + 1];
+ var end = merged[_i + 2];
+ if (_pos < 0) {
+ if (_i > 0 || kind != KIND_TEXT) {
+ out.add(kind == KIND_TEXT ? TEXT_LATCH : (kind == KIND_NUMERIC ? NUMERIC_LATCH
+ : ((end - start) % 6 == 0 ? BYTE_LATCH_6 : BYTE_LATCH)));
+ }
+ _pos = start;
+ _submode = UPPER;
+ _half = -1;
+ }
+ var stop = 0;
+ if (kind == KIND_TEXT) {
+ stop = _pos + left < end ? _pos + left : end;
+ compactText(_pos, stop);
+ left -= stop - _pos;
+ } else if (kind == KIND_NUMERIC) {
+ stop = _pos + 44 < end ? _pos + 44 : end;
+ compactNumeric(_data, _pos, stop, out);
+ left -= 4 * (stop - _pos); // long division: costly per digit
+ } else {
+ var groups = left / 6 > 0 ? left / 6 : 1;
+ stop = _pos + 6 * groups < end ? _pos + 6 * groups : end;
+ compactBytes(_data, _pos, stop, out);
+ left -= stop - _pos;
+ }
+ _pos = stop;
+ if (_pos >= end) {
+ if (kind == KIND_TEXT && _half >= 0) {
+ out.add(30 * _half + 29);
+ }
+ _pos = -1;
+ _i += 3;
+ }
+ }
+ if (_i >= merged.size()) {
+ _merged = [] as Array<Number>;
+ _stage = 3;
+ }
+ }
+
+ // Text values of data[from, to) in the current submode, with switch
+ // codes; values are paired into codewords as they come.
+ private function compactText(from as Number, to as Number) as Void {
+ var text = _text;
+ var data = _data;
+ for (var i = from; i < to; i++) {
+ var c = data[i];
+ if ((text[_submode][c] as Number) < 0) {
+ var next = _submode;
+ for (var p = 0; p < 4; p++) {
+ var m = PREFERENCE[p] as Number;
+ if ((text[m][c] as Number) >= 0) {
+ next = m;
+ break;
+ }
+ }
+ var codes = (SWITCHES[_submode] as Array)[next] as Array<Number>;
+ for (var k = 0; k < codes.size(); k++) {
+ textValue(codes[k]);
+ }
+ _submode = next;
+ }
+ textValue(text[_submode][c] as Number);
}
}
- var out = [] as Array<Number>;
- for (var r = 0; r < merged.size(); r += 3) {
- var kind = merged[r];
- var start = merged[r + 1];
- var end = merged[r + 2];
- if (r > 0 || kind != KIND_TEXT) {
- out.add(kind == KIND_TEXT ? TEXT_LATCH : (kind == KIND_NUMERIC ? NUMERIC_LATCH
- : ((end - start) % 6 == 0 ? BYTE_LATCH_6 : BYTE_LATCH)));
- }
- if (kind == KIND_TEXT) {
- compactText(data, start, end, text, out);
- } else if (kind == KIND_NUMERIC) {
- compactNumeric(data, start, end, out);
+
+ private function textValue(v as Number) as Void {
+ if (_half < 0) {
+ _half = v;
} else {
- compactBytes(data, start, end, out);
+ out.add(30 * _half + v);
+ _half = -1;
}
}
- return out;
}
function kindOf(b as Number, text as Array<Array<Number>>) as Number {
@@ -133,30 +256,6 @@ module Pdf417 {
return KIND_BYTE;
}
- function compactText(data as ByteArray, start as Number, end as Number, text as Array<Array<Number>>, out as Array<Number>) as Void {
- var interim = [] as Array<Number>;
- var submode = UPPER;
- for (var i = start; i < end; i++) {
- var c = data[i];
- if ((text[submode][c] as Number) < 0) {
- var next = submode;
- for (var p = 0; p < 4; p++) {
- var m = PREFERENCE[p] as Number;
- if ((text[m][c] as Number) >= 0) {
- next = m;
- break;
- }
- }
- interim.addAll((SWITCHES[submode] as Array)[next] as Array<Number>);
- submode = next;
- }
- interim.add(text[submode][c] as Number);
- }
- for (var i = 0; i < interim.size(); i += 2) {
- out.add(30 * interim[i] + (i + 1 < interim.size() ? interim[i + 1] : 29));
- }
- }
-
// 44 digits per group: the value of "1" followed by the digits, in base 900.
function compactNumeric(data as ByteArray, start as Number, end as Number, out as Array<Number>) as Void {
for (var g = start; g < end; g += 44) {
@@ -261,6 +360,8 @@ module Pdf417 {
private var _words as Array<Number> = [] as Array<Number>;
private var _ec as Array<Number> = [] as Array<Number>;
private var _factors as Array<Number> = [] as Array<Number>;
+ private var _compactor as Compactor? = null;
+ private var _dataWords as Array<Number>? = null;
private var _codes as Array<Array<Number>> = [] as Array<Array<Number>>;
private var _at as Number = 0;
private var _modules as ByteArray = []b;
@@ -301,12 +402,34 @@ module Pdf417 {
return _phase >= 3 || error != null;
}
+ // Compaction in steps (after a quick check that the text could fit
+ // the screen at all), then level, layout and padding, and the
+ // tables in a step of their own.
private function prepare() as Void {
if (_data.size() == 0) {
error = "PDF417 needs some text";
return;
}
- var data = dataCodewords(_data, table(Rez.JsonData.Pdf417Text) as Array<Array<Number>>);
+ var data = _dataWords;
+ if (data == null) {
+ var compactor = _compactor;
+ if (compactor == null) {
+ // At best 44 digits take 15 codewords.
+ var least = _data.size() * 15 / 44;
+ if (least > MAX_CODEWORDS || (columns == 0 && least > screenCapacity())) {
+ error = "too large for this screen as PDF417 (at least " + least + " codewords)";
+ return;
+ }
+ compactor = new Compactor(_data, table(Rez.JsonData.Pdf417Text) as Array<Array<Number>>);
+ _compactor = compactor;
+ }
+ if (!compactor.step(COMPACT_PER_STEP)) {
+ return;
+ }
+ _compactor = null;
+ _dataWords = compactor.out;
+ return;
+ }
if (level < 0) {
level = levelFor(_option, data.size());
}
@@ -329,6 +452,7 @@ module Pdf417 {
}
_words = [length] as Array<Number>;
_words.addAll(data);
+ _dataWords = null;
for (var i = 0; i < padding; i++) {
_words.add(PAD);
}
@@ -343,6 +467,22 @@ module Pdf417 {
_phase = 1;
}
+ // The most codewords any layout can hold on this screen (1 px per
+ // module, up to 90 rows).
+ private function screenCapacity() as Number {
+ var best = 0;
+ for (var cols = 1; cols <= 30; cols++) {
+ var w = 17 * (cols + 4) + 1 + 2 * QUIET;
+ for (var r = 90; r >= 3; r -= 3) {
+ if (Codes.fits(w, r * ROW_HEIGHT + 2 * QUIET, 1, _screenW, _screenH, _round)) {
+ best = cols * r > best ? cols * r : best;
+ break;
+ }
+ }
+ }
+ return best;
+ }
+
// The largest module size, then the fewest columns, that fit.
private function chooseLayout(codewords as Number) as Boolean {
for (var px = 6; px >= 1; px--) {
@@ -362,11 +502,13 @@ module Pdf417 {
return false;
}
- // Reed-Solomon mod 929 (pdf417gen's form), EC_STEP_WORDS words per step.
+ // Reed-Solomon mod 929 (pdf417gen's form), EC_WORK_PER_STEP data
+ // codewords times EC codewords per step.
private function ecStep() as Void {
var n = _ec.size();
var f = _factors;
- var end = _at + EC_STEP_WORDS < _words.size() ? _at + EC_STEP_WORDS : _words.size();
+ var words = EC_WORK_PER_STEP / n > 0 ? EC_WORK_PER_STEP / n : 1;
+ var end = _at + words < _words.size() ? _at + words : _words.size();
for (var i = _at; i < end; i++) {
var temp = (_words[i] + _ec[n - 1]) % 929;
for (var x = n - 1; x >= 0; x--) {
@@ -392,7 +534,7 @@ module Pdf417 {
// A few rows per step: start, indicators, data, stop.
private function rowsStep() as Void {
var width = 17 * (columns + 4) + 1;
- for (var k = 0; k < 4 && _at < rows; k++) {
+ for (var k = 0; k < ROWS_PER_STEP && _at < rows; k++) {
var row = _at;
var cluster = _codes[row % 3];
var x = put(row, 0, START, 17, width);
diff --git a/source/codes/Qr.mc b/source/codes/Qr.mc
index 1fa69da..1fac051 100644
--- a/source/codes/Qr.mc
+++ b/source/codes/Qr.mc
@@ -9,11 +9,14 @@
// codewords 0xEC 0x11), split it into blocks, add Reed-Solomon
// codewords (QrTables.BLOCKS), interleave.
// 3. Draw the function patterns, place the codewords in the zigzag.
-// 4. Score all 8 masks with the ISO 18004:2015 penalty rules exactly as
-// segno computes them (format and version areas still light), keep
-// the lowest, then draw the format and version information.
+// 4. Pack the rows and columns 16 modules per word, then score all 8
+// masks with the ISO 18004:2015 penalty rules exactly as segno
+// computes them (format and version areas still light), with bit
+// operations on the packed lines; keep the lowest.
+// 5. Apply that mask, then draw the format and version information.
// Each step does a bounded amount of work (one RS block, a few columns or
-// rows), because version 40 is far beyond one event's watchdog budget.
+// rows, a few hundred packed words), because version 40 is far beyond one
+// event's watchdog budget.
// Verified against segno with forced version and mask, and by decoding.
import Toybox.Lang;
@@ -26,15 +29,31 @@ module Qr {
const ALPHANUMERIC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; // lint: allow-literal (QR alphanumeric set)
// Format bits per level L, M, Q, H.
const LEVEL_BITS = [1, 0, 3, 2];
- // Modules looked at per job step: well below the 120,000-bytecode
- // watchdog limit of the smallest targets.
- const MODULES_PER_STEP = 2500;
- // Mask scoring costs more per module: the row pass applies the mask and
- // scores N1-N3, the column pass N1 and N3. Sized for the Forerunner
- // 255S's 120,000 bytecodes per event, where 1,200 row modules tripped
- // the watchdog in the UI flow.
- const ROW_MODULES_PER_STEP = 500;
- const COLUMN_MODULES_PER_STEP = 1000;
+ // Modules placed, packed or unmasked per job step. Every step size here
+ // was checked in the simulator by running two steps per event on the
+ // Forerunner 255S (120,000 bytecodes per event): each step stays below
+ // half the watchdog limit.
+ const PLACE_MODULES_PER_STEP = 300;
+ const PACK_MODULES_PER_STEP = 400;
+ const APPLY_MODULES_PER_STEP = 600;
+ // Mask scoring works on packed lines (16 modules per word, see
+ // startPacking); words scored per step. The row pass masks the line and
+ // scores N1, N2, N3 and the dark count, the column pass N1 and N3.
+ const ROW_WORDS_PER_STEP = 100;
+ const COLUMN_WORDS_PER_STEP = 150;
+ // Light bits before each packed line.
+ const PAD = 4;
+ // Phase 0: characters segmented or written per step (the segmentation
+ // costs about 150 bytecodes per character).
+ const SEGMENT_CHARS_PER_STEP = 80;
+ const WRITE_CHARS_PER_STEP = 300;
+ // Phase 1: data symbols times EC symbols per step (about 35 bytecodes each).
+ const ECC_WORK_PER_STEP = 400;
+ // Phase 2: codewords interleaved per step.
+ const INTERLEAVE_PER_STEP = 600;
+ const PREP_SEGMENT = 0;
+ const PREP_WRITE = 1;
+ const PREP_SPLIT = 2;
// The GF(256) field of QR (x^8 + x^4 + x^3 + x^2 + 1), shared.
var field as ReedSolomon.Field? = null;
@@ -70,12 +89,20 @@ module Qr {
return b >= 0x30 && b <= 0x39;
}
+ // The alphanumeric value of byte b, or -1 (table built on first use).
+ var alnumTable as ByteArray? = null;
+
function alnumValue(b as Number) as Number {
- if (b >= 0x80) {
- return -1;
+ var t = alnumTable;
+ if (t == null) {
+ t = new [128]b;
+ var chars = ALPHANUMERIC.toUtf8Array();
+ for (var k = 0; k < chars.size(); k++) {
+ t[chars[k]] = k + 1;
+ }
+ alnumTable = t;
}
- var at = ALPHANUMERIC.find(b.toChar().toString());
- return at == null ? -1 : at;
+ return b < 0x80 ? t[b] - 1 : -1;
}
// Optimal segmentation for a version class: [mode, start, end) triples
@@ -84,64 +111,133 @@ module Qr {
// alphanumeric 11/2, byte 8 bits per character). State j after
// character i: the cheapest encoding so far that continues in mode j;
// choice[i * 3 + j] is the mode character i itself was encoded in.
- function segment(data as ByteArray, v as Number) as Array<Number> {
- var n = data.size();
- var modes = [MODE_BYTE, MODE_ALPHANUMERIC, MODE_NUMERIC];
- var head = [(4 + charCountBits(MODE_BYTE, v)) * 6, (4 + charCountBits(MODE_ALPHANUMERIC, v)) * 6,
- (4 + charCountBits(MODE_NUMERIC, v)) * 6];
- var unit = [48, 33, 20];
- var inf = 0x3FFFFFFF;
- var prev = [head[0], head[1], head[2]];
- // choice[i * 3 + j] = the mode index the segment ending at i in mode j came from.
- var choice = new [n * 3]b;
- for (var i = 0; i < n; i++) {
- var b = data[i];
- var cur = [prev[0] + unit[0], alnumValue(b) >= 0 ? prev[1] + unit[1] : inf, isDigit(b) ? prev[2] + unit[2] : inf];
- choice[i * 3] = 0;
- choice[i * 3 + 1] = 1;
- choice[i * 3 + 2] = 2;
- for (var j = 0; j < 3; j++) {
- for (var k = 0; k < 3; k++) {
- if (k == j || (cur[k] as Number) >= inf) {
- continue;
+ // Resumable: step() handles a bounded number of characters (the
+ // programme, then the walk back and the collection of segments, which
+ // cost far less per character), so a 2,900-byte payload never runs in
+ // one event.
+ class Segmenter {
+ static const MODES = [MODE_BYTE, MODE_ALPHANUMERIC, MODE_NUMERIC];
+ static const INF = 0x3FFFFFFF;
+
+ var segments as Array<Number> = [] as Array<Number>;
+
+ private var _data as ByteArray;
+ private var _head as Array<Number>;
+ private var _prev as Array<Number>;
+ private var _cur as Array<Number> = [0, 0, 0];
+ private var _choice as ByteArray;
+ private var _charModes as ByteArray = []b;
+ private var _i as Number = 0;
+ private var _walk as Number = 0;
+ private var _state as Number = 0;
+ private var _start as Number = 0;
+ private var _collected as Number = 0;
+ private var _done as Boolean = false;
+
+ function initialize(data as ByteArray, v as Number) {
+ _data = data;
+ _head = [(4 + charCountBits(MODE_BYTE, v)) * 6, (4 + charCountBits(MODE_ALPHANUMERIC, v)) * 6,
+ (4 + charCountBits(MODE_NUMERIC, v)) * 6];
+ _prev = [_head[0], _head[1], _head[2]];
+ _choice = new [data.size() * 3]b;
+ }
+
+ // Up to `chars` more characters (of the programme, then of each walk
+ // back); true when segments is ready.
+ function step(chars as Number) as Boolean {
+ var n = _data.size();
+ if (_i < n) {
+ program(chars);
+ } else if (_walk > 0) {
+ walkBack(chars * 4);
+ } else {
+ collect(chars * 4);
+ }
+ return _done;
+ }
+
+ private function program(chars as Number) as Void {
+ var n = _data.size();
+ var head = _head;
+ var prev = _prev;
+ var cur = _cur;
+ var choice = _choice;
+ var stop = _i + chars < n ? _i + chars : n;
+ for (var i = _i; i < stop; i++) {
+ var b = _data[i];
+ cur[0] = prev[0] + 48;
+ cur[1] = alnumValue(b) >= 0 ? prev[1] + 33 : INF;
+ cur[2] = isDigit(b) ? prev[2] + 20 : INF;
+ choice[i * 3] = 0;
+ choice[i * 3 + 1] = 1;
+ choice[i * 3 + 2] = 2;
+ for (var j = 0; j < 3; j++) {
+ for (var k = 0; k < 3; k++) {
+ if (k == j || cur[k] >= INF) {
+ continue;
+ }
+ // State j may follow a character in mode k: a new
+ // segment in mode j starts after it (its header paid now).
+ var switched = (cur[k] + 5) / 6 * 6 + head[j];
+ if (switched < cur[j]) {
+ cur[j] = switched;
+ choice[i * 3 + j] = k;
+ }
}
- // State j may follow a character in mode k: a new segment
- // in mode j starts after it (its header paid now).
- var switched = ((cur[k] as Number) + 5) / 6 * 6 + (head[j] as Number);
- if (switched < (cur[j] as Number)) {
- cur[j] = switched;
- choice[i * 3 + j] = k;
+ }
+ var swap = prev;
+ prev = cur;
+ cur = swap;
+ }
+ _prev = prev;
+ _cur = cur;
+ _i = stop;
+ if (_i >= n) {
+ // The cheapest final mode starts the walk back.
+ var best = 0;
+ for (var j = 1; j < 3; j++) {
+ if (_prev[j] < _prev[best]) {
+ best = j;
}
}
+ _state = best;
+ _charModes = new [n]b;
+ _walk = n;
}
- prev = cur;
}
- // The cheapest final mode, then walk back.
- var best = 0;
- for (var j = 1; j < 3; j++) {
- if ((prev[j] as Number) < (prev[best] as Number)) {
- best = j;
+
+ // From the end: the state after character i says which mode i was in.
+ private function walkBack(chars as Number) as Void {
+ var stop = _walk - chars > 0 ? _walk - chars : 0;
+ for (var i = _walk - 1; i >= stop; i--) {
+ var charMode = _choice[i * 3 + _state];
+ _charModes[i] = charMode;
+ _state = charMode;
+ }
+ _walk = stop;
+ if (_walk == 0) {
+ _choice = []b;
}
}
- // Walk back: the state after character i says which mode i was in.
- var charModes = new [n]b;
- var state = best;
- for (var i = n - 1; i >= 0; i--) {
- var charMode = choice[i * 3 + state];
- charModes[i] = charMode;
- state = charMode;
- }
- var segments = [] as Array<Number>;
- var start = 0;
- for (var i = 1; i <= n; i++) {
- if (i == n || charModes[i] != charModes[start]) {
- segments.add(modes[charModes[start]] as Number);
- segments.add(start);
- segments.add(i);
- start = i;
+
+ // From the start: runs of one mode become segments.
+ private function collect(chars as Number) as Void {
+ var n = _data.size();
+ var stop = _collected + chars < n ? _collected + chars : n;
+ for (var i = _collected + 1; i <= stop; i++) {
+ if (i == n || _charModes[i] != _charModes[_start]) {
+ segments.add(MODES[_charModes[_start]] as Number);
+ segments.add(_start);
+ segments.add(i);
+ _start = i;
+ }
+ }
+ _collected = stop;
+ if (_collected >= n) {
+ _charModes = []b;
+ _done = true;
}
}
- return segments;
}
// Bits needed for segments at version v.
@@ -162,25 +258,28 @@ module Qr {
return total;
}
- // A growing bit stream packed into codewords.
+ // A growing bit stream packed into codewords. Bits wait in `_acc`
+ // until a whole byte is ready.
class BitWriter {
var bytes as ByteArray = []b;
var bitCount as Number = 0;
+ private var _acc as Number = 0;
+ private var _accBits as Number = 0;
+
function initialize() {
}
+ // The low `count` bits of value (count <= 16), most significant first.
function put(value as Number, count as Number) as Void {
- for (var i = count - 1; i >= 0; i--) {
- if (bitCount % 8 == 0) {
- bytes.add(0);
- }
- if (((value >> i) & 1) == 1) {
- var at = bitCount / 8;
- bytes[at] = bytes[at] | (0x80 >> (bitCount % 8));
- }
- bitCount++;
+ _acc = (_acc << count) | (value & ((1 << count) - 1));
+ _accBits += count;
+ bitCount += count;
+ while (_accBits >= 8) {
+ _accBits -= 8;
+ bytes.add((_acc >> _accBits) & 0xFF);
}
+ _acc = _acc & ((1 << _accBits) - 1);
}
}
@@ -230,8 +329,32 @@ module Qr {
private var _score as Number = 0;
private var _dark as Number = 0;
private var _maskPass as Number = 0;
- private var _masked as ByteArray = []b;
- private var _prevRow as ByteArray = []b;
+ // Phase 0 (buildCodewords).
+ private var _prep as Number = PREP_SEGMENT;
+ private var _class as Number = 0;
+ private var _segmenter as Segmenter? = null;
+ private var _segments as Array<Number> = [] as Array<Number>;
+ private var _writer as BitWriter? = null;
+ private var _charAt as Number = -1;
+ private var _encoder as ReedSolomon.Encoder? = null;
+ // Packed lines (startPacking): rows and columns, their non-function
+ // modules, the current mask's patterns, scratch lines.
+ private var _words as Number = 0;
+ private var _rows as Array<Number> = [] as Array<Number>;
+ private var _rowFree as Array<Number> = [] as Array<Number>;
+ private var _cols as Array<Number> = [] as Array<Number>;
+ private var _colFree as Array<Number> = [] as Array<Number>;
+ private var _lineMask as Array<Number> = [] as Array<Number>;
+ private var _n1Valid as Array<Number> = [] as Array<Number>;
+ private var _n2Valid as Array<Number> = [] as Array<Number>;
+ private var _rowPattern as Array<Array<Number>> = [] as Array<Array<Number>>;
+ private var _columnPattern as Array<Array<Number>> = [] as Array<Array<Number>>;
+ private var _patternMask as Number = -1;
+ private var _cur as Array<Number> = [] as Array<Number>;
+ private var _prev as Array<Number> = [] as Array<Number>;
+
+ // The penalty of each mask, in order (empty with a forced mask).
+ var scores as Array<Number> = [] as Array<Number>;
// ec: 0 L, 1 M, 2 Q, 3 H. forceVersion 0 = smallest that fits;
// forceMask -1 = best by penalty.
@@ -245,7 +368,7 @@ module Qr {
// Progress 0-100 for the UI.
function percent() as Number {
- return _phase * 100 / 6;
+ return _phase * 100 / 7;
}
// The code as a generic matrix with QR's 4-module quiet zone.
@@ -264,55 +387,146 @@ module Qr {
} else if (_phase == 3) {
placeStep();
} else if (_phase == 4) {
- maskStep();
+ packStep();
} else if (_phase == 5) {
- finish();
+ maskStep();
+ } else if (_phase == 6) {
+ applyStep();
}
- return _phase >= 6 || error != null;
+ return _phase >= 7 || error != null;
}
- // Phase 0: segments, version, bit stream, blocks.
+ // Phase 0: segments and version, then the bit stream, then blocks,
+ // each in bounded steps:
+ // segment the dynamic programme for one version class (versions
+ // 1-9, 10-26, 27-40 count characters with different
+ // widths), SEGMENT_CHARS_PER_STEP characters per step,
+ // until a class fits the payload;
+ // write the segments into the bit stream, WRITE_CHARS_PER_STEP
+ // characters per step, then terminator and padding;
+ // split the codewords into the Reed-Solomon blocks.
private function buildCodewords() as Void {
+ if (_prep == PREP_SEGMENT) {
+ segmentStep();
+ } else if (_prep == PREP_WRITE) {
+ writeStep();
+ } else {
+ splitBlocks();
+ }
+ }
+
+ private function segmentStep() as Void {
if (_data.size() == 0) {
error = "QR needs some text";
return;
}
- var segments = null;
- var v = _version;
- if (v > 0) {
- segments = segment(_data, v);
- if (segmentBits(segments, v) > dataCodewords(v, _ec) * 8) {
- error = "too long for QR version " + v;
+ var lowest = [1, 10, 27] as Array<Number>;
+ var highest = [9, 26, 40] as Array<Number>;
+ var forced = _version;
+ var segmenter = _segmenter;
+ if (segmenter == null) {
+ if (forced > 0) {
+ _class = forced <= 9 ? 0 : (forced <= 26 ? 1 : 2);
+ }
+ // Skip a class the payload can't fit even as digits (10
+ // bits per 3 characters), without segmenting for it.
+ while (forced == 0 && _class < 2 && _data.size() * 10 / 3 > dataCodewords(highest[_class], _ec) * 8) {
+ _class++;
+ }
+ segmenter = new Segmenter(_data, lowest[_class]);
+ _segmenter = segmenter;
+ }
+ if (!segmenter.step(SEGMENT_CHARS_PER_STEP)) {
+ return;
+ }
+ _segmenter = null;
+ var segments = segmenter.segments;
+ var bits = segmentBits(segments, lowest[_class]);
+ if (forced > 0) {
+ if (bits > dataCodewords(forced, _ec) * 8) {
+ error = "too long for QR version " + forced;
return;
}
} else {
- for (var c = 0; c < 3 && v == 0; c++) {
- var lo = [1, 10, 27][c] as Number;
- var hi = [9, 26, 40][c] as Number;
- var s = segment(_data, lo);
- var bits = segmentBits(s, lo);
- for (var tryV = lo; tryV <= hi; tryV++) {
- if (bits <= dataCodewords(tryV, _ec) * 8) {
- v = tryV;
- segments = s;
- break;
- }
+ for (var v = lowest[_class]; v <= highest[_class] && _version == 0; v++) {
+ if (bits <= dataCodewords(v, _ec) * 8) {
+ _version = v;
}
}
- if (v == 0) {
- error = "too long for a QR code at this error correction level (" + _data.size() + " bytes, at most "
- + dataCodewords(40, _ec) + " in byte mode)";
+ if (_version == 0) {
+ _class++;
+ if (_class > 2) {
+ error = "too long for a QR code at this error correction level (" + _data.size() + " bytes, at most "
+ + dataCodewords(40, _ec) + " in byte mode)";
+ }
return;
}
}
- _version = v;
- var capacity = dataCodewords(v, _ec);
- var w = new BitWriter();
- var segs = segments as Array<Number>;
- for (var s = 0; s < segs.size(); s += 3) {
- writeSegment(w, segs[s], segs[s + 1], segs[s + 2]);
+ _segments = segments;
+ _writer = new BitWriter();
+ _cursor = 0;
+ _charAt = -1;
+ _prep = PREP_WRITE;
+ }
+
+ // Write segment characters; _cursor is the segment (index into the
+ // triples), _charAt the next character, -1 before its header.
+ private function writeStep() as Void {
+ var w = _writer as BitWriter;
+ var segs = _segments;
+ var d = _data;
+ var budget = WRITE_CHARS_PER_STEP;
+ while (budget > 0 && _cursor < segs.size()) {
+ var mode = segs[_cursor];
+ var end = segs[_cursor + 2];
+ if (_charAt < 0) {
+ _charAt = segs[_cursor + 1];
+ w.put(mode, 4);
+ w.put(end - _charAt, charCountBits(mode, _version));
+ }
+ var i = _charAt;
+ // Whole groups (3 digits, 2 alphanumerics) per step.
+ var group = mode == MODE_NUMERIC ? 3 : (mode == MODE_ALPHANUMERIC ? 2 : 1);
+ var take = budget / group * group;
+ var stop = i + (take > group ? take : group);
+ stop = stop < end ? stop : end;
+ budget -= stop - i;
+ if (mode == MODE_NUMERIC) {
+ while (i < stop) {
+ var count = stop - i >= 3 ? 3 : stop - i;
+ var value = 0;
+ for (var k = 0; k < count; k++) {
+ value = value * 10 + d[i + k] - 0x30;
+ }
+ w.put(value, [0, 4, 7, 10][count] as Number);
+ i += count;
+ }
+ } else if (mode == MODE_ALPHANUMERIC) {
+ while (i < stop) {
+ if (stop - i >= 2) {
+ w.put(alnumValue(d[i]) * 45 + alnumValue(d[i + 1]), 11);
+ i += 2;
+ } else {
+ w.put(alnumValue(d[i]), 6);
+ i += 1;
+ }
+ }
+ } else {
+ for (; i < stop; i++) {
+ w.put(d[i], 8);
+ }
+ }
+ _charAt = stop;
+ if (stop >= end) {
+ _cursor += 3;
+ _charAt = -1;
+ }
+ }
+ if (_cursor < segs.size()) {
+ return;
}
// Terminator, byte alignment, pad codewords.
+ var capacity = dataCodewords(_version, _ec);
var terminator = capacity * 8 - w.bitCount;
w.put(0, terminator < 4 ? terminator : 4);
if (w.bitCount % 8 != 0) {
@@ -323,8 +537,14 @@ module Qr {
w.bytes.add(pad);
pad = pad == 0xEC ? 0x11 : 0xEC;
}
- // Split into blocks.
- var i = ((v - 1) * 4 + _ec) * 5;
+ _segments = [] as Array<Number>;
+ _prep = PREP_SPLIT;
+ }
+
+ private function splitBlocks() as Void {
+ var bytes = (_writer as BitWriter).bytes;
+ _writer = null;
+ var i = ((_version - 1) * 4 + _ec) * 5;
var t = QrTables.BLOCKS;
var at = 0;
for (var g = 0; g < 2; g++) {
@@ -333,7 +553,7 @@ module Qr {
for (var b = 0; b < count; b++) {
var block = new [len] as Array<Number>;
for (var k = 0; k < len; k++) {
- block[k] = w.bytes[at + k];
+ block[k] = bytes[at + k];
}
at += len;
_blocks.add(block);
@@ -343,88 +563,89 @@ module Qr {
_phase = 1;
}
- private function writeSegment(w as BitWriter, mode as Number, start as Number, end as Number) as Void {
- var count = end - start;
- w.put(mode, 4);
- w.put(count, charCountBits(mode, _version));
- var d = _data;
- if (mode == MODE_NUMERIC) {
- var i = start;
- while (i < end) {
- var take = end - i >= 3 ? 3 : end - i;
- var value = 0;
- for (var k = 0; k < take; k++) {
- value = value * 10 + d[i + k] - 0x30;
- }
- w.put(value, [0, 4, 7, 10][take] as Number);
- i += take;
- }
- } else if (mode == MODE_ALPHANUMERIC) {
- var i = start;
- while (i < end) {
- if (end - i >= 2) {
- w.put(alnumValue(d[i]) * 45 + alnumValue(d[i + 1]), 11);
- i += 2;
- } else {
- w.put(alnumValue(d[i]), 6);
- i += 1;
- }
- }
- } else {
- for (var i = start; i < end; i++) {
- w.put(d[i], 8);
+ // Phase 1: Reed-Solomon codewords, ECC_WORK_PER_STEP data symbols
+ // times EC symbols per step (a version 40 block is 30 x 118).
+ private function eccStep() as Void {
+ var encoder = _encoder;
+ if (encoder == null) {
+ var i = ((_version - 1) * 4 + _ec) * 5;
+ var n = QrTables.BLOCKS[i] as Number;
+ var fresh = !theField().hasGenerator(n);
+ encoder = new ReedSolomon.Encoder(theField(), _blocks[_cursor], n);
+ _encoder = encoder;
+ if (fresh) {
+ return; // the generator took this step
}
}
- }
-
- // Phase 1: Reed-Solomon codewords, one block per step.
- private function eccStep() as Void {
- var i = ((_version - 1) * 4 + _ec) * 5;
- var n = QrTables.BLOCKS[i] as Number;
- _ecBlocks.add(ReedSolomon.ecc(theField(), _blocks[_cursor], n));
+ if (!encoder.step(ECC_WORK_PER_STEP / encoder.remainder.size())) {
+ return;
+ }
+ _encoder = null;
+ _ecBlocks.add(encoder.remainder);
_cursor++;
if (_cursor >= _blocks.size()) {
+ _cursor = 0;
+ _codewords = []b;
_phase = 2;
}
}
- // Phase 2: interleave codewords; draw the function patterns.
- private function interleaveAndDrawFunctions() as Void {
- var out = []b;
- var longest = 0;
- for (var b = 0; b < _blocks.size(); b++) {
- if (_blocks[b].size() > longest) {
- longest = _blocks[b].size();
- }
- }
- for (var k = 0; k < longest; k++) {
- for (var b = 0; b < _blocks.size(); b++) {
- if (k < _blocks[b].size()) {
- out.add(_blocks[b][k]);
+ // Phase 2: interleave the codewords, INTERLEAVE_PER_STEP per step
+ // (_cursor is the position within the blocks, data then EC), then
+ // draw the function patterns in a step of their own.
+ private function interleaveStep() as Void {
+ var longest = _blocks[_blocks.size() - 1].size(); // group-2 blocks are the longer ones
+ var ecLen = _ecBlocks[0].size();
+ var out = _codewords;
+ var budget = INTERLEAVE_PER_STEP;
+ while (_cursor < longest + ecLen && budget > 0) {
+ var k = _cursor;
+ var blocks = k < longest ? _blocks : _ecBlocks;
+ var at = k < longest ? k : k - longest;
+ for (var b = 0; b < blocks.size(); b++) {
+ if (at < blocks[b].size()) {
+ out.add(blocks[b][at]);
}
}
+ budget -= blocks.size();
+ _cursor++;
}
- var ecLen = _ecBlocks[0].size();
- for (var k = 0; k < ecLen; k++) {
- for (var b = 0; b < _ecBlocks.size(); b++) {
- out.add(_ecBlocks[b][k]);
- }
+ if (_cursor < longest + ecLen) {
+ return;
}
- _codewords = out;
_blocks = [] as Array<Array<Number>>;
_ecBlocks = [] as Array<Array<Number>>;
- drawFunctionPatterns();
- _cursor = _size - 1; // right column of the current column pair
- _bitIndex = 0;
- _phase = 3;
+ _cursor = -1;
}
- private function drawFunctionPatterns() as Void {
+ // After interleaving, _cursor counts down through the drawing:
+ // -1 timing and finder patterns, -2 - a alignment row a, then the
+ // format and version areas.
+ private function interleaveAndDrawFunctions() as Void {
+ if (_cursor >= 0) {
+ interleaveStep();
+ return;
+ }
+ var pos = QrTables.ALIGNMENT[_version - 1] as Array<Number>;
+ if (_cursor == -1) {
+ drawTimingAndFinders();
+ } else if (-2 - _cursor < pos.size()) {
+ drawAlignmentRow(pos, -2 - _cursor);
+ } else {
+ reserveFormatAndVersion();
+ _cursor = _size - 1; // right column of the current column pair
+ _bitIndex = 0;
+ _phase = 3;
+ return;
+ }
+ _cursor--;
+ }
+
+ private function drawTimingAndFinders() as Void {
var n = 17 + 4 * _version;
_size = n;
_m = new [n * n]b;
_function = new [n * n]b;
- // Timing patterns.
for (var i = 0; i < n; i++) {
setFunction(6, i, i % 2 == 0 ? 1 : 0);
setFunction(i, 6, i % 2 == 0 ? 1 : 0);
@@ -433,17 +654,21 @@ module Qr {
drawFinder(3, 3);
drawFinder(n - 4, 3);
drawFinder(3, n - 4);
- // Alignment patterns, except where they'd overlap the finders.
- var pos = QrTables.ALIGNMENT[_version - 1] as Array<Number>;
+ }
+
+ // Alignment patterns in row a, except where they'd overlap the finders.
+ private function drawAlignmentRow(pos as Array<Number>, a as Number) as Void {
var count = pos.size();
- for (var a = 0; a < count; a++) {
- for (var b = 0; b < count; b++) {
- if ((a == 0 && b == 0) || (a == 0 && b == count - 1) || (a == count - 1 && b == 0)) {
- continue;
- }
- drawAlignment(pos[a], pos[b]);
+ for (var b = 0; b < count; b++) {
+ if ((a == 0 && b == 0) || (a == 0 && b == count - 1) || (a == count - 1 && b == 0)) {
+ continue;
}
+ drawAlignment(pos[a], pos[b]);
}
+ }
+
+ private function reserveFormatAndVersion() as Void {
+ var n = _size;
// Format areas and the dark module's place stay light until the
// mask is chosen (segno evaluates masks that way; ISO 18004:2015
// 7.8 leaves format information out of the evaluation).
@@ -503,7 +728,7 @@ module Qr {
private function placeStep() as Void {
var n = _size;
var totalBits = _codewords.size() * 8;
- var budget = MODULES_PER_STEP;
+ var budget = PLACE_MODULES_PER_STEP;
while (_cursor >= 1 && budget > 0) {
var right = _cursor == 6 ? 5 : _cursor;
var upward = ((right + 1) & 2) == 0;
@@ -523,32 +748,167 @@ module Qr {
}
if (_cursor < 1) {
_codewords = []b;
- _maskIndex = _forceMask >= 0 ? _forceMask : 0;
_cursor = 0;
- _score = 0;
- _dark = 0;
+ startPacking();
_phase = 4;
}
}
- // Phase 4: score masks (or apply the forced one). Per mask, two
- // passes: the rows (apply the mask into a copy, then N1, N2, N3 and
- // the dark count), then the columns (N1, N3), each in steps sized to
- // its cost per module.
+ // ---- Packed lines (M8) ----------------------------------------
+ // Mask scoring works on lines packed 16 modules per Number, so the
+ // penalty rules run as a few bit operations per 16 modules instead
+ // of a loop per module. Line layout: module i of a row or column is
+ // bit (i + PAD) of the line, counted from bit 0 of word 0; PAD light
+ // bits before the line and at least 16 after it let the N3 test look
+ // 4 modules back and 10 ahead without special cases (outside the
+ // symbol counts as light, which is what segno's clipped checks do).
+ // Rows and columns are packed once; each mask is then
+ // data XOR (pattern AND not-function), word by word.
+
+ private function startPacking() as Void {
+ var n = _size;
+ var w = (n + PAD) / 16 + 2;
+ _words = w;
+ // Zeroed line by line with addAll (native), not word by word.
+ var zeros = new [w] as Array<Number>;
+ for (var k = 0; k < w; k++) {
+ zeros[k] = 0;
+ }
+ _rows = [] as Array<Number>;
+ _rowFree = [] as Array<Number>;
+ _cols = [] as Array<Number>;
+ _colFree = [] as Array<Number>;
+ for (var line = 0; line < n; line++) {
+ _rows.addAll(zeros);
+ _rowFree.addAll(zeros);
+ _cols.addAll(zeros);
+ _colFree.addAll(zeros);
+ }
+ _lineMask = bitRange(PAD, n + PAD - 1);
+ _n1Valid = bitRange(PAD, n + PAD - 5);
+ _n2Valid = bitRange(PAD, n + PAD - 2);
+ _cur = new [w] as Array<Number>;
+ _prev = new [w] as Array<Number>;
+ }
+
+ // Words with bits first..last set.
+ private function bitRange(first as Number, last as Number) as Array<Number> {
+ var out = new [_words] as Array<Number>;
+ for (var k = 0; k < _words; k++) {
+ var word = 0;
+ for (var b = 0; b < 16; b++) {
+ var p = k * 16 + b;
+ if (p >= first && p <= last) {
+ word |= 1 << b;
+ }
+ }
+ out[k] = word;
+ }
+ return out;
+ }
+
+ // Phase 4: pack rows and columns (modules and the non-function
+ // mask), a few rows per step.
+ private function packStep() as Void {
+ var n = _size;
+ var w = _words;
+ var rows = PACK_MODULES_PER_STEP / n;
+ for (var r = 0; r < rows && _cursor < n; r++) {
+ var y = _cursor;
+ var py = y + PAD;
+ var colWord = py >> 4;
+ var colBit = 1 << (py & 15);
+ for (var x = 0; x < n; x++) {
+ var at = y * n + x;
+ var px = x + PAD;
+ var rowAt = y * w + (px >> 4);
+ var rowBit = 1 << (px & 15);
+ var colAt = x * w + colWord;
+ if (_m[at] != 0) {
+ _rows[rowAt] = _rows[rowAt] | rowBit;
+ _cols[colAt] = _cols[colAt] | colBit;
+ }
+ if (_function[at] == 0) {
+ _rowFree[rowAt] = _rowFree[rowAt] | rowBit;
+ _colFree[colAt] = _colFree[colAt] | colBit;
+ }
+ }
+ _cursor++;
+ }
+ if (_cursor < n) {
+ return;
+ }
+ _function = []b;
+ _cursor = 0;
+ _maskIndex = _forceMask >= 0 ? _forceMask : 0;
+ _patternMask = -1;
+ _score = 0;
+ _dark = 0;
+ _maskPass = 0;
+ _phase = 5;
+ }
+
+ // Mask m as packed lines: rowPattern[r] is row r (mod 12), across
+ // the columns; columnPattern[c] is column c (mod 12), down the
+ // rows. Every mask repeats every 12 modules along a row or a
+ // column (mask 4 every 6 across and 4 down, the others every 2, 3
+ // or 6), and every 12 lines; so twelve values are computed and
+ // repeated over 48 bits (three words, a multiple of 12), then over
+ // the line. maskBit gets coordinates shifted by 12 (the same mask
+ // value) so they are never negative.
+ private function buildPatterns(m as Number) as Void {
+ _rowPattern = [] as Array<Array<Number>>;
+ _columnPattern = [] as Array<Array<Number>>;
+ for (var r = 0; r < 12; r++) {
+ _rowPattern.add(repeated(m, r, true));
+ _columnPattern.add(repeated(m, r, false));
+ }
+ _patternMask = m;
+ }
+
+ private function repeated(m as Number, fixed as Number, alongRow as Boolean) as Array<Number> {
+ // Twelve mask values, starting at line position -PAD (+12).
+ var twelve = new [12]b;
+ for (var i = 0; i < 12; i++) {
+ if (alongRow ? maskBit(m, 12 - PAD + i, fixed) : maskBit(m, fixed, 12 - PAD + i)) {
+ twelve[i] = 1;
+ }
+ }
+ var period = [0, 0, 0];
+ var at = 0;
+ for (var p = 0; p < 48; p++) {
+ if (twelve[at] != 0) {
+ period[p >> 4] = (period[p >> 4] as Number) | (1 << (p & 15));
+ }
+ at = at == 11 ? 0 : at + 1;
+ }
+ var out = new [_words] as Array<Number>;
+ for (var k = 0; k < _words; k++) {
+ out[k] = (period[k % 3] as Number) & (_lineMask[k] as Number);
+ }
+ return out;
+ }
+
+ // Phase 5: score masks (or take the forced one). Per mask: build its
+ // patterns (one step), then the rows (N1, N2, N3 and the dark count)
+ // and the columns (N1, N3), a bounded number of words per step.
private function maskStep() as Void {
if (_forceMask >= 0) {
_bestMask = _forceMask;
- _phase = 5;
+ _phase = 6;
+ return;
+ }
+ if (_patternMask != _maskIndex) {
+ buildPatterns(_maskIndex);
return;
}
var n = _size;
- var lines = (_maskPass == 0 ? ROW_MODULES_PER_STEP : COLUMN_MODULES_PER_STEP) / n;
+ var lines = (_maskPass == 0 ? ROW_WORDS_PER_STEP : COLUMN_WORDS_PER_STEP) / _words;
if (lines < 1) {
lines = 1;
}
for (var l = 0; l < lines && _cursor < n; l++) {
if (_maskPass == 0) {
- applyMaskRow(_cursor);
scoreRow(_cursor);
} else {
scoreColumn(_cursor);
@@ -567,6 +927,7 @@ module Qr {
var total = n * n;
var deviation = (_dark * 100 - 50 * total).abs();
_score += 10 * (deviation / (5 * total));
+ scores.add(_score);
if (_score < _bestScore) {
_bestScore = _score;
_bestMask = _maskIndex;
@@ -576,126 +937,135 @@ module Qr {
_score = 0;
_dark = 0;
if (_maskIndex >= 8) {
- _masked = []b;
- _prevRow = []b;
- _phase = 5;
+ _phase = 6;
}
}
- private function applyMaskRow(y as Number) as Void {
- var n = _size;
- if (_masked.size() != n * n) {
- _masked = new [n * n]b;
- }
- var m = _maskIndex;
- var row = y * n;
- for (var x = 0; x < n; x++) {
- var at = row + x;
- var bit = _m[at];
- if (_function[at] == 0 && maskBit(m, x, y)) {
- bit = bit ^ 1;
- }
- _masked[at] = bit;
+ // Masked line into `into`: data XOR (pattern AND free).
+ private function maskLine(data as Array<Number>, free as Array<Number>, pattern as Array<Number>, line as Number,
+ into as Array<Number>) as Void {
+ var base = line * _words;
+ for (var k = 0; k < _words; k++) {
+ into[k] = (data[base + k] as Number) ^ ((pattern[k] as Number) & (free[base + k] as Number));
}
}
private function scoreRow(y as Number) as Void {
- var n = _size;
- var row = _masked.slice(y * n, y * n + n);
- _score += lineScore(row);
- var prev = _prevRow;
+ var swap = _prev;
+ _prev = _cur;
+ _cur = swap;
+ var cur = _cur;
+ maskLine(_rows, _rowFree, _rowPattern[y % 12], y, cur);
+ _score += lineScore(cur);
+ var pop = popTable();
var dark = 0;
- for (var x = 0; x < n; x++) {
- var c = row[x];
- dark += c;
- // N2: a 2x2 block of one colour with the row above.
- if (y > 0 && x > 0 && c == row[x - 1] && c == prev[x] && c == prev[x - 1]) {
- _score += 3;
- }
+ for (var k = 0; k < _words; k++) {
+ var c = cur[k] as Number;
+ dark += pop[c & 0xFF] + pop[c >> 8];
}
_dark += dark;
- _prevRow = row;
+ if (y == 0) {
+ return;
+ }
+ // N2: 3 for every 2x2 block of one colour (with the row above).
+ var prev = _prev;
+ var blocks = 0;
+ for (var k = 0; k < _words - 1; k++) {
+ var c = (cur[k] as Number) | ((cur[k + 1] as Number) << 16);
+ var q = (prev[k] as Number) | ((prev[k + 1] as Number) << 16);
+ var same = ~((c ^ (c >> 1)) | (q ^ (q >> 1)) | (c ^ q)) & (_n2Valid[k] as Number);
+ if (same != 0) {
+ blocks += pop[same & 0xFF] + pop[same >> 8];
+ }
+ }
+ _score += 3 * blocks;
}
private function scoreColumn(x as Number) as Void {
- var n = _size;
- var column = new [n]b;
- for (var y = 0; y < n; y++) {
- column[y] = _masked[y * n + x];
- }
- _score += lineScore(column);
+ maskLine(_cols, _colFree, _columnPattern[x % 12], x, _cur);
+ _score += lineScore(_cur);
}
- // N1 and N3 of one row or column, in one pass. N1: 3 + (run - 5)
- // for every run of 5 or more equal modules. N3 as segno counts it:
- // 40 for each 1011101 with light modules (up to 4, fewer at the edge)
- // before or after it; after a counted match the search continues
- // behind it, otherwise 4 modules on (found with a rolling 7-bit window).
- private function lineScore(line as ByteArray) as Number {
- var n = line.size();
+ // N1 and N3 of one packed row or column.
+ // N1: 3 + (run - 5) for every run of 5 or more equal modules. A
+ // run of length L has L - 4 windows of 5 equal modules, so N1
+ // is (windows) + 2 * (groups of windows).
+ // N3, as segno counts it: 40 for each 1011101 with 4 light
+ // modules before or after it (fewer at the edge); after a
+ // counted match the search continues behind it, otherwise 4
+ // modules on. Matches are rare, so they are walked one by one.
+ // Positions s below are bit positions 4 before a match's first
+ // module, so every look stays within the two words combined in e.
+ private function lineScore(line as Array<Number>) as Number {
+ var pop = popTable();
var score = 0;
- var run = 1;
- var window = line[0];
+ var carry = 0;
var nextAllowed = 0;
- for (var k = 1; k < n; k++) {
- var c = line[k];
- if (c == line[k - 1]) {
- run++;
- } else {
- if (run >= 5) {
- score += run - 2;
- }
- run = 1;
+ for (var k = 0; k < _words - 1; k++) {
+ var e = (line[k] as Number) | ((line[k + 1] as Number) << 16);
+ var same = ~(e ^ (e >> 1));
+ var windows = same & (same >> 1) & (same >> 2) & (same >> 3) & (_n1Valid[k] as Number);
+ if (windows != 0) {
+ var starts = windows & ~((windows << 1) | carry) & 0xFFFF;
+ score += pop[windows & 0xFF] + pop[windows >> 8] + 2 * (pop[starts & 0xFF] + pop[starts >> 8]);
}
- window = ((window << 1) | c) & 0x7F;
- if (window == 0x5D && k >= 6) {
- var idx = k - 6;
- if (idx >= nextAllowed) {
- var after = idx + 7;
- if (idx == 0 || idx == n - 7 || allLight(line, idx - 4 < 0 ? 0 : idx - 4, idx)
- || allLight(line, after, after + 4 > n ? n : after + 4)) {
- score += 40;
- nextAllowed = idx + 7;
- } else {
- nextAllowed = idx + 4;
- }
+ carry = (windows >> 15) & 1;
+ var match = (e >> 4) & ~(e >> 5) & (e >> 6) & (e >> 7) & (e >> 8) & ~(e >> 9) & (e >> 10) & 0xFFFF;
+ if (match == 0) {
+ continue;
+ }
+ var light = ~(e | (e >> 1) | (e >> 2) | (e >> 3)) | ~((e >> 11) | (e >> 12) | (e >> 13) | (e >> 14));
+ for (var b = 0; b < 16; b++) {
+ var s = k * 16 + b;
+ if (((match >> b) & 1) == 0 || s < nextAllowed) {
+ continue;
+ }
+ if (((light >> b) & 1) != 0) {
+ score += 40;
+ nextAllowed = s + 7;
+ } else {
+ nextAllowed = s + 4;
}
}
}
- if (run >= 5) {
- score += run - 2;
- }
return score;
}
- private function allLight(line as ByteArray, from as Number, to as Number) as Boolean {
- for (var i = from; i < to; i++) {
- if (line[i] != 0) {
- return false;
- }
+ // Phase 6: apply the chosen mask to the module matrix, a few rows
+ // per step (version 40 has 31,329 modules: far too many for one
+ // event).
+ private function applyStep() as Void {
+ if (_patternMask != _bestMask) {
+ buildPatterns(_bestMask);
+ return;
}
- return true;
- }
-
- // Phase 5: apply the chosen mask, draw format and version info.
- private function finish() as Void {
var n = _size;
- var m = _bestMask;
- for (var y = 0; y < n; y++) {
+ var rows = APPLY_MODULES_PER_STEP / n;
+ for (var r = 0; r < rows && _cursor < n; r++) {
+ var y = _cursor;
+ maskLine(_rows, _rowFree, _rowPattern[y % 12], y, _cur);
+ var row = y * n;
for (var x = 0; x < n; x++) {
- var at = y * n + x;
- if (_function[at] == 0 && maskBit(m, x, y)) {
- _m[at] = _m[at] ^ 1;
- }
+ var p = x + PAD;
+ _m[row + x] = ((_cur[p >> 4] as Number) >> (p & 15)) & 1;
}
+ _cursor++;
}
- drawFormat(m);
+ if (_cursor < n) {
+ return;
+ }
+ _rows = [] as Array<Number>;
+ _rowFree = [] as Array<Number>;
+ _cols = [] as Array<Number>;
+ _colFree = [] as Array<Number>;
+ _rowPattern = [] as Array<Array<Number>>;
+ _columnPattern = [] as Array<Array<Number>>;
+ drawFormat(_bestMask);
if (_version >= 7) {
drawVersion();
}
- result = new MatrixCode(n, _m, _version, m);
- _function = []b;
- _phase = 6;
+ result = new MatrixCode(n, _m, _version, _bestMask);
+ _phase = 7;
}
private function drawFormat(mask as Number) as Void {
@@ -707,21 +1077,21 @@ module Qr {
var bits = ((data << 10) | rem) ^ 0x5412;
var n = _size;
for (var i = 0; i <= 5; i++) {
- setFunction(8, i, (bits >> i) & 1);
+ setModule(8, i, (bits >> i) & 1);
}
- setFunction(8, 7, (bits >> 6) & 1);
- setFunction(8, 8, (bits >> 7) & 1);
- setFunction(7, 8, (bits >> 8) & 1);
+ setModule(8, 7, (bits >> 6) & 1);
+ setModule(8, 8, (bits >> 7) & 1);
+ setModule(7, 8, (bits >> 8) & 1);
for (var i = 9; i < 15; i++) {
- setFunction(14 - i, 8, (bits >> i) & 1);
+ setModule(14 - i, 8, (bits >> i) & 1);
}
for (var i = 0; i < 8; i++) {
- setFunction(n - 1 - i, 8, (bits >> i) & 1);
+ setModule(n - 1 - i, 8, (bits >> i) & 1);
}
for (var i = 8; i < 15; i++) {
- setFunction(8, n - 15 + i, (bits >> i) & 1);
+ setModule(8, n - 15 + i, (bits >> i) & 1);
}
- setFunction(8, n - 8, 1);
+ setModule(8, n - 8, 1);
}
private function drawVersion() as Void {
@@ -735,10 +1105,29 @@ module Qr {
var bit = (bits >> i) & 1;
var a = n - 11 + i % 3;
var b = i / 3;
- setFunction(a, b, bit);
- setFunction(b, a, bit);
+ setModule(a, b, bit);
+ setModule(b, a, bit);
+ }
+ }
+
+ private function setModule(x as Number, y as Number, dark as Number) as Void {
+ _m[y * _size + x] = dark;
+ }
+ }
+
+ // A 256-entry table of set-bit counts, built on first use.
+ var popCounts as ByteArray? = null;
+
+ function popTable() as ByteArray {
+ var t = popCounts;
+ if (t == null) {
+ t = new [256]b;
+ for (var i = 1; i < 256; i++) {
+ t[i] = t[i >> 1] + (i & 1);
}
+ popCounts = t;
}
+ return t;
}
// Mask pattern m at column x, row y: true flips a data module.
diff --git a/source/codes/ReedSolomon.mc b/source/codes/ReedSolomon.mc
index a69fb1a..58339ae 100644
--- a/source/codes/ReedSolomon.mc
+++ b/source/codes/ReedSolomon.mc
@@ -1,12 +1,18 @@
// Author: cirakg & claude ai
-// Summary: Reed-Solomon error-correction codewords over GF(2^m) for QR (and later Data Matrix and Aztec).
+// Summary: Reed-Solomon error-correction codewords over GF(2^m) for QR, Data Matrix and Aztec, resumable for large symbols.
//
// A Field is GF(2^m) with a primitive polynomial and the exponent the
// generator's roots start at (QR: 0x11D, base 0; Data Matrix: 0x12D,
-// base 1). Generator polynomials are cached per field and length. ecc()
-// is the remainder of data * x^n divided by the generator: the standard
-// systematic encoding, one multiply-accumulate per data symbol and EC
-// symbol, so a caller budgets blocks per job step.
+// base 1). Generator polynomials are cached per field and length. The
+// error-correction symbols are the remainder of data * x^n divided by the
+// generator: the standard systematic encoding, one multiply-accumulate per
+// data symbol and EC symbol.
+//
+// Large Aztec symbols need GF(4096) and up to 832 EC symbols, far more than
+// one event's watchdog budget, so every part can run in steps: the field's
+// tables (grow), the generator (buildGenerator) and the division (Encoder).
+// Fields of up to 256 elements are built at once (a few thousand
+// bytecodes).
import Toybox.Lang;
@@ -14,31 +20,64 @@ module ReedSolomon {
class Field {
var size as Number;
+ // exp has 2 * size entries once ready (exp[a + b] needs no modulo).
var exp as Array<Number>;
var log as Array<Number>;
var base as Number;
+ private var _primitive as Number;
+ private var _filled as Number = 0;
+ private var _x as Number = 1;
private var _generators as Dictionary<Number, Array<Number>> = {} as Dictionary<Number, Array<Number>>;
+ // The generator being built in steps: its length, coefficients so far
+ // (leading 1 first) and the next root.
+ private var _pendingN as Number = 0;
+ private var _pending as Array<Number> = [] as Array<Number>;
+ private var _pendingRoot as Number = 0;
// primitive: the field polynomial including x^m (0x11D for GF(256)).
function initialize(primitive as Number, fieldSize as Number, generatorBase as Number) {
size = fieldSize;
base = generatorBase;
- exp = new [2 * fieldSize] as Array<Number>;
+ _primitive = primitive;
+ exp = new [fieldSize - 1] as Array<Number>;
log = new [fieldSize] as Array<Number>;
- var x = 1;
- for (var i = 0; i < fieldSize - 1; i++) {
+ log[0] = 0;
+ if (fieldSize <= 256) {
+ grow(fieldSize);
+ }
+ }
+
+ function isReady() as Boolean {
+ return exp.size() == 2 * size;
+ }
+
+ // Fill up to `count` more table entries; true when the field is ready.
+ function grow(count as Number) as Boolean {
+ if (isReady()) {
+ return true;
+ }
+ var stop = _filled + count < size - 1 ? _filled + count : size - 1;
+ var x = _x;
+ for (var i = _filled; i < stop; i++) {
exp[i] = x;
log[x] = i;
x <<= 1;
- if (x >= fieldSize) {
- x ^= primitive;
+ if (x >= size) {
+ x ^= _primitive;
}
}
- // Doubled table: exp[a + b] needs no modulo.
- for (var i = fieldSize - 1; i < 2 * fieldSize; i++) {
- exp[i] = exp[i - (fieldSize - 1)];
+ _x = x;
+ _filled = stop;
+ if (_filled < size - 1) {
+ return false;
}
- log[0] = 0;
+ // Doubled table: exp[size - 1 + j] = exp[j] for j = 0 .. size.
+ var first = exp;
+ exp = first.slice(0, null) as Array<Number>;
+ exp.addAll(first);
+ exp.add(first[0]);
+ exp.add(first[1]);
+ return true;
}
function multiply(a as Number, b as Number) as Number {
@@ -48,63 +87,115 @@ module ReedSolomon {
return exp[log[a] + log[b]];
}
- // Coefficients of the generator with n roots, highest degree first,
- // without the leading 1.
- function generator(n as Number) as Array<Number> {
- var cached = _generators[n];
- if (cached != null) {
- return cached;
+ // True when generator(n) is cached.
+ function hasGenerator(n as Number) as Boolean {
+ return _generators.hasKey(n);
+ }
+
+ // Build generator(n) with about `work` coefficient updates (at least
+ // one root); true when it is cached.
+ function buildGenerator(n as Number, work as Number) as Boolean {
+ if (_generators.hasKey(n)) {
+ return true;
+ }
+ if (_pendingN != n) {
+ _pendingN = n;
+ _pending = [1] as Array<Number>;
+ _pendingRoot = 0;
}
- var g = [1] as Array<Number>;
- for (var i = 0; i < n; i++) {
+ var g = _pending;
+ var budget = work;
+ while (_pendingRoot < n && budget > 0) {
// Multiply by (x - a^(base + i)); in GF(2^m) minus is plus.
- var root = exp[(base + i) % (size - 1)];
+ var logRoot = (base + _pendingRoot) % (size - 1);
var next = new [g.size() + 1] as Array<Number>;
- for (var k = 0; k < next.size(); k++) {
- next[k] = 0;
- }
- for (var k = 0; k < g.size(); k++) {
- next[k] = next[k] ^ g[k];
- next[k + 1] = next[k + 1] ^ multiply(g[k], root);
+ next[0] = g[0];
+ for (var k = 1; k < g.size(); k++) {
+ var c = g[k - 1];
+ next[k] = g[k] ^ (c == 0 ? 0 : exp[log[c] + logRoot]);
}
+ var last = g[g.size() - 1];
+ next[g.size()] = last == 0 ? 0 : exp[log[last] + logRoot];
+ budget -= g.size();
g = next;
+ _pendingRoot++;
+ }
+ _pending = g;
+ if (_pendingRoot < n) {
+ return false;
}
- var coefficients = g.slice(1, null) as Array<Number>;
- _generators[n] = coefficients;
- return coefficients;
+ _generators[n] = g.slice(1, null) as Array<Number>;
+ _pendingN = 0;
+ _pending = [] as Array<Number>;
+ return true;
+ }
+
+ // Coefficients of the generator with n roots, highest degree first,
+ // without the leading 1.
+ function generator(n as Number) as Array<Number> {
+ buildGenerator(n, 0x7FFFFFFF);
+ return _generators[n] as Array<Number>;
}
}
- // The n error-correction symbols of data (both as symbol values).
+ // The n error-correction symbols of data (both as symbol values), in
+ // one go. Jobs use Encoder to spread a long block over several steps.
function ecc(field as Field, data as Array<Number>, n as Number) as Array<Number> {
- var g = field.generator(n);
- var rem = new [n] as Array<Number>;
- for (var k = 0; k < n; k++) {
- rem[k] = 0;
- }
- var exp = field.exp;
- var log = field.log;
- var logG = new [n] as Array<Number>;
- for (var k = 0; k < n; k++) {
- logG[k] = g[k] == 0 ? -1 : log[g[k]];
- }
- for (var i = 0; i < data.size(); i++) {
- var factor = data[i] ^ rem[0];
- // Shift the remainder left by one symbol.
- for (var k = 0; k < n - 1; k++) {
- rem[k] = rem[k + 1];
+ var encoder = new Encoder(field, data, n);
+ encoder.step(data.size());
+ return encoder.remainder;
+ }
+
+ // ecc() as a resumable computation: step() feeds a number of data
+ // symbols; `remainder` holds the error-correction symbols at the end.
+ // The field must be ready and generator(n) cached (or cheap).
+ class Encoder {
+ var remainder as Array<Number>;
+
+ private var _exp as Array<Number>;
+ private var _log as Array<Number>;
+ private var _logG as Array<Number>;
+ private var _data as Array<Number>;
+ private var _i as Number = 0;
+
+ function initialize(field as Field, data as Array<Number>, n as Number) {
+ var g = field.generator(n);
+ _exp = field.exp;
+ _log = field.log;
+ _data = data;
+ remainder = new [n] as Array<Number>;
+ _logG = new [n] as Array<Number>;
+ for (var k = 0; k < n; k++) {
+ remainder[k] = 0;
+ _logG[k] = g[k] == 0 ? -1 : _log[g[k]];
}
- rem[n - 1] = 0;
- if (factor != 0) {
- var lf = log[factor];
- for (var k = 0; k < n; k++) {
- var lg = logG[k];
- if (lg >= 0) {
- rem[k] = rem[k] ^ exp[lf + lg];
+ }
+
+ // Up to `symbols` more data symbols; true when all are in.
+ function step(symbols as Number) as Boolean {
+ var rem = remainder;
+ var n = rem.size();
+ var exp = _exp;
+ var logG = _logG;
+ var stop = _i + symbols < _data.size() ? _i + symbols : _data.size();
+ for (var i = _i; i < stop; i++) {
+ var factor = _data[i] ^ rem[0];
+ // Shift the remainder left by one symbol (natively).
+ rem = rem.slice(1, null) as Array<Number>;
+ rem.add(0);
+ if (factor != 0) {
+ var lf = _log[factor];
+ for (var k = 0; k < n; k++) {
+ var lg = logG[k];
+ if (lg >= 0) {
+ rem[k] = rem[k] ^ exp[lf + lg];
+ }
}
}
}
+ remainder = rem;
+ _i = stop;
+ return _i >= _data.size();
}
- return rem;
}
}
diff --git a/source/settings/CodeForm.mc b/source/settings/CodeForm.mc
index 89d519d..810cd6e 100644
--- a/source/settings/CodeForm.mc
+++ b/source/settings/CodeForm.mc
@@ -115,7 +115,8 @@ module CodeForm {
// Encodes a code once to prove it is valid and to check that it fits this
// screen. When step() returns true: error is set, or the code is fine
-// (notes may hold remarks) and, for 2D codes, cache holds Matrix.toBytes().
+// (notes may hold remarks) and, for 2D codes, cache holds the packed matrix
+// (Codes.Packed).
class CodeCheck extends Jobs.Job {
// Linear codes draw with 10 quiet modules on each side (CodeView).
@@ -129,7 +130,7 @@ class CodeCheck extends Jobs.Job {
private var _payload as String;
private var _options as Number;
private var _job as Codes.MatrixJob? = null;
- private var _matrix as Codes.Matrix? = null;
+ private var _packer as Codes.Packer? = null;
private var _started as Boolean = false;
private var _w as Number;
private var _h as Number;
@@ -157,11 +158,14 @@ class CodeCheck extends Jobs.Job {
_payload = "";
return false;
}
- var matrix = _matrix;
- if (matrix != null) {
- // Packing gets a step of its own (it runs over every module).
- cache = matrix.toBytes();
- _matrix = null;
+ var packer = _packer;
+ if (packer != null) {
+ // Packing runs in steps of its own (it visits every module).
+ if (!packer.step(budget)) {
+ return false;
+ }
+ cache = packer.result;
+ _packer = null;
return true;
}
var job = _job as Codes.MatrixJob;
@@ -181,7 +185,7 @@ class CodeCheck extends Jobs.Job {
if (!Codes.fits(totalW, totalH, px, _w, _h, _round)) {
notes.add("Too large for this screen: needs " + px + " px per module, " + totalW * px + " x " + totalH * px + " px");
}
- _matrix = m;
+ _packer = new Codes.Packer(m);
return false;
}
diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc
index 88de402..20b9bf2 100644
--- a/source/ui/CodeView.mc
+++ b/source/ui/CodeView.mc
@@ -12,10 +12,20 @@
// circle (round screens) or the screen (Codes.fits): at least 2 px for
// square codes, 1 px for PDF417 (a stacked linear code).
// * 2D codes are encoded once, as a job on a timer ("Encoding... 40%"),
-// and cached sealed with the entry's key (Vault.saveCodeCache), so the
-// next opening is instant.
+// and cached packed and sealed with the entry's key
+// (Vault.saveCodeCache), so later openings skip the encoding. Either
+// way a short job turns the packed modules into dark runs (Codes.Runs),
+// and onUpdate draws one rectangle per run (module by module would
+// trip the watchdog with large codes). Codes with more runs than one
+// event can draw are painted into a buffered bitmap in steps first
+// (RunsPainter), and onUpdate draws the bitmap.
// * Human-readable text under linear codes when enabled and there's room.
-// * Brightness: the backlight is turned on when shown and on every tap.
+// * Brightness (option, on by default): AMOLED watches get full
+// brightness, MIP watches the backlight, renewed every RENEW_MS for up
+// to BOOST_MS (about a minute, within the burn-in protection limit of
+// AMOLED screens); a tap starts the minute again. The view's one
+// ticker timer does the renewing (and redraws the progress while
+// encoding), so a code screen never needs more timers.
// * Too big: a message with the size needed.
// The payload is decrypted when the screen opens and wiped once encoded.
@@ -31,15 +41,23 @@ class CodeView extends WatchUi.View {
static const QUIET_MODULES = 10;
static const MIN_LINEAR_PX = 1;
static const MIN_MATRIX_PX = 2;
+ static const BOOST_MS = 60000;
+ static const RENEW_MS = 5000;
+ static const TICK_MS = 250;
var header as Entry;
- private var _code as Codes.LinearCode or Codes.Matrix or Codes.CodeError or Null = null;
+ private var _code as Codes.LinearCode or RunsPainter or Codes.CodeError or Null = null;
private var _showText as Boolean = true;
private var _shownLogged as Boolean = false;
- private var _job as Codes.MatrixJob? = null;
+ private var _job as Codes.DisplayJob or RunsPainter or Null = null;
+ private var _cache as ByteArray? = null;
private var _runner as Jobs.JobRunner? = null;
private var _ticker as Timer.Timer? = null;
private var _startedAt as Number = 0;
+ // Brightness: when the current minute started and the last renewal;
+ // _boostStart is -1 when there is nothing to renew.
+ private var _boostStart as Number = -1;
+ private var _lastBoost as Number = 0;
// The cache-saving one-shot timer (kept so it is not collected).
var saveTimer as Timer.Timer? = null;
@@ -56,6 +74,8 @@ class CodeView extends WatchUi.View {
function onHide() as Void {
stopJob();
+ _boostStart = -1;
+ stopTicker();
_code = null;
}
@@ -63,10 +83,9 @@ class CodeView extends WatchUi.View {
var vault = AppState.theVault();
try {
if (Codes.isMatrix(header.type)) {
- var cached = vault.loadCodeCache(header.id);
- var matrix = cached != null ? Codes.Matrix.fromBytes(cached) : null;
- if (matrix != null) {
- _code = matrix;
+ var packed = Codes.Packed.parse(vault.loadCodeCache(header.id));
+ if (packed != null) {
+ startJob(new Codes.DisplayJob(null, packed));
return;
}
}
@@ -79,7 +98,7 @@ class CodeView extends WatchUi.View {
var screen = System.getDeviceSettings();
var job = Codes.matrixJob(entry.type, payload, entry.options, screen.screenWidth, screen.screenHeight, Ui.isRound());
if (job != null) {
- startJob(job);
+ startJob(new Codes.DisplayJob(job, null));
} else {
_code = new Codes.CodeError(Codes.name(entry.type) + " is not supported yet");
}
@@ -92,53 +111,104 @@ class CodeView extends WatchUi.View {
}
}
- private function startJob(job as Codes.MatrixJob) as Void {
+ // The first step runs now: a small cached code is then ready for the
+ // first draw.
+ private function startJob(job as Codes.DisplayJob or RunsPainter) as Void {
_startedAt = System.getTimer();
_job = job;
+ if (job.step(1)) {
+ onEncoded();
+ return;
+ }
var runner = new Jobs.JobRunner(job, 1, method(:onEncoded));
_runner = runner;
runner.start();
- var ticker = new Timer.Timer();
- ticker.start(method(:onTick), 250, true);
- _ticker = ticker;
+ startTicker();
+ }
+
+ private function startTicker() as Void {
+ if (_ticker == null) {
+ var ticker = new Timer.Timer();
+ ticker.start(method(:onTick), TICK_MS, true);
+ _ticker = ticker;
+ }
+ }
+
+ private function stopTicker() as Void {
+ var ticker = _ticker;
+ if (ticker != null) {
+ ticker.stop();
+ }
+ _ticker = null;
}
+ // Progress redraws while a job runs; brightness renewals during the
+ // minute. The ticker stops when neither is needed.
function onTick() as Void {
- WatchUi.requestUpdate();
+ if (_job != null) {
+ WatchUi.requestUpdate();
+ }
+ if (_boostStart >= 0) {
+ var now = System.getTimer();
+ if (now - _boostStart >= BOOST_MS) {
+ _boostStart = -1;
+ } else if (now - _lastBoost >= RENEW_MS) {
+ _lastBoost = now;
+ light();
+ }
+ }
+ if (_job == null && _boostStart < 0) {
+ stopTicker();
+ }
}
function onEncoded() as Void {
var job = _job;
stopJob();
+ if (job instanceof RunsPainter) {
+ _code = job;
+ WatchUi.requestUpdate();
+ return;
+ }
if (job == null) {
return;
}
var err = job.error;
- var matrix = job.matrix();
- if (err != null || matrix == null) {
+ var runs = job.runs;
+ if (err != null || runs == null) {
_code = new Codes.CodeError(err != null ? err : "could not encode");
} else {
- Diag.data("matrix_encoded", "type=" + header.type + " size=" + matrix.width + "x" + matrix.height
- + " ms=" + (System.getTimer() - _startedAt));
- _code = matrix;
- // Sealing and storing the cache gets its own timer event: the
- // job's last step already used much of this event's budget.
- var t = new Timer.Timer();
- t.start(method(:saveCache), 50, false);
- saveTimer = t;
+ var cache = job.cache;
+ if (cache != null) {
+ Diag.data("matrix_encoded", "type=" + header.type + " size=" + runs.width + "x" + runs.height
+ + " ms=" + (System.getTimer() - _startedAt));
+ // Sealing and storing the cache gets its own timer event: the
+ // job's last step already used much of this event's budget.
+ _cache = cache;
+ var t = new Timer.Timer();
+ t.start(method(:saveCache), 50, false);
+ saveTimer = t;
+ }
+ var painter = new RunsPainter(runs, Codes.minModulePx(header.type));
+ if (!painter.ready()) {
+ startJob(painter);
+ return;
+ }
+ _code = painter;
}
WatchUi.requestUpdate();
}
- // Store the encoded matrix sealed in the vault (one-shot timer).
+ // Store the packed matrix sealed in the vault (one-shot timer).
function saveCache() as Void {
saveTimer = null;
- var code = _code;
- if (!(code instanceof Codes.Matrix)) {
+ var cache = _cache;
+ _cache = null;
+ if (cache == null) {
return;
}
try {
- AppState.theVault().saveCodeCache(header.id, code.toBytes());
+ AppState.theVault().saveCodeCache(header.id, cache);
} catch (e instanceof VaultError) {
// Locked meanwhile: no cache this time.
}
@@ -149,25 +219,33 @@ class CodeView extends WatchUi.View {
if (runner != null) {
runner.cancel();
}
- var ticker = _ticker;
- if (ticker != null) {
- ticker.stop();
- }
_runner = null;
- _ticker = null;
_job = null;
}
- // Full backlight while a code is shown (AMOLED watches limit how long;
- // the system then dims it, and a tap asks again).
+ // Start (or restart, on a tap) the bright minute.
function boostBrightness() as Void {
if (!AppState.options.fullBrightness || !(Attention has :backlight)) {
return;
}
+ _boostStart = System.getTimer();
+ _lastBoost = _boostStart;
+ light();
+ startTicker();
+ }
+
+ // Full brightness on AMOLED screens, the backlight on MIP screens.
+ private function light() as Void {
try {
- Attention.backlight(true);
+ if (System.getDeviceSettings().requiresBurnInProtection) {
+ Attention.backlight(1.0);
+ } else {
+ Attention.backlight(true);
+ }
} catch (e instanceof Lang.Exception) {
- // BacklightOnTooLongException: the system's limit was reached.
+ // BacklightOnTooLongException: the system's limit was reached;
+ // a tap asks again.
+ _boostStart = -1;
}
}
@@ -182,10 +260,11 @@ class CodeView extends WatchUi.View {
var job = _job;
if (code instanceof Codes.LinearCode) {
drawLinear(dc, code, smallFont, smallH);
- } else if (code instanceof Codes.Matrix) {
+ } else if (code instanceof RunsPainter) {
drawMatrix(dc, code, smallFont, smallH);
} else if (job != null) {
- dc.drawText(w / 2, h / 2 - smallH / 2, smallFont, "Encoding… " + job.percent() + "%", Graphics.TEXT_JUSTIFY_CENTER);
+ dc.drawText(w / 2, h / 2 - smallH / 2, smallFont, (job instanceof RunsPainter ? "Drawing… " : "Encoding… ")
+ + job.percent() + "%", Graphics.TEXT_JUSTIFY_CENTER);
} else {
drawMessage(dc, code instanceof Codes.CodeError ? code.message : "…", smallFont, smallH);
}
@@ -252,42 +331,130 @@ class CodeView extends WatchUi.View {
// A 2D code at the largest module size that fits (Codes.fits: inside the
// circle on round screens), centred.
- private function drawMatrix(dc as Graphics.Dc, code as Codes.Matrix, font as Graphics.FontType, fontH as Number) as Void {
- var w = dc.getWidth();
- var h = dc.getHeight();
+ private function drawMatrix(dc as Graphics.Dc, painter as RunsPainter, font as Graphics.FontType, fontH as Number) as Void {
+ var drawn = painter.draw(dc);
+ if (drawn < 0) {
+ // The system reclaimed the bitmap: paint it again.
+ _code = null;
+ startJob(painter);
+ return;
+ }
+ if (drawn == 0) {
+ var code = painter.runs;
+ var minPx = Codes.minModulePx(header.type);
+ drawMessage(dc, "Too large for this screen: needs " + minPx + " px per module, " + (code.width + 2 * code.quiet) * minPx
+ + " x " + (code.drawnHeight() + 2 * code.quiet) * minPx + " px", font, fontH);
+ }
+ }
+}
+
+// Draws Codes.Runs centred at the largest module size (16 down to minPx)
+// that fits. Up to DIRECT_RUNS runs per watchdog unit are drawn straight
+// from the list in onUpdate (one fillRectangle each, measured in the
+// watchdog stress build); bigger codes are painted into a two-colour
+// buffered bitmap, RUNS_PER_STEP runs per job step, and onUpdate draws the
+// bitmap.
+class RunsPainter extends Jobs.Job {
+ static const DIRECT_RUNS = 1500;
+ static const RUNS_PER_STEP = 1000;
+
+ var runs as Codes.Runs;
+ var px as Number = 0;
+
+ private var _bitmap as Graphics.BufferedBitmapReference? = null;
+ private var _at as Number = 0;
+
+ function initialize(code as Codes.Runs, minPx as Number) {
+ Job.initialize();
+ runs = code;
+ var screen = System.getDeviceSettings();
var totalW = code.width + 2 * code.quiet;
var totalH = code.drawnHeight() + 2 * code.quiet;
- var minPx = Codes.minModulePx(header.type);
- var px = 0;
for (var p = 16; p >= minPx && px == 0; p--) {
- if (Codes.fits(totalW, totalH, p, w, h, Ui.isRound())) {
+ if (Codes.fits(totalW, totalH, p, screen.screenWidth, screen.screenHeight, Ui.isRound())) {
px = p;
}
}
+ }
+
+ // True when the runs go straight to the screen.
+ function direct() as Boolean {
+ return runs.runs.size() <= DIRECT_RUNS * Jobs.stepsPerTick();
+ }
+
+ // True when draw() can run: too large, direct, or the bitmap is painted.
+ function ready() as Boolean {
+ return px == 0 || direct() || (_bitmap != null && _at >= runs.runs.size());
+ }
+
+ function percent() as Number {
+ return 100 * _at / runs.runs.size();
+ }
+
+ function step(budget as Number) as Boolean {
+ if (ready()) {
+ return true;
+ }
+ var ref = _bitmap;
+ var bitmap = ref != null ? ref.get() : null;
+ if (bitmap == null) {
+ // First step, or the system reclaimed the bitmap: start over.
+ ref = Graphics.createBufferedBitmap({
+ :width => runs.width * px,
+ :height => runs.drawnHeight() * px,
+ :palette => [Graphics.COLOR_WHITE, Graphics.COLOR_BLACK] as Array<Graphics.ColorValue>
+ });
+ _bitmap = ref;
+ _at = 0;
+ bitmap = ref.get();
+ if (bitmap == null) {
+ px = 0; // no memory for it: shown as too large
+ return true;
+ }
+ var bdc = (bitmap as Graphics.BufferedBitmap).getDc();
+ bdc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ bdc.clear();
+ return false;
+ }
+ var stop = _at + RUNS_PER_STEP < runs.runs.size() ? _at + RUNS_PER_STEP : runs.runs.size();
+ var bdc = (bitmap as Graphics.BufferedBitmap).getDc();
+ bdc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ paint(bdc, runs, 0, 0, px, _at, stop);
+ _at = stop;
+ return _at >= runs.runs.size();
+ }
+
+ // 1 drawn, 0 too large for the screen, -1 the bitmap was reclaimed by
+ // the system (step() again, then draw).
+ function draw(dc as Graphics.Dc) as Number {
if (px == 0) {
- drawMessage(dc, "Too large for this screen: needs " + minPx + " px per module, " + totalW * minPx + " x "
- + totalH * minPx + " px", font, fontH);
- return;
+ return 0;
}
- var x0 = (w - code.width * px) / 2;
- var y0 = (h - code.drawnHeight() * px) / 2;
+ var x0 = (dc.getWidth() - runs.width * px) / 2;
+ var y0 = (dc.getHeight() - runs.drawnHeight() * px) / 2;
+ if (direct()) {
+ dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ paint(dc, runs, x0, y0, px, 0, runs.runs.size());
+ return 1;
+ }
+ var ref = _bitmap;
+ if (ref == null || ref.get() == null) {
+ _bitmap = null;
+ _at = 0;
+ return -1;
+ }
+ dc.drawBitmap(x0, y0, ref);
+ return 1;
+ }
+
+ // Runs from..to-1 as rectangles at (x0, y0), px pixels per module.
+ static function paint(dc as Graphics.Dc, code as Codes.Runs, x0 as Number, y0 as Number, px as Number, from as Number,
+ to as Number) as Void {
var rowPx = code.rowHeight * px;
- dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
- var m = code.modules;
- for (var y = 0; y < code.height; y++) {
- var row = y * code.width;
- var x = 0;
- while (x < code.width) {
- if (m[row + x] == 1) {
- var start = x;
- while (x < code.width && m[row + x] == 1) {
- x++;
- }
- dc.fillRectangle(x0 + start * px, y0 + y * rowPx, (x - start) * px, rowPx);
- } else {
- x++;
- }
- }
+ var list = code.runs;
+ for (var i = from; i < to; i++) {
+ var r = list[i];
+ dc.fillRectangle(x0 + ((r >> 10) & 0x3FF) * px, y0 + (r >> 20) * rowPx, (r & 0x3FF) * px, rowPx);
}
}
}
diff --git a/source/util/Jobs.mc b/source/util/Jobs.mc
index d7342e3..595562e 100644
--- a/source/util/Jobs.mc
+++ b/source/util/Jobs.mc
@@ -8,10 +8,17 @@
// that can take long (key derivation, SHA-512 batches, 2D barcode encoding,
// large imports) is a Job whose step() does a bounded amount of work.
//
-// JobRunner drives a job from a timer, one step per tick, and calls back when
-// it is done. Tests call Jobs.runToCompletion() instead, synchronously.
+// Steps are sized for the Forerunner 255/255S (the only 512 KB targets).
+// Every 768 KB target allows twice as much per event
+// (tools/gen_products.py --check fails if a product breaks that rule), so
+// JobRunner runs two steps per tick there, which halves the time of an
+// import or a large QR code.
+//
+// JobRunner drives a job from a timer and calls back when it is done.
+// Tests call Jobs.runToCompletion() instead, synchronously.
import Toybox.Lang;
+import Toybox.System;
import Toybox.Timer;
module Jobs {
@@ -69,10 +76,25 @@ module Jobs {
}
function onTick() as Void {
- if (_job.step(_budget)) {
- cancel();
- _onDone.invoke();
+ for (var i = 0; i < stepsPerTick(); i++) {
+ if (_job.step(_budget)) {
+ cancel();
+ _onDone.invoke();
+ return;
+ }
}
}
}
+
+ // App memory above this means a 240,000-bytecode watchdog.
+ const LARGE_WATCH_MEMORY = 600000;
+ var _stepsPerTick as Number = 0;
+
+ // 1 on the 512 KB watches (120,000 bytecodes per event), 2 elsewhere.
+ function stepsPerTick() as Number {
+ if (_stepsPerTick == 0) {
+ _stepsPerTick = System.getSystemStats().totalMemory > LARGE_WATCH_MEMORY ? 2 : 1;
+ }
+ return _stepsPerTick;
+ }
}
diff --git a/test/codes/CodeCacheTest.mc b/test/codes/CodeCacheTest.mc
index 812acc9..2ad7f05 100644
--- a/test/codes/CodeCacheTest.mc
+++ b/test/codes/CodeCacheTest.mc
@@ -1,5 +1,5 @@
// Author: cirakg & claude ai
-// Summary: Tests for the encrypted barcode matrix cache: round trip, keys of locked entries, invalidation and tampering.
+// Summary: Tests for the encrypted barcode matrix cache: round trip, packed format, runs, keys of locked entries, invalidation and tampering.
import Toybox.Lang;
import Toybox.Test;
@@ -53,6 +53,60 @@ function codeCacheOfLockedEntriesNeedsTheSession(logger as Test.Logger) as Boole
pdf.rowHeight = 3;
var round = Codes.Matrix.fromBytes(pdf.toBytes());
return code == VaultError.LOCKED && back != null && Bytes.equal(back, [3, 1, 4, 1, 0xE0]b)
- && Codes.Matrix.fromBytes([3, 1, 4, 1, 0xE0]b) != null && Codes.Matrix.fromBytes([3, 1, 4]b) == null
+ && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4, 1, 0xE0]b) != null && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4]b) == null
&& round != null && round.rowHeight == 3 && Bytes.equal(round.modules, [1, 0, 1, 0, 1, 1]b);
}
+
+// Format 2 holds widths over 255 (wide PDF417); a 0.7 cache (one width
+// byte) is refused, so the code is encoded again.
+(:test)
+function codeCachePackedFormat(logger as Test.Logger) as Boolean {
+ var bits = new [300 * 2]b;
+ for (var i = 0; i < bits.size(); i++) {
+ bits[i] = (i * 7 / 3) % 2;
+ }
+ var wide = new Codes.Matrix(300, 2, 2, bits);
+ var bytes = wide.toBytes();
+ var back = Codes.Matrix.fromBytes(bytes);
+ var oldFormat = Codes.Packed.parse([3, 1, 4, 1, 0xE0]b);
+ var wrongSize = Codes.Packed.parse(bytes.slice(0, bytes.size() - 1));
+ return bytes[0] == 2 && back != null && back.width == 300 && back.height == 2 && Bytes.equal(back.modules, bits)
+ && oldFormat == null && wrongSize == null && Codes.Packed.parse(null) == null;
+}
+
+// The runs drawn on screen are exactly the dark modules, row by row,
+// including runs across byte boundaries and whole dark or light bytes.
+(:test)
+function codeRunsCoverExactlyTheDarkModules(logger as Test.Logger) as Boolean {
+ var codes = [Qr.encode("runs, runs, runs", 1, 0, -1) as Qr.MatrixCode];
+ var matrices = [new Codes.Matrix(codes[0].size, codes[0].size, 4, codes[0].modules)] as Array<Codes.Matrix>;
+ var stripes = new [37 * 5]b;
+ for (var i = 0; i < stripes.size(); i++) {
+ stripes[i] = (i % 37) < 20 || (i % 37) > 33 ? 1 : 0;
+ }
+ matrices.add(new Codes.Matrix(37, 5, 1, stripes));
+ for (var c = 0; c < matrices.size(); c++) {
+ var m = matrices[c];
+ var finder = new Codes.RunFinder(Codes.Packed.parse(m.toBytes()) as Codes.Packed);
+ Jobs.runToCompletion(finder, 1);
+ var drawn = new [m.width * m.height]b;
+ var runs = finder.result.runs;
+ for (var i = 0; i < runs.size(); i++) {
+ var r = runs[i];
+ var y = r >> 20;
+ var x = (r >> 10) & 0x3FF;
+ for (var k = 0; k < (r & 0x3FF); k++) {
+ if (drawn[y * m.width + x + k] == 1) {
+ logger.error("matrix " + c + ": run " + i + " overlaps");
+ return false;
+ }
+ drawn[y * m.width + x + k] = 1;
+ }
+ }
+ if (!Bytes.equal(drawn, m.modules)) {
+ logger.error("matrix " + c + ": runs differ from the modules");
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/test/codes/QrTest.mc b/test/codes/QrTest.mc
index 50cb35a..2f81887 100644
--- a/test/codes/QrTest.mc
+++ b/test/codes/QrTest.mc
@@ -1,5 +1,5 @@
// Author: cirakg & claude ai
-// Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), decoding dumps, capacity, timing.
+// Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), packed mask scores against a per-module reference, decoding dumps, capacity, timing.
import Toybox.Lang;
import Toybox.System;
@@ -21,6 +21,128 @@ module QrFixtures {
}
}
+// The mask penalty computed module by module, the way the encoder did
+// before M8 packed its lines: the reference for the packed scorer. It
+// scores a finished symbol (forced mask) with its format and version areas
+// made light again, as the encoder scores them.
+(:test)
+module QrReference {
+
+ function penalty(code as Qr.MatrixCode) as Number {
+ var n = code.size;
+ var m = Bytes.copy(code.modules);
+ for (var i = 0; i <= 8; i++) {
+ if (i != 6) {
+ m[i * n + 8] = 0;
+ m[8 * n + i] = 0;
+ }
+ }
+ for (var i = 0; i < 8; i++) {
+ m[8 * n + n - 1 - i] = 0;
+ m[(n - 1 - i) * n + 8] = 0;
+ }
+ m[(n - 8) * n + 8] = 0;
+ if (code.version >= 7) {
+ for (var i = 0; i < 18; i++) {
+ var a = n - 11 + i % 3;
+ var b = i / 3;
+ m[b * n + a] = 0;
+ m[a * n + b] = 0;
+ }
+ }
+ var score = 0;
+ var dark = 0;
+ for (var y = 0; y < n; y++) {
+ var row = m.slice(y * n, y * n + n);
+ score += line(row);
+ for (var x = 0; x < n; x++) {
+ dark += row[x];
+ if (y > 0 && x > 0 && row[x] == row[x - 1] && row[x] == m[(y - 1) * n + x] && row[x] == m[(y - 1) * n + x - 1]) {
+ score += 3;
+ }
+ }
+ }
+ for (var x = 0; x < n; x++) {
+ var column = new [n]b;
+ for (var y = 0; y < n; y++) {
+ column[y] = m[y * n + x];
+ }
+ score += line(column);
+ }
+ var total = n * n;
+ return score + 10 * ((dark * 100 - 50 * total).abs() / (5 * total));
+ }
+
+ function line(l as ByteArray) as Number {
+ var n = l.size();
+ var score = 0;
+ var run = 1;
+ var window = l[0];
+ var nextAllowed = 0;
+ for (var k = 1; k < n; k++) {
+ var c = l[k];
+ if (c == l[k - 1]) {
+ run++;
+ } else {
+ if (run >= 5) {
+ score += run - 2;
+ }
+ run = 1;
+ }
+ window = ((window << 1) | c) & 0x7F;
+ if (window == 0x5D && k >= 6) {
+ var idx = k - 6;
+ if (idx >= nextAllowed) {
+ var after = idx + 7;
+ if (idx == 0 || idx == n - 7 || light(l, idx - 4 < 0 ? 0 : idx - 4, idx)
+ || light(l, after, after + 4 > n ? n : after + 4)) {
+ score += 40;
+ nextAllowed = idx + 7;
+ } else {
+ nextAllowed = idx + 4;
+ }
+ }
+ }
+ }
+ return run >= 5 ? score + run - 2 : score;
+ }
+
+ function light(l as ByteArray, from as Number, to as Number) as Boolean {
+ for (var i = from; i < to; i++) {
+ if (l[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+// The packed scorer gives every mask exactly the per-module penalty, for
+// small and large versions, every level, and versions with version areas.
+(:test)
+function qrPackedScoresMatchReference(logger as Test.Logger) as Boolean {
+ var cases = [["https://example.com/member/12345", 1, 0], ["HELLO WORLD 1234567890", 3, 0], ["Ünïcödé · 2026", 0, 7],
+ ["a", 2, 0], ["Version ten payload", 1, 10], ["0123456789", 0, 21]];
+ for (var c = 0; c < cases.size(); c++) {
+ var row = cases[c] as Array;
+ var job = new Qr.QrJob(row[0] as String, row[1] as Number, row[2] as Number, -1);
+ Jobs.runToCompletion(job, 1);
+ if (job.scores.size() != 8) {
+ logger.error("case " + c + ": " + job.scores.size() + " scores");
+ return false;
+ }
+ for (var mask = 0; mask < 8; mask++) {
+ var forced = Qr.encode(row[0] as String, row[1] as Number, (job.result as Qr.MatrixCode).version, mask);
+ var expected = QrReference.penalty(forced as Qr.MatrixCode);
+ if (job.scores[mask] != expected) {
+ logger.error("case " + c + " mask " + mask + ": packed " + job.scores[mask] + ", reference " + expected);
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
// README 12.4 check 1 for QR: identical to segno with forced version and
// mask, and the same automatic version and mask choice.
(:test)
diff --git a/tools/gen_products.py b/tools/gen_products.py
index 7504287..ff9cf67 100644
--- a/tools/gen_products.py
+++ b/tools/gen_products.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Author: cirakg & claude ai
-# Summary: Check that every product in manifest.xml is a watch with API 5.0+ and at least 512 KB for watch apps.
+# Summary: Check each manifest product: a watch with API 5.0+, 512 KB or more, and the watchdog the job runner assumes.
"""Check the manifest's product list against the downloaded device files.
The target rule (README section 14, decision D4): a product belongs in
@@ -9,6 +9,12 @@ variant supports Connect IQ API 5.0.0 or newer, and watch apps get at least
512 KB of memory. The data comes from each device's compiler.json, which
`make sdk` downloads for exactly the products in the manifest.
+The app's job runner (source/util/Jobs.mc) also assumes a watchdog limit
+from the memory size: watches with more than 600,000 bytes for watch apps
+run two job steps per event, which needs 240,000 bytecodes per event
+(simulator.json's watchdogCount); the others run one step and need at
+least 120,000.
+
Usage: gen_products.py --check [--manifest manifest.xml] [--devices DIR]
Exit: 0 every product qualifies, 1 some don't, 2 device files missing.
@@ -29,6 +35,9 @@ EXIT_OK, EXIT_UNQUALIFIED, EXIT_MISSING = 0, 1, 2
IQ_NS = "{http://www.garmin.com/xml/connectiq}"
MIN_API = (5, 0, 0)
MIN_WATCH_APP_MEMORY = 512 * 1024
+# Jobs.LARGE_WATCH_MEMORY and the watchdog each memory class needs.
+LARGE_WATCH_MEMORY = 600_000
+WATCHDOG_SMALL, WATCHDOG_LARGE = 120_000, 240_000
WATCH_GROUP = "Watches/Wearables"
DEFAULT_DEVICES = Path.home() / ".Garmin" / "ConnectIQ" / "Devices"
@@ -42,6 +51,7 @@ class Device:
group: str
min_api: tuple[int, ...]
watch_app_memory: int
+ watchdog: int
def problems(self) -> list[str]:
reasons = []
@@ -51,6 +61,12 @@ class Device:
reasons.append(f"API {'.'.join(map(str, self.min_api))} is below 5.0.0")
if self.watch_app_memory < MIN_WATCH_APP_MEMORY:
reasons.append(f"watch apps get {self.watch_app_memory // 1024} KB, below 512 KB")
+ needed = WATCHDOG_LARGE if self.watch_app_memory > LARGE_WATCH_MEMORY else WATCHDOG_SMALL
+ if self.watchdog < needed:
+ reasons.append(
+ f"watchdog allows {self.watchdog} bytecodes per event; the job runner needs {needed} at"
+ f" {self.watch_app_memory // 1024} KB (change Jobs.stepsPerTick)"
+ )
return reasons
@@ -61,12 +77,17 @@ def parse_version(text: str) -> tuple[int, ...]:
def load_device(devices: Path, product: str) -> Device:
compiler = json.loads((devices / product / "compiler.json").read_text(encoding="utf-8"))
memory = {app["type"]: int(app["memoryLimit"]) for app in compiler["appTypes"]}
+ simulator = devices / product / "simulator.json"
+ watchdog = 0
+ if simulator.exists():
+ watchdog = int(json.loads(simulator.read_text(encoding="utf-8")).get("watchdogCount", 0))
return Device(
product=product,
name=str(compiler.get("displayName", product)),
group=str(compiler.get("webDocDeviceGroup", "")),
min_api=min(parse_version(p["connectIQVersion"]) for p in compiler["partNumbers"]),
watch_app_memory=memory.get("watchApp", 0),
+ watchdog=watchdog,
)
diff --git a/tools/tests/test_gen_products.py b/tools/tests/test_gen_products.py
index 47424d2..e57973f 100644
--- a/tools/tests/test_gen_products.py
+++ b/tools/tests/test_gen_products.py
@@ -1,5 +1,5 @@
# Author: cirakg & claude ai
-# Summary: Tests for tools/gen_products.py (the manifest product rule).
+# Summary: Tests for tools/gen_products.py (the manifest product rule and the watchdog the job runner assumes).
"""Tests for tools/gen_products.py, with synthetic device definitions."""
from __future__ import annotations
@@ -17,6 +17,7 @@ MANIFEST = """<iq:manifest version="3" xmlns:iq="http://www.garmin.com/xml/conne
def add_device(devices: Path, product: str, api: list[str], memory: int, group: str = gp.WATCH_GROUP) -> None:
folder = devices / product
folder.mkdir(parents=True)
+ set_watchdog(devices, product, gp.WATCHDOG_LARGE if memory > gp.LARGE_WATCH_MEMORY else gp.WATCHDOG_SMALL)
compiler = {
"displayName": product.upper(),
"webDocDeviceGroup": group,
@@ -26,6 +27,10 @@ def add_device(devices: Path, product: str, api: list[str], memory: int, group:
(folder / "compiler.json").write_text(json.dumps(compiler))
+def set_watchdog(devices: Path, product: str, count: int) -> None:
+ (devices / product / "simulator.json").write_text(json.dumps({"watchdogCount": count}))
+
+
def write_manifest(tmp_path: Path, products: list[str]) -> Path:
path = tmp_path / "manifest.xml"
path.write_text(MANIFEST.format(products="".join(f'<iq:product id="{p}"/>' for p in products)))
@@ -55,9 +60,23 @@ def test_missing_device_files(tmp_path: Path) -> None:
def test_problem_messages() -> None:
- device = gp.Device("x", "X", "Edge", (4, 2, 0), 131072)
+ device = gp.Device("x", "X", "Edge", (4, 2, 0), 131072, 120000)
assert device.problems() == [
"group is 'Edge', not 'Watches/Wearables'",
"API 4.2.0 is below 5.0.0",
"watch apps get 128 KB, below 512 KB",
]
+
+
+def test_watchdog_must_match_the_job_runner(tmp_path: Path) -> None:
+ devices = tmp_path / "Devices"
+ add_device(devices, "big_slow", ["6.0.0"], 786432)
+ set_watchdog(devices, "big_slow", 120000) # would run 2 steps on 120,000
+ add_device(devices, "small_fast", ["6.0.0"], 524288)
+ set_watchdog(devices, "small_fast", 240000) # fine: 1 step, more room
+ assert gp.check(write_manifest(tmp_path, ["small_fast"]), devices) == gp.EXIT_OK
+ assert gp.check(write_manifest(tmp_path, ["big_slow"]), devices) == gp.EXIT_UNQUALIFIED
+ problems = gp.load_device(devices, "big_slow").problems()
+ assert problems == [
+ "watchdog allows 120000 bytecodes per event; the job runner needs 240000 at 768 KB (change Jobs.stepsPerTick)"
+ ]