Files
codex-bot 5ba2f24a6e 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
2026-09-09 15:16:30 +01:00

14 KiB

name, description
name description
vela-quickapp-dev 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 <script>export default {}</script>; 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 <script>export default {}</script> 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 <script> block from the .ux with a regex, run it in a vm context (export default → globalThis.spec =), then assert on handlers/state. Test style: page() helper clones spec + private state; drive press(key) sequences; assert display. Edge cases covered so far: subtract/multiply/divide, decimals, duplicate dots, +/-, percent, AC, divide-by-zero → 'Error' and recovery, chained ops, repeated equals, operator replacement, input length cap, leading zeros. Also assert template binding ({{ display }}) and that every <text ... onclick="press('K')"> key is present exactly once. Keep tests in tests/calculator.cjs; run with plain node (no framework).

Hardware input limits (researched 2026-09-08)

Full sweep of all ~350 iot.mi.com doc pages (components, features, lifecycle, APILevel2-4): documented input events are ONLY click/longpress/touchstart/touchmove/end/swipe + onBackPress() lifecycle (fires on right-swipe or physical back button; return true to intercept). NO documented crown/bezel/rotary API, no programmable side-button API, no key-event API. User asked for crown-rotate-to-select + bottom-button-press; proposed approximation: keys in a scrollable <list> (crown scrolls lists on Vela), highlight focused key, confirm via tap or intercepted back-press. This is UNVERIFIED on hardware — treat as experiment; fallback is swipe navigation (documented). Don't claim crown support exists; don't build non-touch input schemes on undocumented APIs without telling the user.

Doc-spelunking technique that worked: the docs site is a VuePress-style SPA; the real nav/route table lives in /vela/quickapp/assets/js/app.<hash>.js (regex for "(/zh/...html)" paths there), and page content is in numbered chunks /assets/js/<n>.<hash>.js — grep those for keywords instead of crawling HTML (plain HTML pages render mostly-empty nav shells; some fetches 403 with browser-y User-Agent headers but succeed with none/urllib default).

Crown / buttons — API reality (RESOLVED 2026-09-09)

Crown rotation is NOT exposed to quick apps on the S4 41mm. Triple-confirmed: (1) Vela docs have no crown event; (2) the largest open-source S4/S3 app wgpro (github.com/typheye/wgpro branch vela-watch, GPLv3, real working app w/ full on-watch keyboard) uses ZERO crown/rotary/rotation handlers — only click/touchstart/touchend/touchmove/longpress/swipe/onBackPress/change/scroll(onscroll/onscrolltop/onscrollbottom/onscrolltouchup); (3) the @crownrotationchanged + requestFocus(true) API is HarmonyOS ArkUI (Huawei watch spec, watchdoc.quickapp.cn), not Vela — a CSDN article labels it "HarmonyOS ArkUI 表冠事件". Wired it into SNAKE 0.2 and USER CONFIRMED the crown does nothing. Crown press only = back/upper button (onBackPress). No side-button or key-event API. Swipe is the correct game input. UNTESTED hypothesis: crown rotation may auto-scroll / on Vela (HarmonyOS default) — could test with a list+onscroll counter, likely also unsupported.

BandBBS threads 25447 & 24538 warn that on VelaOS 5.0, installing S3/S4 quick apps can black-screen/reboot — relevant to recurring black-screen crashes.

Sundial app (~/vela-sun, com.ilias.sundial) — v0.1 FAILED ON WATCH

User reports black screen and soft reboot after about 10 seconds with v0.1. v0.2 replaced the chained percentage transform on the sun marker with documented bound-style left/top coordinates (positionMarker() computes px from sunAngle/orbitR; markerStyle object bound via {{ markerStyle }}), removed the zero-size sunOrbit container, bumped to 0.2/code 2. NOT YET CONFIRMED on watch. Transform/solar-math/geometry all still pass locally; the native crash only reproduces on hardware. Do not claim a root cause without watch confirmation. Sun-path dial: NOAA solar-position algorithm in src/common/solar.js (pure, no Vela APIs — declination/hour angle/sidereal time). Page: 400px sky disc on 466px round screen, amber sun orbits upper arc (sunrise left → sunset right), grey moon lower arc at night; center clock + DAYLIGHT/NIGHT + countdown. 30s refresh; timer cleared onShow/onHide/onDestroy. Location hardcoded LAT/LON in onInit (Paris default) — v0.2 candidate: system.geolocation auto-detect (S4-supported per docs). TDD technique refined: extract <script> with /<script>([\s\S]?)</script>/, strip import with /import\s+\w+\s+from\s+'[^']';?/ (NEVER [^;]+ — a ';' inside a later comment makes the regex eat half the file), replace 'export default' with assignment, run via vm.runInContext('(' + body + ')') or module.exports pattern; stub $element/() => null, setInterval/clearInterval, and inject a FakeDate class extending Date for fixed-time tests. Also strip module-level const NAME = ... declarations (regex ^const\s+\w+\s*=[^\n]*\n gm) and re-inject them as sandbox globals — a top-level const breaks the (' + body + ') wrap. assert/strict deepEqual fails on VM-realm objects (different Object.prototype) — compare .x/.y fields, not the object. dayProgress semantics: scan last 48h for latest −→+ (sunrise) and +→− (sunset) crossings, scan forward for next crossings; night returns 1 until solar midnight (midpoint sunset→nextSunrise) then 0; polar cases → boundary 0/1. Geometry/contrast tests in tests/visual.cjs parse the .ux style block and assert orbit/text-block fits the chord; text width (not block width) is the right containment measure.

