85 KiB
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.mdis 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 itDONEhere and updateREBUILD-STATUS.mdif it changes the overall picture. Deeper context lives inHARDWARE-VERIFICATION.mdandSUITE-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.spec → CIM-Service-Suite.exe, installer.iss). 88 tests pass.
- See
docs/SUITE-ARCHITECTURE.mdfor 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-driversoutput containingftdibus.inf(driver-store query, no admin needed to read), or- registry
HKLM\SYSTEM\CurrentControlSet\Services\FTDIBUSpresence.
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(orlauncher.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
- Create
cim_suite/modules/<app>/legacy/and drop the app's VB6 source in it. - While it has no Python yet, surface it as a card: in
cim_suite/shell/registry.py, appendComingSoonModule("<app>", "<Title>", "<one-line summary>")to the list (import it fromcim_suite.core.module). - 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()). - Add its bundled assets to
packaging/suite.specdatasif 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(Protocolsuspend/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_sourceis already set, so a same-sourceresume()skips reload and keeps the partially-loaded models as the cache; the controller's_settletimer also still firesloadFinishedon the hidden widget. Benign on the sim. Fix when verifying on hardware: inDa07Module.suspend()stop_settleand, ifcontroller._loading, force a reload on the nextresume(). 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 todata_callback/error_callback(core/transport/base.py).SerialTransport._readercalls_set_connected(False)in its error path (reached only on an unexpected drop; a deliberatestop()exits via the while-condition, so no double-report). Both controllers wiretransport.connection_callback = self.connectionChanged.emitinattach(), 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_callback → errorOccurred → 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 rawOSError. - IOModbus catalog import (
modules/iomodbus/ui/main_window.py:132): raw parser exception on a malformedIOModbus.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 4–6. 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_cell →
codecs.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.mdanddocs/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: reusableQStyledItemDelegatethat 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.serialadded and decoded from the'E'frame as an optional backward-compatible trailing tab-delimited token — real frames that lack it decode toserial=""safely, with no impact on channel-name parsing. Flagged HW-pending (seeHARDWARE-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 — seedocs/HARDWARE-VERIFICATION.md(new sensor-history section). Decode followsdocs/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_statusis 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) readingStationStatus, 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 emitsstatusChanged), a panel under…/ui/. - Reference:
docs/da12c_status.py(FStatus,parse_f_frame);docs/HARDWARE-VERIFICATION.md(Fstatus-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_typeformats 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— addB/Aif 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_nameviaset_setting). - Reference:
docs/da12c_status.py(Command.add_sensor,Command.set_sensor_name); legacyNewSensors.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 indocs/DA-07 ICD-GAP-ANALYSIS.mdand 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 loop — not 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):
- 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~J00on error). Needs a~Mencoder (BE reg/count viato_hex4, not the LE value codecs), a~Jdecoder branch, and a small Modbus-Test tab. No clash with the inbound~Malarm frame — direction differs and the reply is~J. - 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~Kdecoder + a read-only readout. - Config backup/restore + erase —
Restore.frm/ErasePods.frm· Missing-feature · P3 ·[needs-confirm].~R0= erase EEPROM + restore (ICD §11);~X1erase-all is already encoded (erase_all) but unwired. - Factory serial/MAC write —
EditSerial.frm/GenFromMac.frm· Missing-feature · P3 ·[needs-confirm]. - Firmware download —
Download.frm· Missing-feature · P3 ·[needs-hw]. Largest; defer. - Universal-Modbus driver editor —
ModbusDriver.frm,~W· Missing-feature · P3 · 07C-only.~Wis only emitted byMODEL_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; newAlarmIndicatorTable; encoderclear_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 1–2 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 14–15 of a 16-char serial-style
name — the first 1–2 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 14–15 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).
- (
[no-hw], sim-verifiable) Drop the phantomdispeverywhere: the~Edecoder read,ChannelRecord.disp, the Channels-tab Disp column +_EDIT_MAP[7],controller.set_channel_disp, and the simulator'sdispbyte. The outboundCH_DISP = 9write is also spurious — ICD §11's~Dfield map has no field 9. Treat the~Etail as the optional CT serial only (ICD §5.E). - (
[needs-capture]) Settle where the displayed channel name comes from. The~Adevice-type descriptor carries pipe-delimited default names (ICD §5.A1) and the tool can write a per-channel name (~Dfield 11), but the ICD's inbound payloads don't show where a custom name echoes back — most likely the~Pframe, which we do not decode today (ICD §5.P; this is the real name/serial source). Add a~Pdecoder branch once a capture shows its exact payload. One real~E+~Pcapture (recipe in HW-VERIFICATION item 8, viaCIM_DA07_CAPTURE) is the discriminator; save it as atest_decoder.pyfixture.
- Files:
protocol/decoder.py(Ebranch + newPbranch),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_averages→request_server_config (~F = request a server config update)
and request_inputs→erase_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 likeerase_all); delete the "request values" framing; fixsimulator.pyto model real semantics (G = clear its outbox, F = server-config stub); amendHARDWARE-VERIFICATION.mditem 7 (live values come from idle-polling, not~F/~G) and its 2026-06-03M-frame guess (inbound~M= alarm-indicator settings, ICD §5.M, already decoded;~Wis 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 inHARDWARE-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
1–4 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 nnis a per-device Devices-tab context action ("Re-acquire Device…", confirm-guarded, beside Remove);~Iand~Lare 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_keepaliveno-ops when the link isn't open (guards the warm-cachesuspend()race, where the transport is stopped but still attached). (b) Deterministic refresh completion: the controller ends the load the instant it sees the first~Hframe (the SVC_POLL phase, ICD §4.3), stopping the settle timer and firingloadFinishedimmediately; 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 the25 s auto-disable is
[needs-confirm]. Both flagged inHARDWARE-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 0–8 (masks /24–/31) are applied
correctly — stored 9–15 go out with scrambled middle octets, 16–255 (including
the common /16 and /8) degenerate to 0.0.0.0. Decision: the tool rejects input
outside 1–8 with a message naming the firmware limitation; values 9–255 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_ataccessor + 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 aregistry.pymodule 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.pyreader, e.g.list_devices()returning thedevicesrows (+ optionally a last-known-name join); the store API is currently append-only plus per-MAC queries. --simulateuses a separatedevices.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 undercim_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) anddocs/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)