ui-redesign #1

Merged
stanta merged 43 commits from ui-redesign into main 2026-07-28 22:05:07 +00:00
Owner
No description provided.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add _fit_fold as a last-resort decode when plateau factoring fails: a
Hooke-Jeeves pattern search jointly fits (type, freq, Q, gain, fold G)
against G * filter coefficients. Plain coordinate descent stalled above
the acceptance gate near the device's actual near-singular band-1
denominator; the pattern-search acceleration and higher iteration/floor
budget clear it reliably while still rejecting garbage input.

Also make the existing plateau-swap loop ambiguity-aware: LS(f,Q,g) and
HS(f,Q,-g) share identical a1/a2 (a genuine RBJ shelf duality, matching
the documented "same transfer function, label-only difference" quirk),
so dividing by either plateau can look like a clean shelf of the wrong
kind. When both candidates disagree in type, defer to the joint fit,
which isn't fooled since only the true generating type's error keeps
shrinking toward zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add HotplugState edge detector and device_present() function to detect
when the DUSK-SP (re)appears. Implement cmd_watch_device() which polls
for device presence and re-applies the last preset on absent->present
transition. Add cmd_hotplug_install() to print a systemd user unit for
automatic hotplug re-apply service. Both new commands are tested with
TestHotplugTransitions showing edge detection works correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix resource leak: usb.util.dispose_resources(dev) now runs in finally block
to clean up handle even if apply_runtime() fails. Guard device_present()
against transient usb.core.USBError so backend hiccups don't crash the watcher
loop. Watcher loop now resilient: runs forever, treats transient errors as
device absence, and logs all higher-level errors while continuing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement cmd_watch() with mtime-based polling to detect file changes.
Wraps save_last in try/except OSError to handle filesystem errors
without killing the watcher. Disposes USB handle with proper error
handling in failure branch for wedge recovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds three new JSON API endpoints for managing named EQ presets:
- GET /api/presets: list all saved presets sorted by name
- POST /api/preset-save: save/overwrite a preset with EQ state
- POST /api/preset-delete: delete a preset by name

Presets are stored as JSON files in presets/ directory next to the
server script. Names are validated (1-64 chars: alphanumeric, space, . _ -)
and rejected if "." or "..". Band data is stored raw (including enabled
flags) to allow round-trip through the UI editor.

Includes full test coverage in tests/test_ui_server.py (TestPresets).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sort frequency pairs before bucketing to ensure consistent ordering.
Keep both min and max dB points per pixel column instead of just the first,
preserving narrow resonances in IEM measurements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add pyproject.toml: pip install provides duskeq CLI (+ duskeq-ui entry point);
  duskeq-ui requires source checkout or PyInstaller binary (HTML adjacent).
- Add duskeq.spec: one-file PyInstaller build bundling duskeq_ui.html.
- Patch duskeq_ui.py: HTML resolution now respects PyInstaller _MEIPASS fallback.
- Update README: document installation methods (pip for CLI, source/PyInstaller for UI).

All 78 tests pass; venv verification: duskeq --help, duskeq udev, duskeq-ui entry
point all functional. PyInstaller not installed in this environment; spec validated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add dq.require_known_device() calls to the top of api_apply, api_reset,
and api_flash in duskeq_ui.py to prevent writing to unsupported devices
when DUSKEQ_DEVICE env var is set. The guard raises DuskError which the
router maps to HTTP 400.

Add comprehensive TDD tests to TestDeviceAllowlistGuard covering:
- All three endpoints reject unsupported devices with proper error
- Without the env var, the guard passes and device availability is checked