Version bump discipline (observed user preference)

User expects visible versionName increments (0.1, 0.2 … 1.0) AND versionCode +1 on EVERY delivered build; they track iteration by the visible number. Deliver each build at a distinct dist/ path and name the app/page state in the reply.

Round-screen layout (v1.6 lessons, user-confirmed problems)

Flat full-height lists clip at circle edges (rows near top/bottom of viewport exceed the chord), and scroll-mapping drifted when CSS margins didn't match the JS ITEM_H constant. Working pattern: short viewport (keysH ~300), explicit item width ≤ chord at viewport edges, NO vertical margins on list-items (bake spacing into height), top+bottom spacer list-items of keysH/2-itemH/2 so the FIRST and LAST item can scroll to the viewport center, focus = round((scrollY + KEYS_H/2 - ITEM_H/2 - PAD)/ITEM_H), and scrollTo({top: PAD + focused*ITEM_H - KEYS_H/2 + ITEM_H/2}) after each press to re-center. Contrast: #3a3a3e keys on #000 (11.3:1) — user called the old #242426 'horrible'. Geometry/contrast checks live in tests/visual.cjs (asserts parsed from the .ux source).

Confirmed template directives (read from official docs)

  • Loop: <div for="{{ value in list }}" tid="uniqueField"> — $item/$idx defaults; tid is the per-element unique key (like React key); default tid is $idx. Index-based reuse is correct for a snake that slides (node i stays the head slot).
  • Conditional: if="{{ cond }}" / elif / else (adjacent siblings) add/remove nodes; show="{{ cond }}" toggles visibility without removal.
  • swipe event payload is { direction: 'left'|'right'|'up'|'down' }; bind via onswipe="handler" (compiles to events:{swipe:fn}). Confirmed in compiled output, not just docs.

Snake app (~/vela-snake, com.ilias.snake) — NOT YET CONFIRMED ON WATCH

Classic snake: pure logic in src/common/snake.js (initialSnake/step/turn/spawnFood — step returns {snake,ate,alive}; self-collision excludes the vacating tail; turn blocks 180° reversal). Page renders segments with the for directive, food as one div, show-gated game-over overlay, onswipe direction input, 300ms setInterval tick, tap overlay to restart. 16x16 grid, 20px cells, 320px board centered on the round 466 screen (designWidth 466). Renders on watch (user saw the board in 0.2 and flagged the undersized ring — the designWidth 480 bug, fixed in 0.3). Crown confirmed unsupported. TDD: tests/snake.cjs (logic) + tests/page.cjs (wiring) pass.

Publishing to Gitea (this user's self-hosted instance)

Vela apps publish to the user's self-hosted Gitea (https://gitea.oimcloud.myaddr.tools/, account codex-bot), one repo per app (vela-sundial, vela-snake, vela-calculator). Full workflow — create repo via API, git init/commit/push (credential.helper=store with 0600 ~/.git-credentials), attach the .rpk as a tagged release asset, open issues — in references/gitea-publish.md. Git pitfall: .gitignore with node_modules/ (trailing slash) does NOT match the node_modules symlink (→ /tmp/hw/node_modules), so the symlink gets committed. Use node_modules (no slash) and git rm --cached node_modules if already tracked. Never commit sign/*.pem (private key) or dist/.

References and limits

Official: https://iot.mi.com/vela/quickapp/en/ Examples: oryonatan/xiaomi-band-development (Songbird confirmed Band 8 Pro), Searchstars/HyperBilibili (S4 reports), CheongSzesuen/B2048 (Band 10), typheye/wgpro branch vela-watch (real working S4/S3 app, best reference for input handling — clone & grep for on* handlers). TG Wear targets watches but was NOT verified on hardware in research. Old m0tral MiColor2_BossPuzzle is DevEco/HML for Color2/S1/S2, not proof of S4 compatibility. IDE real-device debugging documentation describes restricted partner firmware; do not promise retail access or suggest firmware flashing as a debugging shortcut. Request exact firmware/version and actual logs when runtime behavior is unresolved.

Legacy references/nyan-watch-example.md is historical and may contain disproven guesses; prefer this file and current evidence.