aboutsummaryrefslogtreecommitdiff
path: root/spike/settings/source
diff options
context:
space:
mode:
Diffstat (limited to 'spike/settings/source')
-rw-r--r--spike/settings/source/DebugSelfTest.mc35
-rw-r--r--spike/settings/source/SpikeApp.mc43
-rw-r--r--spike/settings/source/SpikeDelegate.mc35
-rw-r--r--spike/settings/source/SpikeReport.mc144
-rw-r--r--spike/settings/source/SpikeView.mc53
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;
+ }
+ }
+}