diff options
Diffstat (limited to 'spike/settings/source')
| -rw-r--r-- | spike/settings/source/DebugSelfTest.mc | 35 | ||||
| -rw-r--r-- | spike/settings/source/SpikeApp.mc | 43 | ||||
| -rw-r--r-- | spike/settings/source/SpikeDelegate.mc | 35 | ||||
| -rw-r--r-- | spike/settings/source/SpikeReport.mc | 144 | ||||
| -rw-r--r-- | spike/settings/source/SpikeView.mc | 53 |
5 files changed, 310 insertions, 0 deletions
diff --git a/spike/settings/source/DebugSelfTest.mc b/spike/settings/source/DebugSelfTest.mc new file mode 100644 index 0000000..ef50148 --- /dev/null +++ b/spike/settings/source/DebugSelfTest.mc @@ -0,0 +1,35 @@ +// Author: cirakg & claude ai +// Summary: Debug-only self-test: fills one field and runs the settings-change path once, so the simulator shows real output. +// +// The simulator has no phone, so this stands in for one save from Garmin +// Connect: if Text 1 is empty it receives a fake 100-character test string, +// then the normal onSettingsChanged path runs (analysis plus write-back). +// It prints a "SPIKE ready" line that build-spike.sh waits for. Release +// builds compile the empty (:release) version, so the beta you upload never +// changes your settings by itself. + +import Toybox.Application.Properties; +import Toybox.Lang; +import Toybox.System; + +module DebugSelfTest { + + // Fake 100-character test string in the same format as the phone tests. + (:debug) + const SAMPLE = "N=100|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghEND"; + + (:debug) + function run(app as SpikeApp) as Void { + var current = Properties.getValue("text1"); + if (current == null || (current instanceof String && current.length() == 0)) { + Properties.setValue("text1", SAMPLE); + app.onSettingsChanged(); + } + System.println("SPIKE ready total_chars=" + app.report.totalChars + " changes=" + app.report.changeCount + + " status='" + Properties.getValue("statusField") + "'"); + } + + (:release) + function run(app as SpikeApp) as Void { + } +} diff --git a/spike/settings/source/SpikeApp.mc b/spike/settings/source/SpikeApp.mc new file mode 100644 index 0000000..c5adcb2 --- /dev/null +++ b/spike/settings/source/SpikeApp.mc @@ -0,0 +1,43 @@ +// Author: cirakg & claude ai +// Summary: Settings spike entry point: analyses every setting on start and on each change, and writes a status back. +// +// The spike measures what Garmin doesn't document (README section 16, M1): +// how long a text setting may be, which characters survive, whether values +// the watch writes back reach the phone, and when onSettingsChanged fires. +// It is a throwaway app with its own app id; nothing here is reused by OTP +// Wallet except the measured numbers. + +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + +class SpikeApp extends Application.AppBase { + + // The analysis shown on screen, refreshed on start and on every change. + var report as SpikeReport; + + function initialize() { + AppBase.initialize(); + report = new SpikeReport(); + } + + // Read the values the watch already has (from the last sync). + function onStart(state as Dictionary?) as Void { + report.analyseOnStart(); + DebugSelfTest.run(self); + } + + function onStop(state as Dictionary?) as Void { + } + + function getInitialView() as [Views] or [Views, InputDelegates] { + var view = new SpikeView(report); + return [view, new SpikeDelegate(view)]; + } + + // Called when you save the settings on the phone while the app runs. + function onSettingsChanged() as Void { + report.settingsChanged(); + WatchUi.requestUpdate(); + } +} diff --git a/spike/settings/source/SpikeDelegate.mc b/spike/settings/source/SpikeDelegate.mc new file mode 100644 index 0000000..94b3ece --- /dev/null +++ b/spike/settings/source/SpikeDelegate.mc @@ -0,0 +1,35 @@ +// Author: cirakg & claude ai +// Summary: Input of the spike screen: UP/DOWN or swipes scroll, START/tap jumps back to the top. + +import Toybox.Lang; +import Toybox.WatchUi; + +class SpikeDelegate extends WatchUi.BehaviorDelegate { + + // Lines moved per button press or swipe. + private const SCROLL_STEP = 3; + // Large enough to reach the first line from anywhere. + private const TO_TOP = -1000; + + private var _view as SpikeView; + + function initialize(view as SpikeView) { + BehaviorDelegate.initialize(); + _view = view; + } + + function onNextPage() as Boolean { + _view.scroll(SCROLL_STEP); + return true; + } + + function onPreviousPage() as Boolean { + _view.scroll(-SCROLL_STEP); + return true; + } + + function onSelect() as Boolean { + _view.scroll(TO_TOP); + return true; + } +} diff --git a/spike/settings/source/SpikeReport.mc b/spike/settings/source/SpikeReport.mc new file mode 100644 index 0000000..09f27fa --- /dev/null +++ b/spike/settings/source/SpikeReport.mc @@ -0,0 +1,144 @@ +// Author: cirakg & claude ai +// Summary: Reads every spike setting, measures lengths and characters, writes the status back, builds the screen lines. +// +// Per text field it reports: length in characters and UTF-8 bytes, the first +// 12 and last 8 characters (the test strings start with "N=<length>|" and +// end with "END", so truncation is obvious), and which of the characters +// & < > \ " { } % and non-ASCII it contains. +// +// Write-back test: on every settings change the watch writes a status line +// into the read-only "statusField" and clears "secretField". Opening the +// settings on the phone afterwards shows whether the phone displays values +// the watch wrote. + +import Toybox.Application; +import Toybox.Application.Properties; +import Toybox.Lang; +import Toybox.System; + +module SpikeKeys { + const TEXT_FIELDS = ["text1", "text2", "text3", "text4", "text5", "text6", "text7", "text8"]; + const SECRET = "secretField"; + const STATUS = "statusField"; + const OTHER_FIELDS = ["switchField", "listField", "numberField", "longField"]; + const HEAD_CHARS = 12; + const TAIL_CHARS = 8; + const ASCII_BACKSLASH = 92; + const ASCII_QUOTE = 34; +} + +class SpikeReport { + + // Lines for SpikeView, rebuilt by analyse(). + var lines as Array<String> = [] as Array<String>; + // onSettingsChanged calls since the app started, and the time of the last. + var changeCount as Number = 0; + var lastChange as String = "never"; + // Totals over the eight text fields, from the latest analysis. + var totalChars as Number = 0; + var totalBytes as Number = 0; + // Length of the password field when it was last read (before clearing). + var passwordReceived as Number = 0; + // The status line the watch wrote last ("" before the first change). + var lastWriteBack as String = ""; + + function initialize() { + } + + // On start: no write-back, just show what the last sync delivered. + function analyseOnStart() as Void { + passwordReceived = readString(SpikeKeys.SECRET).length(); + analyse(); + } + + // Settings were saved on the phone: measure, write back, show. + function settingsChanged() as Void { + changeCount++; + lastChange = clockText(); + passwordReceived = readString(SpikeKeys.SECRET).length(); + analyse(); + lastWriteBack = "seen " + totalChars + " chars at " + lastChange + ", change #" + changeCount; + Properties.setValue(SpikeKeys.STATUS, lastWriteBack); + Properties.setValue(SpikeKeys.SECRET, ""); + analyse(); + } + + // Rebuild every screen line from the current property values. + function analyse() as Void { + lines = [] as Array<String>; + totalChars = 0; + totalBytes = 0; + var fieldLines = [] as Array<String>; + for (var i = 0; i < SpikeKeys.TEXT_FIELDS.size(); i++) { + describeText(fieldLines, "T" + (i + 1), readString(SpikeKeys.TEXT_FIELDS[i])); + } + lines.add("Settings spike"); + lines.add("Changes seen: " + changeCount); + lines.add("Last change: " + lastChange); + lines.add("Total: " + totalChars + " chars, " + totalBytes + " B"); + lines.addAll(fieldLines); + lines.add("Password: got " + passwordReceived + " chars,"); + lines.add(" now " + readString(SpikeKeys.SECRET).length() + " (watch clears it)"); + for (var i = 0; i < SpikeKeys.OTHER_FIELDS.size(); i++) { + lines.add(SpikeKeys.OTHER_FIELDS[i] + ": " + valueText(SpikeKeys.OTHER_FIELDS[i])); + } + lines.add("Status now: " + readString(SpikeKeys.STATUS)); + lines.add("Wrote: " + (lastWriteBack.equals("") ? "nothing yet" : lastWriteBack)); + } + + // Length, ends and special characters of one text field. + private function describeText(out as Array<String>, label as String, value as String) as Void { + var chars = value.length(); + var bytes = value.toUtf8Array().size(); + totalChars += chars; + totalBytes += bytes; + out.add(label + ": " + chars + " chars, " + bytes + " B"); + if (chars == 0) { + return; + } + var head = part(value, 0, chars < SpikeKeys.HEAD_CHARS ? chars : SpikeKeys.HEAD_CHARS); + var tailStart = chars > SpikeKeys.TAIL_CHARS ? chars - SpikeKeys.TAIL_CHARS : 0; + out.add(" " + head + " .. " + part(value, tailStart, chars)); + out.add(" has: " + specialsIn(value, chars != bytes)); + } + + // Which of the characters the plan worries about occur in the value. + private function specialsIn(value as String, nonAscii as Boolean) as String { + var probes = [ + "&", "<", ">", SpikeKeys.ASCII_BACKSLASH.toChar().toString(), + SpikeKeys.ASCII_QUOTE.toChar().toString(), "{", "}", "%" + ]; + var found = ""; + for (var i = 0; i < probes.size(); i++) { + if (value.find(probes[i]) != null) { + found += probes[i] + " "; + } + } + if (nonAscii) { + found += "non-ASCII"; + } + return found.equals("") ? "none" : found; + } + + // A property as a String; anything missing or of another type reads as "". + private function readString(key as String) as String { + var value = Properties.getValue(key); + return value instanceof String ? value : ""; + } + + // Any property as display text. + private function valueText(key as String) as String { + // getValue throws for unknown keys and never returns null for declared ones. + return Properties.getValue(key).toString() as String; + } + + private function part(value as String, startIndex as Number, endIndex as Number) as String { + var result = value.substring(startIndex, endIndex); + return result == null ? "" : result; + } + + private function clockText() as String { + var time = System.getClockTime(); + return time.hour.format("%02d") + ":" + time.min.format("%02d") + ":" + time.sec.format("%02d"); + } +} diff --git a/spike/settings/source/SpikeView.mc b/spike/settings/source/SpikeView.mc new file mode 100644 index 0000000..68bacc7 --- /dev/null +++ b/spike/settings/source/SpikeView.mc @@ -0,0 +1,53 @@ +// Author: cirakg & claude ai +// Summary: Scrollable text screen showing the spike's measurements. +// +// Lines come from SpikeReport. The first visible line moves with +// SpikeDelegate (UP/DOWN buttons or swipes); text stays in the middle band of +// round screens so nothing is cut off at the top or bottom. + +import Toybox.Graphics; +import Toybox.Lang; +import Toybox.WatchUi; + +class SpikeView extends WatchUi.View { + + // Share of the screen height left free at the top and at the bottom. + private const MARGIN_FRACTION = 0.14; + + private var _report as SpikeReport; + private var _firstLine as Number = 0; + + function initialize(report as SpikeReport) { + View.initialize(); + _report = report; + } + + // Move the first visible line by delta lines, staying within the text. + function scroll(delta as Number) as Void { + var last = _report.lines.size() - 1; + _firstLine += delta; + if (_firstLine > last) { + _firstLine = last; + } + if (_firstLine < 0) { + _firstLine = 0; + } + WatchUi.requestUpdate(); + } + + function onUpdate(dc as Graphics.Dc) as Void { + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); + dc.clear(); + var font = Graphics.FONT_XTINY; + var lineHeight = dc.getFontHeight(font); + var top = (dc.getHeight() * MARGIN_FRACTION).toNumber(); + var bottom = dc.getHeight() - top; + var x = dc.getWidth() / 2; + var y = top; + var lines = _report.lines; + for (var i = _firstLine; i < lines.size() && y + lineHeight <= bottom; i++) { + dc.drawText(x, y, font, lines[i], Graphics.TEXT_JUSTIFY_CENTER); + y += lineHeight; + } + } +} |
