Skip to content

Stop a refused replace from ending in "stored but not activated" - #83

Merged
freethinkel merged 2 commits into
mainfrom
issue-62-stored-not-activated
Oct 20, 2026
Merged

freethinkel merged 2 commits into
mainfrom
issue-62-stored-not-activated

Conversation

@freethinkel

@freethinkel freethinkel commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Closes #62. This fixes both paths in our code that end in the exact error the reporter quoted. The rest is watch-side firmware state that matches a documented hardware finding; it is unverified on this reporter's watch, so reopen the issue if their answers point elsewhere.

Is it our bug?

The file is fine. Our error handling turned a recoverable watch state into a dead end.

The DosWatch files are ordinary

I downloaded all four marketplace uploads (1sephn6x6t56opy, detq31fmwvb5xkz, ivo9bn9oyd2vqjn, h1cvpr8a1b7x9an) and compared them with the 100 stock faces and the fixtures:

DosWatch (4 uploads) stock corpus (100)
size 186–232 KB 8.7 KB – 3 MB, median 90 KB
resources 158–171 6–175
largest image side 465–467 px up to 470 px
pixel formats cf 4/5 only cf 4/5 in all 100
decoded pixel area ~0.73 Mpx median 0.68 Mpx, max 26 Mpx
name field byte 15 0x0a 0x0a in 45 of 100
header CRC, resource CRC, length fields, trailer all valid —
TLV tag paths, data-source ids all also seen in stock faces —

The most-flashed upload has about 160 flashes, and this is the only report.

The watch state is documented, and it is recoverable

watch_health/docs/cmf-protocol.md §9.5h is a real Pro 2 run of the same symptom. After a reset, the watch answered finish: 0x0a to every upload: appends, a stale id, and replacing 4 of its 6 stock dials. It accepted only one target, replacing stock dial 274. After that install, uploads worked normally again. The same section shows that 0x0a also covers what looks like a storage refusal: a 498 KB file failed three times, then went through after a smaller install. So 0x0a describes the watch's state, not the content of the file.

What our code did with that state

stored but not activated is our label for finish: 0x0a. uploadWatchface only shows it on the append that follows a refused overwrite. Two things led there:

  1. A refused replace of a dial the watch listed was read as "that dial isn't installed". The code dropped the dial from the list and sent the whole file again as an append. On a full watch that append can only be refused. This is §9.5h exactly: pick a stock dial in the slot dialog, the watch says 0a, we waste a second transfer, show "stored but not activated", and our list now has one fewer real dial. The next flash then goes out as an append and gets refused again. Now the dial stays listed and the error says: the watch refused to replace “Dichotomy” — flash again and pick a different dial to replace. Picking a different target is the recovery §9.5h found. The append fallback still applies to ids we only guessed from this browser's own records, which can go stale after a reset.
  2. The loop guard fired on the wrong retry. On that append-after-a-guessed-id path, a second 0a is the first append refusal. Unable to Upload a Watch Face a Second Time #43 already established that this is how the watch says it is full, so it now opens the slot dialog. After a slot the user picked themselves, a second 0a still shows as an error, so the dialog can't loop.

Still unverified

  • Which replace targets a given watch accepts. §9.5h only says "one of the six stock dials, try them". That is marked with a ponytail: comment.
  • Why the reporter's official app also fails. Nothing in our upload path writes anything beyond the handshake + file + finish ack that the Go tool and Gadgetbridge send. §9.5g notes that Nothing X kept installing while the Go tool's appends failed. So the official app's failure is either the same firmware state or something we can't see from here. The questions below should tell us which.

Verification

  • tests/watch-dials.browser.test.ts: a bare Watch whose transport answers every finished transfer with 0a.
    • A refused replace of an a055-listed dial keeps the dial, costs one transfer, and asks for another target. Without the fix this fails with watch did not accept the file (stored but not activated; …).
    • Re-flashing a dial that is gone from a full watch → NoFreeSlotError (slot dialog). Without the fix this fails with the same message.
    • 0a again after a slot picked from the dialog → plain error, not the dialog again.
  • npm run check: 0 errors. oxlint/oxfmt clean on the touched files.
  • npm test: 56/56 files, 320 passed.
  • Not tested on a watch.

Draft reply to the reporter

Thanks for reporting this, and sorry for the trouble.

We checked every DosWatch upload on the marketplace against the official watch faces: size, image count, header, checksums, and everything inside. They look like normal faces, and the most-installed one has been flashed about 160 times without other reports. We don't think the file damaged the watch.

What you ran into looks like a known watch state. After a reset, the watch can refuse almost every install with the same code. Our site showed that code as "stored but not activated". In our own testing the watch started accepting faces again once one install replaced a different built-in face. Our site was making that harder: when the watch refused to replace a face, the site dropped that face from its list and gave up. That's fixed now. Once it's deployed, please try this:

  1. Connect the watch on the site and flash any small face.
  2. When it asks which face to replace, pick one. If the watch refuses, flash again and pick a different one. In our test only one of the six built-in faces was accepted.
  3. After one install goes through, installs from the official app should work again too.

If it still fails, please tell us:

  1. Which watch it is: CMF Watch Pro 2 or Pro 3.
  2. The firmware version (in the CMF Watch app under device info, or in our watch panel after connecting).
  3. Which DosWatch you installed. There are several uploads with that name; a link to the marketplace page is best.
  4. The exact error text, from our site and from the official app. Screenshots are perfect.
  5. What "reset" was: a factory reset from the watch's settings, or a restart.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LX5gm5ode3C7m9UdSZPtHp

freethinkel and others added 2 commits October 20, 2026 13:24
"stored but not activated" is our own label for finish: 0a, and it is
only ever shown on the append that follows a refused overwrite. The
loop guard keyed on "this is a retry", so it also fired when the
overwrite target was a guessed id (a re-flash of a dial that is gone
from the watch) rather than a slot the user picked. That append is the
first one, and 0a on an append is the full-watch answer (#43) — it now
reaches the slot dialog. After a slot picked in the dialog, a second 0a
still surfaces as an error.

Refs #62

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LX5gm5ode3C7m9UdSZPtHp
A 0a against an old_wf_id that a055 reported this session was read as
"that dial isn't installed": the id was dropped from the list and the
file went out again as an append. On real hardware a reset watch
refused most stock dials as replace targets and every append, and took
one stock dial (watch_health docs/cmf-protocol.md §9.5h), so that path
threw away a real slot, spent a second transfer on a certain refusal
and ended in "stored but not activated". Now the dial stays listed and
the error asks for a different target, which is the recovery §9.5h
found. Guessed ids (this browser's records) still fall back to append.

Refs #62

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LX5gm5ode3C7m9UdSZPtHp
@freethinkel freethinkel changed the title Send a full watch's 0a to the slot dialog after a stale re-flash Stop a refused replace from ending in "stored but not activated" Oct 20, 2026
@freethinkel
freethinkel merged commit bd89cf4 into main Oct 20, 2026
1 check passed
@freethinkel
freethinkel deleted the issue-62-stored-not-activated branch October 20, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

watchface stored but not activated;

1 participant