// Author: cirakg & claude ai // Summary: The Add code form (README 10.2): reads the code* fields, and CodeCheck, the job that proves a code encodes and fits. // // read() checks the fields that need no encoding (name, symbology, length). // Whether the contents are valid for the symbology is decided by the real // encoder (README 12.1), in CodeCheck: // * Linear codes encode at once; the encoder's message ("EAN-13: check // digit should be 7, got 3. Choose 'Fix if wrong' to correct it") becomes // the status, and its note ("check digit 7 added") a remark. // * 2D codes run their MatrixJob step by step (the watchdog), then the // matrix is packed for the encrypted cache in a step of its own, so the // first opening on the watch is instant. // * A code that encodes but doesn't fit this screen is still added, with // the "Too large for this screen" warning (README 12.2). import Toybox.Lang; import Toybox.System; module CodeForm { const SWITCH = "codeAdd"; const STATUS = "codeStatus"; const PAYLOAD = "codePayload"; const NAME_MAX_BYTES = Entry.MAX_NAME_BYTES; const FIELDS = ["codeName", "codeSymbology", "codePayload", "codeEcLevel", "codeCheckDigit", "codeShowText", "codeGroup", "codeLocked"]; // Field values after a reset (resources/settings/properties.xml); // the contents first. function defaults() as Dictionary { return { PAYLOAD => "", "codeName" => "", "codeSymbology" => Codes.QR, "codeEcLevel" => 1, "codeCheckDigit" => Codes.CHECK_ADD_VERIFY, "codeShowText" => true, "codeGroup" => "", "codeLocked" => false } as Dictionary; } // The entry described by the form (not yet checked by the encoder). function read(store as Forms.Store) as Forms.Parsed { var parsed = new Forms.Parsed("code"); for (var i = 0; i < FIELDS.size(); i++) { parsed.content.add(store.get(FIELDS[i] as String)); } var e = new Entry(); e.kind = Entry.KIND_CODE; e.source = Entry.SOURCE_MANUAL; e.name = Forms.text(store, "codeName"); e.group = Forms.text(store, "codeGroup"); e.locked = Forms.flag(store, "codeLocked"); if (e.name.length() == 0) { return parsed.fail("Name: enter a name for the card"); } var problem = Forms.checkName("Name", e.name, NAME_MAX_BYTES); if (problem == null) { problem = Forms.checkName("Group", e.group, Entry.MAX_GROUP_BYTES); } if (problem != null) { return parsed.fail(problem); } var symbology = Forms.number(store, "codeSymbology"); if (symbology == null || symbology < Codes.QR || symbology > Codes.AZTEC) { return parsed.fail("Type: choose one of the 12 barcode types"); } e.type = symbology; var ec = Forms.number(store, "codeEcLevel"); var check = Forms.number(store, "codeCheckDigit"); if (ec == null || ec < 0 || ec > Codes.OPT_EC_MASK) { ec = 1; } if (check == null || check < Codes.CHECK_ADD_VERIFY || check > Codes.CHECK_OFF) { check = Codes.CHECK_ADD_VERIFY; } e.options = ec | (check << Codes.OPT_CHECK_SHIFT) | (Forms.flag(store, "codeShowText") ? Codes.OPT_SHOW_TEXT : 0); var payload = contents(Forms.rawText(store, PAYLOAD), symbology); if (payload instanceof String) { return parsed.fail(payload); } e.payload = payload; parsed.entry = e; return parsed; } // The contents as stored (trimmed for linear codes, whose digits are // often typed with spaces around), or the problem. function contents(text as String, symbology as Number) as ByteArray or String { return contentsOf(text, Text.trim(text), symbology); } // As contents(), with the text already trimmed (ManageRun trims // kilobytes in steps). function contentsOf(text as String, trimmed as String, symbology as Number) as ByteArray or String { var value = Codes.isLinear(symbology) ? trimmed : text; if (trimmed.length() == 0) { return "Contents: missing"; } var bytes = Bytes.fromUtf8(value); if (bytes.size() > Entry.MAX_PAYLOAD_BYTES) { return "Contents: too long (" + bytes.size() + " bytes, at most " + Entry.MAX_PAYLOAD_BYTES + ")"; } return bytes; } function reset(store as Forms.Store, everything as Boolean) as Void { store.set(PAYLOAD, ""); if (everything) { var d = defaults(); var keys = d.keys(); for (var i = 0; i < keys.size(); i++) { store.set(keys[i], d[keys[i]] as String or Number or Boolean); } } store.set(SWITCH, false); } } // 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 the packed matrix // (Codes.Packed). class CodeCheck extends Jobs.Job { // Linear codes draw with 10 quiet modules on each side (CodeView). static const LINEAR_QUIET = 10; var error as String? = null; var notes as Array = [] as Array; var cache as ByteArray? = null; private var _symbology as Number; private var _payload as String; private var _options as Number; private var _job as Codes.MatrixJob? = null; private var _packer as Codes.Packer? = null; private var _started as Boolean = false; private var _w as Number; private var _h as Number; private var _round as Boolean; function initialize(symbology as Number, payload as String, options as Number) { Job.initialize(); _symbology = symbology; _payload = payload; _options = options; var screen = System.getDeviceSettings(); _w = screen.screenWidth; _h = screen.screenHeight; _round = screen.screenShape == System.SCREEN_SHAPE_ROUND; } function step(budget as Number) as Boolean { if (!_started) { _started = true; if (!Codes.isMatrix(_symbology)) { checkLinear(); return true; } _job = Codes.matrixJob(_symbology, _payload, _options, _w, _h, _round); _payload = ""; return false; } 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; if (!job.step(budget)) { return false; } _job = null; var m = job.matrix(); var err = job.error; if (err != null || m == null) { error = withName(err != null ? err : "could not encode"); return true; } var totalW = m.width + 2 * m.quiet; var totalH = m.drawnHeight() + 2 * m.quiet; var px = Codes.minModulePx(_symbology); 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"); } _packer = new Codes.Packer(m); return false; } private function checkLinear() as Void { var result = Codes.encode(_symbology, _payload, _options); _payload = ""; if (result instanceof Codes.CodeError) { error = withName(result.message); return; } var note = result.note; if (note != null) { notes.add(note); } // The widest band is a little under the screen width on round // screens (CodeView measures it exactly when drawing). var available = _round ? _w * 96 / 100 : _w - 4; var needed = result.modules.size() + 2 * LINEAR_QUIET; if (needed > available) { notes.add("Too large for this screen: needs " + needed + " px, " + available + " px available"); } } // "too long for QR..." becomes "QR: too long for QR...", but a message // that already names the symbology is kept as it is. private function withName(message as String) as String { var name = Codes.name(_symbology); return message.find(name) == 0 ? message : name + ": " + message; } }