Skip to content

Refuse images far past the screen before they reach the watch - #78

Merged
freethinkel merged 1 commit into
mainfrom
refuse-oversized-images
Oct 20, 2026
Merged

freethinkel merged 1 commit into
mainfrom
refuse-oversized-images

Conversation

@freethinkel

Copy link
Copy Markdown
Owner

A user flashed a face with a 1386×1386 AOD image to a CMF Watch 3 Pro. The firmware decodes every bitmap into RAM when it loads a face, ran out of memory, and the watch now restarts in a loop (logo, black, restart) that neither Nothing X nor FMC can connect through.

Changes

  • Flash guard. assertFlashable in core/format/bin.ts refuses a file with any image side over MAX_IMAGE_SIDE (480). It runs in flashFx, which both the editor and the marketplace install go through, so a face that is already published is covered too. Official faces top out at 467 (scanned all of tests/__fixtures__). A file we can't parse still goes to the watch's own installer, as it did before.
  • Fit on add. resourceFromFile scales an added image down to the 466 px panel. It never scales up. A replaced JPEG frame gets re-encoded the same way.
  • Troubleshooting page. The "worst case" line no longer says a bad face is always harmless. A new section, "The watch restarts in a loop", lists the recovery steps worth trying and says plainly that none of them is confirmed.

Known ceiling

480 is a cap on each image's side. It is not a measured memory budget. A manual resize or a turned full-screen background can still go past it, and the flash guard then refuses the file. This is deliberately strict until someone measures where the firmware actually falls over.

Tests

  • tests/flashable.test.ts: an official face passes, and a 1386×1386 resource is refused.
  • tests/fit-screen.browser.test.ts: a dropped 1386×693 image becomes 466×233, and a small one keeps its size.
  • pnpm test: 307 passed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nha4vjfwn4ueN8dzTVyDWr

A user flashed a face with a 1386x1386 AOD image to a CMF Watch 3 Pro.
The firmware decodes every bitmap into RAM when it loads a face, ran out,
and the watch now restarts in a loop that nothing can connect through.

Two guards:
- Before any install, editor or marketplace, flashFx runs
  assertFlashable, which refuses a file with an image side over 480 px.
  Official faces top out at 467. A file we can't parse is let through to
  the watch's own installer, as before.
- Images added in the editor (new frames, replaced frames, ring art,
  JPEG backgrounds) are scaled down to fit the 466 px panel.

The troubleshooting page no longer says a bad face is always harmless,
and lists the recovery steps worth trying. None of them is confirmed yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nha4vjfwn4ueN8dzTVyDWr
@freethinkel
freethinkel merged commit b03e1a6 into main Oct 20, 2026
2 checks passed
freethinkel added a commit that referenced this pull request Oct 20, 2026
The 480 px cap from #78 refused a factory Watch 3 Pro face (Gravity_well,
576x530), so it was stricter than what the firmware accepts. Every other stock
face, Pro 2 and Pro 3, stays at or under 470.

The hard limit is now 580. Between 470 and 580, flashFx holds the upload and the
new size dialog says what the risk is. The face goes to the watch only after
"Install anyway". Accepting marks the pending payload, so a full watch's slot
dialog retry doesn't ask again. Closing the dialog any other way drops the flash.

On the marketplace this turns about 56 refusals into confirmations. Faces
over 580 are still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vSR9vwRaYBqnq7ioL9dQ1
freethinkel added a commit that referenced this pull request Oct 20, 2026
The 480 px cap from #78 refused a factory Watch 3 Pro face (Gravity_well,
576x530), so it was stricter than what the firmware accepts. Every other stock
face, Pro 2 and Pro 3, stays at or under 470.

The hard limit is now 580. Between 470 and 580, flashFx holds the upload and the
new size dialog says what the risk is. The face goes to the watch only after
"Install anyway". Accepting marks the pending payload, so a full watch's slot
dialog retry doesn't ask again. Closing the dialog any other way drops the flash.

On the marketplace this turns about 56 refusals into confirmations. Faces
over 580 are still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vSR9vwRaYBqnq7ioL9dQ1
freethinkel added a commit that referenced this pull request Oct 20, 2026
… dial go (#84)

* Refuse an AOD screen with no thumbnail, count Pro 3 slots, let a gone dial go

Three of the four open points in #49:

- The FMC face that crash-loops a Watch 3 Pro on AOD (market 6vkrot44d1oxb19) has
  an AOD screen with no 0x28 preview node. Every stock AOD screen has one: 129/129
  on the Pro 3, 94/94 on the Pro 2. The face reported as working (f4uj4uqrpefo4f2)
  has one too. assertFlashable now refuses such a file before any install, and the
  editor's export puts a blank preview back (withPreviews) and bakes it, so opening
  the face in the editor and flashing it from there fixes it. The tree lets the
  Preview layer be deleted, which is how a face loses it.
- The slot count is per model: 7 on a Watch 3 Pro, as the reporter counted after a
  factory reset, 6 otherwise. Neither number is measured here.
- A dial flashed from this browser stays in the watch list until FMC itself
  overwrites it. a055 never lists side-loaded dials, so the list only knows them
  from a local claim, and deleting one on the watch can't reach that claim. Those
  dials now get a remove button that drops the claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vSR9vwRaYBqnq7ioL9dQ1

* Install images up to 580 px, but only after the user accepts the risk

The 480 px cap from #78 refused a factory Watch 3 Pro face (Gravity_well,
576x530), so it was stricter than what the firmware accepts. Every other stock
face, Pro 2 and Pro 3, stays at or under 470.

The hard limit is now 580. Between 470 and 580, flashFx holds the upload and the
new size dialog says what the risk is. The face goes to the watch only after
"Install anyway". Accepting marks the pending payload, so a full watch's slot
dialog retry doesn't ask again. Closing the dialog any other way drops the flash.

On the marketplace this turns about 56 refusals into confirmations. Faces
over 580 are still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vSR9vwRaYBqnq7ioL9dQ1

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant