Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b732ff0c38 | ||
|
|
ad75baac82 | ||
|
|
168b5098a0 | ||
|
|
f847941ef5 | ||
|
|
c563d92218 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
node_modules/
|
||||
node_modules
|
||||
build/
|
||||
dist/
|
||||
sign/*.pem
|
||||
|
||||
@@ -1,209 +1,53 @@
|
||||
# Xiaomi Vela Hello World for Watch S4
|
||||
# SUNDIAL — sun-path dial for Xiaomi Watch S4
|
||||
|
||||
This is the smallest practical Xiaomi Vela JS watch application. It shows two centered lines on a round black screen:
|
||||
A round watch face-style app: an amber sun orbits the upper arc during daylight
|
||||
(sunrise left → sunset right), a grey moon traces the lower arc at night. Center
|
||||
shows the clock, DAYLIGHT/NIGHT phase, and a countdown to the next sunrise/sunset.
|
||||
Location is hardcoded (Paris by default — edit `LAT`/`LON` in `src/pages/index/index.ux`).
|
||||
|
||||
**Status: WORKING ON WATCH** (user-confirmed since v0.2).
|
||||
|
||||
## Features
|
||||
|
||||
- NOAA solar-position algorithm (Meeus), fully offline — no GPS/network
|
||||
- Two-tone day/night dial with horizon line, bezel ring, and hour ticks
|
||||
- Sun halo glow; moon marker at night
|
||||
- 30s refresh; timer cleared on hide/destroy (battery-friendly)
|
||||
- designWidth 466 = 1:1 mapping to the S4 41mm round screen
|
||||
|
||||
## Project layout
|
||||
|
||||
```text
|
||||
Hello World!
|
||||
Xiaomi Watch S4
|
||||
```
|
||||
|
||||
It compiles entirely from a Linux terminal. Xiaomi AIoT-IDE is not required.
|
||||
|
||||
## 1. What you need
|
||||
|
||||
- A 64-bit Linux system
|
||||
- Node.js and npm
|
||||
- OpenSSL, but only if you want a signed release package
|
||||
- Internet access for the first `npm install`
|
||||
|
||||
Node.js 20 LTS is a conservative choice. The included project has also been build-tested with Node.js 24 and npm 11.
|
||||
|
||||
On Ubuntu or Debian, install the basic tools:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y curl openssl
|
||||
```
|
||||
|
||||
Install Node.js using your preferred package manager or a version manager such as `nvm`. Confirm that it works:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
## 2. Understand the tiny project
|
||||
|
||||
```text
|
||||
xiaomi-vela-s4-hello-world/
|
||||
├── package.json
|
||||
├── README.md
|
||||
├── sign/
|
||||
└── src/
|
||||
├── app.ux
|
||||
├── manifest.json
|
||||
src/
|
||||
├── app.ux # app lifecycle (must wrap JS in <script>export default…)
|
||||
├── manifest.json # com.ilias.sundial, watch, designWidth 466
|
||||
├── common/
|
||||
│ ├── icon.png
|
||||
│ └── icon.svg
|
||||
└── pages/
|
||||
└── index/
|
||||
└── index.ux
|
||||
│ ├── solar.js # pure solar math (no Vela APIs) — unit-tested
|
||||
│ └── icon.png # 192×192 circular app icon
|
||||
└── pages/index/index.ux # dial page: template + logic + styles
|
||||
tests/ # Node test harnesses (plain `node`, no framework)
|
||||
sign/ # private.pem + certificate.pem (NOT committed)
|
||||
```
|
||||
|
||||
- `package.json` installs Xiaomi's command-line build tool and defines the build commands.
|
||||
- `src/manifest.json` defines the package, icon, watch target, design width, and start page.
|
||||
- `src/app.ux` contains application lifecycle hooks.
|
||||
- `src/pages/index/index.ux` contains the UI and its styles.
|
||||
- `sign/` is where release certificate files go. Keep private keys private.
|
||||
|
||||
Vela `.ux` files resemble small single-file web components. A page may contain `template`, `style`, and `script` blocks. This Hello World page needs only `template` and `style`.
|
||||
|
||||
## 3. Compile a debug RPK without Xiaomi's IDE
|
||||
|
||||
Open a terminal in the project folder and run:
|
||||
## Build & release
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm ci # installs locked aiot-toolkit 2.0.5
|
||||
npm run release # signed .rpk → dist/
|
||||
node tests/solar.cjs # math tests
|
||||
node tests/visual.cjs # geometry/contrast tests parsed from the .ux
|
||||
```
|
||||
|
||||
The important output is created under `dist/` and ends in `.debug.rpk`:
|
||||
## Install
|
||||
|
||||
```bash
|
||||
find dist -maxdepth 1 -type f -name '*.rpk' -print
|
||||
```
|
||||
Side-load `dist/com.ilias.sundial.release.*.rpk` onto the watch via AstroBox.
|
||||
Releases: https://gitea.oimcloud.myaddr.tools/codex-bot/vela-sundial
|
||||
|
||||
The `build/` directory contains intermediate generated files. You normally do not edit either `build/` or `dist/`.
|
||||
## Version history
|
||||
|
||||
To include a separate source map while debugging:
|
||||
|
||||
```bash
|
||||
npm run build:source-map
|
||||
```
|
||||
|
||||
To see every compiler option supported by your installed toolkit:
|
||||
|
||||
```bash
|
||||
npm run help
|
||||
```
|
||||
|
||||
## 4. Create a signed release RPK
|
||||
|
||||
The release builder expects these two files:
|
||||
|
||||
```text
|
||||
sign/private.pem
|
||||
sign/certificate.pem
|
||||
```
|
||||
|
||||
Generate a local development certificate from the project root:
|
||||
|
||||
```bash
|
||||
openssl req \
|
||||
-newkey rsa:2048 \
|
||||
-nodes \
|
||||
-keyout sign/private.pem \
|
||||
-x509 \
|
||||
-days 3650 \
|
||||
-out sign/certificate.pem \
|
||||
-subj '/CN=Hello Vela Development/O=Example/C=US'
|
||||
```
|
||||
|
||||
Then compile the release package:
|
||||
|
||||
```bash
|
||||
npm run release
|
||||
find dist -maxdepth 1 -type f -name '*.release.*.rpk' -print
|
||||
```
|
||||
|
||||
Do not commit or share `sign/private.pem`. If a companion Android application communicates with the watch app, Xiaomi's wearable integration may require matching signing identity; changing certificates later can also prevent an update from being accepted as the same app.
|
||||
|
||||
## 5. Change the app
|
||||
|
||||
Edit `src/pages/index/index.ux`. For example, replace:
|
||||
|
||||
```xml
|
||||
<text class="hello">Hello World!</text>
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```xml
|
||||
<text class="hello">My first app</text>
|
||||
```
|
||||
|
||||
Then rebuild:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
When publishing an update, increase `versionCode` in `src/manifest.json`. Also update `versionName` when you want the visible version string to change.
|
||||
|
||||
## 6. Important Vela rules
|
||||
|
||||
- A template must have exactly one root element.
|
||||
- Visible text must be inside a `<text>` component.
|
||||
- Vela uses flex layout, but its CSS support is smaller than a browser's CSS support.
|
||||
- Package names should use reverse-domain form, such as `com.yourname.hello`.
|
||||
- `designWidth: 480` lets Vela scale the 480-pixel design to the physical display.
|
||||
- APIs such as sensors, storage, network, or vibration must usually be declared in the manifest's `features` array before use. This Hello World app requests no APIs.
|
||||
|
||||
## 7. Installing on a real Watch S4
|
||||
|
||||
Compilation and installation are separate. The terminal workflow above reliably creates the `.rpk`; transferring it to a physical watch depends on the exact Watch S4 model, firmware region, Mi Fitness version, and whether developer or debugging access is enabled for that firmware.
|
||||
|
||||
Do not treat the `.rpk` as an Android APK, and do not flash random firmware. Use Xiaomi's supported wearable deployment route for your device/account. Some retail or global firmware builds do not expose the same development or sideload controls as Chinese developer setups.
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
### `aiot: command not found`
|
||||
|
||||
Always run the local tool through npm:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Do not rely on a global `aiot` installation.
|
||||
|
||||
### Dependency installation fails
|
||||
|
||||
Clear only this project's installed dependencies and retry:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
```
|
||||
|
||||
If you use a company proxy, configure npm for that proxy instead of disabling TLS verification.
|
||||
|
||||
### No `.rpk` appears
|
||||
|
||||
Read the first compiler error, then verify:
|
||||
|
||||
```bash
|
||||
npx aiot build -h
|
||||
find src -maxdepth 4 -type f -print
|
||||
```
|
||||
|
||||
The required files are `src/manifest.json`, `src/app.ux`, and the routed page component.
|
||||
|
||||
### Release build says signing files are missing
|
||||
|
||||
Run the OpenSSL command in section 4 and confirm:
|
||||
|
||||
```bash
|
||||
test -s sign/private.pem && test -s sign/certificate.pem && echo 'Signing files exist'
|
||||
```
|
||||
|
||||
## Official references
|
||||
|
||||
- Xiaomi Vela JS documentation: <https://iot.mi.com/vela/quickapp/en/>
|
||||
- Project structure: <https://iot.mi.com/vela/quickapp/zh/guide/start/project-overview.html>
|
||||
- Manifest configuration: <https://iot.mi.com/vela/quickapp/zh/guide/framework/manifest.html>
|
||||
- Compiler parameters: <https://iot.mi.com/vela/quickapp/zh/guide/start/toolkit-params.html>
|
||||
- UI introduction: <https://iot.mi.com/vela/quickapp/zh/guide/start/user-interface.html>
|
||||
|
||||
The project intentionally uses only the public `aiot-toolkit` command-line package. It does not require Xiaomi AIoT-IDE.
|
||||
- **0.1** — first build; black-screened + soft-rebooted on watch (crash suspect: chained
|
||||
percentage transforms on a zero-size container)
|
||||
- **0.2** — replaced % transforms with bound px coordinates (`markerStyle`); WORKS
|
||||
- **0.3** — designWidth 480→466 (fixed off-center sky disc), bezel ring, hour ticks,
|
||||
two-tone dial, sun halo, orbit r=150
|
||||
- **0.5** — user-supplied circular icon
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/tmp/hw/node_modules
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 31 KiB |
+17
-2
@@ -1,4 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
||||
<rect width="192" height="192" rx="48" fill="#ff6900"/>
|
||||
<text x="96" y="116" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="72" font-weight="700" fill="#ffffff">Hi</text>
|
||||
<circle cx="96" cy="96" r="96" fill="#0e1f3a"/>
|
||||
<g fill="#ffc93c">
|
||||
<polygon points="96,96 96,20 118,22"/>
|
||||
<polygon points="96,96 118,22 146,40"/>
|
||||
<polygon points="96,96 146,40 170,76"/>
|
||||
<polygon points="96,96 170,76 172,96"/>
|
||||
<polygon points="96,96 172,96 170,116"/>
|
||||
<polygon points="96,96 170,116 146,152"/>
|
||||
<polygon points="96,96 146,152 118,170"/>
|
||||
<polygon points="96,96 118,170 96,172"/>
|
||||
<polygon points="96,96 96,172 74,170"/>
|
||||
<polygon points="96,96 74,170 46,152"/>
|
||||
<polygon points="96,96 46,152 22,116"/>
|
||||
<polygon points="96,96 22,116 20,96"/>
|
||||
<polygon points="96,96 20,96 22,76"/>
|
||||
</g>
|
||||
<circle cx="96" cy="96" r="30" fill="#ffc93c"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 797 B |
+2
-2
@@ -2,8 +2,8 @@
|
||||
"package": "com.ilias.sundial",
|
||||
"name": "SUNDIAL",
|
||||
"icon": "/common/icon.png",
|
||||
"versionName": "0.3",
|
||||
"versionCode": 3,
|
||||
"versionName": "0.5",
|
||||
"versionCode": 5,
|
||||
"minAPILevel": 1,
|
||||
"deviceTypeList": [
|
||||
"watch"
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const assert = require('node:assert/strict');
|
||||
const ux = fs.readFileSync('src/pages/index/index.ux', 'utf8');
|
||||
const script = ux.match(/<script>([\s\S]*?)<\/script>/)[1];
|
||||
function page() {
|
||||
const spec = vm.runInNewContext(script.replace('export default', 'globalThis.spec =') + '\nspec');
|
||||
return Object.assign({}, spec.private, spec);
|
||||
}
|
||||
const p = page();
|
||||
assert.equal(typeof p.press, 'function', 'Keypress handler must exist');
|
||||
['2','+','3','='].forEach(k => p.press(k));
|
||||
assert.equal(p.display, '5');
|
||||
console.log('PASS addition through page keypress handler');
|
||||
const cases = [
|
||||
['subtract',['9','-','4','='],'5'],
|
||||
['multiply',['6','x','7','='],'42'],
|
||||
['divide',['8','/','2','='],'4'],
|
||||
['decimals',['0','.','1','+','0','.','2','='],'0.3'],
|
||||
['duplicate dot',['1','.','.','2'],'1.2'],
|
||||
['sign',['5','+/-','x','2','='],'-10'],
|
||||
['percent',['5','0','%'],'0.5'],
|
||||
['percent add',['2','0','0','+','1','0','%','='],'220'],
|
||||
['percent multiply',['2','0','0','x','1','0','%','='],'20'],
|
||||
['clear',['9','+','AC','2','='],'2'],
|
||||
['zero division',['8','/','0','='],'Error'],
|
||||
['recover',['8','/','0','=','3'],'3'],
|
||||
['chain',['2','+','3','x','4','='],'20'],
|
||||
['replace operator',['2','+','x','3','='],'6'],
|
||||
['repeat equals',['2','+','3','=','=', '='],'11'],
|
||||
['fresh entry',['2','+','3','=','7'],'7'],
|
||||
['input cap',Array(20).fill('9'),'999999999'],
|
||||
['leading zero',['0','0','2'],'2']
|
||||
];
|
||||
for(const [name,keys,expected] of cases) {
|
||||
const c=page(); keys.forEach(k=>c.press(k)); assert.equal(c.display,expected,name);
|
||||
}
|
||||
assert.match(ux, /\{\{ display \}\}/);
|
||||
const keys=[...ux.matchAll(/<text[^>]*onclick="press\('([^']+)'\)"/g)].map(m=>m[1]);
|
||||
assert.equal(keys.length,19,'every button must have handler');
|
||||
assert.equal(new Set(keys).size,19);
|
||||
console.log('PASS',cases.length,'edge cases and all 19 button bindings');
|
||||
Reference in New Issue
Block a user