Seek Thermal Firmware Dump

Reads the 4 MiB SPIFI flash of a Seek Thermal camera over WebUSB and decrypts any firmware images it finds. Everything runs locally in your browser — nothing is uploaded anywhere. Read-only: this view issues no flash write, erase, upload, commit, or reset command.

1 · Connect

No device selected.

Any Seek Thermal device is offered (USB vendor 0x289d) — Compact, Compact PRO, Nano and so on. Pick yours in the browser's device chooser.

2 · Dump

Start dump uses the known selector map. Dump all selectors instead probes every BeginFirmwareUpgrade selector 0x00–0xFF and saves whatever each one returns — slower, but it also captures blocks the curated map skips. Unmapped selectors simply stall and are recorded.

Idle.
Options — the defaults are correct; you do not need to change anything here.

chunk is the size of each control-IN read. 64 is the EP0 packet size and the value that works on every camera tested; raising it to 256 is roughly four times faster, and if a camera stalls at that size the page drops back toward 64 on its own and carries on. gap-fill is the byte written into the one 64 KiB block USB cannot reach. recipient defaults to interface and falls back to device if the browser cannot claim interface 0 — the firmware accepts both.

Optional — for older or locked firmware

Dump older / locked firmware

Older Seek firmware locks the boot, calibration and app-image banks (0x14010000–0x14070000) behind an authenticated read channel and exposes no selector for the top 2 MiB. On such a unit the normal Start dump above stalls on those banks and returns only a partial, header-less image that will not decrypt. This variant speaks the legacy protocol: it unlocks the protected banks with the firmware's own channel-0x12 token so their firmware images come through — including 0x14060000 — and marks the blocks the old protocol genuinely cannot reach as gaps. It is still read-only: the token only arms a read window, and nothing is ever written.

Needs a connected device (step 1). If the normal dump already reads every window, use that instead — this is only for units where it stalls. If windows come back truncated, open Options in step 2 and lower chunk.

One caveat. The unlock token is build-specific; the one built in was recovered from a Compact PRO (PIR324) unit. On firmware with a different token the protected banks just stall and are gap-filled — the same result as the normal dump, and still safe. The app images come through encrypted (the key's boot bank at 0x14000000 stays blocked on every channel), but that no longer matters: decryption recovers the key from each encrypted image itself, so any slot that is actually captured still decrypts here.

Dump all selectors here probes every selector 0x00–0xFF on the authenticated channel (with the unlock token), so it captures every block the older firmware exposes, not just the mapped ones.

Idle.

Optional — not part of the two steps above

Decrypt a dump you already have

Step 2 already decrypts everything it reads, so you only need this if your flash image came from somewhere else — an earlier run, J-Link, or an SPI programmer. The file is read locally and never uploaded. This part works in every browser, including Safari and iOS, because it needs no USB access.

No file chosen.

What you get

A single .zip download containing:

flash_4m_usb_partial_gap_ff.binthe assembled 4 MiB flash image
windows/*.bineach 64 KiB block exactly as it came off the wire
decrypted/*.binevery firmware image slot, decrypted (if a key was found)
decrypted/*.txtper-slot report: key, key location, cipher profile, SP, entry, SHA-256
manifest.jsonexact read/gap/decryption metadata
README.mdwhat the capture is and what its limits are

The key is recovered from the dump itself, so no key file or device secret is needed.

Decryption is still best-effort. If no key is found — for example on a unit whose bootloader derives the key from chip OTP, which is not present in a flash dump — that slot is simply skipped and the rest of the archive is unaffected.

Platform notes

macOS

Works out of the box in Chrome, Edge or any Chromium browser. No driver install, no admin rights. Quit any other program that is already talking to the camera first — only one process can hold the USB interface at a time.

Linux

Give your user access to the device, then unplug and replug it:

# /etc/udev/rules.d/70-seek-thermal.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="289d", MODE="0666", TAG+="uaccess"

sudo udevadm control --reload-rules && sudo udevadm trigger

Snap and Flatpak Chromium builds are often sandboxed away from raw USB devices; the .deb or .rpm build is the reliable one.

Windows

Windows needs the WinUSB driver bound to the camera before any browser can reach it. Use Zadig: Options → List All Devices, then in the dropdown pick the entry named after your camera — for example CompactPRO FF — choose WinUSB as the driver, and click Replace Driver. If your camera appears more than once, take the entry labelled (Interface 0). This takes the device away from the vendor software until you revert it in Device Manager (Uninstall device → Delete driver, then replug).

Android

Works in Chrome for Android with a USB-OTG cable, provided the phone can supply bus power to the camera. Android grants USB access through its own permission dialog. The download lands in your Downloads folder like any other file.

iPhone and iPad

Cannot read a camera, and this will not change. Safari does not implement WebUSB, and on iOS and iPadOS every browser — including Chrome and Firefox — is required to render with WebKit, so none of them expose the API. There is no flag, extension, or app that works around it. Use a desktop or an Android phone to dump.

The optional decryptor does work here: decrypting a dump you already have needs no USB access, so you can do that from an iPhone or iPad, or from Firefox and Safari on the desktop.

Read-only guarantee

This page can issue exactly five vendor commands. That is its entire vocabulary — there is no code path to anything else:

OpcodeNameDirWhy it is used
0x35GetErrorCodeINcheck status after each step
0x3cSetOperationModeOUTonly to select mode 0, and only if not already there
0x3dGetOperationModeINread the current mode
0x4fGetFeaturedFirmwareDataINthe actual flash read
0x52BeginFirmwareUpgradeOUTvolatile read-window selector only — it selects which 64 KiB block the next reads return, and writes nothing

SetFeaturedFirmwareData, CompleteMemoryUpgrade, ResetDevice and every upload, commit and erase command are absent from this view. USBDevice.reset() is never called either; the retry path just closes and reopens the handle. Decryption happens entirely in memory on the copy in your browser and never touches the device.

Writing to the camera lives on its own page, Firmware & flashing, which lists the commands it adds. Nothing on this page can reach them, and no code path here calls into that view.

The legacy firmware dump uses the same five commands. Its only difference is that BeginFirmwareUpgrade carries the firmware's own channel-0x12 unlock token (an 18-byte payload) so older firmware will arm a read window over its protected banks. The token gates the read-window selector only — still nothing is written to the device.

The 0x14060000 gap

The USB command set exposes a read-window selector for every 64 KiB block of the 4 MiB flash except 0x14060000..0x1406ffff. That block is filled with 0xff in the combined image and recorded in manifest.json.

In practice this costs you nothing. On most cameras that block is simply erased flash, which reads as 0xff anyway — so the filler matches what is really there and the image is complete.

And when the block is in use, what it holds is a redundant copy. These cameras keep the same firmware image in several 64 KiB slots: across every multi-slot dump checked while building this page, all slots decrypted to byte-identical firmware — in one case across four slots that had been written with two different keys. On the one unit seen with a real image at 0x14060000, it was byte-for-byte the same as the copies at 0x14050000 and 0x14070000, so the archive still contains that firmware, just read from a different address.

That redundancy is also why a camera whose 0x14060000 reads back as 0xff is fine: the bootable firmware still exists in the other slots. Nothing on this page writes to the device in any case — the gap affects only the file you download.

If you do want a strictly complete byte-for-byte image — to diff flash against a reference, say — read it over SWD/J-Link or with an SPI programmer.