Skip to content

Preserve firmware settings across updates#93

Open
IgorArkhipov wants to merge 3 commits into
ergohaven:mainfrom
IgorArkhipov:igor/firmware-settings-recovery
Open

Preserve firmware settings across updates#93
IgorArkhipov wants to merge 3 commits into
ergohaven:mainfrom
IgorArkhipov:igor/firmware-settings-recovery

Conversation

@IgorArkhipov

@IgorArkhipov IgorArkhipov commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Firmware updates no longer force users to reconstruct portable keyboard settings such as Auto Layer, Quick Tap, and layer names by hand. Entropy keeps verified, serial-backed snapshots and offers to restore only settings that remain compatible with the connected firmware.

Recovery authority comes from fresh firmware metadata and strict device reads, so cached definitions and fallback zero values cannot become trusted state. Restore, Keep current, and Later are resolved before connection-time writes; restored values advance history only after delayed exact readback. Snapshot history uses atomic replacement, keeps three trust boundaries, and quarantines invalid files.

.entlayout v4 carries the same portable-settings contract for manual recovery while retaining v1-v3 import compatibility. This PR ships prompted recovery plus a deferred banner; a dedicated history browser remains follow-up UI work.

Validation

  • cargo test --locked 206 tests passed
  • cargo build --release --locked passed
  • python3 scripts/check_i18n.py passed
  • git diff --check origin/main...HEAD passed
  • Windows cross-check reached dependency compilation but local host lacks Windows C SDK headers (assert.h required by ring)

RU

Кратко

После обновления прошивки пользователям больше не нужно вручную восстанавливать переносимые настройки клавиатуры, такие как Auto Layer, Quick Tap и названия слоев. Entropy сохраняет проверенные конфигурации, привязанные к серийному номеру, и предлагает восстановить только настройки, совместимые с подключенной прошивкой.

Источником истины для восстановления служат свежие метаданные прошивки и строгие чтения с устройства, поэтому кэшированные определения и подставленные нулевые значения не могут считаться доверенным состоянием. Варианты Restore, Keep current и Later выбираются до записей при подключении; восстановленные значения попадают в историю только после отложенной точной проверки чтением. История снимков обновляется атомарной заменой, учитывает три границы доверия и помещает поврежденные файлы в карантин.

Формат .entlayout v4 использует тот же контракт переносимых настроек для ручного восстановления и сохраняет совместимость импорта с версиями v1-v3. Этот PR добавляет восстановление по запросу и отложенный баннер; отдельный интерфейс просмотра истории остается последующей задачей.

Проверка

  • cargo test --locked: прошли 206 тестов
  • cargo build --release --locked: прошел
  • python3 scripts/check_i18n.py: прошел
  • git diff --check origin/main...HEAD: прошел
  • Проверка Windows дошла до компиляции зависимостей, но на локальной машине отсутствуют заголовочные файлы Windows C SDK (ring требуется assert.h)

@IgorArkhipov
IgorArkhipov force-pushed the igor/firmware-settings-recovery branch from 2913223 to 90f0271 Compare July 17, 2026 21:05

@kissetfall kissetfall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serial-backed, verified recovery model is the right direction, but the current implementation has several lifecycle and data-integrity blockers that need to be resolved before merge.

  1. The connect path writes layer names before recovery is captured or the user makes a decision. device_connect_task.rs runs layer_name_sync_updates and calls set_qmk_setting_string before portable capture; prepare_settings_recovery is only called later from device_connect_apply.rs. On a firmware update this can overwrite the new firmware's layer names before Restore / Keep current / Later is shown. Please establish the recovery decision before all device writes and add a regression test proving that connection performs no writes while the decision is pending.

  2. Verified history is not updated by all portable-setting writers. record_verified_qmk_value is currently reached only by layer names, part of touchpad, and tap-hold. Auto Shift, Magic, Mouse Keys, module fields, Bluetooth, layer LEDs/RGB, Grave Escape, and .entlayout v4 import still write through paths that do not update the recovery capture/history. This means a later firmware update can offer stale values as trusted. Please route every portable write through one owning verified-write abstraction, including import, and cover each writer family.

  3. A successful restore shrinks the trusted snapshot. RecoveryHistory::apply_verified creates the new fingerprint snapshot from result.verified, which contains only selected changed fields, while future recovery consults only the newest snapshot. Compatible fields that did not change disappear after the first update and cannot be recovered on the next one. Build the new snapshot from the complete trustworthy compatible set (unchanged strict captures plus successfully restored/read-back fields, excluding failed/unavailable fields) and add a consecutive-upgrades regression test.

  4. Runtime state can diverge from both firmware and history. After restore, poll_settings_recovery stores the verified fields but does not apply them to UI state or reload the device, so the UI continues showing pre-restore values. Deferred recovery also owns stale cloned history/capture; verified changes made while the banner is deferred can later be overwritten by Keep current or Restore. Use a single current owner for capture/history, refresh it at action time, and reconcile the whole UI from verified readback or reconnect after restore.

  5. The recovery worker is not integrated with the application HID lifecycle. The window is not blocking, device switching/reconnect is gated only on layer_write_task, and the user can start another connection while recovery owns the HID handle. Recovery needs the same exclusive lifecycle contract as other background HID work, including disconnect, device switch, partial failure, and cancellation tests.

  6. The new recovery UI bypasses Entropy's design system. It uses raw egui::Window, egui::Button/ui.button, and Frame::popup, and the working state does not provide the standard blocking modal/backdrop. Please reuse the shared modal surface/backdrop and modern_button, with backward/negative actions before the forward/positive action.

Requested regression coverage:

  • no device write before Restore / Keep current / Later;
  • all portable writer families and .entlayout import update trusted state;
  • two consecutive firmware upgrades preserve the full compatible snapshot;
  • defer → edit setting → review does not lose the edit;
  • restore success updates UI, while partial failure preserves only verified fields;
  • disconnect/reconnect/device switch while recovery is active cannot race the HID owner.

@IgorArkhipov
IgorArkhipov force-pushed the igor/firmware-settings-recovery branch from 4b3fbc7 to 88449a9 Compare July 19, 2026 06:06
@IgorArkhipov

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review!

Addressed in 4b3fbc7:

  • Recovery now refreshes history and strict capture at action time, so deferred edits are not overwritten by stale pending state.
  • Restore builds its new fingerprint snapshot from complete current capture plus verified restores, and reconnects afterward to reload UI from firmware.
  • Recovery retains exclusive HID ownership until its worker exits; reconnects and device scanning are gated while it is active, including cancellation.
  • Portable verified-write history now covers QMK settings writers, RGB readback after save, and successful .entlayout imports. I also included One Shot, which was another missed portable writer.
  • Recovery UI now uses the shared modal/backdrop and modern_button components, with Later/Keep Current before Restore.
  • Added regression coverage for preserving trusted fields across consecutive firmware fingerprints.

@kissetfall kissetfall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verified-write coverage, reconnect after restore, HID gating, history promotion guard, and design-system integration are meaningful improvements. The current head still has recovery data-integrity and lifecycle blockers, and it now conflicts with current main.

  1. Connection still mutates firmware before the recovery decision. In device_connect_task.rs, layer_name_sync_updates calls set_qmk_setting_string around the layer-name loading stage. Portable settings are captured much later, and prepare_settings_recovery runs only after the connect task returns. A firmware update can therefore overwrite the new firmware layer names before Restore, Keep Current, or Later is shown. Establish the decision before every write, including layer-name sync, and test that connection produces zero HID writes while a decision is pending.

  2. A successful Restore persists the pre-restore values, not the verified restored values. poll_settings_recovery calls promote_full_restore with captured(self.recovery_capture), but recovery_capture is the current firmware capture taken before the worker writes. RecoveryTaskResult reports only field IDs and never returns the verified PortableSetting values. The next update can therefore trust the values that Restore just replaced. Build the promoted snapshot from exact post-write readback plus compatible unchanged fields, and assert the actual stored values across two consecutive firmware upgrades.

  3. Restore and Keep Current do not perform the promised strict capture at action time. start_settings_restore and keep_current_settings both reuse self.recovery_capture from connection time. If the prompt was deferred and firmware changed externally, or another verified edit occurred, the decision is made from stale state. Run a fresh serialized device read when the action is selected, then compute the diff and history update from that result. The selected trusted snapshot must also be fingerprint-aware instead of blindly taking snapshots().first().

  4. Linked-QSID partial writes are not represented safely. write_setting writes the primary QSID and then linked QSIDs. If a later linked write fails, firmware has already changed, but the whole field is reported Failed; restored_count can remain zero, so no reconnect occurs and no precise verified state is persisted. Return per-QSID verified outcomes or roll back atomically, and reconnect/read back after any attempted mutation, including partial failure.

  5. Disconnect and worker-failure handling can restore a dead handle. Write errors are converted to String, RecoveryTaskResult always returns HidDevice, and poll_settings_recovery unconditionally puts it back into self.hid_device. A physical disconnect therefore does not enter the normal disconnected lifecycle. If the worker channel dies, the handle is neither restored nor cleared and the hardcoded English status leaves the app in an ambiguous connected state. Preserve disconnect classification, return an optional valid handle, clear/reconnect through the shared lifecycle, and localize the status.

  6. Full-capture history replacement is still incorrect on the same fingerprint. RecoveryHistory::apply_verified always extends the newest fields when fingerprints match, even for Restore and KeepCurrent. Unavailable or removed fields therefore survive as stale trusted data despite the comment saying full decisions replace the compatible capture. Replace the map for full-capture sources and add a same-fingerprint regression test.

  7. Rebase onto current main after #91 and #82 and use the single current HID owner/exit drain. The branch conflicts in app initialization/state/lifecycle, connection, and Tap Hold. Recovery must participate in ordinary window close, device switch, cache refresh, import, disconnect, and reconnect without creating a parallel owner.

Required end-to-end coverage:

  • connect after firmware update performs no writes before a recovery decision;
  • Later, then an external or verified setting change, uses a fresh action-time capture;
  • Restore persists the exact restored values and a second firmware update offers those values;
  • linked write partial failure and physical disconnect reconcile firmware, UI, handle, and history;
  • same-fingerprint Keep Current and Restore remove stale unavailable fields;
  • ordinary ViewportEvent::Close drains the shared HID lifecycle exactly once.

@IgorArkhipov
IgorArkhipov force-pushed the igor/firmware-settings-recovery branch from 03eee6c to 579f5bc Compare July 20, 2026 00:14
@IgorArkhipov

Copy link
Copy Markdown
Contributor Author

Addressed:

  • Mutating Restore actions now return HID, then reconnect selected device so page state reloads from verified firmware state.
  • Display-preset restoration now runs only after recovery decision reaches Idle, never before Enrollment/Restore work.
    Successful portable imports record verified settings into recovery history.
  • Keep Current with no HID restores its original Enrollment state instead of changing prompt type.
  • Combo Term now uses shared queued write/readback path, which reconciles UI state and promotes verified QSID 2 values into recovery history.
  • Added recovery enrollment and Combo Term reconciliation regressions.

@kissetfall kissetfall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed head 579f5bc. The connect path is now read-only before the recovery decision, action-time capture and post-mutation readback are present, mutating restore reconnects, linked partial failures are reconciled, and same-fingerprint full captures replace fields. All four hosted builds are green and local tests pass 266/266. Blocking regressions remain.

  1. Recovery UI has reverted outside the Entropy design system. settings_recovery.rs uses raw egui::Window, egui::Frame::popup, ui.button, and egui::Button, with no shared blocking backdrop. The action order is Restore before Keep Current/Later, opposite the required backward/negative-before-positive order. Restore the shared modal/backdrop and modern_button implementation, including the working state and deferred banner.

  2. A full capture cannot replace history with an empty verified set. RecoveryHistory::apply_verified returns false before checking TrustSource when fields is empty, and poll_settings_recovery skips apply_verified when verified is empty. If every field becomes unavailable, Keep Current/Restore leaves the old trusted snapshot intact, so stale values can be offered again. Full-capture sources must be allowed to replace the snapshot with an empty map; add enrollment and same-fingerprint all-unavailable regressions.

  3. Initial connect capture still converts a physical disconnect into per-field Unavailable entries instead of aborting the connection. The action-time capture classifies disconnects, but capture_portable_setting on the connect path only stores error strings and can return a ConnectResult with a dead handle. Preserve disconnect classification through initial strict capture and test it.

@IgorArkhipov
IgorArkhipov force-pushed the igor/firmware-settings-recovery branch from 579f5bc to 09e5b98 Compare July 20, 2026 15:37
@IgorArkhipov

Copy link
Copy Markdown
Contributor Author

Addressed:

  • Restored shared recovery modal/backdrop styling for prompt and working states; deferred banner uses modern_button. Reordered actions to Keep Current, Later, then Restore.
  • Full-capture sources now replace snapshots even when every field is unavailable. Added regression coverage for both existing and new fingerprints.
  • Initial portable capture now preserves disconnect classification and aborts connection instead of returning unavailable entries with a dead HID handle. Added regression coverage.

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.

2 participants