aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-18 21:10:37 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-18 21:10:37 +0200
commit773df56883b9961b607a1994920f5ef5299423b8 (patch)
treee813e8f975e3cfa7a992222e5bf63ef615469916
parent2292bf700d28ade39cf2d64b45509e6db21034bc (diff)
feat(app): add minimal watch app with smoke test and per-size icons
The M0 placeholder app: AppBase entry point, home screen with name and version, debug-only diagnostics (start-up time, memory) and a unit test that runs in the headless simulator. Builds with strict type checking and zero warnings for all 72 products; icons.jungle gives every product a launcher icon of its exact size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--icons.jungle78
-rw-r--r--manifest.xml4
-rw-r--r--monkey.jungle13
-rw-r--r--resources-icons/38x38/drawables.xml7
-rw-r--r--resources-icons/38x38/launcher_icon.pngbin0 -> 2231 bytes
-rw-r--r--resources-icons/40x40/drawables.xml7
-rw-r--r--resources-icons/40x40/launcher_icon.pngbin0 -> 2509 bytes
-rw-r--r--resources-icons/54x54/drawables.xml7
-rw-r--r--resources-icons/54x54/launcher_icon.pngbin0 -> 3219 bytes
-rw-r--r--resources-icons/56x56/drawables.xml7
-rw-r--r--resources-icons/56x56/launcher_icon.pngbin0 -> 3524 bytes
-rw-r--r--resources-icons/60x60/drawables.xml7
-rw-r--r--resources-icons/60x60/launcher_icon.pngbin0 -> 3506 bytes
-rw-r--r--resources-icons/61x61/drawables.xml7
-rw-r--r--resources-icons/61x61/launcher_icon.pngbin0 -> 3767 bytes
-rw-r--r--resources-icons/65x65/drawables.xml7
-rw-r--r--resources-icons/65x65/launcher_icon.pngbin0 -> 4002 bytes
-rw-r--r--resources-icons/70x70/drawables.xml7
-rw-r--r--resources-icons/70x70/launcher_icon.pngbin0 -> 4185 bytes
-rw-r--r--resources/drawables/drawables.xml11
-rw-r--r--resources/drawables/launcher_icon.pngbin0 -> 3506 bytes
-rw-r--r--resources/strings/strings.xml8
-rw-r--r--source/app/AppInfo.mc16
-rw-r--r--source/app/OtpWalletApp.mc40
-rw-r--r--source/ui/HomeDelegate.mc16
-rw-r--r--source/ui/HomeView.mc42
-rw-r--r--source/util/Diag.mc50
-rw-r--r--test.jungle6
-rw-r--r--test/SmokeTest.mc41
29 files changed, 381 insertions, 0 deletions
diff --git a/icons.jungle b/icons.jungle
new file mode 100644
index 0000000..1adf941
--- /dev/null
+++ b/icons.jungle
@@ -0,0 +1,78 @@
+# Author: cirakg & claude ai
+# Summary: Per-device launcher-icon resources; generated by tools/make_icon.py --all (do not edit).
+#
+# Each product gets the icon folder matching the launcher-icon size in its
+# device definition, so the compiler never has to scale (and warn).
+
+instinctcrossoveramoled.resourcePath = $(instinctcrossoveramoled.resourcePath);resources-icons/38x38
+enduro3.resourcePath = $(enduro3.resourcePath);resources-icons/40x40
+enduro4.resourcePath = $(enduro4.resourcePath);resources-icons/40x40
+fenix7.resourcePath = $(fenix7.resourcePath);resources-icons/40x40
+fenix7pro.resourcePath = $(fenix7pro.resourcePath);resources-icons/40x40
+fenix7pronowifi.resourcePath = $(fenix7pronowifi.resourcePath);resources-icons/40x40
+fenix7s.resourcePath = $(fenix7s.resourcePath);resources-icons/40x40
+fenix7spro.resourcePath = $(fenix7spro.resourcePath);resources-icons/40x40
+fenix7x.resourcePath = $(fenix7x.resourcePath);resources-icons/40x40
+fenix7xpro.resourcePath = $(fenix7xpro.resourcePath);resources-icons/40x40
+fenix7xpronowifi.resourcePath = $(fenix7xpronowifi.resourcePath);resources-icons/40x40
+fenix8solar47mm.resourcePath = $(fenix8solar47mm.resourcePath);resources-icons/40x40
+fenix8solar51mm.resourcePath = $(fenix8solar51mm.resourcePath);resources-icons/40x40
+fenix9prosolar47mm.resourcePath = $(fenix9prosolar47mm.resourcePath);resources-icons/40x40
+fenix9prosolar51mm.resourcePath = $(fenix9prosolar51mm.resourcePath);resources-icons/40x40
+fr255.resourcePath = $(fr255.resourcePath);resources-icons/40x40
+fr255m.resourcePath = $(fr255m.resourcePath);resources-icons/40x40
+fr255s.resourcePath = $(fr255s.resourcePath);resources-icons/40x40
+fr255sm.resourcePath = $(fr255sm.resourcePath);resources-icons/40x40
+fr955.resourcePath = $(fr955.resourcePath);resources-icons/40x40
+venusq2.resourcePath = $(venusq2.resourcePath);resources-icons/40x40
+venusq2m.resourcePath = $(venusq2m.resourcePath);resources-icons/40x40
+fr165.resourcePath = $(fr165.resourcePath);resources-icons/54x54
+fr165m.resourcePath = $(fr165m.resourcePath);resources-icons/54x54
+fr170.resourcePath = $(fr170.resourcePath);resources-icons/54x54
+fr170m.resourcePath = $(fr170m.resourcePath);resources-icons/54x54
+fr57042mm.resourcePath = $(fr57042mm.resourcePath);resources-icons/54x54
+fr70.resourcePath = $(fr70.resourcePath);resources-icons/54x54
+venu441mm.resourcePath = $(venu441mm.resourcePath);resources-icons/54x54
+vivoactive6.resourcePath = $(vivoactive6.resourcePath);resources-icons/54x54
+approachs50.resourcePath = $(approachs50.resourcePath);resources-icons/56x56
+vivoactive5.resourcePath = $(vivoactive5.resourcePath);resources-icons/56x56
+approachs7042mm.resourcePath = $(approachs7042mm.resourcePath);resources-icons/60x60
+approachs7243mm.resourcePath = $(approachs7243mm.resourcePath);resources-icons/60x60
+d2mach1.resourcePath = $(d2mach1.resourcePath);resources-icons/60x60
+descentg2.resourcePath = $(descentg2.resourcePath);resources-icons/60x60
+descentmk343mm.resourcePath = $(descentmk343mm.resourcePath);resources-icons/60x60
+descentmk351mm.resourcePath = $(descentmk351mm.resourcePath);resources-icons/60x60
+epix2.resourcePath = $(epix2.resourcePath);resources-icons/60x60
+epix2pro42mm.resourcePath = $(epix2pro42mm.resourcePath);resources-icons/60x60
+epix2pro47mm.resourcePath = $(epix2pro47mm.resourcePath);resources-icons/60x60
+epix2pro51mm.resourcePath = $(epix2pro51mm.resourcePath);resources-icons/60x60
+fenix843mm.resourcePath = $(fenix843mm.resourcePath);resources-icons/60x60
+fenix943mm.resourcePath = $(fenix943mm.resourcePath);resources-icons/60x60
+fenix9pro43mm.resourcePath = $(fenix9pro43mm.resourcePath);resources-icons/60x60
+fenixe.resourcePath = $(fenixe.resourcePath);resources-icons/60x60
+fr265.resourcePath = $(fr265.resourcePath);resources-icons/60x60
+fr265s.resourcePath = $(fr265s.resourcePath);resources-icons/60x60
+instinct3amoled45mm.resourcePath = $(instinct3amoled45mm.resourcePath);resources-icons/60x60
+instinct3amoled50mm.resourcePath = $(instinct3amoled50mm.resourcePath);resources-icons/60x60
+marq2.resourcePath = $(marq2.resourcePath);resources-icons/60x60
+marq2aviator.resourcePath = $(marq2aviator.resourcePath);resources-icons/60x60
+venu2s.resourcePath = $(venu2s.resourcePath);resources-icons/61x61
+approachs7247mm.resourcePath = $(approachs7247mm.resourcePath);resources-icons/65x65
+d2mach2.resourcePath = $(d2mach2.resourcePath);resources-icons/65x65
+d2mach2pro.resourcePath = $(d2mach2pro.resourcePath);resources-icons/65x65
+fenix847mm.resourcePath = $(fenix847mm.resourcePath);resources-icons/65x65
+fenix8pro47mm.resourcePath = $(fenix8pro47mm.resourcePath);resources-icons/65x65
+fenix947mm.resourcePath = $(fenix947mm.resourcePath);resources-icons/65x65
+fenix9pro47mm.resourcePath = $(fenix9pro47mm.resourcePath);resources-icons/65x65
+fenix9pro51mm.resourcePath = $(fenix9pro51mm.resourcePath);resources-icons/65x65
+fr57047mm.resourcePath = $(fr57047mm.resourcePath);resources-icons/65x65
+fr965.resourcePath = $(fr965.resourcePath);resources-icons/65x65
+fr970.resourcePath = $(fr970.resourcePath);resources-icons/65x65
+venu445mm.resourcePath = $(venu445mm.resourcePath);resources-icons/65x65
+venux1.resourcePath = $(venux1.resourcePath);resources-icons/65x65
+approachs7047mm.resourcePath = $(approachs7047mm.resourcePath);resources-icons/70x70
+d2airx10.resourcePath = $(d2airx10.resourcePath);resources-icons/70x70
+venu2.resourcePath = $(venu2.resourcePath);resources-icons/70x70
+venu2plus.resourcePath = $(venu2plus.resourcePath);resources-icons/70x70
+venu3.resourcePath = $(venu3.resourcePath);resources-icons/70x70
+venu3s.resourcePath = $(venu3s.resourcePath);resources-icons/70x70
diff --git a/manifest.xml b/manifest.xml
index cffbe88..2fcc76d 100644
--- a/manifest.xml
+++ b/manifest.xml
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
+ Author: cirakg & claude ai
+ Summary: Connect IQ manifest: watch-app, API 5.0+, no permissions, the 72 target watches.
+-->
+<!--
Connect IQ manifest for OTP Wallet (README sections 4 and 14).
* type watch-app with minApiLevel 5.0.0: device app, so it never times out
diff --git a/monkey.jungle b/monkey.jungle
new file mode 100644
index 0000000..e0fbe95
--- /dev/null
+++ b/monkey.jungle
@@ -0,0 +1,13 @@
+# Author: cirakg & claude ai
+# Summary: Monkey C build configuration: manifest, source and resource paths.
+#
+# Build configuration for the Monkey C compiler ("jungle" file).
+#
+# Debug and release builds use this file alone. Unit-test builds add
+# test.jungle on top (monkeyc -f "monkey.jungle;test.jungle"), so test code
+# never reaches a debug or release build. Release builds (monkeyc -r) also drop
+# everything annotated (:debug), such as source/util/Diag.mc's logging.
+project.manifest = manifest.xml
+
+base.sourcePath = source
+base.resourcePath = resources
diff --git a/resources-icons/38x38/drawables.xml b/resources-icons/38x38/drawables.xml
new file mode 100644
index 0000000..ff1989f
--- /dev/null
+++ b/resources-icons/38x38/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 38x38 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/38x38/launcher_icon.png b/resources-icons/38x38/launcher_icon.png
new file mode 100644
index 0000000..020ec63
--- /dev/null
+++ b/resources-icons/38x38/launcher_icon.png
Binary files differ
diff --git a/resources-icons/40x40/drawables.xml b/resources-icons/40x40/drawables.xml
new file mode 100644
index 0000000..6d638f8
--- /dev/null
+++ b/resources-icons/40x40/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 40x40 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/40x40/launcher_icon.png b/resources-icons/40x40/launcher_icon.png
new file mode 100644
index 0000000..39193ac
--- /dev/null
+++ b/resources-icons/40x40/launcher_icon.png
Binary files differ
diff --git a/resources-icons/54x54/drawables.xml b/resources-icons/54x54/drawables.xml
new file mode 100644
index 0000000..b703a0e
--- /dev/null
+++ b/resources-icons/54x54/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 54x54 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/54x54/launcher_icon.png b/resources-icons/54x54/launcher_icon.png
new file mode 100644
index 0000000..a6cd756
--- /dev/null
+++ b/resources-icons/54x54/launcher_icon.png
Binary files differ
diff --git a/resources-icons/56x56/drawables.xml b/resources-icons/56x56/drawables.xml
new file mode 100644
index 0000000..f738a3a
--- /dev/null
+++ b/resources-icons/56x56/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 56x56 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/56x56/launcher_icon.png b/resources-icons/56x56/launcher_icon.png
new file mode 100644
index 0000000..6174adf
--- /dev/null
+++ b/resources-icons/56x56/launcher_icon.png
Binary files differ
diff --git a/resources-icons/60x60/drawables.xml b/resources-icons/60x60/drawables.xml
new file mode 100644
index 0000000..e1f5e59
--- /dev/null
+++ b/resources-icons/60x60/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 60x60 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/60x60/launcher_icon.png b/resources-icons/60x60/launcher_icon.png
new file mode 100644
index 0000000..a4b5902
--- /dev/null
+++ b/resources-icons/60x60/launcher_icon.png
Binary files differ
diff --git a/resources-icons/61x61/drawables.xml b/resources-icons/61x61/drawables.xml
new file mode 100644
index 0000000..3f96368
--- /dev/null
+++ b/resources-icons/61x61/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 61x61 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/61x61/launcher_icon.png b/resources-icons/61x61/launcher_icon.png
new file mode 100644
index 0000000..f53aa14
--- /dev/null
+++ b/resources-icons/61x61/launcher_icon.png
Binary files differ
diff --git a/resources-icons/65x65/drawables.xml b/resources-icons/65x65/drawables.xml
new file mode 100644
index 0000000..9450580
--- /dev/null
+++ b/resources-icons/65x65/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 65x65 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/65x65/launcher_icon.png b/resources-icons/65x65/launcher_icon.png
new file mode 100644
index 0000000..892b731
--- /dev/null
+++ b/resources-icons/65x65/launcher_icon.png
Binary files differ
diff --git a/resources-icons/70x70/drawables.xml b/resources-icons/70x70/drawables.xml
new file mode 100644
index 0000000..f7a578e
--- /dev/null
+++ b/resources-icons/70x70/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 70x70 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources-icons/70x70/launcher_icon.png b/resources-icons/70x70/launcher_icon.png
new file mode 100644
index 0000000..7aba911
--- /dev/null
+++ b/resources-icons/70x70/launcher_icon.png
Binary files differ
diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml
new file mode 100644
index 0000000..2a191f6
--- /dev/null
+++ b/resources/drawables/drawables.xml
@@ -0,0 +1,11 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Default launcher icon resource (per-size icons come from resources-icons/).
+-->
+<!--
+ Bitmaps. The launcher icon is generated by tools/make_icon.py (a padlock on
+ a teal disc); regenerate it rather than editing the PNG by hand.
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/resources/drawables/launcher_icon.png b/resources/drawables/launcher_icon.png
new file mode 100644
index 0000000..a4b5902
--- /dev/null
+++ b/resources/drawables/launcher_icon.png
Binary files differ
diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml
new file mode 100644
index 0000000..7aab88a
--- /dev/null
+++ b/resources/strings/strings.xml
@@ -0,0 +1,8 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: User-visible strings of the app (English).
+-->
+<!-- User-visible strings. English only for now (README section 14). -->
+<strings>
+ <string id="AppName">OTP Wallet</string>
+</strings>
diff --git a/source/app/AppInfo.mc b/source/app/AppInfo.mc
new file mode 100644
index 0000000..5b2a586
--- /dev/null
+++ b/source/app/AppInfo.mc
@@ -0,0 +1,16 @@
+// Author: cirakg & claude ai
+// Summary: Build-time facts about the app, such as its semantic version.
+//
+// AppInfo: build-time facts about the app.
+//
+// VERSION follows semantic versioning (README section 22). It is shown on the
+// home screen now and in the About screen and settings later; keep it equal to
+// the newest version in CHANGELOG.md once releases start.
+
+import Toybox.Lang;
+
+module AppInfo {
+
+ // Semantic version of the app: MAJOR.MINOR.PATCH.
+ const VERSION = "0.1.0";
+}
diff --git a/source/app/OtpWalletApp.mc b/source/app/OtpWalletApp.mc
new file mode 100644
index 0000000..8858878
--- /dev/null
+++ b/source/app/OtpWalletApp.mc
@@ -0,0 +1,40 @@
+// Author: cirakg & claude ai
+// Summary: Application entry point (Connect IQ AppBase): lifecycle and first view.
+//
+// OtpWalletApp: the application entry point (Connect IQ AppBase).
+//
+// M0 skeleton: the app shows a home screen with its name and version. Later
+// milestones add the vault, settings processing, locking and the glance.
+//
+// Connect IQ lifecycle: initialize -> onStart -> getInitialView -> (views run)
+// -> onStop.
+//
+// Security note for later milestones: onStop is where every key and decrypted
+// secret gets wiped. It runs when the user leaves the app, and also when
+// Garmin's system passcode shuts running apps down.
+
+import Toybox.Application;
+import Toybox.Lang;
+import Toybox.WatchUi;
+
+class OtpWalletApp extends Application.AppBase {
+
+ function initialize() {
+ AppBase.initialize();
+ // Start of the start-up timer reported by `make perf` (debug builds only).
+ Diag.markStart();
+ }
+
+ // Called after the app is loaded, before the first view is shown.
+ function onStart(state as Dictionary?) as Void {
+ }
+
+ // Called when the app exits, for any reason.
+ function onStop(state as Dictionary?) as Void {
+ }
+
+ // The first screen and its input handler.
+ function getInitialView() as [Views] or [Views, InputDelegates] {
+ return [new HomeView(), new HomeDelegate()];
+ }
+}
diff --git a/source/ui/HomeDelegate.mc b/source/ui/HomeDelegate.mc
new file mode 100644
index 0000000..fe23ae6
--- /dev/null
+++ b/source/ui/HomeDelegate.mc
@@ -0,0 +1,16 @@
+// Author: cirakg & claude ai
+// Summary: Input handling for the home screen.
+//
+// HomeDelegate: input handling for the home screen.
+//
+// M0 has no actions yet. Inheriting BehaviorDelegate keeps the default
+// behaviours, so BACK (button or swipe) leaves the app on every device.
+
+import Toybox.WatchUi;
+
+class HomeDelegate extends WatchUi.BehaviorDelegate {
+
+ function initialize() {
+ BehaviorDelegate.initialize();
+ }
+}
diff --git a/source/ui/HomeView.mc b/source/ui/HomeView.mc
new file mode 100644
index 0000000..f73c783
--- /dev/null
+++ b/source/ui/HomeView.mc
@@ -0,0 +1,42 @@
+// Author: cirakg & claude ai
+// Summary: M0 placeholder home screen showing the app name and version.
+//
+// HomeView: the M0 placeholder home screen. It draws the app name and version
+// in the centre of the screen. Milestone M4 replaces it with the entry list.
+//
+// Performance note: everything that allocates (loading the name string,
+// building the version text) happens once in initialize(), not in onUpdate().
+
+import Toybox.Graphics;
+import Toybox.Lang;
+import Toybox.WatchUi;
+
+class HomeView extends WatchUi.View {
+
+ private var _title as String;
+ private var _version as String;
+ // Set after the first complete draw, so start-up diagnostics run once.
+ private var _firstFrameDone as Boolean = false;
+
+ function initialize() {
+ View.initialize();
+ _title = WatchUi.loadResource(Rez.Strings.AppName) as String;
+ _version = "v" + AppInfo.VERSION;
+ }
+
+ function onUpdate(dc as Graphics.Dc) as Void {
+ var centerX = dc.getWidth() / 2;
+ var centerY = dc.getHeight() / 2;
+ var justify = Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER;
+
+ dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
+ dc.clear();
+ dc.drawText(centerX, centerY, Graphics.FONT_MEDIUM, _title, justify);
+ dc.drawText(centerX, centerY + dc.getFontHeight(Graphics.FONT_MEDIUM), Graphics.FONT_XTINY, _version, justify);
+
+ if (!_firstFrameDone) {
+ _firstFrameDone = true;
+ Diag.firstFrame();
+ }
+ }
+}
diff --git a/source/util/Diag.mc b/source/util/Diag.mc
new file mode 100644
index 0000000..aea2a70
--- /dev/null
+++ b/source/util/Diag.mc
@@ -0,0 +1,50 @@
+// Author: cirakg & claude ai
+// Summary: Debug-only diagnostics (start-up time, memory) for `make perf` and `make memtest`.
+//
+// Debug builds print one machine-readable line per event to the simulator
+// console: "DIAG", the event name, then key=value pairs, for example
+// DIAG first_frame startup_ms=1 mem_used=6984 mem_total=781888
+// tools/check_budgets.py reads these lines and compares them with
+// tools/budgets.json. Plain key=value pairs are used instead of JSON because
+// the simulator console escapes quotes. Release builds compile the empty
+// (:release) versions instead, so a real watch running a release build never
+// logs anything.
+//
+// Security rule: only timings and memory counters ever go through here, never
+// secrets, codes or entry names. This is the only file allowed to call
+// System.println outside tests (enforced by tools/lint_project.py).
+
+import Toybox.Lang;
+import Toybox.System;
+
+module Diag {
+
+ // System.getTimer() value when the app object was created.
+ (:debug)
+ var startMs as Number = 0;
+
+ // Start the start-up timer. Called from OtpWalletApp.initialize().
+ (:debug)
+ function markStart() as Void {
+ startMs = System.getTimer();
+ }
+
+ (:release)
+ function markStart() as Void {
+ }
+
+ // Report start-up time and memory use after the first complete frame.
+ // startup_ms covers app object creation to the end of the first draw. The
+ // time the system needs to load the program before that is invisible to
+ // the app; app-diag.sh measures it from outside (README section 18).
+ (:debug)
+ function firstFrame() as Void {
+ var stats = System.getSystemStats();
+ System.println("DIAG first_frame startup_ms=" + (System.getTimer() - startMs)
+ + " mem_used=" + stats.usedMemory + " mem_total=" + stats.totalMemory);
+ }
+
+ (:release)
+ function firstFrame() as Void {
+ }
+}
diff --git a/test.jungle b/test.jungle
new file mode 100644
index 0000000..513ffff
--- /dev/null
+++ b/test.jungle
@@ -0,0 +1,6 @@
+# Author: cirakg & claude ai
+# Summary: Adds the test/ sources for unit-test builds only.
+#
+# Unit-test builds only (`make test`): adds the test/ folder to the sources.
+# Used together with monkey.jungle: monkeyc -f "monkey.jungle;test.jungle" --unit-test
+base.sourcePath = $(base.sourcePath);test
diff --git a/test/SmokeTest.mc b/test/SmokeTest.mc
new file mode 100644
index 0000000..60e98c1
--- /dev/null
+++ b/test/SmokeTest.mc
@@ -0,0 +1,41 @@
+// Author: cirakg & claude ai
+// Summary: Smoke test proving the unit-test pipeline works end to end.
+//
+// SmokeTest: proves the unit-test pipeline works end to end: compile with
+// --unit-test, run in the headless simulator, parse the results with
+// tools/ciq_test_report.py. The real test suites start with milestone M2.
+
+import Toybox.Lang;
+import Toybox.Test;
+
+// ASCII codes of the characters a version may contain.
+const ASCII_DOT = 46;
+const ASCII_ZERO = 48;
+const ASCII_NINE = 57;
+
+// AppInfo.VERSION must look like MAJOR.MINOR.PATCH: three non-empty runs of
+// digits separated by two dots, nothing else.
+(:test)
+function smokeVersionIsSemver(logger as Test.Logger) as Boolean {
+ var dots = 0;
+ var digitsInPart = 0;
+ var chars = AppInfo.VERSION.toCharArray();
+ for (var i = 0; i < chars.size(); i++) {
+ var code = chars[i].toNumber();
+ if (code == ASCII_DOT) {
+ if (digitsInPart == 0) {
+ logger.error("empty part in version " + AppInfo.VERSION);
+ return false;
+ }
+ dots++;
+ digitsInPart = 0;
+ } else if (code >= ASCII_ZERO && code <= ASCII_NINE) {
+ digitsInPart++;
+ } else {
+ logger.error("unexpected character in version " + AppInfo.VERSION);
+ return false;
+ }
+ }
+ logger.debug("version " + AppInfo.VERSION);
+ return dots == 2 && digitsInPart > 0;
+}