Skip to content

feat: Backup and Restore for model and radio settings#134

Merged
pfeerick merged 41 commits intoEdgeTX:mainfrom
Amigache:radio-backup
Mar 7, 2026
Merged

feat: Backup and Restore for model and radio settings#134
pfeerick merged 41 commits intoEdgeTX:mainfrom
Amigache:radio-backup

Conversation

@Amigache
Copy link
Contributor

The Radio Backup feature provides a comprehensive two-way backup solution for EdgeTX radio models with dual workflows: Restore Backup (upload and restore models to SD card) and Create Backup (export models from SD card). The restore flow allows users to upload multiple file formats (.etx, .zip, or individual .yml files), mix models from different sources, and preview/select which models to restore. The create flow enables exporting selected models in three formats: single .etx file (EdgeTX standard), .zip archive, or individual .yml files, with optional labels.yml inclusion. Both workflows leverage native File System Access API for direct SD card access in browsers and Electron.

The standout feature is the intelligent collision detection system that automatically identifies models that already exist on the SD card and provides three resolution strategies: overwrite all existing models, manually rename conflicting models, or use auto-rename to assign available slots (model01-model60). Users can view side-by-side diffs with line-by-line highlighting to compare backup versions against existing models before deciding. The system automatically manages labels.yml synchronization, updating it with restored models and filtering it when creating backups of selected models.

The implementation includes a GraphQL backend API with queries for collision checking and model retrieval, mutations for backup registration and restoration, and an in-memory LRU cache storing up to 4 recent backups. The UI features drag-and-drop upload, real-time progress tracking, comprehensive error handling, file validation (100MB limit), and full internationalization support across 12 languages. All operations are non-blocking with visual feedback, making it a production-ready, enterprise-grade backup solution.

@pfeerick pfeerick added the contest Submission for a developer contest label Jan 29, 2026
@pfeerick
Copy link
Member

pfeerick commented Feb 8, 2026

Can you check what is going on with your dependencies... it seems the reason none of the CI taks were successful is because you are pulling from github rather than npm. i.e. see last commit in #135 - I was thinking at first it was a repo permissions issue, but locking @electron/node-gyp is now making so that the ci tasks are starting to run now, and flag lint and type issues, etc.

Pin @electron/node-gyp to npm version 10.2.0-electron.2 to avoid CI failures caused by pulling from GitHub directly. This resolves dependency installation issues in CI environments.
@codecov
Copy link

codecov bot commented Feb 8, 2026

Codecov Report

