Four TDD tasks: pure catalog_view.build_device_rows, config.supported_devices
wrapper, the QTableWidget Supported Devices dialog (replacing Manage User
Devices) with user-only delete, and lint/suite/docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only browser of all catalog devices (factory + user), evolving the
Manage User Devices dialog into a sortable table: Name/ID/Manufacturer/
Min FW/Channels/Settings/Origin, with Delete enabled only for user rows
and overridden factory rows marked. Pure build_device_rows helper backs
it. Not the deferred Spec #2 editor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JSON user layer merged over the bundled IOModbus.txt factory catalog
(user devices shadow factory by id), import/export of the legacy text
format, lenient import validation, and a Catalog menu (import/export/
manage). Rich per-field editor deferred to Spec #2.
Raises ValueError (already caught by load_user_devices) for null and
non-list 'devices' shapes; adds parametrized degradation tests, a
user_only export test, and two minor config.py cleanups.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Layered catalog: bundled IOModbus.txt stays the read-only factory baseline;
a validated JSON user layer (user_devices.json) merges on top, shadowing
factory devices by id. Text format demoted to import/export interchange.
Scope: storage + merge + validation + import + export + thin menu glue.
The rich add/edit/override editor UI is deferred to Spec #2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The disp (units) and calc (stats) bytes rendered as raw integers. They now
show legible labels: disp -> native/Fahrenheit/Kelvin (unknown =
conversion-table[n]); calc -> sigma/variance/MKT/rate-sec/... -- codes,
order, and fall-backs mirror docs/da12c_status.py (UNITS_CODES/STATS_TYPES).
- new pure da12/sensor_enums.py (code<->label maps + helpers)
- sensors_tab: a local _EnumBandDelegate extends GroupBandDelegate so the
group band still paints on every column while the Disp/Calc columns get a
strict dropdown; an off-list value (e.g. a custom conversion-table[n]) is
inserted at the top so editing can't silently drop it; on_edit maps the
chosen label back to the raw code the station expects
Part 1 (backfill MODEL_CHANNEL_MAP + {A} cross-check) stays open: it needs
the authoritative current product names (the firmware doc's CI-/CT- labels
are outdated vs our CP- names) and the type-code field is HW-flagged -- both
need SME/hardware input, not derivable from project files.
621 passed; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Firmware implements three maintenance commands the tool never exposed
(ICD §11):
- ~I clear all channel disable flags (re-enable channels)
- ~T nn flag a device for reset/re-acquire without removing it
- ~L force a server update of all sensors
Added encoder fns + controller methods. ~T is a per-device Devices-tab
context action ("Re-acquire Device…", confirm-guarded, beside Remove); ~I
and ~L are station-wide toolbar actions ("Re-enable Channels", "Force Server
Update"). Effects are needs-confirm on hardware; the wiring is fully tested.
613 passed; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
~R msgID (SVC_MSG_REFRESH = 0x01) is the station asking the tool to refresh
after the server changed settings (ICD §5.R). It was decoded to None and
dropped.
- messages: DisplayMessage (bit-significant msg_id + refresh_requested)
- decoder: ~R branch
- controller: serverUpdateAvailable signal, emitted on the refresh bit
- main_window: non-modal status-bar hint (mirrors legacy vsStatus caption);
chose a hint over auto-refresh so the station can't trigger a surprise
multi-second reload
609 passed; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ~H tail carries, after the device-status nibbles, one triple per active
alarm-indicator group: index(byte) + local(nibble) + server(nibble), states
OK/WARN/ALARM/ERROR (ICD §5.H/§8.4). It was kept as an opaque tail and the
Alarm tab's Local/Server columns were hard-coded "—".
- decoder: pure parse_indicator_states(tail, device_count) helper (the
controller supplies device_count = config.max_devices, since the pure
decoder can't know it)
- messages: AlarmIndicator gains local/server; new IndicatorState
- models: AlarmIndicatorTable.set_live_state, preserved across ~M upserts
- controller._apply_status: apply triples after the device nibbles
- simulator _p_status: emit capacity device nibbles + a triple per active
indicator (seeded local OK / server WARN)
- alarm_tab: render real live state, severity-tinted -- closes the BL-E2
deferred-polish (a) placeholder
- load-bearing decoder test pins the nibble-vs-byte boundary + state enum
HW-pending (flagged): device-nibble count = MAX_DEVICES(16), and the §8.4
nibble being a single-valued enum.
606 passed; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(a) Traffic-capture keepalive: while a capture is active the Traffic tab
arms a 20s QTimer that calls controller.send_keepalive() -> writes a ~Z idle
frame, staying under the firmware's >25s bus-silence auto-disable (ICD
§4.2/§10.1). Tied to watching state (not tab visibility); send_keepalive
no-ops when the link isn't open (warm-cache suspend race).
(b) Deterministic refresh completion: the load now ends on the first ~H
frame (SVC_POLL phase, ICD §4.3) -- stop the settle timer, fire loadFinished
immediately -- with the 1.5s idle-settle timer kept as a fallback. Moved the
per-frame load bookkeeping before the ACK so the synchronous simulator
recursion still reports progress in arrival order and H-completion fires on
the true last frame.
HW-pending (flagged in HARDWARE-VERIFICATION.md): the H-termination path is
sim-validated but the only real capture showed no ~H (settle-timer fallback
covers it); the keepalive's effect on the >25s auto-disable is needs-confirm.
601 passed; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Special option 0x42 is inert/harmful on DA-07 -- it freezes service comms
while bridging to an uninitialized DNT radio UART (ICD §1, BUG-ICD-02). It
is a DA-33-only diagnostic.
Added a guarded set_special_option(option) encoder (~O nn) plus a named
SPECIAL_OPTION_RADIO_BRIDGE = 0x42 constant; the builder raises ValueError
on 0x42 so any future "expose all options" work inherits the block instead
of hand-rolling a raw ~O frame.
599 passed; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Outbound ~F/~G were mislabeled: ~F = request server config update (not
"read averages"), ~G = ResetHistory() which erases the station's outgoing
buffer (destructive, not "read inputs"). Live values arrive unsolicited via
the idle-poll loop, so there is no "request values" command at all.
- encoder: request_averages->request_server_config, request_inputs->
erase_outgoing_buffer (docstrings cite ICD 11; ~G flagged destructive)
- controller: matching renamed methods
- simulator _dispatch: ~F accept/no-echo stub, ~G clears _buffered_records
backlog; removed orphaned _emit_averages
- docs: amend HARDWARE-VERIFICATION item 7 and the M-frame guess
- tests: assert ~F/~G emit no value frames and ~G clears the backlog
595 passed; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the DA-07 firmware Interface Control Document (a from-source review of
the EOL DA-07 firmware) and a gap analysis comparing it against the module.
Cross-check surfaced several real-hardware issues the simulator can't catch:
- ~F/~G are mislabeled as value-reads; ~G is actually ResetHistory (erases
the outgoing buffer). Encoder, controller, and simulator all agree on the
wrong meaning. (BL-E6, P1)
- BL-E5 root cause found: the ~E decoder reads a phantom `disp` byte and
sources the channel name from what is really the optional CT serial tail.
- Coverage gaps: ~P decoder, ~H alarm Local/Server state, ~M/~J Modbus
passthrough, ~K diagnostics, and several outbound commands.
Categorize findings (Incorrect / Missing / Fragile-hardening / Doc /
Confirmed) with per-item [no-hw]/[needs-capture]/[needs-confirm] tags;
refine BL-E1/E2/E5 and add BL-E6..E12 in priority order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- BL-S3: one-time move of legacy DA-12 Roaming data (DACal.csv, DA-Logs\) into the
Local data dir; no-clobber, flag-guarded, run once from Da12Module.create_widget.
- BL-I3: strict pick-list QComboBox delegate on the IOModbus register grids, preserving
off-list device values; label routes through the existing write path (view-layer only).
593 tests pass; ruff clean.
create_widget()/open_module() callers (test_module.py, test_shell.py) triggered the
real one-time migration against real AppData. Root autouse fixture stubs it everywhere
except the migration tests, which exercise the real impl against explicit temp dirs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>