commit 5ba2f24a6ea3beb1e2c826d5b70bcca206f67dfe Author: codex-bot Date: Wed Sep 9 15:16:30 2026 +0100 Vela quick-app dev skill + knowledge base (S4 41mm) - SKILL.md: toolchain, project structure, round-screen layout, TDD harness, input/crown findings, per-app notes - references: gitea-publish workflow, S4 session log, historical nyan-watch notes - README: overview + key facts diff --git a/README.md b/README.md new file mode 100644 index 0000000..01efbd0 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Vela Quick App Development + +A developer skill and knowledge base for building **VelaJS quick apps** for Xiaomi watches — specifically the **Xiaomi Watch S4 41mm** (466×466 round AMOLED). + +This repo documents everything learned while building working third-party apps for the S4: the working toolchain, the gotchas that caused black-screen crashes, the round-screen layout math, the input-event reality (including the crown question), and the TDD harness used to test `.ux` page logic without a device. + +## Contents + +- **`SKILL.md`** — the main skill: workflow discipline, project structure, build toolchain, confirmed observations, round-screen layout, TDD technique, input/crown findings, and per-app notes. +- **`references/gitea-publish.md`** — how the built apps are published to self-hosted Gitea (repo creation, `.rpk` release assets, issues). +- **`references/watch-s4-session-log.md`** — chronological debugging log from the S4 dev session. +- **`references/nyan-watch-example.md`** — historical notes from early failed experiments (superseded; kept for reference). + +## The apps built with this skill + +| Repo | App | Package | +|---|---|---| +| `vela-sundial` | Solar-path dial watch face | `com.ilias.sundial` | +| `vela-snake` | Snake game | `com.ilias.snake` | +| `vela-calculator` | Round-screen calculator | `com.example.hellovela` | + +## Key facts (tl;dr) + +- **Toolchain:** `aiot-toolkit` (2.0.5), plain-JS `aiot release` with signed RPK — works on the S4. Generic `hap-toolkit` did not. +- **Critical fix:** every `.ux` page must contain ``; a bare/static page throws `Cannot read properties of undefined (reading 'data')` at launch. +- **Screen:** 466×466 physical → set `config.designWidth` to **466** (not the 480 default) or layout renders ~3% undersized/off-center. +- **Input:** `click`, `longpress`, `touchstart/move/end`, `swipe`, `onBackPress()`. **Crown rotation is NOT exposed** on Vela (the `crownrotationchanged` event is HarmonyOS-only). +- **Template directives:** `for="{{ v in list }}"` (with `tid`), `if`/`elif`/`else`, `show`. + +This is a living document — treat it as notes with provenance, not as a spec. Claims that say "unverified" or "not confirmed" mean exactly that. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..4e4fdc3 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,70 @@ +--- +name: vela-quickapp-dev +description: Use when building VelaJS quick apps for Xiaomi watches. +--- + +# Xiaomi Vela watch development + +## Workflow and evidence discipline +Use `aiot-toolkit`, not generic `hap-toolkit`, for Vela `.ux` projects. Read supplied project instructions and lockfiles. Build success and ZIP validation do NOT prove watch execution. Use systematic-debugging; do not repeatedly send speculative packages after failures. Never infer firmware corruption, certificate restrictions, or required bytecode solely from black-screen symptoms. + +User uses AstroBox with Watch S4 41mm and requests minimal resource usage. Increment BOTH visible versionName and integer versionCode for each delivered fix. Preserve signing identity and package name for updates. Keep each delivered artifact at a distinct versioned path. + +## Project +src/app.ux must wrap JavaScript in ``; bare JS in app.ux is not a valid substitute. Routes refer to paths relative to src; `pages/index` maps to `src/pages/index/index.ux`. Icon declared in manifest must exist. Typical manifest uses deviceTypeList [watch], minAPILevel 1, config.designWidth, router.entry/pages and display.backgroundColor. Do not confuse physical pixels, design width and DP. S4 41mm is 466x466 physical; set `config.designWidth` to 466 so 466px layout maps 1:1 to the screen. A designWidth of 480 (a common default) makes 466px elements render at ~97% — everything looks undersized/off-center and any ring is smaller than the real screen edge. This mismatch caused both the sundial's off-center sky disc and the snake's too-small bezel. + +## Build +Use project's locked dependencies with npm ci and local npm scripts. `aiot build` and `aiot release` do NOT necessarily enable bytecode/protobuf; inspect effective options and archive. Explicit `--enable-jsc --enable-protobuf` produces .jsc/.style.bin/.template.bin. Release needs sign/private.pem and certificate.pem; preserve private keys, never share them. Official tooling includes development signing keys; do not imply they are Xiaomi production authorization. + +## Confirmed observations and open bugs +- User's HyperBili circle RPK runs on their S4 41mm via AstroBox. Cached ~/hyperbili-s4.rpk. This establishes a working control, not compatibility of every S4-family package. +- NYAN builds through 0.9 failed to launch; changing designWidth, path, signing, bytecode and toolkit did NOT establish a cause. Claiming they were byte-for-byte equivalent to HyperBili was incorrect. Toolkit/Rspack version markers alone do not prove compatibility failures. +- Supplied ZIP: ~/Downloads/PairDrop_files_20260908_1846/xiaomi-vela-s4-hello-world.zip. Built under /tmp/hw with aiot-toolkit 2.0.5 + supplied lock (Rspack 1.7.12), plain signed release. User reports app stays open black with nonfunctional back-arrow gesture; not successful rendering. +- Concrete local reproduction: generated page WITHOUT `` calls its script initializer with `{}` and attempts access to undefined moduleOwn.data. In Node harness, import extracted page JS as .mjs, provide global $app_require$ stub, call default loader({}, {}, {}, exports, {}), then exports.entry({}): throws `Cannot read properties of undefined (reading 'data')`. +- Adding that script export makes the same probe pass. With aiot.__ce__ stub returning {type,props,children}, page.default.template({}) returns both intended text nodes. This is a compilation/initialization harness, NOT real Vela runtime or display validation. Hello Vela 1.1 (versionCode 2) delivered at ~/hellovela-release-1.1.rpk; USER CONFIRMED WORKING on Watch S4 41mm. No other source/layout/toolchain changes in this test. This establishes that plain-JS aiot-toolkit 2.0.5/Rspack 1.7.12 release packages work on this device; earlier blanket claims requiring JSC or older Rspack are disproven. Working page needs explicit script export in this compiler path. Calculator mockup continuation lives at ~/vela-calculator (same com.example.hellovela identity/signing, versionName 1.2/versionCode 3), preserving this working setup. + +## Round-screen mockups +User's photo confirmed calculator v1.2 renders all keys, but user requested a layout more fitted to the circular display rather than a small rectangular keypad. Prefer wider middle rows and tapered top/bottom rows, larger labels and touch areas; check row bounds against the circular safe area numerically. + +## TDD for watch apps (user-requested workflow) +When adding logic (not just mockups), write a Node test harness FIRST and watch it fail. Extract the ` + + +``` + +Animated variant (installs fine; launches unverified on device — suspect): +```js +onInit() { + let i = 0; + this._timer = setInterval(() => { i = (i + 1) % FRAMES.length; this.frame = FRAMES[i]; }, 300); +}, +onDestroy() { if (this._timer) clearInterval(this._timer); } +``` + +## Chronology of this debugging session (read before repeating these steps) + +1. **v1 (hap-toolkit 2.1.1, animated, missing icon):** install rejected — missing `/common/icon.png`. Fix: generate 192x192 png with PIL. (Real fix, verified.) +2. **v2 (hap-toolkit, static cat+text, icon present, versionCode 2):** installs, icon shows in app list, but launch = black flash ~0.2s then back to app selector. +3. **v3:** removed `setInterval` arrow-closure animation, kept static `private` data → **same crash**. +4. **v0.4 (user asked for incrementing 0.1/0.2-style versions on every fix):** stripped page to ONE ``, NO `` block. A page with NO script (or bare JS) compiles but its generated initializer calls into undefined VM data → black screen on device. Reproduced in Node: extracted page JS + `$app_require$` stub → `exports.entry({})` throws `Cannot read properties of undefined (reading 'data')`. +3. `aiot release` (plain, no flags) with `sign/private.pem` + `sign/certificate.pem` (openssl self-signed is fine). +4. Install via AstroBox → launches and renders. +- Hello Vela v1.1 = first working launch. Calculator v1.2 (mockup, 19 keys, 480px page) = photo-confirmed rendering. v1.3 = round-fitted layout (tapered rows). +- Plain JS (no JSC) release works. JSC/protobuf and Rspack version were NOT the issue; earlier speculation that they were required was wrong. + +## DEAD ENDS (do not repeat as hypotheses) +- hap-toolkit 2.1.1 output: installs but black-flashes at launch. Root cause never isolated; abandoned for aiot-toolkit. Do not claim 'hap-toolkit is incompatible' as established fact — it's an unexplained observation. +- None of these changed the black-flash: designWidth number vs "device-width", page path shape (Index/ vs pages/), signing identity, --enable-jsc --enable-protobuf, aiot-toolkit 2.0.4-beta.3 + rspack 1.2.1 pin. The bug was the missing script block all along. +- Claims made during the session that were WRONG: "byte-for-byte comparable to working rpk" (not true), "rspack 1.7.12 runtime incompatible with Vela 4.0" (disproven), "JSC required" (disproven). + +## Controls and references +- Working control rpk: HyperBili circle build for S4 41mm (device id o63): https://hyperbili.astralsight.space/circle_com.searchstars.hyperbilibili.rpk — runs on user's watch. Cached at ~/hyperbili-s4.rpk. +- AstroBox source (install flow): github.com/AstralSightStudios/AstroBox-Public — src-tauri/src/miwear/device/thirdpartyapp.rs, resutils.rs (file-type sniff: zip tail containing 'toolkit' = quickapp), error.rs (PrepareStatus codes: Busy/Duplicated/ExceedQuantityLimit/LowStorage/Downgrade/LowBattery). +- Community: bandbbs.cn (Chinese hub, login-walled), luming.cool tutorials, oryonatan/xiaomi-band-development (Band 8 Pro sideload via Mi Fitness hidden ThirdAppDebugFragment). +- m0tral's MiColor2_* repos are Huawei DevEco/HML apps for Watch Color 2/S1/S2 — different platform, NOT evidence for S4. + +## Doc site structure (iot.mi.com/vela/quickapp) +SPA. Route table in assets/js/app..js. Content chunks assets/js/..js. English pages under /en/, Chinese under /zh/ (zh sometimes richer). APILevel version notes under /zh/guide/version/apilevel{2,3,4}.html. +Key pages: guide/framework/script/lifecycle.html (page: onInit/onReady/onShow/onHide/onDestroy/onBackPress/onRefresh/onConfigurationChanged; app: onCreate/onShow/onHide/onDestroy/onError), components/general/events.html (click/longpress/touch*/swipe only), features/system/event.html (pub/sub events; S4 supported). +NO crown/rotary/bezel/side-button API anywhere in docs. + +## Calculator app state (~/vela-calculator, package com.example.hellovela) +v1.2 (code 3) mockup confirmed; v1.3 (code 4) round-fitted; v1.4 in progress: functional logic via TDD (tests/calculator.cjs — Node vm harness over the .ux script block). Test list includes: arithmetic, decimals, +/-, percent semantics (50% → 0.5; 200+10% → 220; 200x10% → 20), AC, div-by-zero 'Error' + recovery, chained ops, repeated equals, operator replacement, input cap 9 digits, leading-zero suppression. Layout template uses {{ display }} binding and onclick="press('K')" on every key; the test asserts all 19 keys bound uniquely. +Then user pivoted: wants crown-rotate-to-select-key + bottom-button-press-to-confirm. No documented API for either; proposed -scroll approximation pending user approval. + +## Calculator iterations v1.4–v1.6 (crown experiment + round-list pattern) +- v1.4: functional logic + crown-select experiment (keys in scrollable , onscroll → focused index, tap or onBackPress confirms). USER CONFIRMED: crown DOES scroll the quick-app list on Watch S4 (numbers moved off screen = list scrolled). First hardware evidence that crown → list onscroll works for sideloaded quick apps, even though undocumented. +- v1.5: fixes from user feedback — items had no explicit width (content overflowed 'off screen'), float precision (0.1+0.2 → '0.30000000000000004'; fixed with Math.round(r*1e9)/1e9), display auto-shrink (computed displaySize 56/46/38px by length). +- v1.6: user reported (1) keys cut off at circle edges, (2) couldn't reach bottom of scroll, (3) focus should be the MIDDLE item not the top, (4) #242426 keys unreadable. Root causes + fixes: + * Center-focus mapping mathematically can't reach first/last items → add top+bottom spacer list-items of height keysH/2 − itemH/2 (123px); focus = round((scrollY + KEYS_H/2 − ITEM_H/2 − PAD)/ITEM_H); after each press scrollTo({top: PAD + focused*ITEM_H − KEYS_H/2 + ITEM_H/2, behavior:'smooth'}) to re-center. + * Items must have explicit width ≤ chord at the viewport's worst y; contrast #3a3a3e on #000 = 11.3:1, focused amber #f3a633 with black text = 10.3:1. + * tests/visual.cjs parses the .ux style block and asserts circle containment, viewport-center focus mapping, full reachability, and WCAG contrast — this caught the containment violation (item sticking 140px out of the circle) that the build alone never surfaces. +- v1.6 delivered; user then asked for new app ideas; TOTP authenticator proposed as top pick. + +## Sundial app (~/vela-sun, com.ilias.sundial v0.1 code 1) — second app, delivered +Sun-path dial using NOAA solar math (src/common/solar.js, bundled into page by compiler). TDD: tests/solar.cjs (Paris noon alt ~45°, Sydney Jan, Svalbard polar night/midnight sun, dayProgress 0→1, night pre-dawn 0 vs post-sunset 1 split at solar midnight, formatCountdown), tests/page.cjs (page update() logic with FakeDate class extending Date for fixed-time tests), tests/visual.cjs (circle geometry from .ux styles). Bugs the tests caught: sunrise-bracket scan direction (h=48→1 finds OLDEST crossing; must scan h=1→48 for LATEST), night ambiguity resolved via solar-midnight rule (night returns 1 until midpoint sunset→nextSunrise, then 0), import-strip regex [^;]+; ate half the file because a ';' appeared inside a later comment (use /import\s+\w+\s+from\s+'[^']*';?/), sun orbit radius 200 > sky dial 200 (reduced to 160), center text block chord check should measure text width not block width. Location hardcoded Paris (48.8566, 2.3522) in onInit — geolocation auto-detect is the v0.2 candidate. User asked for 'cool apps' menu; TOTP authenticator (system.crypto + system.storage) pitched as the killer app.