tachyon: make setup recover from unfamiliar GPTs and fix 24.04 onboarding - #933
tachyon: make setup recover from unfamiliar GPTs and fix 24.04 onboarding#933mrlambchop wants to merge 9 commits into
Conversation
…ores Two independent bugs, both of which corrupt a device quietly rather than failing. 1. `particle tachyon setup` resolved the misc partition's address from the GPT the device had BEFORE the OS was flashed, then programmed that address afterwards. On a 20.04 -> 24.04 setup the layouts differ, so the first-boot configuration (password, Wi-Fi, registration code) landed on whatever partition moved into those sectors. On a device already running 24.04 it failed outright with "Partition misc not found in device partition table", because 24.04 has never shipped a misc partition at all. createConfigBlobStep now only builds the blob. A new verifyConfigPartitionStep reads the table the IMAGE declares (from its rawprogram XMLs) and refuses a setup that cannot store the configuration -- before a multi-GB write rather than after it. The address itself is resolved from the live GPT after the OS is written, in the window where the device is already held in EDL for the second qdl session. If misc is missing at that point the error names the image and says the OS did install, instead of surfacing the raw partition-table message. Both the ubuntu20 and ubuntu24 workflows get the new step. 2. restore()'s retry path called this.backup(). Wrong method, and backup() does not even take input-dir, so a failed restore silently re-ran a BACKUP -- taking a fresh copy of the modem NV the restore had just half-written and overwriting the good archive with it. It now retries restore(). Restores are also verified rather than assumed. Backups get a backup.json sidecar (schema version, device id, per-file LUN/LBA geometry, byte length, sha256), and a restore checks every file exists, matches its digest and fits its target partition BEFORE the first byte is written, then reads the partitions back off the device and compares. Only the first N bytes are compared, since the read returns the whole partition while the backup may be shorter. Archives without the sidecar still restore, with the size checks and no digests. Backups now default to a backups/ folder in the Particle data directory instead of whatever directory the command ran from -- modem NV is not reproducible and a backup one `cd` from being lost is not a backup. Restore and the factory-restore backup check both still accept the old working-directory location. PARTITIONS_TO_BACKUP is unchanged. persist stays out: the WLAN and BT MACs are in modem NV (read over AT with +QNVR), and /persist holds only rmtfs symlinks that create-modem-symlinks.sh recreates, reformatting the partition if it has to. 1088 unit tests pass (29 new), 10 integration, lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SSL8pbMoVJzoAkb6uXtkZA
A provisioning descriptor's size_in_kb is only what was REQUESTED at provisioning time; UFS rounds it up to a whole number of allocation units, so it is a lower bound on what a LUN actually has and cannot settle "is this partition table too big for LUN n". The device can: the firehose resolves NUM_DISK_SECTORS against the real LUN when it writes the GPT, so the header's backup-header LBA is that LUN's last sector. We already read every gpt_main<n>.bin off the device on each flash, backup and restore, and were throwing that away. readLunCapacities() now reports it and the partition read logs it per LUN, which makes the geometry answerable from any device instead of inferred from an XML. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SSL8pbMoVJzoAkb6uXtkZA
There was a problem hiding this comment.
Pull request overview
This PR fixes two defects in the Particle CLI's Tachyon setup and backup/restore flows. In setup, the first-boot configuration blob was being addressed from the pre-flash GPT and written after the OS was flashed, so on 20.04→24.04 flashes the blob landed on the wrong sectors (or failed outright on 24.04, which had no misc). The fix splits blob creation from address resolution: a new pre-flight step verifies the target image declares an adequate misc partition, and the address is now resolved from the newly-flashed live GPT. In backup/restore, the retry path mistakenly called backup() instead of restore(); the fix corrects the retry, adds a backup.json sidecar (schema version, device ID, geometry, sizes, SHA-256), performs up-front bounds/integrity checks and post-restore read-back verification, moves backups under the Particle data directory (with legacy-location fallback), and logs real UFS LUN geometry from the device GPT.
Changes:
- Setup: add
verifyConfigPartitionStep(image preflight) and resolvemiscfrom the post-flash GPT viaresolveConfigPartition, keeping the device in EDL between OS and config writes. - Add image/GPT parsing helpers (
readPartitionsFromImage,parseRawProgramPartitions,readLunCapacities) and shared constants totachyon-utils.js. - Backup/restore: fix retry to call
restore(), addbackup.jsonmetadata plus pre-write validation and post-write read-back verification, and store backups durably under the Particle data directory.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/tachyon/workflow.js | Inserts verifyConfigPartitionStep into the ubuntu20/ubuntu24 workflows. |
| src/lib/tachyon/steps.js | Splits config creation, image preflight, and post-flash misc resolution; keeps device in EDL for the config write. |
| src/lib/tachyon/steps.test.js | New tests for verifyConfigPartitionStep and flashOSAndConfigStep ordering/failure messaging. |
| src/lib/tachyon-utils.js | Adds image/GPT partition parsing, LUN capacity reads/logging, and exported config-partition constants. |
| src/lib/tachyon-utils.test.js | New tests for rawprogram parsing, image partition reads, and LUN capacities. |
| src/cmd/backup-restore-tachyon.js | Fixes retry to restore(), adds metadata sidecar, up-front verification, read-back verification, and durable backup dir. |
| src/cmd/backup-restore-tachyon.test.js | New tests covering metadata, verification, and the retry fix. |
| src/cmd/tachyon-factory-restore.js | Aligns _backupDir with the backup command default and broadens hasBackups(). |
| src/cli/tachyon.js | Updates backup/restore option help text for the new default directory. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`particle tachyon setup` and `particle tachyon identify` fail outright on any device already running 24.04: Error: Partition boot_a not found in device partition table getTachyonInfo reads fsg, boot_a and boot_b, and partitionDefinitions throws on any partition missing from the device GPT. The 20.04 layout has boot_a/boot_b; the 24.04 layout does not. So identification dies before setup reaches anything else -- including the misc handling in the previous commit. boot_a/boot_b are read for exactly one purpose: sniffing an Ubuntu 20.04 / 24.04 marker out of their first sector. Nothing else consumes them. So they are now declared optional: partitionDefinitions skips an absent partition the caller has named optional and throws for every other one, and getIdentification reads an empty buffer where the file was never produced. That alone leaves osVersion 'Unknown' on 24.04, which is worse than it needs to be. The absent boot partitions are themselves the signal: 24.04 dropped boot_a/boot_b and replaced the slotted system_a/system_b with a single `system`. When there is no boot partition to sniff and the table has `system` without `system_a`, identify it as Ubuntu 24.04. The guard on `system_a` keeps a slotted layout from being claimed by the fallback. Found on head2 running 24.04, against the image built by tachyon-composer#76. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SSL8pbMoVJzoAkb6uXtkZA
CI builds Ubuntu 24.04 headless images, but the ubuntu24 workflow declared only the desktop variant. Setting up a headless 24.04 device therefore had no setupCompletedMessage to print, and the final step rendered literally: undefinedLearn more about Tachyon at our developer site: ... Adds the headless variant, matching ubuntu20's wording minus the 5G modem line that the 24.04 desktop variant also omits, and keeping the 24.04 support link. With two variants declared, pickVariant now also offers the choice interactively instead of silently defaulting. Drops '(beta)' from the workflow name and removes the selectionWarning, which existed only to announce beta status. selectionWarning is optional and guarded at its one call site, so removing it is inert. Observed setting up head2 from the tachyon-composer#76 image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SSL8pbMoVJzoAkb6uXtkZA
The ubuntu24 workflow was missing getCountryStep and getESIMProfilesStep. ubuntu20
has both. So 24.04 setup never asked for a country and never called
PUT /v1/products/:productId/devices/:deviceId/target_profile, and the config blob
was written with no `esim` key at all.
The consumer has been there the whole time. particle-linux's bootstrap declares
`esim?: { profiles: [{ iccid, activation_code }], target_profile }` in its
BootstrapConfig and calls storeEsimBootstrap() whenever the blob carries it,
persisting the profile set and the target profile. 24.04 devices simply never got
one -- a device worked only if its eUICC happened to be provisioned already.
Both steps are OS-agnostic: getCountryStep prompts (or takes `country` from a
loaded config in silent mode) and getESIMProfilesStep is non-fatal, warning and
returning { esim: null } if the lookup fails. createBlobFile snapshots the whole
context minus a small exclusion list and drops nulls, so `country` and `esim`
reach the blob with no further plumbing.
Ordering matters and is now asserted: the country has to resolve before the
profile lookup (it is the countryCode argument), and both before the blob is
built, since the blob is a snapshot.
Adds workflow.test.js, which pins the two step lists to be identical so this
cannot silently drift again -- verified it fails when the change is reverted.
1101 passing, 10 pending, lint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSL8pbMoVJzoAkb6uXtkZA
Both 24.04 completion messages listed "Connect to the Particle Cloud" but not "Activate the built-in 5G modem". 20.04 lists both. That was accurate while ubuntu24 had no getESIMProfilesStep, and stopped being accurate the moment it gained one, so add the line to the desktop and headless variants -- matching ubuntu20's wording and its punctuation (desktop uses full stops, headless does not). Tied to behaviour rather than left as prose: the new test asserts a workflow claims modem activation if and only if its step list contains getESIMProfilesStep, across every variant. Copy and capability now cannot drift apart the way they just did. NOT yet verified end to end on hardware. head2's eUICC is already provisioned, so re-running setup there does not exercise a device that arrives without a profile -- which is the case this message now promises. Confirm on an unprovisioned unit before release. 1102 passing, 10 pending, lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SSL8pbMoVJzoAkb6uXtkZA
Backups now land in the Particle data directory rather than the working
directory, but the completion message still printed `outputFile`, the bare
basename. Run on head1:
$ cd ~/h1-backup-test && particle tachyon backup
Created File: manufacturing_backup_422a060000000000caf5911b.zip
Restore it with: particle tachyon restore --filepath manufacturing_backup_...zip
$ ls
(empty)
The archive was in ~/.particle/backups the whole time, and the restore command
it suggests fails from any directory but that one -- which is not the directory
the user is standing in when they read it.
`compressDir` already returns `zipPath` alongside `outputFile`; use it for both
the console lines and the log line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSL8pbMoVJzoAkb6uXtkZA
|
Note that there's no headless variant for latest for 24.04 |
|
Added DO-NOT-MERGE to make it clear that this needs to wait until the changes to Ubuntu 24.04 in Tachyon composer are finalized. |
A Tachyon image carries the firehose, rawprogram and patch XML that write its target GPT, but setup first called getTachyonInfo() and aborted if the existing GPT was blank, corrupt or unfamiliar. This made recovery depend on the layout being replaced.\n\nKeep the read as best-effort metadata discovery so recognised devices still supply their region and board automatically. If that read fails because of partition contents, continue with the device ID already reported in EDL and let options/config/defaults select the target image. Preserve the existing retry-and-abort behaviour for a real transport loss.\n\nAdd command-level regression coverage for unsupported and unparseable GPTs reaching the workflow, recognised layouts retaining metadata, and connection failures retrying or stopping.
4329338 to
7111fea
Compare
087b8c8 to
640ece5
Compare
What
Nine commits that make
particle tachyon setupwork on 24.04, and maketachyon backup/tachyon restorelayout-independent and verified.3acc9124misc; verify NV restoresda0ccaa0d8431859boot_a/boot_be19bb1229dccf64154bd8d90ba0dea1f7111fea6640ece58No dependency changes.
package.jsonis untouched — in particular this does notadd
@particle/tachyon-image, which stays dormant until the A/B work lands.Why
1. The config blob was written using the wrong GPT
createConfigBlobStepresolved themiscLBA from the pre-flash GPT andflashOSAndConfigStepprogrammed that XML after the OS was written. On a 20.04 → 24.04setup that means the blob went to the old
miscoffset — i.e. to whatever the new imageput on those sectors. On a device already running 24.04 it failed outright with
Partition misc not found in device partition table, because 24.04 had nomiscat all(fixed by tachyon-composer #76).
Reordered to:
createBlobFile, unchanged);verifyConfigPartitionStepreads the partition table out ofthe image about to be flashed and fails if it has no
misc, ifmiscis smaller than256 sectors, or if the blob would not fit. This runs before the multi-GB write, so a
mismatch costs seconds rather than a full flash;
miscby label and generate the QDL program XML.flash()already ranflashTachyonthenflashTachyonXmlas two qdl invocations, sostep 4 slots between them.
createConfigBlobStepnow returns only{ configBlobPath }.Step 5 (
resolveConfigPartition) is the authoritative gate: ifmiscis absent from thepost-flash GPT it fails with a message naming the image and stating that the OS was
installed but the configuration was not applied, rather than silently writing the blob to
whatever now occupies those sectors.
keepInEdl = skipReset || Boolean(configBlobPath).Reset behaviour is unchanged from
master:flashTachyonXml({ skipReset })is calledexactly as before. Only the internal name changed (
shouldResetOS→keepInEdl).2.
restore()'s retry path calledbackup()backup-restore-tachyon.js:151-153caught a restore failure and retried withthis.backup({ 'input-dir': … })— wrong method, andbackup()does not takeinput-dir. Now retriesthis.restore(...)with restore's own arguments.Also: backups stored durably under the Particle data directory with the path always
printed; a
backup.jsonsidecar (schema version, device ID, source LUN/LBA geometry,byte lengths, SHA-256 per file); pre-write validation that each file fits its target
partition; and post-write read-back hash verification. Legacy archives without
backup.jsonare still accepted.3.
identifythrew on any device withoutboot_apartitionDefinitionsthrewPartition boot_a not found in device partition tableforany layout lacking it. 20.04 has
boot_a/boot_b; 24.04 does not — sotachyon setupdied at "Unable to get device info." on a 24.04 board. Adds an
optionalPartitionslist(threaded through
prepareFlashFiles) so a missing optional partition yieldsnullandis filtered out instead of throwing, and uses the absence of both boot partitions
together with a single non-A/B
systemto reportUbuntu 24.04.4. 24.04 offered no headless variant
The
ubuntu24workflow had a desktop variant only, so step 10 printedundefinedLearn more about Tachyon…— a missingsetupCompletedMessage. Adds theheadlessvariant with its own completion message, and drops(beta)from the 24.04name plus the now-stale
selectionWarning.5. 24.04 setup never fetched the eSIM profiles
The
ubuntu24workflow was missinggetCountryStepandgetESIMProfilesStep.ubuntu20has both. So 24.04 setup never asked for a country, never calledPUT /v1/products/:productId/devices/:deviceId/target_profile, and wrote the config blobwith no
esimkey at all.The consumer has been there the whole time — particle-linux's bootstrap declares
and calls
storeEsimBootstrap()whenever the blob carries it. 24.04 devices simply nevergot one, so a device worked only if its eUICC happened to already be provisioned.
Both steps are OS-agnostic:
getCountryStepprompts (or takescountryfrom a loadedconfig in silent mode), and
getESIMProfilesStepis non-fatal — it warns and returns{ esim: null }if the lookup fails.createBlobFilesnapshots the whole context minus asmall exclusion list and drops nulls, so
countryandesimreach the blob with no extraplumbing.
Ordering matters and is now asserted: the country must resolve before the profile lookup
(it is the
countryCodeargument), and both before the blob is built, since the blob is asnapshot of the context.
6. 24.04's completion message did not mention the modem
Both 24.04 variants listed "Connect to the Particle Cloud" but not "Activate the built-in
5G modem"; 20.04 lists both. That was accurate while
ubuntu24had nogetESIMProfilesStep, and stopped being accurate the moment it gained one. Added to thedesktop and headless variants, matching
ubuntu20's wording and punctuation.Tied to behaviour rather than left as prose: a test asserts a workflow claims modem
activation if and only if its step list contains
getESIMProfilesStep, across everyvariant — so the copy and the capability cannot drift apart again the way they just did.
7. The outgoing GPT is no longer a setup precondition
Commits
7111fea6and640ece58fix the underlying recovery contract rather thanadding another known layout to the allow-list.
Why setup read the old GPT in the first place
The original structure appears to have been a convenience:
getTachyonInfo()produced one object containing the EDL device ID plus values inferred from storage:
fsg;nvdata1;boot_a/boot_b;Region and board then selected the matching image automatically, while the remaining
fields were printed to the user. That is useful metadata, but setup treated failure to
obtain any of it as proof that flashing could not work. The conclusion does not follow:
the EDL enumeration already supplies the identity used by setup, and the selected image
carries
prog_firehose_ddr.elf, its rawprogram XMLs, patch XMLs, and Primary/BackupGPT writes.
New behavior
The Particle Cloud lookup starts as soon as USB enumeration supplies the EDL device ID,
in parallel with the slower best-effort storage read. A blank, corrupt, or unfamiliar GPT
now emits a warning and setup continues. Region and board are resolved independently, in
this precedence:
The physical device wins over the cloud because the cloud record is only as fresh as its
last connection. Today the cloud supplies region from
modem_firmware_version; it doesnot report EVT/DVT board type, so an unreadable board still prompts unless the operator
provided
--boardor saved it in the setup configuration. The selected source is shownin the device information. There is no silent NA/DVT fallback.
Effect on the onboarding flow
failure is non-fatal.
one field without being trusted for the other.
value remains unknown, setup asks the operator instead of guessing.
read, and
miscconfiguration steps then continue unchanged.A real
TachyonConnectionErroris deliberately different: setup still offers a retryand stops if the user declines, because the device is no longer satisfying the one
hardware precondition -- being reachable in EDL.
Downsides and remaining constraints
device cannot answer, the output labels cloud-derived values.
board, and overridesdiscovery. Reusing a config created for different hardware can therefore select the wrong
image; explicit
--region/--boardremain the final override.is intentional: guessing NA/DVT could install an image for the wrong cellular bands or
hardware revision.
flash. Removing it entirely would be faster, but would regress automatic image
selection for the normal readable-device case.
compatible. The package must still contain a valid programmer/GPT and fit the target
UFS geometry.
miscread remains intentional. It occurs only after the image hasinstalled its own GPT and resolves where the configuration blob belongs. If that new
image lacks a valid
misc, the OS is installed but setup reports that configurationcould not be applied.
The setup tests cover recognized and unsupported layouts, cloud lookup ordering, every
precedence tier, prompt fallback, loaded board configuration, connection failure, and
connection retry. Utility tests cover cloud firmware parsing, non-fatal lookup failure,
and refusing to classify an unfamiliar GPT as DVT.
Hardware regression: 24.04 single-slot to 20.04 A/B
Tested on head2, device
422a060000000000d0c7965f, using the exact command:Before installing the PR build, the old
/optbinary stopped before any write:After installing the PR build at the same path, the command:
misc;misc;The booted board reports Ubuntu 20.04.6 LTS, kernel
5.4.219, root/dev/sda11, Wi-Fi associated, and the Particle, GNSS, and RIL services active.miscresolves to/dev/sda5, is exactly 1 MiB, and hashes as an all-zero 1 MiBbuffer after first boot, showing that bootstrap consumed and erased the configuration.
The older 20.04 image reports systemd
degraded; observed failures across these bootswere
persist.mount,sfsconfig.service, andthermal-engine.service. Those boot-timeimage issues were recorded but are outside this CLI change.
Hardware verification of the final precedence resolver
The
087b8c8dARM64 build exercised the final behavior at/opt/particle/bin/particle. The final640ece58adds only the numbered precedence comment in source; its exact ARM64 artifact was built and installed afterward. The same20.04 package was run again, with the saved setup configuration streamed after removing
its
regionandboardkeys so discovery—not the config—had to resolve both fields.The live output was:
This exercises the per-field merge rather than an all-or-nothing fallback: the physical
GPT supplied DVT, while the physical
fsgpartition was readable but contained noSG560D-NA/SG560D-EMregion marker, so the cloud filled only region.fsgitselfand its manufacturing-data
EFSheader are present; this is not modem-data damage.Setup again completed all 12 steps. The board booted Ubuntu 20.04.6, associated to Wi-Fi,
and the Particle, GNSS, and RIL services are active. The post-flash
miscis 1 MiB andhashes as an all-zero buffer after bootstrap consumed the configuration.
Tests
1117 passing, 10 pending, lint clean. New coverage in
steps.test.js,tachyon-utils.test.jsandbackup-restore-tachyon.test.jsfor: theGPT being read after the OS flash and the new
miscLBA being used; an image with nomisc, or an undersized one, failing at preflight before any flash; a device with nomiscpost-flash failing at the second gate; restore retryingrestore; restorerejecting a file that would overrun its target; read-back hash verification; legacy
archives with no
backup.json; andpartitionDefinitionstolerating absent optionalpartitions.
Hardware
head2, against composer build
1.2.14-dev+build.64a0cda(tachyon-composer #76). Theboard has been on 24.04 since 27 Aug, so the first two runs are 24.04 → 24.04 — which is the
case defect 1 broke outright. The 20.04 → 24.04 direction is not exercised here.
Run 1
undefinedUbuntu 24.04.4 LTS, kernel6.8.0-1058-particlemisc -> sda3, exactly 1048576 bytes, and entirely zero after first boot — the blobwas written to the correct post-flash offset, consumed by the particle-linux bootstrap
and wiped
particlein/etc/shadow)(
modemst1bf3b6f09…,modemst2a00c952e…,fscfa43239b…,fsgabccf416…,nvdata1/nvdata2106f0647…), withnvdata1/2still at the 20.04 1.5 MiB sizesRun 2 — repeat setup, again on 24.04
Both runs exercise the case that used to fail outright with
Partition boot_a not found in device partition table→ "Unable to get device info."That failure is itself the evidence the board was on 24.04: the very first attempt in
this session died there, because 24.04 has no
boot_a.Device info:now reportsOS Version: Ubuntu 24.04and setup continuesthe new image preflight ran and passed before any flashing
root=PARTLABEL=system/sda4miscagain exactly 1 MiB and again entirely zero after first bootparticle-tachyon-ril-ctl inforeports IMEI-18644300100010 91,IMEI-2
…92, ICCID8935711800001245077, EID89044045…, firmwareSG560DNAPAR60A03fsg,fsc,nvdata1andnvdata2are byte-identical across both flashes.modemst1/modemst2differ between the two runs, which is expected and notflash-related — they are the modem's live working EFS partitions. Verified directly:
with no flashing at all,
modemst2changed hash within 90 s of idle runtime(
f05c5fc1…→99ff9042…). After the first flash they matched the pre-flash captureexactly, which is what shows the flash itself does not touch them.
On-image suites (run 2 device,
TMPDIR=/tmp)particle-linux-testparticle-tachyon-gnss-testparticle-tachyon-rild-testThe failures are pre-existing image issues, not regressions from this PR — see the
composer PR for the reasoning. Note the suites must be run with a valid
TMPDIR: underadb shell,TMPDIRdefaults to the Android path/data/local/tmp, which does notexist on this image, and the suites' output redirects fail in a way that looks like a
CLI fault (
'particlectl systemdoc' exited 2).Landing note
Land after tachyon-composer #76 and after an image containing
miscis published —this PR's post-flash gate fails by design against an image that has no
misc.