Files
cimtechniques-service-suite/docs/BACKLOG.md

85 KiB
Raw Blame History

CIMTechniques Service Suite — Backlog

Running list of tweaks, fixes, and planned work across the suite — the shared cim_suite/core + cim_suite/shell and the modules under cim_suite/modules/ (DA-12, DA-07, and IOModbus are all rebuilt). Add items as they come up; pull from here when starting a work session.

This is the live to-do list; REBUILD-STATUS.md is the point-in-time status. They're a pair — open either first and it points to the other. When you finish a backlog item, mark it DONE here and update REBUILD-STATUS.md if it changes the overall picture. Deeper context lives in HARDWARE-VERIFICATION.md and SUITE-ARCHITECTURE.md.

Status key: TODO · IN PROGRESS · DONE · WONTFIX Priority: P1 (do soon) · P2 (normal) · P3 (nice-to-have)

▶ Next up (2026-06-12): BL-E13 — DA-07 subnet bits/mask dual display + entry — spec is written and approved (docs/superpowers/specs/2026-06-12-da07-subnet-mask-display-and-input-design.md); next step is the implementation plan, then build.


Suite / monorepo

BL-0 — Monorepo reshape (suite phase 1) · DONE

Completed 2026-06-02. Reshaped the repo into the cim_suite monorepo: DA-12 is module #1 at cim_suite/modules/da12/; shared plumbing extracted into cim_suite/core/ (theme, TableTab, transport base/serial, codecs primitives, generic config, Module contract); cim_suite/shell/ card launcher + SuiteWindow; suite-wide packaging (suite.specCIM-Service-Suite.exe, installer.iss). 88 tests pass.

  • See docs/SUITE-ARCHITECTURE.md for the full picture.

BL-S0 — Dashboard service-cable detection · DONE

Completed 2026-06-02. Suite launcher scans COM ports on dashboard entry, shows detected service cables (FTDI VID 0x0403 flagged) in an inline CablePanel, and gates module entry until one is selected. A NoCableDialog warns with Rescan/Quit when no cables are present. The selected source (COM port or simulator) is handed to the opened module.

  • See docs/superpowers/specs/2026-06-02-dashboard-service-cable-detection-design.md.

BL-S0a — Post-connect device validation · P2 · TODO

Added 2026-06-02. After opening a module on a chosen cable, confirm the device actually responds and warn if it looks like the wrong cable. Cables cannot be mapped to a specific module by USB descriptor — both DA-12 and other CIM devices share FTDI 0403:6001, so mismatch detection requires a protocol-level probe. Out of scope for the 2026-06-02 dashboard cable-detection work.

BL-S1 — Name and drop app #2 / app #3 VB6 source · P2 · TODO

Added 2026-06-02. When the source for the next VB6 service apps is available, drop each into cim_suite/modules/<app>/legacy/ so the rebuild can begin without restructuring anything. No code changes needed — just an inert drop folder to mark the landing zone.

BL-S2 — Rebuild app #2 (DA-07) into a live module · DONE

Completed 2026-06-02. DA-07 ("eLink") rebuilt as module #2 following the DA-12 pattern: its own pure protocol layer (the DA-07 wire format differs — ~<payload><checksum>\r framing, little-endian numbers, IEEE-754 floats; see the spec), domain models for the Station→Device→Channel hierarchy, an in-memory simulator, and a 4-tab UI (Station / Devices / Channels / Calibration). Registered in shell/registry.py; runs end-to-end against the simulator (--module da07 --simulate). 88 DA-07 tests pass; full suite 224 green; ruff clean. cim_suite/core was reused as-is — no new shared code was needed (the codecs diverge enough to live in da07/protocol).

  • See docs/superpowers/specs/2026-06-02-da07-service-tool-rebuild-design.md.

BL-S4 — Rebuild app #3 (IOModbus) into a live module · DONE

Completed 2026-06-03. IOModbus rebuilt as module #3 following the DA-12/DA-07 pattern, but with a fundamentally different shape: it is a standard, config-driven Modbus RTU master (not a CIMTechniques stream). Its own pure protocol layer (Modbus CRC-16, the 11 register data-type codecs, an 8-byte request builder, and a transaction-scoped ResponseAssembler — request/response, not self-delimiting), a catalog parser for IOModbus.txt (bundled as a resource), an in-memory Modbus-slave simulator, domain models (catalog → discovered devices → live register cells), a timer-stepped scan + poll controller (with an _awaiting interlock so it's correct on hardware and deterministic in tests), calibration, logging, and a single-screen UI (comm toolbar / Available Devices / Device Settings + I/O Channels grids / User Alerts). Registered in shell/registry.py (replaces the ComingSoon entry); runs end-to-end against the simulator (--module iomodbus --simulate). 88 IOModbus tests; full suite 408 green; ruff clean. cim_suite/core reused as-is (codecs diverge enough to live in iomodbus/protocol).

  • See docs/superpowers/specs/2026-06-03-iomodbus-service-tool-rebuild-design.md.

BL-S3 — Config/data path migration note · P2 · DONE

Added 2026-06-02. Completed 2026-06-05. The new cim_suite/core/config.py uses Qt GenericConfigLocation (AppData\Local) for config and GenericDataLocation for data, scoped under CIMTechniques/DA12 Service Tool. Empirically verified on Windows: the config dir is byte-for-byte identical to the old AppConfigLocation path (so da12_config.json needs no migration), but the data dir (DACal.csv, DA-Logs\) moved from the old AppDataLocation (%APPDATA%\CIMTechniques\DA12 Service Tool, Roaming) to Local.

Implemented a one-time migration shim: da12/config.py::migrate_legacy_data() moves every file from the old Roaming data dir into the current Local data dir, preserving the DA-Logs\ layout. It never overwrites a destination that already exists (new data wins; a collision leaves the source untouched), catches per-file OSError (best-effort), removes the old dir only once it is empty of files, and is guarded by a data_migrated flag in da12_config.json so it runs at most once. _legacy_data_dir() reconstructs the Roaming path from %APPDATA%. Triggered once as the first line of Da12Module.create_widget, before the logger reads data_dir(). DA-07 / IOModbus were built post-reshape and never used the old scheme, so they need no migration.

  • Files: cim_suite/modules/da12/config.py, cim_suite/modules/da12/module.py, tests/da12/test_config_migration.py (11 tests).
  • See docs/superpowers/specs/2026-06-05-config-migration-and-picklist-dropdown-design.md.

BL-S5 — Cable-driver installer on the launcher (shown only when drivers are missing) · P2 · TODO

Added 2026-06-12. Bundle the FTDI VCP driver installer with the suite and surface an "Install cable driver" affordance on the launcher — but only when the driver is not already installed, so the normal case stays clean. Helps non-technical field users whose laptop has never seen an FTDI cable (today the cable silently never appears in the cable panel and the user has no idea why).

Key wrinkle — detection cannot use the port list. When the FTDI driver is missing, a plugged-in cable enumerates as an unknown USB device and pyserial sees no COM port at all — indistinguishable from "no cable plugged in." Driver presence must be checked against Windows itself, not scan_ports(). Candidate checks (pick during design; should live in a pure, testable helper, e.g. core/transport/driver_check.py):

  • pnputil /enum-drivers output containing ftdibus.inf (driver-store query, no admin needed to read), or
  • registry HKLM\SYSTEM\CurrentControlSet\Services\FTDIBUS presence.

Delivery decision (made 2026-06-12): bundle, don't download. Ship the FTDI CDM installer (~2 MB) inside both artifacts (Inno installer and portable zip) so it works offline on locked-down field laptops — that's exactly the machine that needs it. Verify FTDI's redistribution license terms before shipping (FTDI does permit redistribution of the unmodified CDM package; confirm current terms).

Caveats to resolve during design:

  • Driver installation itself requires admin elevation — at odds with the per-user no-UAC install story (BL-P1). The button should launch the FTDI installer (which prompts UAC itself) and set expectations; strictest IT-managed shops may still need IT to push the driver.
  • Online machines usually get the driver automatically from Windows Update on first plug-in — the feature mainly pays off offline, which argues for the bundled copy.
  • UI placement: a quiet row/notice in or under the launcher's cable panel (shell/cable_panel.py), per the Instrument spec — not a permanent toolbar item.
  • Files (likely): new cim_suite/core/transport/driver_check.py, cim_suite/shell/cable_panel.py (or launcher.py), packaging/suite.spec + packaging/build.ps1 + packaging/installer.iss (bundle the redistributable), docs/RELEASE-PACKAGING.md (license note).

Adding app #2 / #3 as a module

  1. Create cim_suite/modules/<app>/legacy/ and drop the app's VB6 source in it.
  2. While it has no Python yet, surface it as a card: in cim_suite/shell/registry.py, append ComingSoonModule("<app>", "<Title>", "<one-line summary>") to the list (import it from cim_suite.core.module).
  3. Rebuild it (its own spec → plan → implement cycle), then replace the ComingSoonModule entry with the real <App>Module() implementing the contract (id, title, summary, available, icon, create_widget(parent), shutdown()).
  4. Add its bundled assets to packaging/suite.spec datas if it ships fonts/data.

Packaging / distribution

BL-P1 — End-user distribution (per-user installer + portable, signing-ready) · DONE

Completed 2026-06-08. The suite now ships two artifacts a non-admin user can run on a locked-down laptop: a per-user installer (PrivilegesRequired=lowest%LOCALAPPDATA%\Programs, no UAC) and a portable zip (unzip-and-run), both built in one shot by packaging\build.ps1. The exe carries the brand icon (packaging\icon.ico, generated from docs/samples/icon.png) and version metadata. Code signing is wired into the build but inert until CIM_SIGN_CERT/CIM_SIGN_PARAMS are set (OV / Azure Trusted Signing — not EV; see docs/RELEASE-PACKAGING.md). A bundled READ-ME-FIRST.txt walks users through the SmartScreen warning. Pillow is a build-only dep (make_icon.py) and is excluded from the frozen app.

  • Spec/plan: docs/superpowers/specs/2026-06-08-end-user-distribution-design.md, docs/superpowers/plans/2026-06-08-end-user-distribution.md.

BL-P2 — Buy + enable a code-signing certificate · P1 · TODO

