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
This commit is contained in:
@@ -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 `<script>export default {}</script>`; 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.
|
||||||
@@ -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 `<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 <list>/<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.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Publishing Vela apps to the user's Gitea
|
||||||
|
|
||||||
|
Self-hosted Gitea: https://gitea.oimcloud.myaddr.tools/ — account `codex-bot` (id 55, admin), email codex-bot@bots.com.
|
||||||
|
Auth: `git config --global credential.helper store` + `~/.git-credentials` (chmod 600) holding `https://codex-bot:<token>@gitea.oimcloud.myaddr.tools`. git identity = codex-bot / codex-bot@bots.com. Token is a 40-char hex (Gitea access token).
|
||||||
|
|
||||||
|
## Create a repo (Gitea API)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=<40-char-hex> BASE=https://gitea.oimcloud.myaddr.tools
|
||||||
|
curl -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"vela-snake","description":"...","private":false,"auto_init":false}' \
|
||||||
|
"$BASE/api/v1/user/repos"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Push source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init -b main; git add -A; git commit -m "..."
|
||||||
|
git remote add origin "$BASE/codex-bot/<repo>.git"; git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attach the .rpk as a tagged release asset (two API calls)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) create release -> returns JSON with an 'id'
|
||||||
|
curl -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"tag_name":"v0.3","target_commitish":"main","name":"SNAKE 0.3","body":"..."}' \
|
||||||
|
"$BASE/api/v1/repos/codex-bot/<repo>/releases"
|
||||||
|
# 2) upload the binary (multipart)
|
||||||
|
curl -X POST -H "Authorization: token $TOKEN" -F "attachment=@dist/foo.rpk" \
|
||||||
|
"$BASE/api/v1/repos/codex-bot/<repo>/releases/<id>/assets?name=foo.rpk"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open an issue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"...","body":"..."}' "$BASE/api/v1/repos/codex-bot/<repo>/issues"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
- `node_modules` is a symlink → /tmp/hw/node_modules. `.gitignore` must say `node_modules` (NO trailing slash) — a trailing slash matches directories only, so the symlink was committed until fixed with `git rm --cached node_modules`.
|
||||||
|
- `sign/*.pem` holds the signing key + cert — keep gitignored; NEVER commit private.pem.
|
||||||
|
- `dist/` and `build/` are gitignored; the installable .rpk is delivered via a release asset, not committed to git.
|
||||||
|
- Verify nothing secret leaked: `git ls-files | grep -E 'sign|dist|node_modules|\.pem|\.rpk'` should be empty (the node_modules symlink must also be excluded).
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# nyan-watch build log (2026-09-07, Watch S4 41mm)
|
||||||
|
|
||||||
|
Minimal resource-light app: one page, ASCII nyan cat on black, no permissions, no images besides icon.
|
||||||
|
Project at `~/nyan-watch/` (user's machine). As of this log the app is NOT yet verified launching on device — this file is the debugging record, not a proven example.
|
||||||
|
|
||||||
|
**Status: v0.8 built (aiot release --enable-jsc --enable-protobuf, own release cert in sign/). STILL black-flashes (~0.02s, nothing visible).** Remaining suspects: (1) release-cert trust on international S4 firmware — diff META-INF/CERT (ours 2.4KB self-signed vs HyperBili's 18KB); (2) designWidth "device-width" vs 466 in config; (3) S4 real-device-debugging OTA requirement (docs say partner-only); (4) watch quick-app partition state. **Control experiment queued: sideload HyperBili's S4 rpk (`~/hyperbili-s4.rpk`, downloaded from hyperbili.astralsight.space) — if IT also fails, the watch/runtime is at fault, not our build.**
|
||||||
|
|
||||||
|
## src/manifest.json (v0.6+ shape — pages/index layout, aiot-toolkit)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"package": "com.ilias.nyanwatch",
|
||||||
|
"name": "NYAN",
|
||||||
|
"icon": "/common/icon.png",
|
||||||
|
"versionName": "0.7",
|
||||||
|
"versionCode": 7,
|
||||||
|
"minPlatformVersion": 1000,
|
||||||
|
"features": [{ "name": "system.router" }],
|
||||||
|
"config": { "logLevel": "log", "designWidth": "device-width" },
|
||||||
|
"deviceTypeList": ["watch"],
|
||||||
|
"display": { "backgroundColor": "#000000" },
|
||||||
|
"router": { "entry": "pages/index", "pages": { "pages/index": { "component": "index" } } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## src/common/icon.png — REQUIRED
|
||||||
|
192x192 png (rainbow stripes + cat box, drawn with PIL). Without this file the watch installer rejects the rpk with a generic "install failed" even though `hap build` succeeds.
|
||||||
|
|
||||||
|
## src/app.ux
|
||||||
|
```js
|
||||||
|
export default { onCreate() {}, onDestroy() {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
## src/Index/index.ux (page dir at src/Index/, NOT src/pages/Index/)
|
||||||
|
Static variant that actually launches on device (animation removed while bisecting a launch crash — the setInterval-with-arrow-closure version installed fine but black-flashed back to the selector):
|
||||||
|
```
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<text class="nyan">{{ frame }}</text>
|
||||||
|
<text class="hello">{{ title }}</text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
private: { frame: ",----,\n| |\n| o |\n|____|", title: "NYAN" }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page { flex-direction: column; background-color: #000000; align-items: center; justify-content: center; }
|
||||||
|
.nyan { font-size: 16px; color: #ff7f00; font-weight: bold; }
|
||||||
|
.hello { font-size: 24px; color: #ffffff; font-weight: bold; margin-top: 10px; }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 `<text>`, NO `<script>` at all, added `display.designWidth: 466` → **same crash; this build briefly crashed the whole watch UI** (watch self-restored, no reboot). Since even script-less pages crash, page code is exonerated — the package format/runtime is the suspect.
|
||||||
|
5. **v0.5:** migrated to `aiot-toolkit` 2.0.5 (Xiaomi's real toolchain; bundled official debug certs; jsc bytecode pipeline). Manifest restructured to the Songbird known-good shape: `deviceTypeList:["watch"]`, `features:[system.router]`, `config:{logLevel:"log", designWidth:466}`. **Device verification pending.**
|
||||||
|
6. In parallel, two research subagents swept English (GitHub/Reddit/XDA/mibandnotify) + Chinese sources (米坛社区 bandbbs.cn, luming.cool, CSDN) for anyone with a VERIFIED self-built quick app running on Watch S4/S3 hardware, known 黑屏闪退 causes, and example projects. **Result:** found two verified-real-device projects (see SKILL.md "Verified proven-real-device manifests"): CheongSzesuen/B2048 (AstroBox-published, designWidth "device-width", aiot release --enable-jsc --enable-protobuf) and hrk666666/tgwear-quickapp (minPlatformVersion 1100, aiot build --enable-custom-component). Neither uses hap-toolkit.
|
||||||
|
7. **v0.6 (aiot-toolkit 2.0.5, pages/index layout, static page, designWidth 466):** STILL the same ~0.02s black flash with nothing visible. So layout+toolchain alone were not sufficient.
|
||||||
|
8. **v0.7:** config.designWidth changed to `"device-width"` (B2048 style). SAME crash.
|
||||||
|
9. **v0.8:** `aiot release --enable-jsc --enable-protobuf` with own openssl cert (sign/private.pem + certificate.pem). Package now structurally identical to a proven-working S4 rpk (jsc, .style.bin/.template.bin, manifest-watch.json, same CERT/hash.json scheme — verified by diffing against HyperBili's circle_com.searchstars.hyperbilibili.rpk, cached at ~/hyperbili-s4.rpk). STILL the same ~0.02s black flash. This exonerates the package format entirely; next diagnostic is the HyperBili control install, then watch runtime/partition state or cert trust.
|
||||||
|
|
||||||
|
Also mined from the research: oryonatan repo has known install-error codes from community PRs/issues — error code 3 = storage full, error -6 = pre-installation failed (Redmi Watch 5); INJECT_EVENTS permission needed on Xiaomi/Redmi/POCO phones for the ADB deploy path.
|
||||||
|
|
||||||
|
**Standing hypotheses for the flash (in order to try):** (1) build with `aiot release --enable-jsc --enable-protobuf` (bytecode+protobuf format may be required by the S4 engine); (2) bump `minPlatformVersion` to 1100; (3) debug-signature rejection on international S4 firmware — diff META-INF/CERT against a community rpk that DOES run (download a B2048 AstroBox release rpk and compare); (4) Watch S4 may require the real-device-debugging firmware upgrade for third-party apps (official docs: partner-only OTA).
|
||||||
|
|
||||||
|
## Tooling notes from this session
|
||||||
|
|
||||||
|
- Compiler error `编译失败:请确认manifest.json中配置的文件路径存在:Index/index` means the page dir is in the wrong place — it must be `src/Index/index.ux` (both toolkits).
|
||||||
|
- `resolveFile` in `@hap-toolkit/packager` accepts `.mix`, `.ux`, `.ts` extensions.
|
||||||
|
- Watch for escaped newlines (`\\n` vs `\n`) in multi-line ASCII strings — a doubled backslash renders art on one line.
|
||||||
|
- aiot-toolkit prints output path as `~/.temp_nyan-watch/dist/...` but actually writes to the project's own `dist/` — check `ls dist/`.
|
||||||
|
- aiot build output is plain JS (not jsc) for pages in default `aiot build`; B2048 uses `aiot release --enable-jsc --enable-protobuf` — if a plain `build` rpk black-flashes, try the release flags before suspecting page code.
|
||||||
|
- Symptom guide: install rejected = manifest/icon validation (missing icon file); icon shows but app black-flashes on launch = see chronology above (with hap-toolkit: package format, not page code).
|
||||||
|
- Sideload path: drag `.rpk` into AstroBox (user has it installed).
|
||||||
|
- AstroBox inspects the rpk zip tail for the string "toolkit" (build.txt JSON) to classify it as a quickapp — never strip META-INF/build.txt.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Watch S4 41mm session log (Sept 2026) — verified facts vs dead ends
|
||||||
|
|
||||||
|
## Device
|
||||||
|
Xiaomi Watch S4 41mm, round 1.32" 466x466 AMOLED, HyperOS/Vela 4.0, BES2700BP, 32MB RAM. Sideload via AstroBox (drag-and-drop). AstroBox passes an EMPTY package name + versionCode=1 to the watch; the watch parses the rpk manifest itself during install.
|
||||||
|
|
||||||
|
## VERIFIED working pipeline (user-confirmed on hardware)
|
||||||
|
1. Source: aiot-toolkit 2.0.5 project (lockfile pins @rspack/core 1.7.12).
|
||||||
|
2. Page .ux MUST contain a real `<script>export default {...}</script>` 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.<hash>.js. Content chunks assets/js/<n>.<hash>.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 <list>-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 <list>, 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.
|
||||||
Reference in New Issue
Block a user