All 92 tests passing (4 new + 88 existing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
api_preset_save validated the payload via _clean_bands but then wrote
payload.get("bands", []) - the raw request body - to disk. _clean_bands
silently drops any band with enabled:false, so a disabled band carrying an
HTML/JS injection string in freq passed validation only because it was
skipped, yet still landed verbatim in the persisted preset JSON. The
frontend's loadPresetIntoEditor force-enables all bands on load and
buildTable renders band fields via innerHTML, so loading the poisoned
preset executes attacker script same-origin, which can then POST
/api/flash without the confirm dialog.

Persist _clean_bands' return value (validated, type-coerced, enabled-only)
instead of the raw payload. No functionality loss: the frontend already
sends enabled-only bands in its payload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cmd_watch's poll loop only caught (DuskError, ValueError) around the
parse/apply block. Editors and REW commonly save via write-temp-then-
rename ("atomic save"): cmd_watch detects the mtime change via stat, then
calls parse_rew(args.file), which does its own open() - if the rename
lands in between, that open() raises FileNotFoundError (an OSError
subclass), uncaught, which killed the watcher process entirely.

Add OSError to the except tuple so a transient file-not-found during an
atomic save is reported and retried on the next tick, like other
recoverable errors, instead of crashing the watcher.
(cmd_watch_device already guards its own device_present() call separately
and is unaffected.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_never_writes_index_ten_or_higher called
dq.apply_runtime(tp, self.BANDS * 1, -4.0) - self.BANDS * 1 is a no-op
list multiplication (single-band list, not 10 bands), so apply_runtime
only ever wrote to indexes 0-1. The assertion `words[1] <= 9` was
trivially true regardless of whether the index >= 10 guard existed,
making the test vacuous for the hardware-safety invariant it claims to
pin ("never write runtime DSP index >= 10").

Reuse the existing 10-frequency list from
test_ten_band_mode_folds_into_index_zero (hoisted to a shared
TEN_BAND_FREQS class tuple) so the test actually drives ten bands and
writes span indexes 0-9.

Also add test_write_dsp_refuses_alias_index, exercising the write_dsp
MAX_SAFE_INDEX guard directly (indexes 10, 11, -1 all raise ValueError)
rather than only indirectly through apply_runtime's band count - so the
guard itself is pinned and fails loudly if ever weakened or removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds the hardware-free-testable portion of Task 21: extract_factory_presets
walks a list of EQ modes (cmd 90 switch + cmd 346/446 reads only, never cmd
190/220), restoring the original mode in a finally block even if a mid-loop
switch fails. Pinned via a FakeTransport that answers mode read/switch and
coefficient reads, asserting the no-write invariant, in-order traversal, and
finally-restore-on-failure.

Adds `duskeq.py factory-presets [--modes] [--save-dir]` (read-only-ish CLI,
warns before switching modes), GET /api/factory-presets in duskeq_ui.py
(folds fold_db/preamp into bands exactly like rew_lines), and a "Factory
curves" fetch/load control in the presets panel that feeds
loadPresetIntoEditor. Live device verification and docs/factory-curves/
capture are deferred to the hardware pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 20 Step 1 gate: full suite green with zero warnings under -W all.
- redirect_stderr around the 3 factory restore-failure tests (warning is deliberately provoked)
- close HTTPError response + server_close()/thread join in ServerTestCase teardown

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live exploration on chip 31988 confirms the Task 21 premise: switch_eq_mode
(cmd 90) reloads the runtime DSP. Walked modes 1-12; modes 1-5 are distinct
factory curves, mode 6+ read back identical to mode 5 (firmware keeps the
last valid preset loaded rather than erroring on out-of-range mode numbers -
read_eq_mode echoes back whatever was written even out of range). A 0.4s
settle after switch is necessary and sufficient; immediate reads sometimes
still show the previous mode's curve. Restoring the original mode reproduced
the original custom curve exactly.

Captured docs/factory-curves/mode-{1..5}.txt via `factory-presets --save-dir`.
All bands decoded as clean PK filters (no raw-biquad fallback needed, so no
fold-fit regression cases to add). Findings recorded as comments atop
cmd_factory_presets. Device left on mode 0 with the original custom curve
intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Was asserting 400/500 (assumes no device); with a device attached the happy
path returns 200. Now accepts either and checks no-traceback on the error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
19-task roadmap: server hardening, fold-fit decode, persistence+hotplug+watch,
preset library + A/B + blind test, share URLs, test signals, measurement overlay,
autofit, transport abstraction + hidapi, packaging, scan/allowlist, factory-curve
extraction. All tasks reviewed + live-verified on DUSK-SP. 107/107 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
python -m venv . at repo root left bin/ lib/ lib64/ include/ pyvenv.cfg
(broken: built against a now-missing python3.14). Personal presets stay
local; factory DUSK-*.json are tracked via negation. Also ignore
.playwright-mcp/ logs, scratch screenshots, .python-version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 live session characterized the wedged channel as a hard
firmware hang with a per-plug-in CAF transaction budget: ~38 commands
on pyusb (kernel HID driver detached), ~260 via hidraw, at least while
UAC streams are active. Nothing software-side recovers it, and a port
reset while wedged + streaming drops the device off the bus for minutes.

- _backend_order: hid first on all platforms, usb fallback
- _open_ready_usb: at most one recovery reset, then DeviceBlocked
  asking for a replug
- _drop_device: dispose the dead handle before forgetting it
- test_ui_server: stub open_ready for the whole suite — it previously
  sent real /api/reset writes to an attached device and ate its
  command budget
- CLAUDE.md + DUSK_SP_PROGRESS.md: document the recharacterized quirk
  (progress doc also records the factory-mode name identification)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The five captured factory modes match MOONDROP/Crinacle's published DSP
tables filter-for-filter (crinacle.com, 2024-04-10): 1 = DUSK-Default,
2 = DUSK-V, 3 = DUSK-Harman, 4 = DUSK-Bass+, 5 = DUSK-Diffuse-Tilted.
Rename the captures, add a mapping README, and mirror them as loadable
presets/DUSK-*.json. Preset name regex now allows '+' for DUSK-Bass+.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking a marker opens a floating band panel: enable switch, filter
type, and drag/wheel knobs for freq, gain, Q (shift = fine, dbl-click
resets). Marquee/ctrl-click selects multiple bands; a multi panel
applies relative freq/gain/Q trims across the selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
x
stanta merged commit 811f3c24d5 into main 2026-07-28 22:05:07 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
stanta/duskeq!1
No description provided.