❌ Patch coverage is 36.02812% with 728 lines in your changes missing coverage. Please review.
✅ Project coverage is 34.95%. Comparing base (f4bd590) to head (091e38f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...rc/renderer/pages/backup/file/BackupUploadArea.tsx 0.00% 215 Missing ⚠️
src/shared/backend/services/backupStore.ts 53.93% 140 Missing and 24 partials ⚠️
src/renderer/pages/backup/BackupRestoreFlow.tsx 21.08% 125 Missing and 6 partials ⚠️
src/renderer/pages/backup/BackupCreateFlow.tsx 19.67% 93 Missing and 5 partials ⚠️
src/shared/backend/graph/backup/local.ts 49.48% 37 Missing and 12 partials ⚠️
src/shared/backend/graph/sdcard/index.ts 6.45% 28 Missing and 1 partial ⚠️
src/renderer/pages/backup/CollisionModal.tsx 62.00% 13 Missing and 6 partials ⚠️
src/renderer/pages/dev/flash/ExecuationOverlay.tsx 0.00% 12 Missing ⚠️
src/renderer/pages/backup/file/BackupUploader.tsx 84.21% 2 Missing and 1 partial ⚠️
...red/backend/graph/backup/sdcard-directory-store.ts 66.66% 2 Missing and 1 partial ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #134      +/-   ##
==========================================
+ Coverage   34.39%   34.95%   +0.56%     
==========================================
  Files         102      115      +13     
  Lines        3175     4288    +1113     
  Branches      776     1033     +257     
==========================================
+ Hits         1092     1499     +407     
- Misses       1935     2585     +650     
- Partials      148      204      +56     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Define DownloadProgress type locally instead of importing from ky
- Add type resolution for @types/react and @types/react-dom to avoid conflicts
- Fix WebDFUSettings interface with proper null handling for interfaceName
- Wrap children in Fragment in ExecuationOverlay to fix children prop error
- Add explicit ReactNode casts in Layout and FlashExecution components
- Fix code formatting with prettier in Layout.tsx and FlashExecution.tsx
- Wrap Playwright test hooks in test.describe() blocks
- Move beforeEach/afterEach inside proper test suite contexts
- Fixes compatibility-notice, firmware-selection, and flash-firmware e2e tests
- Remove usage of private ._extendTest() API which causes Playwright test failures
- Inline platform fixtures directly into baseTest using public .extend()
- Fixes 'Playwright Test did not expect test.describe() to be called here' error
- Resolves compatibility with current Playwright version
…Test export chain

- Export 'test' from browserTest.ts and electronTest.ts
- Import both as aliases in pageTest.ts and conditionally export
- Simplify baseTest to extend platformTest directly
- This preserves the Playwright test type and resolves 'did not expect test.describe()' errors
Restores the original test fixture chain using ._extendTest() API and
proper TestType typing in pageTest.ts. This matches the working
configuration from main branch.
Resolves version mismatch between playwright@1.58.0 and @playwright/test@1.29.1
that caused 'test.describe() called in unexpected context' errors when running
yarn e2e:web.
- Add backup.spec.ts with 17 tests covering:
  - Query: localBackup, sdcardModelsWithNames, checkModelCollisions, availableModelSlots
  - Mutation: registerLocalBackup, restoreBackupToSdcard, createBackupFromSdcard, downloadIndividualModels
- Fix Uint8Array conversion for Blob in jsdom environment
- Revert e2e spec files to main branch version
@Amigache
Copy link
Contributor Author

Amigache commented Feb 8, 2026

I think it's already passed the tests, I don't know why codecov hasn't run again. To be honest, this technology stack is very new to me; I left the project almost a year ago and picked it up again a month ago. I'm also not very familiar with this CI. If there's anything else I can do...

- Add BackupScreen.spec.tsx with 4 tests for tab navigation
- Add DiffViewerModal.spec.tsx with 12 tests for diff viewer functionality
- Add CollisionModal.spec.tsx with 15 tests for collision handling
- Add BackupFileSummary.spec.tsx with 8 tests for file summary display
- Add 4 error handling tests to backup.spec.ts for GraphQL resolvers

Total: 43 new tests, bringing total test count to 212.
All tests passing with no lint errors.
- Add BackupCreateFlow.spec.tsx with 13 tests covering rendering, UI elements, layout, and interactions
- Add BackupRestoreFlow.spec.tsx with 19 tests covering upload area, collision detection, modals, and props
- Add BackupUploader.spec.tsx with 18 tests covering GraphQL queries/mutations, callbacks, and state management

Total: 50 new tests added (262 tests passing overall)
Increases test coverage for radio-backup feature
…ility

- Cast createElement mock implementation as 'typeof document.createElement'
- Fixes TypeScript errors with overloaded createElement signatures
- Resolves build failures in BackupCreateFlow and BackupRestoreFlow tests
@pfeerick
Copy link
Member

pfeerick commented Feb 8, 2026

I don't know why codecov hasn't run again.

It runs on every commit pushed, but edits in place. The "error" from it simply means there is code that does not have corresponding tests... not ideal, but since I this is a stack I'm also not very familiar with either, unfortunately something I'm not going to pay too much attention to. My primary interest is "does it work" ;) And I saw all green ticks except for the skipped jobs (which should now not be skipped because of #135 - preview can only be triggered by code in this repo, not a fork due to github security/permissions), so I have high hopes :)

Use the "View deployment" button below to see the live preview site (or see the deployment comments in #135).

@pfeerick
Copy link
Member

pfeerick commented Feb 19, 2026

Ok, I think I fixed one or two issues that was in the code, and hopefully didn't break anything in the process.

It seems to be working ok for both B&W and colorlcd still. Given some of the tests, I was concerned it would only work with colorlcd, it backed up and restored just fine. I added src/shared/firmware-constants.ts and we might have to think that through a little more, as it may cap things out at 60 models... maybe two MAX_MODEL constants, one for B&W and one for color, and it determines which to use based on the presence of labels.yml? What do you think? If I'm reading it right, at least it will back them all up and only fail to restore if over the MAX_MODEL count.

I also noticed a bug in the .etx file generation... labels.yml should not be optional (if present, i.e. colorlcd) for that mode, and it also requires the RADIO\radio.yml folder/file present to function with Companion. Personally I would be fine with this being marked as "colourlcd only" for now, but that isn't within the spirit or parameters of the contest - it really should work with B&W radios also - perhaps simply through detection of the labels.yml for simplicity?

Also, for the collision detection... what should we say it does when enabled... as presently the user only know what it does when disabled ;)

Note: I am avoiding the sdcard-directory-store.ts duplication side of things. as I can only just about manage what I've done so far as NodeJS is not my thing at all 😆 I think think in the long term that probably needs to be done differently, but it works well enough now.

Also, future housekeeping todo, should ignore MacOS ._ prefix files, such as ._labels.yml mo

- Changed the prompt from "When disabled, you'll be asked how to handle models that already exist on your SD card" to "When enabled, you'll be asked how to handle models that already exist on your SD card" across multiple locale files (be, cs, da, de, es, fr, it, ru, sv, uk, zh).

feat(backup): include labels.yml in .etx backups for Companion compatibility

- Updated the BackupCreateFlow component to always include labels when the radio has them for .etx format.
- Added a tooltip to indicate that labels.yml is always included in .etx backups.

refactor(backup): improve collision detection and model slot handling

- Replaced overwriteExisting state with collisionDetection in BackupRestoreFlow.
- Updated logic to check for collisions based on the new collisionDetection state.
- Enhanced model slot naming based on radio type (B&W vs ColourLCD) using new utility functions.

test(backup): add tests for model slots and RADIO/radio.yml handling

- Added tests to ensure correct model slot availability based on existing models and labels.yml presence.
- Included tests for the inclusion and restoration of RADIO/radio.yml in backups.

refactor(backupStore): streamline model file validation and handling

- Introduced isValidModelFile function to filter out invalid model files (macOS resource forks and labels).
- Updated backup and restore functions to utilize the new validation logic.

refactor(firmware-constants): enhance model slot management

- Added getMaxModels and modelSlotName functions to manage model slots based on radio type.
- Deprecated MAX_MODELS in favor of using specific constants for B&W and ColourLCD radios.

fix(zipParser): ensure valid model files are extracted from ZIP

- Updated the extractYamlFromZip function to filter out invalid model files using isValidModelFile.
@Amigache
Copy link
Contributor Author

Significant improvements have been made to the Backup & Restore system, starting with full dual support for B&W and ColourLCD radios. The radio type is now auto-detected by checking for the presence of labels.yml inside /MODELS, and slot filenames are generated accordingly. B&W radios use zero-padded, 0-indexed filenames (model00.yml–model59.yml), while ColourLCD radios use 1-indexed filenames (model1.yml–model99.yml). To reflect this, MAX_MODELS_BW = 60 and MAX_MODELS_COLOR = 99 were introduced along with a getMaxModels() helper. The original MAX_MODELS = 60 constant remains as a deprecated alias for backward compatibility, or maybe delete it.

For .etx backups, labels.yml is now always included when present, as it is required for Companion compatibility. The checkbox is displayed as checked and disabled with a tooltip explanation. RADIO/radio.yml is also included and restored when available, and gracefully skipped on B&W radios. For .zip and individual exports, labels.yml remains user-controlled.

The collision detection toggle logic has been corrected. When enabled (default), users are prompted to rename, overwrite, or cancel if conflicts are detected. When disabled, models are overwritten silently. The description was updated to match the actual behavior.

Filtering was added for macOS resource fork files (._*) across model listing, backup creation, restore, collision checks, and ZIP parsing, preventing these metadata files from being treated as valid models.

Finally, a production Electron crash was fixed. Dynamic imports of fflate and yaml in the renderer caused Webpack to create separate chunks that could not be loaded from app.asar, resulting in a chunk loading error. All dynamic imports in BackupRestoreFlow.tsx were replaced with static imports, ensuring they are bundled into the main chunk and work correctly in production. BackupCreateFlow was unaffected since compression there runs in the Node.js backend via GraphQL rather than in the renderer.

…on and text file support

- Added support for selecting an SD Card to enable restore in English and Chinese locales.
- Updated BackupRestoreFlow to improve collision detection logic and handle radio settings appropriately.
- Enhanced CollisionModal to differentiate between model and radio file collisions, providing better user feedback.
- Modified BackupUploadArea to allow .txt files and improve restore button behavior based on SD Card selection.
- Updated backup tests to cover scenarios involving radio.yml and text files, ensuring proper collision detection and restoration behavior.
- Refactored firmware-constants to remove deprecated MAX_MODELS constant and added utility for identifying model text files.
@Amigache
Copy link
Contributor Author

Teh last commit fixes the loss of RADIO/radio.yml when restoring from a .etx backup and adds proper support for MODELS/.txt files. Previously, restoring from an .etx generated a temporary ZIP containing only parsed model .yml files, which caused radio.yml to be discarded. Now the frontend reuses the original backup ID, ensuring the complete archive (including radio.yml) is restored and used for collision detection. On the backend, radio.yml now respects the overwriteExisting option, so an existing file on the SD card is preserved if overwrite is disabled. If both the backup and the SD contain a radio.yml, it is included in the collision modal as a separate entry (since it cannot be renamed, only overwritten or skipped). Additionally, this commit adds full backup and restore support for MODELS/.txt files, such as model notes created by Companion. A new isModelTextFile() helper identifies valid .txt files, they are now included when creating backups, and they are restored to the SD card while respecting the overwriteExisting behavior, consistent with model files. A deprecated constant is removed, and some aesthetic adjustments are made.

Amigache added 3 commits March 2, 2026 12:41
- Extract labels.yml from backup ZIP during restore instead of ignoring it
- Preserve label assignments, bitmap paths and lastopen timestamps from backup
- Merge backup Labels categories into existing ones on target SD card
- Create labels.yml even when target SD card has none (previously skipped)
- Fall back to empty stubs only when backup has no labels.yml
# Conflicts:
#	locales/be/flashing.json
#	locales/cs/flashing.json
#	locales/da/flashing.json
#	locales/de/flashing.json
#	locales/es/flashing.json
#	locales/fr/flashing.json
#	locales/it/flashing.json
#	locales/ru/flashing.json
#	locales/sv/flashing.json
#	locales/uk/flashing.json
#	locales/zh/flashing.json
#	src/shared/dfu/index.ts
#	yarn.lock
react-ga was removed from the upstream repo (tracking.tsx deleted).
Replace exception() call with console.error to fix CI failures:
- TS2307: Cannot find module 'react-ga'
- no-unsafe-call ESLint error on exception() typed as any
@Amigache
Copy link
Contributor Author

Amigache commented Mar 2, 2026

Fix labels.yml not restored from .etx/.zip backups

When restoring a backup, labels.yml was silently ignored because the entry filter (isValidModelFile) explicitly excludes it, the same filter used for model files. Unlike RADIO/radio.yml and .txt files, there was no separate step to extract it from the ZIP.

Additionally, even when the destination SD card already had a labels.yml, restored models were written with empty stubs (labels: "", bitmap: ""), discarding all label assignments from the backup.

Changes in restoreBackupToDirectory:

Reads MODELS/labels.yml from the backup ZIP (same pattern already used for RADIO/radio.yml)
Uses the backup's label assignments, bitmap paths and timestamps for restored models instead of generating empty stubs
Merges the backup's Labels categories into the destination, preserving any existing ones
Creates labels.yml on the destination even if it didn't exist before (previously skipped entirely)
Falls back to empty stubs only when the backup contains no labels.yml
Renames via modelRenames are handled correctly, the lookup uses the original filename to find the matching entry in the backup labels.

@rotorman
Copy link
Member

rotorman commented Mar 4, 2026

EdgeTX team is happy to announce that this submission is the winning submission of EdgeTX 6th developer contest.

Héctor Narbona Márquez (GitHub and EdgeTX Discord user Amigache) will receive a black RadioMaster TX16 MK3 radio. For successfully also completing the bonus task to create a standalone app with the same features, Héctor will also receive a RadioMaster ER8 ExpressLRS receiver. Both winning items were selected by Héctor from the available options and are generously sponsored by RadioMaster.

The state-of-the-art radio and the receiver will be shipped directly from the RadioMaster factory to Héctor.

TX16S MK3

ER8

We extend our heartfelt congratulations to Héctor Narbona Márquez for this well-deserved win!

@pfeerick pfeerick changed the title Dc6 buddybackup feat: Backup and Restore for model and radio settings Mar 7, 2026
@pfeerick pfeerick merged commit 7433e52 into EdgeTX:main Mar 7, 2026
17 checks passed
@pfeerick pfeerick mentioned this pull request Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contest Submission for a developer contest

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants