Preserve firmware settings across updates#93
Conversation
2913223 to
90f0271
Compare
kissetfall
left a comment
There was a problem hiding this comment.
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.
-
The connect path writes layer names before recovery is captured or the user makes a decision.
device_connect_task.rsrunslayer_name_sync_updatesand callsset_qmk_setting_stringbefore portable capture;prepare_settings_recoveryis only called later fromdevice_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. -
Verified history is not updated by all portable-setting writers.
record_verified_qmk_valueis 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.entlayoutv4 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. -
A successful restore shrinks the trusted snapshot.
RecoveryHistory::apply_verifiedcreates the new fingerprint snapshot fromresult.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. -
Runtime state can diverge from both firmware and history. After restore,
poll_settings_recoverystores 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 clonedhistory/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. -
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. -
The new recovery UI bypasses Entropy's design system. It uses raw
egui::Window,egui::Button/ui.button, andFrame::popup, and the working state does not provide the standard blocking modal/backdrop. Please reuse the shared modal surface/backdrop andmodern_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
.entlayoutimport 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.
4b3fbc7 to
88449a9
Compare
|
Thanks for the detailed review! Addressed in 4b3fbc7:
|
kissetfall
left a comment
There was a problem hiding this comment.
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.
-
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.
-
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.
-
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().
-
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.
-
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.
-
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.
-
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.
03eee6c to
579f5bc
Compare
|
Addressed:
|
kissetfall
left a comment
There was a problem hiding this comment.
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.
-
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.
-
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.
-
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.
579f5bc to
09e5b98
Compare
|
Addressed:
|
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.
.entlayoutv4 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 --locked206 tests passedcargo build --release --lockedpassedpython3 scripts/check_i18n.pypassedgit diff --check origin/main...HEADpassedassert.hrequired byring)RU
Кратко
После обновления прошивки пользователям больше не нужно вручную восстанавливать переносимые настройки клавиатуры, такие как Auto Layer, Quick Tap и названия слоев. Entropy сохраняет проверенные конфигурации, привязанные к серийному номеру, и предлагает восстановить только настройки, совместимые с подключенной прошивкой.
Источником истины для восстановления служат свежие метаданные прошивки и строгие чтения с устройства, поэтому кэшированные определения и подставленные нулевые значения не могут считаться доверенным состоянием. Варианты Restore, Keep current и Later выбираются до записей при подключении; восстановленные значения попадают в историю только после отложенной точной проверки чтением. История снимков обновляется атомарной заменой, учитывает три границы доверия и помещает поврежденные файлы в карантин.
Формат
.entlayoutv4 использует тот же контракт переносимых настроек для ручного восстановления и сохраняет совместимость импорта с версиями v1-v3. Этот PR добавляет восстановление по запросу и отложенный баннер; отдельный интерфейс просмотра истории остается последующей задачей.Проверка
cargo test --locked: прошли 206 тестовcargo build --release --locked: прошелpython3 scripts/check_i18n.py: прошелgit diff --check origin/main...HEAD: прошелringтребуетсяassert.h)