Added 2026-06-08. Until signed, SmartScreen warns on first run and the strictest (AppLocker/WDAC) shops cannot run the app at all. Acquire an OV cert or enrol in Azure Trusted Signing, then follow the turn-on checklist in docs/RELEASE-PACKAGING.md (the build hooks already exist). Tracked separately because it needs a purchasing decision, not code.

BL-P3 — Auto-update checker · P3 · TODO

Added 2026-06-08. Self-serve users have no update path today (the launcher only shows a "What's new" dialog). A future spec: check for a newer published version and prompt to download.

BL-P4 — Screenshots in the run guide · P3 · TODO

Added 2026-06-08. packaging\READ-ME-FIRST.txt is text-only. A screenshot-illustrated version of the SmartScreen "More info → Run anyway" click-path would help non-technical users.


Robustness & cleanup

BL-1 — Graceful shutdown on app close · P2 · DONE

Added 2026-06-02. Completed 2026-06-03 as part of BL-7. On app exit the suite window now releases every warm module's port/timers. SuiteWindow.closeEvent iterates the registered modules and calls shutdown() on each one that's warm (in _warm), then clears the cache. (The original suggestion to call _shutdown_open() is moot — that method was removed when navigation switched to the warm-cache lifecycle; see BL-7.)

  • Files: cim_suite/shell/window.py.

BL-7 — Warm-module cache (skip auto-reload on re-entry) · DONE

Added & completed 2026-06-03. Opening a module no longer tears it down on navigation. Every opened module stays warm (its controller + loaded models retained); SuiteWindow suspend()s the active module (releasing its cable) on leave and resume()s it on return — reconnect but skip the destructive controller.refresh() unless the cable/source changed. Only the active module holds a cable; Refresh stays the explicit reload. Fixes the DA-07 complaint where bouncing to the launcher re-ran the ~25s polled load.

  • Spec/plan: docs/superpowers/specs/2026-06-03-warm-module-cache-design.md, docs/superpowers/plans/2026-06-03-warm-module-cache.md.
  • Files: cim_suite/core/module.py (Protocol suspend/resume), cim_suite/modules/da12/module.py, cim_suite/modules/da07/module.py, cim_suite/shell/window.py.
  • Known follow-up (real-hardware only): if a user navigates away from DA-07 mid-load (the load is async over ~25s on real hardware; the sim loads near- synchronously), _loaded_source is already set, so a same-source resume() skips reload and keeps the partially-loaded models as the cache; the controller's _settle timer also still fires loadFinished on the hidden widget. Benign on the sim. Fix when verifying on hardware: in Da07Module.suspend() stop _settle and, if controller._loading, force a reload on the next resume(). Tracked under BL-E1.

BL-2 — Connection status goes stale on unexpected disconnect · DONE

Added 2026-06-02. Completed 2026-06-03. If the cable was yanked while connected, the reader thread caught the error and flashed a status-bar message, but the "Connected" label stayed — the UI misreported state. connectionChanged only fired from controller.start()/stop().

  • Fix (report-only): the transport now carries a third callback, connection_callback(bool), parallel to data_callback/error_callback (core/transport/base.py). SerialTransport._reader calls _set_connected(False) in its error path (reached only on an unexpected drop; a deliberate stop() exits via the while-condition, so no double-report). Both controllers wire transport.connection_callback = self.connectionChanged.emit in attach(), so the existing _on_connection(False) slots grey the pill and flip the label — no UI changes needed. Fixes BL-5 too (the pill is downstream of the same signal). Auto-reconnect was deliberately left out → BL-9.
  • Files: cim_suite/core/transport/{base,serial_transport}.py, cim_suite/modules/da12/domain/controller.py, cim_suite/modules/da07/domain/controller.py.
  • Tests: tests/core/test_serial_transport.py, tests/test_controller.py, tests/da07/test_controller.py. 283 pass; ruff clean.
  • See docs/superpowers/specs/2026-06-03-connection-state-on-disconnect-design.md.

BL-10 — Clear, user-friendly error messages (kill the VB6 "error 8005" experience) · P2 · TODO

Added 2026-06-12. The legacy VB6 apps reported failures as vague numbered errors that explained nothing. The rebuild is already better (errors are sentences, not numbers), but several user-reachable surfaces still show raw exception text — e.g. Could not open COM5: [WinError 5] Access is denied — which is the same unhelpfulness in modern clothes. Every error a user can see should say, in plain language, what went wrong and what to do about it, with the technical detail preserved but tucked away (a "details" affordance or the activity log), not leading.

