AOTA OTA flow
This is the flow that actually committed a full AOTA image on hardware. It is the most failure-sensitive part of the project, and the order of the steps matters.
Characteristics
Section titled “Characteristics”| Role | UUID |
|---|---|
| Command write | 0000fff2-0000-1000-8000-00805f9b34fb (CMD_W) |
| Command notify | 0000fff1-... (CMD_N) |
| OTA write | 02f00000-0000-0000-0000-00000000ff01 |
| OTA notify | 02f00000-0000-0000-0000-00000000ff02 |
| Shell write / notify | 77d4ff01-... / 77d4ff02-... |
The watch pulls firmware data: after A040 it sends A042 requests and the host answers
with 0x9042 frames on the OTA characteristic.
Prerequisites
Section titled “Prerequisites”- The watch is on the expected firmware and has enough battery.
- Re-pairing has happened after any reset/factory-menu clear. A stale authkey can still
produce
A052, butA040will never arrive. - The AOTA image passes
fwkit verify. - The relevant risks are documented on the Limitations & risks page.
Sequence
Section titled “Sequence”1. Reconnect and authenticate
Section titled “1. Reconnect and authenticate”0x8049 A5 + device name -> 0x00490x804B A5 -> 0x004C nonce0x804D A5 -> 0x00040x8004 time + UTC offset -> (optional)Session key: sha256(nonce || authkey)[:16], where
authkey = sha256(random1 || random2 || secret)[:16]. The secret is read over the shell
channel (AT GETSECRET). Frames after authentication are AES-CBC encrypted with the fixed IV
5051525354555657606162636465665a, PKCS#7 padding, and a CRC32-LE tail.
2. INIT1
Section titled “2. INIT1”CMD_W: FFFF 9052 A5 -> A052 0101 means accepted. Any other value indicates a wrong session/pairing state, and the
sequence ends there.
3. INIT2 — on the OTA characteristic
Section titled “3. INIT2 — on the OTA characteristic”OTA (ff01): FFFF 9040 <version:4 bytes> -> A040 01INIT2 on CMD_W reboots the watch. With a valid session it can drop the link before the
data phase. Sent directly on ff01 it returned A040 in ~30 ms on the reference unit.
The version argument is a 4-byte little-endian-ish build value from the app; 0100005a worked
for a 1.0.0.73-based image. It is not the AOTA header’s version string.
4. Data phase
Section titled “4. Data phase”The watch sends A042 on ff02. Decrypted body:
offset (u32 BE) | length (u32 BE) | progress/unknown (4 bytes) | 1 byteFor each request, the host answers with ceil(length / 224) frames of 0x9042 on ff01.
Each frame is:
F5 | len(u16) | FFFF | count(u16) | index(u16) | 9042 | 224B plaintext firmware | crc32le(u32)index starts at 1 and count = ceil(length / 224) for that request. The watch may
request non-sequential offsets; the host serves exactly what it asks for, and a single linear
pass is not implied.
5. Finish — the step that is easy to miss
Section titled “5. Finish — the step that is easy to miss”The watch can send A041 01 before the final zero-length request. A041 is the commit
prompt, and the end acknowledgement is sent immediately:
CMD_W: FFFF 9041 A5Waiting only for A042 offset=0 length=0 can miss it, because the notification may arrive
after A041; the safe client handles both and sends 0x9041 A5 on either. Skipping this
leaves the staged update uncommitted in testing.
Verified run
Section titled “Verified run”The successful flash transferred the changed res_e range plus metadata, reported A041 01,
accepted 0x9041 A5, then rebooted and played the new animation. A full candidate was ~54 MB;
the actual served range was ~38 MB because unchanged partitions were not requested.
Redacted shape of the end of a successful log:
INIT1 0x9052 (OTA begin)A052 okINIT2 0x9040 version=0100005aA040 ok, entering data phase...served ... (70.4% of image) off=0x323c2d8 len=3680watch status 0xa041 01; sending 0x9041 ENDwaiting for post-flash status ... <= cmd2=0xa042 000000000000000064summary: frames=... bytes=... (70.4% of image)The percentage is relative to the whole image, not the requested range; a successful run can end well below 100%.
Client usage
Section titled “Client usage”A probe that stops at A040 without sending firmware data:
uv run python tools/ota_client.py --stop-after-a040 --version 0100005a -vA full flash:
uv run python tools/ota_client.py \ --image firmware/custom-image.bin \ --version 0100005a-v adds per-request logs. The client writes a summary even on failure. A disconnect during
the data phase is not automatically recoverable; another flash only starts once the watch is
back in the app and info works.
Common failures
Section titled “Common failures”| Symptom | Likely cause |
|---|---|
A052 but never A040 |
stale authkey / pairing state, or INIT2 on the wrong characteristic |
| Link drops right after INIT2 | INIT2 was sent on CMD_W with a valid session |
Transfer stops with A041 and no commit |
end acknowledgement 0x9041 A5 was not sent |
no frames for Ns |
watch rebooted, aborted, or the host lost the connection |
| Requests jump around | normal; the watch requests ranges, not a linear stream |