| name | zepp-os-communication |
| description | Use when sending data between a Zepp OS device app and its phone side service, making HTTP/fetch requests, downloading or transferring files to the watch, or talking to external BLE peripherals. |
Zepp OS Communication
Context: Device App ↔ Side Service (over Bluetooth) + Side Service networking
API_LEVEL: 2.0+ (ZML layer is the modern norm on 3.0/4.0 samples)
Prereq: the four runtime contexts — see zepp-os-fundamentals.
Overview
The watch has no direct internet. The device app (watch) and the side service (companion JS on the phone) talk over Bluetooth, exchanging binary only — you serialize yourself. Three layers sit on top of that transport; pick one and use it on both sides.
Which API on which side (the core confusion)
| Device App (watch) | Side Service (phone) |
|---|
| Raw transport | @zos/ble — send / addListener | messaging.peerSocket — send / addListener('message') |
| Data form | ArrayBuffer only | ArrayBuffer only |
HTTP / fetch | ❌ not available | ✅ here only |
fetch lives only in the side service. Calling it from the device app is the #1 mistake.
Three layers (choose one)
- Raw —
@zos/ble (device) + messaging (side). Maximum control, you hand-roll JSON↔Buffer. Verbose.
- MessageBuilder
[shared/message.js + shared/message-side.js] — older helper: messageBuilder.request().then(), .on('call'), .call(), .on('request', ctx => ctx.response()). Requires a polyfill import and manual connect()/disConnect() (disconnect in onDestroy or you leak).
- ZML —
@zeppos/zml (recommended; used in current samples). Wrap App/Page/AppSideService and get request/response for free. Use this unless you have a reason not to.
ZML quickstart (recommended)
import { BaseApp } from "@zeppos/zml/base-app";
App(BaseApp({ globalData: {}, onCreate() {}, onDestroy() {} }));
import { BasePage } from "@zeppos/zml/base-page";
Page(BasePage({
build() { this.fetchData(); },
fetchData() {
this.request({ method: "GET_DATA" })
.then((data) => { const { result } = data; })
.catch((e) => {});
},
}));
import { BaseSideService } from "@zeppos/zml/base-side";
AppSideService(BaseSideService({
onInit() {},
onRequest(req, res) {
if (req.method === "GET_DATA") {
fetch({ url: "https://api.example.com/x", method: "GET" })
.then((r) => { const body = typeof r.body === "string" ? JSON.parse(r.body) : r.body;
res(null, { result: body }); })
.catch(() => res(null, { result: "ERROR" }));
}
},
onDestroy() {},
}));
Push from side → device: this.call(data) on the side, receive with this.onCall(data) on the device.
fetch (side service only)
- GET shorthand:
await fetch('https://…'). Full form: fetch({ url, method, headers, body }) — only those 4 keys.
res.body may be a JSON string on some devices — always guard: typeof body === 'string' ? JSON.parse(body) : body.
Files
- Download a network file to the phone
[side]: network.downloader.downloadFile({ url, timeout, headers, filePath }) → DownloadTask with onProgress/onSuccess/onFail/onComplete. Defaults to data://download/.
- Send a file phone → watch
[side→device]: side transferFile.getOutbox().enqueueFile(path, params); device transferFile.getInbox().getNextFile(). Listen to progress/change (readyState === 'transferred' | 'error'). See references/transfer-file-*.md.
External BLE peripherals
Talking to a third-party BLE device (not the phone) uses @zos/ble central/master APIs (mstStartScan, mstConnect, mstBuildProfile, mstReadCharacteristic, mstWriteCharacteristic, …). That is separate from device↔phone messaging. Full API: references/ble-device.md.
Common Mistakes
fetch() in the device app → not available. Do it in the side service, return via res()/response.
- Mixing layers (raw
messaging on one side, ZML on the other) → they won't talk. Same layer on both ends.
- Using
@zos/ble on the side service or messaging on the device → swapped APIs. Device = @zos/ble, side = messaging.
- Forgetting to call
res(err, data) in ZML onRequest → the device request() promise never resolves.
- Not handling
res.body as a possible string → JSON.parse errors on some models.
- Manual MessageBuilder without
disConnect() in onDestroy → memory leak.
- Sending JS objects over the raw layer → only
ArrayBuffer is supported; serialize first.
- Persisting config? that's settings-storage, shared between side service and settings app — see
zepp-os-settings-storage, not this skill.
File transfer checklist (side→device)
Progress:
Reference