Approach (decided 2026-06-12): known scenarios first, via a central translation layer. Errors already flow through one narrow channel (transport.error_callbackerrorOccurred → footer/activity log), so add a pure, testable helper — e.g. core/errors.py::friendly_error(exc_or_msg, context) — that pattern-matches known failures to curated text + a suggested action, and passes unknown errors through unchanged (never hide an error we can't translate; show it with the technical text as the detail). Exhaustive sweep of every try/except was considered and deferred — seed the map with the failures we can already name and grow it from field reports.

Known raw-text surfaces today (the seed list):

  • Serial port open failure (core/transport/serial_transport.py:43"Could not open {port}: {exc}"): distinguish port in use (another program — or another copy of this suite — holds the COM port; close it and retry) from access denied and port no longer exists (cable unplugged since the scan; rescan from the launcher).
  • Read error mid-session (serial_transport.py:55-59): almost always the cable was unplugged — say that, not "Serial read error: ClearCommError failed".
  • Write failure (serial_transport.py:71-72): same family.
  • Export failures (core/ui/export_action.py:51): file is open in Excel (Windows lock) vs permission denied vs disk full — all currently one raw OSError.
  • IOModbus catalog import (modules/iomodbus/ui/main_window.py:132): raw parser exception on a malformed IOModbus.txt; should say which line/why and that the existing catalog is untouched.

Notes for design: match on errno/winerror codes where possible (locale- proof), not message substrings; keep the helper Qt-free so it's unit-testable with fabricated exceptions; the footer's 5-second message may be too transient for actionable errors — consider routing translated errors through the chrome warning dialog or activity log when an action is suggested.

  • Files (likely): new cim_suite/core/errors.py (+ tests), the five call sites above; no controller/protocol changes expected.

UI / Design

BL-3 — Frontend design system · P2 · DONE

Added & completed 2026-06-02. Established the visual design system (light, SmartScan-brand azure on cool slate, Lato bundled). Self-contained theme layer at cim_suite/core/ui/theme/ (tokens → fonts → stylesheet → apply_theme), applied app-wide: toolbar/brand header, primary actions, underline tabs, banded gridless tables, semantic alarm colors via tokens, status-bar connection pill, themed dialogs. Documented in DESIGN-SYSTEM.md.

  • Files: cim_suite/core/ui/theme/*, cim_suite/core/ui/table_tab.py, cim_suite/modules/da12/ui/main_window.py, cim_suite/modules/da12/ui/sensors_tab.py, cim_suite/modules/da12/ui/{calibration,com_setup}_dialog.py, packaging/suite.spec, pyproject.toml.

BL-4 — Dark theme variant · P3 · DONE

Added 2026-06-02. Completed 2026-06-10 as part of BL-DS1. Delivered as part of the Instrument design system Phase 1 foundation (BL-DS1 below).

BL-DS1 — Instrument design system Phase 1 (foundation) · P2 · DONE

Completed 2026-06-10. Two-theme token system (light/dark), IBM Plex Sans/Mono fonts bundled, regenerated QSS from the token layer, theme manager with persistence, all painters migrated to live tokens, launcher dark/light toggle. See docs/superpowers/specs/2026-06-10-instrument-design-system-rollout-design.md.

BL-DS-P2 — Instrument design system Phase 2 (window chrome) · P2 · DONE

Completed 2026-06-10. Frameless SuiteWindow (PySideSix-Frameless-Window) with the Instrument title bar (logo, breadcrumb, theme toggle, 42px window buttons), 2px brand accent strip, declarative StatusFooter adopted by DA-12/DA-07/IOModbus (legacy ConnPill QSS retired), Instrument chrome + breadcrumb titles on all child dialogs, and chrome-framed confirm/info/warning dialogs replacing QMessageBox. Remaining verification: the manual Windows 11 snap/drag/DPI checklist in the Phase 2 plan (docs/superpowers/plans/2026-06-10-instrument-phase2-chrome.md, Task 11 Step 3). See docs/superpowers/specs/2026-06-10-instrument-design-system-rollout-design.md.

BL-DS2 — Phase 2 theme-toggle signal wiring note · P3 · DONE

Added 2026-06-10. The launcher theme-toggle label refreshes only on its own click — when the toggle moves into the Phase 2 title bar, wire it to theme.signals.themeChanged (mind the module-global signal lifetime vs widget lifetime, so the widget does not outlive the signal connection). Completed 2026-06-10 — the toggle moved into the Phase 2 title bar; its label is wired to theme.signals.themeChanged (see cim_suite/core/ui/chrome/title_bar.py).

BL-DS-P3 — Instrument design system Phase 3 (core component kit) · P2 · DONE

Completed 2026-06-10. The component kit at cim_suite/core/ui/kit/, built once and offscreen-tested: InstrumentDelegate (cell kinds text/numeric/identifier/status/toggle, §1.2 status shapes with dark-theme glow, 3px selection/alarm edge bars, alarm row tint, hover pencil-chip edit affordance, styled #CellEditor with validators that block Enter, Tab/Shift+Tab save-and-move, mark_pending/resolve write-feedback flash); single-click editing suite-wide via TableTab (DoubleClicked trigger dropped; Qt.ItemIsEditable is the single source of editability; checkable columns render as ToggleSwitches); UnitsHeaderView (microcaps + units line; ⓘ markers retired from column headers — tooltips are the affordance); InstrumentTabWidget (microcaps tabs without mutating tabText()); SummaryStrip; SettingsList (groups/hints/RO chips/toggles/choice dropdowns that store the raw value); ActivityLogCard (+log_bg/log_text tokens); Sidebar; currentColor-tinted SVG icons. Bonus fixes found during review: the app QSS had been suppressing item-brush backgrounds (alarm/staleness tints) suite-wide — the kit delegate now paints them for every cell kind; and two pre-existing rebuild write-storms were fixed (DA-12 _colorize and DA-07 _apply_empty_row_flags mutated items outside the _loading guard, spamming settings writes to real stations — regression-tested). Module adoption (status tags in real columns, units mappings, summary counts, settings-list migration, sidebar/activity-log wiring) is Phases 46. Deferred follow-ups for those phases: live refills overwrite an open editor's typed text (skip the cell being edited — closed in BL-DS-P4); ComboBoxDelegate (DA-07 type column) not yet rebased onto the kit delegate (closed in BL-DS-P5); DA-12 sensors history-via-double-click is unreachable on editable cells (right-click history still works); IOModbus register grids keep their own pick-list delegate until Phase 6. Plan: docs/superpowers/plans/2026-06-10-instrument-phase3-component-kit.md.

BL-DS-P4 — Instrument design system Phase 4 (DA-12 adoption) · P2 · DONE

Completed 2026-06-11. DA-12 fully adopted on the Instrument system. Shipped: spec §5.2 toolbar (← Suite button replacing the shell's ☰ Suite action for DA-12, brand marks + kit ConnectionChip, confirm-guarded "Station commands ▾" menu holding Set Clock/Reboot, primary Connect/Disconnect far right driving controller.stop()); Station tab migrated to the kit SettingsList (spec §5.5 groups Identity/Communication/Sensors/Alarms & beeper/OP05 + an Advanced fallback so unknown wire labels never disappear; curated metadata in da12/ui/station_settings_meta.py; wire-governed read-only; subnet keeps its modal via the new custom-row settingActivated path; in-cell ⓘ markers retired for row tooltips); Sensors/Alarm Limits/Statistics/Calibration adopt the delegate kit (column kinds, §1.2 status tags + ALARM_ROW_ROLE full-row treatment for alarm-class codes only — the old whole-row warn tint is gone BY SPEC, warnings show in the status tag; units header rows; summary strips; limits Enabled column is now a toggle; §5.6 write feedback as resolve-on-echo since the protocol has no NAK); history dialog restyled per §5.10 (surface plot, hair grid, mono faint ticks, 1.6px signal series, dashed limit lines with lows at 55% opacity, top legend, LIVE TREND header strip, theme-switch repaint).

BL-DS-P3 carry-overs closed here: live refills no longer clobber an open editor (TableTab.set_rows + SettingsList.set_value skip the editing cell, regression-tested); group-band bar yields column 0 to the selection/alarm edge bar and uses Metrics.EDGE_BAR_W; SettingsDelegate exported; sensors severity moved off item brushes onto STATUS_ROLE/ALARM_ROW_ROLE; history-via-double-click decided (right-click is canonical; double-click still works on read-only cells); write feedback wired through tab.delegate.

Visual smoke findings fixed in-phase: default window 1100→1280 wide (the §5.2 toolbar needs ~1180px before Qt's overflow chevron hides the primary action), transparent toolbar label/spacer backgrounds, limit-line legend markers stay hidden after redraws.

Also: the group-band edge-bar precedence is shared code, so DA-07's channel grid quietly picks up the same improvement.

  • Plan: docs/superpowers/plans/2026-06-11-instrument-phase4-da12-adoption.md.

BL-DS-P5 — Instrument design system Phase 5 (DA-07 adoption) · P2 · DONE

Completed 2026-06-11. DA-07 fully adopted on the Instrument system. Shipped: spec §5.2 toolbar (← Suite button + brand marks + kit ConnectionChip fed COM/SIM source labels by the module, confirm-guarded "Station commands ▾" menu holding all four rare commands the spec names — Set Clock, Reboot, Re-enable Channels, Force Server Update — primary Connect/Disconnect far right driving controller.stop(); window default 1280×760); Station tab migrated to the kit SettingsList (spec §5.5 groups Identity/Network/Server/Timing & polling plus Measurement — a deliberate addition for the MKT/DP instrument parameters — plus the Advanced fallback so unknown wire labels never disappear; curated metadata in da07/ui/station_settings_meta.py; read-only governed by the wire's 'C'-frame flag; only Poll Devices is a toggle — the unverified mode codes stay numeric per the VB6-fidelity rule; ⓘ markers retired for row tooltips); Devices tab per spec §6 (all 16 slots always visible — the module simulator now seeds capacity 16 — occupied slots normal, empty slots quiet via the new kit QUIET_ROLE faint paint with no toggle/status, the TYPE cell stays the single-click add affordance, no zebra striping, device status tags OK→ok / COM/FLO/ERR→alarm / no-report→off "—", slots·devices summary); Channels grid (status tags + ALARM_ROW_ROLE via the verified BL-E8 severity mapping, Active is now a real toggle column writing set_channel_active, units row, channel/warn/alarm summary); Alarm tab (LOCAL/SERVER cells are §1.2 status tags incl. the hollow "—" off dot, Active toggle, mono PA addresses, groups·active summary); Calibration grid kinds/units/record-count summary. §5.6 write feedback is resolve-on-echo on every editable grid (no NAK in the protocol).

BL-DS-P3 carry-over closed here: ComboBoxDelegate rebased onto the kit InstrumentDelegate (the DA-07 Type column now gets hover affordance, edge bars, alarm tint, QUIET_ROLE; delegate reparented onto the table so write-feedback repaints work).

Visual smoke (both themes, offscreen screenshots): clean; the only artifacts were missing-glyph boxes for ▾/ⓘ/dot characters, an offscreen-platform font-fallback quirk that does not occur in real runs.

  • Plan: docs/superpowers/plans/2026-06-11-instrument-phase5-da07-adoption.md.

BL-DS-P6 — Instrument design system Phase 6 (IOModbus adoption + launcher) · P2 · DONE

Completed 2026-06-11. Closes the Instrument rollout — all six phases shipped. IOModbus and the launcher fully adopted on the Instrument system. Shipped: spec §5.2 toolbar (← Suite button + brand marks + kit ConnectionChip fed COM/SIM labels by the module; the legacy &Catalog menubar folded into a "Catalog ▾" toolbar menu — Import / Export / Supported Devices; the data exports moved into their own "Export ▾" toolbar menu backed by the new shared add_export_menu_actions (cim_suite/core/ui/export_action.py refactored so a private _export_actions builder serves both the toolbar and menu variants); "Log Measurements" trimmed to "Log" with a tooltip; primary Connect/Disconnect far right driving controller.stop(); window default 1280×760 — the themed toolbar measures 1185px against the 1240px budget, regression-asserted in tests/iomodbus/test_main_window_toolbar.py::test_toolbar_fits_the_default_window); device list → kit Sidebar (spec §5.7: mono 2-digit addresses, header refresh icon re-runs the scan, bus-parameters footer MODBUS RTU · <baud> 8N1 / MODBUS RTU · SIMULATED BUS); User Alerts → kit ActivityLogCard (spec §5.8: window-supplied hh:mm:ss stamps, kit 200-line cap); Device Settings → grouped kit SettingsList (spec §5.5: one group headed by the device description — the catalog defines no semantic groups; register-governed read-only with RO chips; pick-list registers as choice rows whose raw IS the catalog label, so write_cellcodecs.selection is unchanged; parentheticals become hints; value updates are echo-driven with no mark_pending, matching the station tabs); Channels grid kit polish (catalog-driven column kinds — TEXT/NUMERIC/Serial # IDENTIFIER — {n} CHANNELS summary + shared edit hint, §5.6 write feedback in RegisterGrid resolved by the forced-re-read echo, a real confirmation; no status tags or units rows — the catalog headings are free text, so mapping them would invent meaning); launcher per spec §5.9 (header with 30px logo, CIMTechniques bold + Service Suite regular wordmark, mono version, quiet What's new; cable card with fully selectable port rows — radio + name + sub detail + right-aligned mono port ID; DetectedPort gained defaulted name/detail fields; 3-up tool cards with a mono microcaps category eyebrow, code-bold name via the new type_styles.card_title() plus descriptor, 2-line description, Open pinned at the bottom; optional module attrs category/brand/descriptor with getattr fallbacks).

A PySide6 slot-lifetime bug was found and fixed mid-phase: replacing the export closures with bound-method slots let test windows be garbage-collected mid-test (closure slots are stored strongly C++-side and were the sole anchor; bound-method slots are weak) — child dialogs crashed with "Internal C++ object already deleted". The contract is documented in _export_actions' docstring and regression-tested by tests/core/test_export_action_lifetime.py.

Kit hardening found in review: the kit SettingsDelegate now preserves off-catalog values in choice editors (prepended to the combo with an editor-stashed effective raws list; commit-without-change emits nothing) — restoring the old PicklistDelegate's hardware-safety behavior suite-wide (DA-12/DA-07 choice rows benefit too). Kit tests in tests/core/kit/test_settings_list.py.

Carry-overs closed here: the five-fold duplicated summary edit hint promoted to kit.EDIT_HINT, and PicklistDelegate rebased onto the kit InstrumentDelegate (the last BL-DS-P3 carry-over). docs/DESIGN-SYSTEM.md was rewritten around the Instrument spec (the per-module adoption checklist preserved).

Visual smoke (both themes, offscreen screenshots): findings fixed in-phase — transparent backgrounds on the launcher card summaries and cable-panel labels; the only remaining artifacts were the known offscreen missing-glyph boxes (▾/✎/dot), an offscreen-platform font-fallback quirk, not real bugs.

  • Plan: docs/superpowers/plans/2026-06-11-instrument-phase6-iomodbus-launcher.md.

BL-DS4 — SIM disconnect has no toolbar reconnect path · P3 · TODO

Added 2026-06-11 (deferred from BL-DS-P4). 2026-06-11: applies to DA-07 too as of Phase 5 (same toolbar pattern, deliberately inherited); still dev-mode only. 2026-06-11: applies to IOModbus too as of Phase 6 (same toolbar pattern, deliberately inherited; still dev-mode only). A related Phase 6 review observation, folded in here rather than filed separately: after a manual Disconnect, leaving for the launcher and returning calls resume(), which reconnects unconditionally — the disconnect intent isn't remembered — and under --simulate the module's sim timer keeps ticking a detached simulator; same dev-mode-only severity. After Disconnect in --simulate, the Connect… button opens the COM-port dialog; there is no way back to the simulator without returning to the launcher. Cosmetic/dev-mode only — real-hardware users always reconnect to a COM port.

BL-DS5 — Toolbar responsiveness below ~1180px · P3 · DONE

Added 2026-06-11 (deferred from BL-DS-P4). 2026-06-11: evaluated for Phase 5 — after folding its four rare commands into the Station commands menu, DA-07's toolbar is no wider than DA-12's, so a compaction system was deferred again; build it once in Phase 6 when IOModbus (the widest case — a whole menubar folds in) adopts the toolbar. The §5.2 toolbar relies on Qt's overflow chevron when the window is narrowed below ~1180px. A deliberate compaction (icon-only actions, collapsing labels) is a Phase 5/6 candidate alongside DA-07/IOModbus toolbar adoption. Closed 2026-06-11 (Phase 6): evaluated with the widest toolbar (IOModbus, which absorbed a whole menubar). After folding Catalog into a toolbar menu, moving the data exports into an "Export ▾" menu, and trimming field labels, all three module toolbars fit the suite's 1280×760 default — the themed IOModbus toolbar measures 1185px against a 1240px budget, regression-asserted in tests/iomodbus/test_main_window_toolbar.py::test_toolbar_fits_the_default_window. Below ~1180px Qt's overflow chevron remains the graceful fallback, acceptable for a desktop instrument tool. A bespoke compaction system is not warranted; reopen if testers report real sub-1280 usage.

BL-DS3 — Phase 3 microcaps via code, not QSS · P3 · DONE

Added 2026-06-10. Qt ignores text-transform in QSS — the microcaps styles (tabs, column headers, group titles) must be delivered by .upper() + type_styles fonts in widget code during Phase 3 component work; they cannot be handled in the stylesheet alone. Completed 2026-06-10 — delivered in code by the Phase 3 kit: kit.UnitsHeaderView (column headers), kit.InstrumentTabWidget (tabs), kit.SettingsDelegate._paint_group (group titles); the dead text-transform QSS lines were removed.

BL-5 — Connection pill recolors but label can still go stale · DONE

Added 2026-06-02. Completed 2026-06-03 as part of BL-2. The status-bar pill (green "Connected" / grey "Disconnected") is driven by connectionChanged, so it inherited the same staleness as BL-2: on an unexpected disconnect the pill stayed green. Fixing BL-2 fixed the pill too — no separate work.

BL-9 — Auto-reconnect after an unexpected disconnect · P3 · TODO

Added 2026-06-03 (deferred from BL-2). BL-2 makes the UI report a dropped link honestly but does not try to recover — the user reconnects / clicks Refresh manually. A follow-up could add a background retry that re-opens the port when the cable returns and resumes streaming. Non-trivial: needs a retry timer + backoff, re-arming the reader thread, and care around the warm-cache suspend()/resume() lifecycle (only the active module holds a cable). Effectively a separate feature; pick up only if field use shows it's wanted.

  • Files (likely): cim_suite/core/transport/serial_transport.py, the controllers' attach()/start().

BL-8 — Contextual ⓘ hover help · DONE

Completed 2026-06-03. Distilled the legacy DA-12 Help.rtf (the old Station-tab help pane) into contextual hover help: a small (U+24D8) marker wherever help exists, with the text shown on a native Qt tooltip. Generic mechanism in core (TableTab.set_column_help / mark_cell_help, plus core/ui/help.py attach_help/normalize_label/HELP_MARK); curated content as data (a shared core/help_text.py glossary reused by both modules, plus per-module help.py). Covers DA-12 grid headers (4 tabs), Station setting rows (matched by wire label), toolbar/tab buttons, and the calibration dialog; DA-07 reuses the shared glossary on its Channels grid and Calibration tab + buttons/dialog, and (added 2026-06-03 from a real DA-07 capture in docs/samples/) its Station tab rows — 21 of 28 settings, keyed by the DA-07's own wire labels; 7 device-specific mode codes are left unmarked pending description (BL-E4). Both simulators are now seeded with their canonical station settings so the Station tab is populated (and the row-help testable) in --simulate. The DA-12 simulator seeds the exact labels a real DA-12 sends (parenthetical hints and all, captured in docs/samples/), and lookup tolerates those hints via core.ui.help.setting_match_key (2026-06-08; this fixed help/subnet/reboot silently missing on real hardware — see BL-D3). The former DA-06-tainted network settings (Local port number, Local IP address, Gateway IP address) were verified against docs/da12c_status.py and now ship with help; only firmware-internal fields (Boot Flags, Disable flags, NVRam buffer size, Service-tool mode, Remote-service-tool IP/port) remain unmarked in da12/help.py::PENDING_VERIFICATION.

  • See docs/superpowers/specs/2026-06-03-contextual-help-design.md and docs/superpowers/plans/2026-06-03-contextual-help.md.
  • Files: cim_suite/core/help_text.py, cim_suite/core/ui/help.py, cim_suite/core/ui/table_tab.py, cim_suite/modules/da12/help.py, cim_suite/modules/da12/ui/*, cim_suite/modules/da12/transport/simulator.py, cim_suite/modules/da07/help.py, cim_suite/modules/da07/ui/*.

DA-12 module

Items specific to the DA-12 module (not suite-wide). Other modules get their own section here as they're built.

BL-D1 — Group multi-channel sensors on the Sensors tab · DONE

Added 2026-06-02. Completed 2026-06-03. App-wide serial → model recognition + channel grouping, reusable by all modules.

  • cim_suite/core/sensor_models.py: identify(serial) maps a sensor serial to model name + channel type via a prefix table (4-char prefixes first, then 2-char fallback); group() clusters rows by adaptive longest-run (same model + shared serial body ≥ 8 chars from position 5); layout() returns display order + group header rows for insertion into table delegates.
  • cim_suite/core/ui/group_band_delegate.py: reusable QStyledItemDelegate that paints a left accent bar and a separator row at group boundaries — never inserts extra model rows.
  • DA-12: Model column added to Sensors, Alarm Limits, Statistics, and Calibration tabs. Group-band grouping applied on the Sensors tab (every serial stays visible as its own row; group header floats above the first channel of each device). Both DA-12 and DA-07 simulators seeded with representative schema serials.
  • DA-07: Channels tab gained Serial + Model columns and within-device group-band grouping. ChannelRecord.serial added and decoded from the 'E' frame as an optional backward-compatible trailing tab-delimited token — real frames that lack it decode to serial="" safely, with no impact on channel-name parsing. Flagged HW-pending (see HARDWARE-VERIFICATION.md).
  • Unrecognized / third-party serials show a blank Model column and are not grouped (treated as individual rows, same as before).
  • Spec: docs/superpowers/specs/2026-06-03-serial-recognition-and-grouping-design.md
  • Plan: docs/superpowers/plans/2026-06-03-serial-recognition-and-grouping.md
  • Key files: cim_suite/core/sensor_models.py, cim_suite/core/ui/group_band_delegate.py, cim_suite/modules/da12/ui/{sensors,alarm_limits,statistics,calibration}_tab.py, cim_suite/modules/da07/protocol/messages.py, cim_suite/modules/da07/protocol/decoder.py, cim_suite/modules/da07/ui/channels_tab.py, cim_suite/modules/da12/transport/simulator.py, cim_suite/modules/da07/transport/simulator.py.
  • 304 tests pass; ruff clean.

BL-D2 — Restore Input + Refresh columns on the Sensors tab · DONE

Completed 2026-06-02. Re-added the two right-most legacy Sensors-grid columns dropped in the rebuild: Input (raw per-channel value, already in SensorRecord.value) and Refresh (seconds since that value last updated). Refresh is driven by a 1-second QTimer in sensors_tab.py reading a new SensorRecord.updated_at (stamped by the controller with time.monotonic() on each C/I frame); only the Refresh cell is tinted by staleness via a new STALENESS theme token (green < 20s / amber < 60s / red after), leaving alarm row coloring intact.

  • Files: cim_suite/modules/da12/protocol/messages.py, cim_suite/modules/da12/domain/{models,controller}.py, cim_suite/core/ui/theme/{tokens,__init__}.py, cim_suite/modules/da12/ui/sensors_tab.py.
  • See docs/superpowers/specs/2026-06-02-da12-sensors-input-refresh-columns-design.md.

BL-D3 — Verify parked DA-06-tainted station-setting help · P3 · DONE

Added 2026-06-03 (from BL-8). Completed 2026-06-08. Three network station settings — Local Port, Local IP Address, Gateway IP Address — had legacy Help.rtf text making DA-06-specific claims (e.g. "port that the DA-06 communicates on", DHCP sentinels 0.0.5.0 / 0.0.4.0). Resolved using real DA-12 Station-tab screenshots (docs/samples/) for the exact wire labels plus the verified docs/da12c_status.py reference for behavior: they now ship as Local port number, Local IP address (0.0.5.0 = DHCP), and Gateway IP address in STATION_SETTINGS, with the unverifiable DA-06 gateway-DHCP sentinel dropped. PENDING_VERIFICATION now holds only firmware-internal fields with no user-facing description (Boot Flags, Disable flags, NVRam buffer size, Service-tool mode, Remote-service-tool IP/port).

  • Files: cim_suite/modules/da12/help.py, cim_suite/modules/da12/transport/simulator.py, cim_suite/core/ui/help.py, cim_suite/modules/da12/{ui/station_tab.py,domain/reboot_settings.py,domain/repo_snapshot.py}.

BL-D4 — Server-connection status indicator · DONE

Added 2026-06-03. Station→server connection state (distinct from the service-cable link) is now shown as a "Server" pill beside the relabeled "Link" pill in the status bar, driven by the {F} frame's server-message counters (mirroring the LAN LED) via a pure ServerLinkMonitor (domain/server_link.py). The counter offsets (3/4/5) and the {E08} comm-loss-timeout mapping are firmware-source-verified but not yet hardware-captured — see the F status-frame section of docs/HARDWARE-VERIFICATION.md. Reference: docs/da12c_status.py.

BL-D5 — Sensor history / trend view (buffered {J} data) · P2 · DONE

Added 2026-06-04 (from the firmware-handoff docs/da12c_status.py review). Completed 2026-06-05. A per-sensor History dialog is now fully implemented. It shows a live QtCharts line chart with Average + Current series, seeded on open by a {c}{J} buffered- history backfill; alarm-limit guide lines and gap/dropout markers are overlaid; live measurements continue to stream into the chart while the dialog is open. Double-click any Sensors-tab row or use the right-click "Show history…" action to launch it. The data table and chart are both exportable: the table via the shared save_sheets_dialog helper (.xlsx), the chart as PNG. The first per-sensor trend view the tool has ever shown — something the legacy VB6 could never do. See BL-6 (export engine) and BL-7 (warm-cache lifecycle).

Protocol: {J} decode added to decoder.py; {c} (request_history) added to encoder.py; frame-length guard raised to match the firmware reference. PlotData message added to messages.py. Rolling in-memory MeasurementHistory (per-sensor) accumulates live points from connect and merges/deduplicates {J} backfill records on demand. Controller exposes request_history and emits historyChanged.

  • Reference: docs/da12c_status.py (parse_plot_data, Command.request_history).

Deferred items (all pre-existing from spec, unchanged):

  • No ground-truth {J} hardware captures yet — see docs/HARDWARE-VERIFICATION.md (new sensor-history section). Decode follows docs/da12c_status.py.
  • Alarm-event background shading, multi-sensor overlay, user-selectable history depth, and deep cross-session history from disk logs remain deferred.

Known follow-up (tracked optimization): the dialog does a full chart + table rebuild on every live measurement point (_on_history_changed_reload); this is fine at DA-12 cadences and is bounded by the rolling record cap, but an incremental append is a tracked optimization for extended high-rate sessions.

BL-D6 — Surface the rest of the {F} station-health signals · P2 · DONE

Added 2026-06-04. Completed 2026-06-04. Reconciled decoder._decode_status with the firmware-authoritative {F} byte layout (docs/da12c_status.py::parse_f_frame): StationStatus now carries named fields (outgoing, retries, values, incoming, errors, time_since_last_min, active_sensors, comm_activity, station_clock, record_count) at fixed offsets — a big-endian clock at [19:27] and a little-endian buffered-record_count tail — replacing the opaque counter list + ad-hoc properties (the BL-D4 server-link monitor was rewired to the named fields, behavior unchanged). The simulator emits the same layout with live demo values. The status bar now surfaces the health signals (status-bar-only by design — no new tab): Sensors N (active count), Buffered N (records waiting to upload, tinted amber when the backlog grows frame-to-frame), the station clock, and a single live activity indicator that brightens when traffic flowed this interval, with the four per-interval throughput counters on its hover tooltip. Aligns to the firmware layout but the F-frame HW-verification flag stays until a real {F} is captured (see docs/HARDWARE-VERIFICATION.md #4). Cross-ref [BL-D4].

  • Spec/plan: docs/superpowers/specs/2026-06-04-da12-station-health-signals-design.md, docs/superpowers/plans/2026-06-04-da12-station-health-signals.md.
  • Files: cim_suite/modules/da12/protocol/{messages,decoder}.py, cim_suite/modules/da12/domain/server_link.py, cim_suite/modules/da12/transport/simulator.py, cim_suite/modules/da12/ui/main_window.py, cim_suite/core/ui/theme/stylesheet.py.
  • 470 tests pass; ruff clean.
Original scope notes (for reference)

[BL-D4] already turned the {F} server-message counters into the status-bar Server pill. The same frame — now fully specified by the firmware handoff — carries more health signals we either decode-but-hide or don't decode at all:

  • Buffered records waiting to upload (the trailing little-endian record count) — how far behind the station is on reporting to its server.
  • Active sensor count.
  • Station clock (32-bit unix seconds) — lets us show the unit's time and sanity- check a set_clock.
  • Per-interval throughput counters (outgoing / retries / values / comm-activity). The handoff pins down the exact byte layout that decoder._decode_status is flagged "unverified" for (field offsets, the little-endian tail), so building this also retires that HW-verification flag once a real capture confirms it. The work is mostly UI: a small station-health panel (or additions to the Station tab) reading StationStatus, plus aligning _decode_status's field names/offsets with the authoritative doc. Counters are per-interval deltas (the firmware zeroes them each frame) — present them as activity/rates, not cumulative totals.
  • Files: cim_suite/modules/da12/protocol/{decoder,messages}.py (reconcile the {F} layout + name the fields), cim_suite/modules/da12/domain/controller.py (already emits statusChanged), a panel under …/ui/.
  • Reference: docs/da12c_status.py (FStatus, parse_f_frame); docs/HARDWARE-VERIFICATION.md (F status-frame section).

BL-D7 — Richer Station settings (network config, reboot flags, bitmask editors) · P3 · TODO

Added 2026-06-04 (from the firmware-handoff docs/da12c_status.py review). The handoff's SETTING_NAMES / DATA_TYPE_CODES / REPORT_MODE_BITS document all 34 station settings as a standalone reference — number, label, data type, and crucially which ones need a reboot to take effect (server IP/port, local IP, subnet, gateway). Today the Station tab is generic: it renders whatever {D}/{E} label and value the device sends, with no grouping, no reboot warning, and bitmask settings shown as a raw integer. Opportunities:

  • Network-config grouping + a "needs reboot" affordance so changing an IP/port warns the user a reboot is required (the doc marks each such setting).
  • Bitmask editors for report-mode (#11: report-at-interval / hourly-stats / report-while-in-alarm) and OP-05 mode (#15), instead of typing a raw integer.
  • RSSI / version data-type codes (B, A) the doc adds beyond what _decode_by_type formats today. Dovetails with the contextual-help work ([BL-8]) and the parked DA-06-tainted network-setting help ([BL-D3]) — the handoff may also help confirm those. Scope against what real units actually expose before building bitmask UIs.
  • Files: cim_suite/modules/da12/ui/station_tab.py, cim_suite/modules/da12/protocol/decoder.py (_decode_by_type — add B/A if wanted), cim_suite/modules/da12/help.py.
  • Reference: docs/da12c_status.py (SETTING_NAMES, DATA_TYPE_CODES, REPORT_MODE_BITS, OP05_MODE_BITS).

BL-D8 — Confirm string-command format digit vs the firmware doc · P3 · TODO

Added 2026-06-04 (from the firmware-handoff docs/da12c_status.py review). The handoff's example Command builders disagree with our encoder on the format digit of two string-ish commands: add-sensor ({S0…} in the doc vs {S700…} ours) and set-sensor-name ({B0…} doc vs {B7…} ours). Our encoder is almost certainly right: it matches the legacy VB6 verbatim — NewSensors.frm:199 sends "S700" & s$ and Main.frm:3173 does SendSetting "B", n%, q$, 7 — the framing the production tool used for years. The doc's own docstring says its builders "cover the common cases," so this is most likely doc looseness on the fmt digit (the firmware may ignore it on these commands). Action: a one-line confirmation with the firmware author (or a real-unit smoke test that add-sensor / rename still takes), then a comment in the encoder noting the resolved digit. No code change expected. Low priority — recorded so the discrepancy isn't rediscovered later.

  • Files: cim_suite/modules/da12/protocol/encoder.py (add_sensors, set_sensor_name via set_setting).
  • Reference: docs/da12c_status.py (Command.add_sensor, Command.set_sensor_name); legacy NewSensors.frm:199, Main.frm:3173.

BL-D9 — Reconcile sensor recognition against the firmware type-code table · P3 · IN PROGRESS

Added 2026-06-04 (from the firmware-handoff docs/da12c_status.py review). Part 2 completed 2026-06-05. Enhances [BL-D1] (serial → model recognition / channel grouping) using the handoff's SENSOR_TYPES. Two distinct pieces of work:

Part 2 — humanize disp/calc columns · DONE (2026-06-05). The Sensors tab's Disp and Calc cells now show legible labels instead of raw integers: disp (units byte) → native/Fahrenheit/Kelvin (unknown = conversion-table[n]), calc (stats byte) → sigma/variance/MKT/rate-sec/… — codes, order, and fall-backs mirror docs/da12c_status.py (UNITS_CODES/STATS_TYPES). Both columns stay editable via a strict dropdown (a local _EnumBandDelegate that extends GroupBandDelegate so the group band still paints, and inserts an off-list value at the top so a custom conversion-table[n] disp can't be lost); the chosen label is mapped back to the raw code in on_edit. New pure da12/sensor_enums.py; 8 tests.

  • Files: cim_suite/modules/da12/sensor_enums.py, cim_suite/modules/da12/ui/sensors_tab.py, tests/da12/{test_sensor_enums,test_sensors_tab_enums}.py.

Part 1 — backfill MODEL_CHANNEL_MAP + {A} cross-check · TODO (needs SME input, not project-file-derivable). Mining the firmware SENSOR_TYPES to add the type codes we lack (CI-series, PS-wireless family, CT-16/17/24/25/26, CT-40/41, CT-21/29, CZ-12 2810/2820) is blocked on the authoritative current names: the firmware lookups carry older CI-/CT- names where we now use current CP- product names (e.g. firmware 0x8B00 = "CZ-15" vs our 8B00 = "CP-15A"), so backfilling from the doc would ship outdated/wrong labels into the recognition map — exactly what the original caveat forbids. This needs the same field lookup table that seeded sensor_models.py on 2026-06-03 (an SME/external source, not in the repo). The {A} type-code cross-check is also deferred: SensorRecord.type_text is the raw, HW-FLAGGED Type field, so a value-vs-serial-prefix integrity check can't be trusted without a hardware capture.

  • Files: cim_suite/core/sensor_models.py (+ optional cross-check helper).
  • Reference: docs/da12c_status.py (SENSOR_TYPES, UNITS_CODES, STATS_TYPES).

DA-07 module

Items specific to the DA-07 ("eLink") module. The module is rebuilt and runs against the simulator (BL-S2); these are follow-ups.

DA-07 firmware ICD gap analysis (2026-06-05). A firmware-source review (docs/DA-07 SERVICE-TOOL-ICD.md) was cross-checked against the module; the findings and rationale live in docs/DA-07 ICD-GAP-ANALYSIS.md and are filed below as BL-E5 (refined) through BL-E12. The new entries carry two tags: a Category (Incorrect / Missing-coverage / Missing-feature / Fragile-hardening / Doc) and an HW tag — [no-hw] (doable now), [needs-capture] (needs one real frame), or [needs-confirm] (needs hardware to validate behaviour). Front-load the [no-hw] items: a natural order is BL-E6 → BL-E8 → BL-E11/BL-E12 (all [no-hw]), then the capture-gated BL-E5 and the feature builds in BL-E2, then the [needs-confirm] validation.

BL-E1 — Hardware verification against a real DA-07 · P1 · TODO

Added 2026-06-02. Like DA-12, several protocol details were reverse-engineered and need confirmation against real hardware: the PullTime epoch/threshold, the H realtime-frame counter layout, the device-type gen_type enum + channel-name strings, the model→name map, and the DA-33 wireless G/RSSI layout. See the DA-07 section in docs/HARDWARE-VERIFICATION.md. No ground-truth DA-07 captures exist yet — the first real frames should become test fixtures.

Warm-cache items (added 2026-06-03, BL-7): (a) confirm a same-source warm resume() on a real DA-07 shows the cached values instantly with no reload and the link is healthy for a subsequent Refresh — the COM-port resume branch (_connect_to_port(load=False), a fresh SerialTransport per resume) is only sim-tested today. (b) handle suspend during an in-flight load (stop _settle, force reload on resume if _loading) — see BL-7's known follow-up.

Update 2026-06-03: fixed the polled handshake and verified it on a real DA-07 (COM5). The rebuild sent A once and answered nothing, so on hardware only the first frame arrived (the simulator hid it by dumping all frames at once). Two-part fix in domain/controller.py: ACK (Z1) every inbound data frame to pull the next, and answer the station's interleaved Z2 idle polls (and Z0 NAK→resend) in _handle_poll — the idle polls turned out to be required, an ACK-only build deadlocked partway. Simulator models the stream via handshake=True; 6 tests in tests/da07/test_handshake.py. A full Refresh now loads config + 45 device types + 28 settings + both present devices + channels (154 frames vs 1). Remaining: confirm live value frames keep streaming after the load via the idle-poll loopnot by requesting them. Outbound ~F/~G are server-config-request / buffer-erase, not value reads (HARDWARE-VERIFICATION item 7 is wrong on this — see BL-E6). The unmodelled M frames are alarm-indicator settings (ICD §5.M), now decoded; the loading overlay is implemented (core/ui/loading_overlay.py). Traffic-capture keepalive is BL-E11.

BL-E2 — Restore deferred DA-07 production features · P2 · IN PROGRESS

Added 2026-06-02. Partially completed 2026-06-04 (polish pass). Deferred from the rebuild (restore locations in the spec §6). Now itemized and prioritized against the firmware ICD (docs/DA-07 SERVICE-TOOL-ICD.md):

  1. Modbus passthrough — Test.frm · Missing-feature · P2 · [no-hw] build / [needs-confirm]. Next to tackle. Raw register read/write to a pod: send ~M aa ff rrrr nnnn (addr/func + big-endian register & count — the one BE exception on the service wire, ICD §9); station replies ~J addr func byteCount data… (or ~J00 on error). Needs a ~M encoder (BE reg/count via to_hex4, not the LE value codecs), a ~J decoder branch, and a small Modbus-Test tab. No clash with the inbound ~M alarm frame — direction differs and the reply is ~J.
  2. Diagnostics — Diags.frm · Missing-feature · P3 · [no-hw]. Send bare ~J; station replies ~K podTime serTime svcTime (per-subsystem timing, ICD §5.K). Add a ~K decoder + a read-only readout.
  3. Config backup/restore + erase — Restore.frm/ErasePods.frm · Missing-feature · P3 · [needs-confirm]. ~R0 = erase EEPROM + restore (ICD §11); ~X1 erase-all is already encoded (erase_all) but unwired.
  4. Factory serial/MAC write — EditSerial.frm/GenFromMac.frm · Missing-feature · P3 · [needs-confirm].
  5. Firmware download — Download.frm · Missing-feature · P3 · [needs-hw]. Largest; defer.
  6. Universal-Modbus driver editor — ModbusDriver.frm, ~W · Missing-feature · P3 · 07C-only. ~W is only emitted by MODEL_DA07C (ICD §5.W) and is not decoded today; skip on plain DA-07/07B.

Also deferred (firmware capability, niche, [no-hw]): debug-mode ~P~Pnn sets svcDebugMode; with ==1 the ~G frame sends each channel's index in place of its status byte (ICD §5.G/§10.2), so the ~G decoder must branch on it if this is restored.

Restore on request, one feature at a time.

Done (2026-06-04 polish pass, un-deferred):

  • Alarm-indicator editor (M/N, legacy Grid3) — revived as the Alarm tab (PA-series alarm-indicator group editor; new AlarmIndicatorTable; encoder clear_alarm_indicators = Q).
  • RS-485 traffic analyzer (Y) — revived as the read-only Traffic tab (start_traffic/stop_traffic).

Deferred polish (2026-06-04 polish spec): (a) the Alarm tab's Local/Server columns render as "—" placeholders DONE via [BL-E7] (2026-06-05) — they now show the real ~H live state, severity-tinted; (b) the Channels tab relies on its in-tab device combo for active-device context rather than a separate "Channels — Device N" header.

The M/N/Y layouts are reconstructed from the VB6 and hardware-unverified (see the DA-07 polish-pass items in docs/HARDWARE-VERIFICATION.md); both run against the simulator only. The legacy Pictures tab remains deferred — its binary .frx image assets aren't available in the readable source.

BL-E3 — DA-33 wireless column support · P3 · TODO

Added 2026-06-02. The model-33 G-frame layout (RSSI / battery / TX-power, different from the analog G) and the per-device wireless columns are stubbed/flagged. The decoder currently parses every G as the non-33 layout. Add model-aware decoding once a DA-33 capture exists. Files: cim_suite/modules/da07/protocol/decoder.py (the G branch), cim_suite/modules/da07/ui/devices_tab.py.

BL-E4 — Describe the 7 unauthored DA-07 station settings · P3 · TODO

Added 2026-06-03 (from BL-8). The DA-07 Station tab now has ⓘ hover help for 21 of its 28 settings (keyed by the real wire labels captured in docs/samples/). Seven device-specific mode codes were left unmarked rather than guessed — their exact mode meanings aren't confirmed: Update Control (0=none 1=warn 2=alarm), Pump Control Address, Flatline Detection (number of scans), Stacklight Style (0-4), Alarm Ind. Operating Mode (0-3), Beeper Operation (0-2), Buffer Operating Mode (0-3). They're listed in cim_suite/modules/da07/help.py::STATION_SETTINGS_TODO; once described (with a DA-07 or an SME), move each into STATION_SETTINGS. A test asserts the two lists stay disjoint and that every key is a real seeded label.

  • Files: cim_suite/modules/da07/help.py.

BL-E5 — Channels tab Tag column drops 12 leading chars on real hardware · P2 · PART 1 DONE

Added 2026-06-04 (reported from a real DA-07). Root cause identified 2026-06-05 from the firmware ICD. Category: Incorrect · HW: root cause [no-hw] (sim-confirmable); name/serial tail [needs-capture].

Part 1 done 2026-06-12, verified against the repo's first real capture (tests/da07/fixtures/capture-2026-06-12-refresh.txt, a full refresh from a DA-07 with CS-31 pods + CP-31 probes): the phantom disp read is gone everywhere (decoder, ChannelRecord, Channels-tab Disp column, set_channel_disp, CH_DISP, simulator), the ~E tail decodes as the optional 8-byte CT serial (now intact — B321281B04CB9CEF, not 21281B04CB9CEF), and the Tag column shows name → serial → catalog-default (legacy precedence; the CS-31 catalog entry has no default names, so its Tag shows the probe serial like the VB6 did). Part 2 RESOLVED 2026-06-12 (steady-state capture): ~P is decoded — it carries a channel's CT sensor serial (P + device + channel + 16-hex), NOT a custom name. A written name (~D field 11, observed on the wire and Z1-ACKed) is never reported back by the station, even after Refresh — channel names are effectively write-only; a typed Tag name lives in the local model and reverts on Refresh (same as the legacy). Closed.

On a real unit the Channels-tab Tag column shows only 1415 of a 16-char serial-style name — the first 12 chars are missing and widening the column doesn't reveal them (ruled out as display clipping; the QTableWidgetItem text is itself short).

Root cause (ICD §5.E, §14.3). The firmware ~E payload is device + chan + C_PARAMS, where C_PARAMS for the DA-07 build is 27 bytes ending at alarm — there is no disp field (the VB6's disp read is a phantom byte) and no name field; after alarm comes only an optional 8-byte CT serial. Our decoder reads alarm_ind, then a phantom disp = r.base1() (consumes 2 wire chars the firmware never sent), then name = r.rest(). On real hardware the bytes after alarm are the CT serial, so disp eats its first byte and name captures the remaining 1415 chars of the 16-char serial — exactly the symptom. The simulator never reproduces it because _p_channel redundantly emits a disp byte and a tab-delimited name, so decoder and simulator agree. This is a source bug, not the offset-guess this entry originally forbade.

Fix (two parts).

  1. ([no-hw], sim-verifiable) Drop the phantom disp everywhere: the ~E decoder read, ChannelRecord.disp, the Channels-tab Disp column + _EDIT_MAP[7], controller.set_channel_disp, and the simulator's disp byte. The outbound CH_DISP = 9 write is also spurious — ICD §11's ~D field map has no field 9. Treat the ~E tail as the optional CT serial only (ICD §5.E).
  2. ([needs-capture]) Settle where the displayed channel name comes from. The ~A device-type descriptor carries pipe-delimited default names (ICD §5.A1) and the tool can write a per-channel name (~D field 11), but the ICD's inbound payloads don't show where a custom name echoes back — most likely the ~P frame, which we do not decode today (ICD §5.P; this is the real name/serial source). Add a ~P decoder branch once a capture shows its exact payload. One real ~E + ~P capture (recipe in HW-VERIFICATION item 8, via CIM_DA07_CAPTURE) is the discriminator; save it as a test_decoder.py fixture.
  • Files: protocol/decoder.py (E branch + new P branch), protocol/messages.py (ChannelRecord), protocol/encoder.py (CH_DISP), domain/controller.py, transport/simulator.py (_p_channel), ui/channels_tab.py, tests/da07/.

BL-E6 — Fix ~F/~G command mislabel (~G is destructive) · P1 · DONE

Added 2026-06-05 (ICD §11, §14.3; gap analysis §2.1, §6). Completed 2026-06-05. Category: Incorrect · HW: [no-hw]

Done. Renamed the two mislabeled outbound commands to their firmware meanings: request_averagesrequest_server_config (~F = request a server config update) and request_inputserase_outgoing_buffer (~G = ResetHistory(), destructive — erases the station's server-upload buffer; docstring flags it for UI confirmation like erase_all), in both protocol/encoder.py and domain/controller.py. The "request values" framing is gone — live values stream unsolicited via the idle-poll loop (_handle_poll), no request needed. Fixed transport/simulator.py::_dispatch to model real semantics (~F = accepted/no-echo server-config stub; ~G = clears a notional _buffered_records backlog), and removed the now-orphaned _emit_averages. Amended HARDWARE-VERIFICATION.md item 7 (no "actively request values" — that path is destructive) and its 2026-06-03 M-frame guess (inbound ~M = alarm-indicator settings, ICD §5.M, already decoded; the UModbussChannInfo guess was ~W, 07C-only). Two new load-bearing tests in test_simulator_integration.py assert ~F/~G emit no value frames and ~G clears the backlog; encoder test updated. 162 DA-07 tests pass.

  • Files: protocol/encoder.py, domain/controller.py, transport/simulator.py, docs/HARDWARE-VERIFICATION.md, tests/da07/{test_encoder,test_simulator_integration}.py.

Per the firmware ICD, outbound ~G = ResetHistory() — it erases the station's outgoing buffer — and ~F = "request a config update from the server", not "send averages/inputs." Our encoder (request_averages~F, request_inputs~G), controller, and the simulator (_dispatch: F→averages, G→inputs) all implement the wrong meaning; the simulator agreeing with the tool is why no test catches it. The two methods are currently unwired in the UI, so this is a latent landmine, not an active fault — but HARDWARE-VERIFICATION.md item 7 suggests calling the destructive one on a timer. Live values actually arrive unsolicited via the idle-poll loop (_handle_poll; firmware SVC_POLL re-pushes averages→current), so no request is ever needed.

  • Do: rename to the firmware meaning (request_server_config=~F, erase_outgoing_buffer=~G, the latter confirm-guarded like erase_all); delete the "request values" framing; fix simulator.py to model real semantics (G = clear its outbox, F = server-config stub); amend HARDWARE-VERIFICATION.md item 7 (live values come from idle-polling, not ~F/~G) and its 2026-06-03 M-frame guess (inbound ~M = alarm-indicator settings, ICD §5.M, already decoded; ~W is 07C-only).
  • Files: protocol/encoder.py, domain/controller.py, transport/simulator.py, docs/HARDWARE-VERIFICATION.md, tests/da07/.

BL-E7 — ~H alarm-indicator Local/Server live state · P2 · DONE

Added 2026-06-05 (ICD §5.H, §8.4; gap analysis §3.2, §4.3). Completed 2026-06-05. Category: Missing-coverage · HW: [no-hw] to parse, [needs-confirm] semantics

Done. The ~H tail's indicator triples — after the device-status nibbles, one index(byte) + local(nibble) + server(nibble) per active group (ICD §5.H/§8.4) — are now decoded by a pure parse_indicator_states(tail, device_count) helper in decoder.py and applied by controller._apply_status (which supplies device_count from the config header's max_devices, since the pure decoder can't know it). The Alarm tab's Local/Server columns now show the real live state (OK/WARN/ALARM/ERROR), severity-tinted via SEVERITY tokens, replacing the hard-coded "—" — this closes the BL-E2 deferred-polish (a) placeholder with firmware-true bytes. The model AlarmIndicator gained local/server fields (preserved across ~M settings upserts); a new IndicatorState message carries a decoded triple. The simulator's _p_status emits capacity device nibbles + a triple per active indicator (seeded local OK / server WARN). The load-bearing decoder test pins the nibble-vs-byte boundary (the off-by-one that would silently misparse on hardware) and the full state enum.

  • HW-pending ([needs-confirm]): device-nibble count = MAX_DEVICES(16), and the §8.4 nibble being a single-valued enum — both flagged in HARDWARE-VERIFICATION.md #3.
  • Files: protocol/decoder.py, protocol/messages.py, domain/models.py, domain/controller.py, transport/simulator.py, ui/alarm_tab.py, tests/da07/{test_decoder,test_controller,test_ui_smoke}.py. 606 pass; ruff clean.

BL-E8 — Decode ~G channel-status high bits (warn/alarm/acked/trim) · P3 · DONE

Added 2026-06-05 (ICD §8.2; gap analysis §2.3). Completed 2026-06-05. Category: Incorrect (partial) · HW: [no-hw]

Done. codecs.chan_status now decodes the alarm high bits it used to drop (0x80 ALARM, 0x40 WARN, 0x20 ack-at-server, 0x10 trim-pending) and appends them as a readable suffix, mirroring device_status's !/© modifiers — so a channel in alarm no longer reads identically to a healthy one. A plain-OK sensor in alarm renders as just the alarm tokens ("ALARM"), not "OK ALARM"; a sensor fault plus warning shows both ("Under WARN"). The Channels-tab Status cell is now severity-tinted: a new status_severity() helper maps the string to red (SEVERITY["ER"] — sensor fault or ALARM) / amber (SEVERITY["HW"] — pure WARN) / no-tint (OK or ack/trim-only), replacing the old "any non-OK → red" rule. The legacy 5/6/7 "Under+/Over+/Error+" enum entries are left in place but flagged in-code as VB6 artifacts the firmware never sends (ICD treats 14 as the mutually-exclusive sensor codes; deferred per "revisit when touching this").

  • Files: protocol/codecs.py (chan_status, _CHAN_FLAGS), ui/channels_tab.py (status_severity, _colorize), tests/da07/{test_codecs,test_ui_smoke}.py (2 new tests). 597 pass; ruff clean.

BL-E9 — Expose maintenance commands: I clear-disables, T re-acquire, L force-update · P3 · DONE

Added 2026-06-05 (ICD §11; gap analysis §3.4, §4.5). Completed 2026-06-05. Category: Missing-feature · HW: [no-hw] to build, [needs-confirm] effect

Done. All three firmware maintenance commands are now exposed: encoder fns (clear_channel_disables=~I, reacquire_device(n)=~T nn, force_server_update=~L)

  • matching controller methods. ~T nn 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", with tooltips). All three are non-destructive nudges so no extra guard beyond the reacquire confirm. Effects are [needs-confirm] on hardware; the encoder/controller/UI wiring is covered (encoder frames, controller writes, the context action presence + call, and the toolbar actions triggering the controller).
  • Files: protocol/encoder.py, domain/controller.py, ui/devices_tab.py, ui/main_window.py, tests/da07/{test_encoder,test_controller,test_ui_smoke}.py. 613 pass; ruff clean.

BL-E10 — Handle ~R display-message ("press refresh") · P3 · DONE

Added 2026-06-05 (ICD §5.R; gap analysis §3.3, §5.3). Completed 2026-06-05. Category: Missing-coverage · HW: [no-hw]

Done. Added a DisplayMessage message (msg_id, bit-significant, with a refresh_requested property for SVC_MSG_REFRESH = 0x01) and an R decoder branch (previously ACKed-and-dropped). The controller emits a new serverUpdateAvailable signal when the refresh bit is set; MainWindow surfaces a non-modal status-bar hint ("The server updated settings — press Refresh to upload them to the tool.", 10 s), mirroring the legacy vsStatus caption (Main.frm 'R' case). Chose the hint over auto-refresh so the station can't trigger a surprise multi-second reload. Decoder/controller/UI tests cover the bit-significant decode, the signal firing only on the refresh bit, and the hint text.

  • Files: protocol/messages.py, protocol/decoder.py, domain/controller.py, ui/main_window.py, tests/da07/{test_decoder,test_controller,test_ui_smoke}.py. 609 pass; ruff clean.

BL-E11 — Refresh-completion + traffic-capture hardening · P2 · DONE

Added 2026-06-05 (ICD §4.2, §4.3, §10.1; gap analysis §5.1, §5.2). Completed 2026-06-05. Category: Fragile-hardening · HW: [no-hw] (completion), [needs-confirm] (keepalive)

Done (both halves). (a) Traffic-capture keepalive: the Traffic tab now arms a QTimer (_KEEPALIVE_MS = 20 000) while a capture is active; each tick calls a new controller.send_keepalive() that writes a ~Z idle frame — staying under the firmware's

25 s bus-silence auto-disable (ICD §4.2/§10.1). The timer is tied to watching state only (not tab visibility, so an intra-module tab switch doesn't kill it), and send_keepalive no-ops when the link isn't open (guards the warm-cache suspend() race, where the transport is stopped but still attached). (b) Deterministic refresh completion: the controller ends the load the instant it sees the first ~H frame (the SVC_POLL phase, ICD §4.3), stopping the settle timer and firing loadFinished immediately; the 1.5 s idle-settle timer is kept as the fallback. Load bookkeeping was moved before the per-frame ACK so the synchronous simulator recursion still reports progress in arrival order and the H-completion fires on the true last frame.

  • HW-pending: the H-termination path is sim-validated but the only real capture showed no ~H (falls back to the settle timer); the 20 s keepalive's effect on the

    25 s auto-disable is [needs-confirm]. Both flagged in HARDWARE-VERIFICATION.md.

  • Files: ui/traffic_tab.py (keepalive timer), domain/controller.py (send_keepalive + H-completion), docs/HARDWARE-VERIFICATION.md, tests/da07/{test_loading,test_ui_smoke}.py. 601 pass; ruff clean.

BL-E12 — Guard against ever sending ~O42 · P3 · DONE

Added 2026-06-05 (ICD §1, BUG-ICD-02; gap analysis §3.4). Completed 2026-06-05. Category: Fragile-hardening · HW: [no-hw]

Done. Added a guarded set_special_option(option) encoder (~O nn, ICD §11) and a named SPECIAL_OPTION_RADIO_BRIDGE = 0x42 constant. The builder raises ValueError on 0x42 (the DA-33-only radio-bridge diagnostic that freezes DA-07 service comms, ICD §1 / BUG-ICD-02), so the guard exists before any future "expose all options" work — that work routes through this builder instead of hand-rolling a raw ~O frame and so inherits the block. Two tests cover the normal path and the 0x42 rejection.

  • Files: protocol/encoder.py, tests/da07/test_encoder.py.

BL-E13 — DA-07 subnet bits/mask dual display + mask-aware entry · P1 · TODO

Added 2026-06-12. ▶ Next up — first thing to work on. Spec written and approved: docs/superpowers/specs/2026-06-12-da07-subnet-mask-display-and-input-design.md. Category: Missing-feature · HW: [no-hw] (firmware-source-verified 2026-06-12)

Replicate the DA-12's subnet UX on the DA-07 Station tab: display the "Subnet Mask Bits" row as both the stored host-bit count and its dotted mask (8 (mask 255.255.255.0)), and edit through a modal accepting either form, always storing the host-bit count.

Firmware-verified findings that shape it (read directly from the DA-07 source, netburner.c:464-481 — recorded as BUG-ICD-13 in the ICD by this work): the DA-07 uses the same XPort host-bits encoding as the DA-12C, but only 0 is a default sentinel (255 is not), and two firmware bugs (a 16-bit shift overflow and a byte-swap precedence bug) mean only stored 08 (masks /24/31) are applied correctly — stored 915 go out with scrambled middle octets, 16255 (including the common /16 and /8) degenerate to 0.0.0.0. Decision: the tool rejects input outside 18 with a message naming the firmware limitation; values 9255 read back from a device display flagged, never crash, never silently rewritten.

Pieces (per spec): new pure da07/protocol/subnet.py + da07/ui/subnet_dialog.py (DA-07-local — deliberately not extracted to core; the DA-12/DA-07 rules differ and rule-of-three says wait), SettingsList custom-row wiring in da07/ui/station_tab.py + station_settings_meta.py, refreshed help.py text, BUG-ICD-13 entry in docs/DA-07 SERVICE-TOOL-ICD.md, three new test files. Next step: implementation plan (writing-plans), then build.


IOModbus module

Items specific to the IOModbus module (rebuilt 2026-06-03, BL-S4). It runs against the simulator; these are follow-ups.

BL-I1 — Hardware verification against real Modbus devices · P1 · TODO

Added 2026-06-03. Several protocol details were reverse-engineered from the VB6 and need confirmation against real hardware (each isolated to a clearly-commented spot — see the IOModbus section of docs/HARDWARE-VERIFICATION.md): Modbus CRC-16 echo (low risk), the type-3 offset constant 19999 (the legacy comment says it was "changed for TEC-9300 EMES" — device-specific), the type-7 32-bit and type-8 64-bit binary combine/byte order (the type-7 combine was hand-patched and looks suspect), the float word order for type 5 vs 6, FC4-vs-FC3/FC2-vs-FC1 selection and Modbus exception handling, and the CI-13/15/18 xCalMult (×100) calibration multiplier. The bundled IOModbus.txt catalog is itself the register-map ground truth. No real RTU frame captures exist yet — the first should become test fixtures.

BL-I2 — Restore deferred IOModbus features · P2 · TODO

Added 2026-06-03. Deferred from the rebuild (documented with restore locations in the spec §7): Modbus TCP / LAN (Main.frm SetupWinsock / SendViaLAN / Winsock1_DataArrival — serial only for now; the PDU/assembler layer is transport-agnostic, so this is a new core TCP transport), the Debug hex-history window (Debug.frm, ShowDebug), the Test manual-function-code window (Test.frm, SendAsciiCommand), the Register-info popup (RegInfo.frm, ShowRegInfo), the IOBuilder catalog editor (legacy/IOBuilder/), and ASCII↔Modbus device coercion (SendAsciiCommand "$…M"). Restore on request, one at a time.

Catalog management update (2026-06-05): a JSON user layer + import/export of the legacy IOModbus.txt format now lets users add/override devices at runtime without a rebuild (Spec #1, docs/superpowers/specs/2026-06-05-iomodbus-catalog-device-management-design.md). User-layer devices shadow factory devices by id; import runs lenient validation and skips structurally-invalid devices. Still deferred to Spec #2: the rich per-field device editor (the real IOBuilder revival), factory-device stable-ids / override provenance / reset-to-factory, and a UI test for the Manage-User-Devices delete dialog. Also deferred from Spec #1's §5: the import flow currently skips lenient-invalid devices and imports the rest silently, rather than presenting a per-device checklist with strict-validation warnings shown — fold that into the Spec #2 editor work.

BL-I3 — Pick-list dropdown editing on the grids · P3 · DONE

Added 2026-06-03. Completed 2026-06-05. Enum/digital cells with a catalog pick list (#value;Label) used to edit as free text — the codec's selection() maps a typed label back to its value, so the user had to type the exact label. Added PicklistDelegate (iomodbus/ui/picklist_delegate.py): a QStyledItemDelegate that gives a writable pick-list cell a strict QComboBox of the catalog labels (in catalog order). If the live device value isn't one of the labels, it's inserted at the top so an off-list value stays visible and isn't lost. The chosen label is written back through the model, so the existing on_edit → write_cell → codecs.selection path maps it to the numeric value unchanged — purely a view-layer affordance (no protocol/model/controller changes). Installed on the shared RegisterGrid, so both the Settings and Channels grids inherit it; plain (non-pick-list) cells fall through to the normal text editor.

  • Files: cim_suite/modules/iomodbus/ui/picklist_delegate.py, cim_suite/modules/iomodbus/ui/register_grid.py (cell_at accessor + delegate install), tests/iomodbus/test_picklist_delegate.py (7 tests).
  • See docs/superpowers/specs/2026-06-05-config-migration-and-picklist-dropdown-design.md.

Device settings repository

BL-R1 — Revert a setting to a previous value (device repository) · DONE

Completed 2026-06-08. From a "Setting history…" view, right-click a row → Set to this value replays the stored machine-form value through the module's matching set_* controller method, after a confirmation. Read-only settings (MAC, firmware, serial, statistics, and type_code < 0 / read_only station lines) show the menu item disabled with a tooltip. The dialog stays module-agnostic via an injected writer_for(setting_key) -> Callable | None; each module's domain/repo_snapshot.py provides setting_writer(controller, key).

  • Files: cim_suite/core/repository/ui/history_dialog.py, cim_suite/modules/{da12,da07}/domain/repo_snapshot.py, the eight {da12,da07}/ui/* call sites.
  • Spec: docs/superpowers/specs/2026-06-08-revert-setting-to-historical-value-design.md.

Future (also enabled, not scheduled): export/import the shared devices.db.

BL-R2 — Offline device catalog (browse every device this install has ever seen) · P2 · TODO

Added 2026-06-12. A browsable inventory of all devices this installation has talked to, viewable without a live connection. The data already exists — devices.db keeps a devices table (MAC, display MAC, module, first/last seen) plus the full setting_history — but today it's write-only from the user's perspective: the only reader is SettingHistoryDialog, reachable solely from inside a connected module session. This item is pure read-side plumbing + a viewer.

Placement (decided 2026-06-12): a suite-level launcher card. A "Device History" (name TBD) tool card on the landing page opens a browser listing every known device — module, MAC, first/last seen, and last-known identity (station name etc., available via current_values) — and drills into the existing module-agnostic SettingHistoryDialog for per-device history. Per-module entry points were considered and rejected (more work, still requires opening a module).

Design notes / constraints:

  • Must not be cable-gated. Launcher module cards stay disabled until a cable source is selected (BL-S0); this card is read-only against SQLite and must open with no cable at all — that's the whole point. It is not a Module (no transport, no suspend/resume); model it as a lightweight launcher action or a new card flavor rather than a registry.py module entry.
  • Read-only when offline. BL-R1's "Set to this value" revert needs a live controller (writer_for); from the offline catalog the action must be absent or disabled — view/export only.
  • New store.py reader, e.g. list_devices() returning the devices rows (+ optionally a last-known-name join); the store API is currently append-only plus per-MAC queries.
  • --simulate uses a separate devices.sim.db; the launcher card should open the DB matching the current mode (sim entries are seeded fakes — don't mix).
  • IOModbus devices won't appear — it's excluded from the repository by design (no stable MAC identity); say so in the empty-state/help text rather than leaving users wondering.
  • Files (likely): cim_suite/core/repository/store.py (list_devices), new browser dialog under cim_suite/core/repository/ui/, cim_suite/shell/launcher.py (card + non-gated open path).
  • Spec to write when picked up; cross-ref docs/superpowers/specs/2026-06-06-device-settings-repository-design.md.

Features (planned)

Long-wanted features that were impossible on the uncompilable VB6. To be filled in.

BL-6 — Spreadsheet export of the current screen · DONE

Completed 2026-06-02. Any data tab exports to a real Excel .xlsx workbook — a metadata block (station context) above the grid exactly as shown, including alarm colors. Two toolbar actions: Export This Tab (single sheet) and Export All Tabs (one sheet per tab). Suite-wide and reusable by every future module: a Qt-free engine at cim_suite/core/export/ (Sheet/Cell + write_xlsx), TableTab.export_sheet() so adding a column exports for free, and an add_export_actions(...) helper each module wires once with its own metadata provider. openpyxl added as a dependency and bundled in the PyInstaller spec. 127 tests pass.

  • Files: cim_suite/core/export/*, cim_suite/core/ui/export_action.py, cim_suite/core/ui/table_tab.py, cim_suite/modules/da12/ui/main_window.py, cim_suite/modules/da12/config.py, packaging/suite.spec, pyproject.toml.
  • See docs/EXPORT.md (adoption checklist) and docs/superpowers/specs/2026-06-02-spreadsheet-export-design.md.

Hardware-verification follow-ups

Tweaks expected once the DA-12 checklist is walked (HARDWARE-VERIFICATION.md): clock epoch/format, sensor type/calc codes, the F status-frame layout. File specific items here as real hardware behavior is observed.

(none captured yet)