Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Phase: — (none active; latest shipped = Phase 1041, MERGED via PR #189 on 2026
Plan: —
Milestone: v3.0 FastSense Companion — SHIPPED 2026-04-30; v3.1 Plant Log Integration — SHIPPED 2026-05-19; v4.0 Multi-User LAN Concurrency — SHIPPED 2026-05 via PR #152 (parallel branch); v1.0 perf line — COMPLETE via PR #114. No milestone in flight.
Status: Phase 1041 complete — inline time-range control (toolbar dropdown + Custom date strip) shipped; PR #189 MERGED 2026-06-03. No planned milestone in flight — repo in polish/housekeeping. Outstanding: 12 wiki-bot dup PRs were closed to 1 (#190) + workflow root-caused (260609-mcz); backlog Phase 999.1 (in-app help system) unplanned; ROADMAP v4.0 boxes stale (shipped on main via #152 — router misreports, see memory gsd-router-stale-v4-misroute).
Last activity: 2026-06-10 - Completed quick task 260610-ov3: Optimize data loading speed in populated FastSense dashboards (DashboardEngine) — per-render Tag-data cache in FastSenseWidget (<=1 resolve per render, consume-once through the engine preview pass), O(1) ctor time-range pull, bench_dashboard_load.m. On branch claude/unruffled-gagarin-e2ca3d; MATLAB-only suite runs deferred (MCP down).
Last activity: 2026-06-24 - Completed quick task 260624-nvf: Fix StatusWidget crash when a status widget is bound to a MonitorTag via 'Tag' — the constructor now routes a monitor-kind Tag to the Threshold/monitor path so refresh() uses deriveStatusFromMonitorTag_ instead of the SensorTag-only obj.Sensor.Y access. Unblocks the README "Build a dashboard" quickstart. Verified in a fresh process: TestStatusWidget+MultiStatus 23/23, dashboard suites 68/68, README dashboard repro renders 4 widgets. Code 14b3d529; on branch claude/recursing-cray-0dbbac.

### Note on parallel v4.0 work (main branch state)

Expand Down Expand Up @@ -107,6 +107,7 @@ Other main PRs (#138, #139, #141, #144, #145, #146) auto-merged without conflict
| 260609-v5p | Speed up DashboardEngine live refresh: data-unchanged fast path in FastSenseWidget.update()/refresh() (fingerprint [n,x1,xend,yend], same append-only contract as PreviewCacheKey_) skips updateData/preview-invalidate/formatTimeAxis on idle ticks; single Tag.getXY per tick (updateTimeRangeCache(x) optional arg); refreshEventMarkers_ O(nE²)→O(nE) isequal diff; computeEventMarkers vectorized accumulators + sortrows-based dedup (max-severity-wins preserved, non-finite sev→1); getEventMarkers preallocation + per-unique-severity color lookup; vectorized formatTimeAxis_. New bench_dashboard_live.m (8 Tag-bound widgets × 50k pts + 200 events): idle tick 281→34 ms (~8×), active tick ~50→30 ms. Verified R2025b: test_dashboard_perf_fixes 9/9 (2 new), preview-envelope 7/7, events-toggle 22/22, time-window 8/8, TestDashboardEngine 18/18, TestDashboardEngineEventMarkers 8/8, TestFastSenseWidgetUpdate 2/2, TestFastSenseWidgetEventMarkers 12/12, TestDashboardDirtyFlag 6/6. Known stale test: flat test_dashboard_engine_event_markers case_render assumes one handle per marker — broken since 260508 color-group batching, fails pre- and post-change identically (Octave mirror that self-skips on Octave). | 2026-06-09 | 8cd6443f, c29be759, cbd66937, 98184f36 | — | [260609-v5p-speed-up-dashboardengine-live-refresh-pa](./quick/260609-v5p-speed-up-dashboardengine-live-refresh-pa/) |
| 260610-hwj | Review-sweep fixes batch 2 (branch claude/review-fixes-batch2, separate PR from the perf pass): GaugeWidget.fromStruct restores Threshold (was Tag — threshold coloring dead after load) + constructor probes for allValues() (pre-v2.0-only method; MonitorTag-bound gauges crashed at construction since the migration); GroupWidget round-trips ExpandedHeight (collapsed groups were stuck collapsed after load); central themeOverride backfill in DashboardWidgetRegistry.fromStruct (dropped on load for every widget except GroupWidget); FastSense exportData routes through lineFullData (disk-backed lines exported empty columns); markerXData test helper parses batched NaN-separated marker polylines (stale since 260508). New test_review_fixes_batch2.m 4/4 R2025b / 3/3+gate Octave 11; event_markers 9/9 (first MATLAB pass ever); SerializerRoundTrip 15/15; Serializer 12/12; toolbar 19/19. | 2026-06-10 | 18387785 | — | [260610-hwj-review-sweep-fixes-batch-2-widget-serial](./quick/260610-hwj-review-sweep-fixes-batch-2-widget-serial/) |
| 260610-ov3 | Optimize data loading speed in populated Tag-bound dashboards (LOAD path; complements 260609-v5p/260610-g0w live-tick passes): render-scoped `RenderDataCache_` in FastSenseWidget collapses 3-4 redundant Tag resolves per render() to <=1 — probe pull seeds the cache, binding (addLine for non-state kinds; State keeps addTag staircase), yInit autoscale, and updateTimeRangeCache all reuse it; consume-once lifetime (warm through DashboardEngine's post-render computePreviewEnvelope pass, cleared on preview read + refresh()/update() entry); ctor `updateTimeRangeCache()` no-arg now uses O(1) Tag.getTimeRange() instead of full getXY (also fixes disk-backed ctor range = inf/-inf). Disk widgets: 2 SQLite range queries/render → 1, and they now contribute slider-preview envelopes at load (previously silently empty). New bench_dashboard_load.m (12×50k pts, 4 disk-backed): Render 6834→6711 ms Octave (graphics-bound; data-load slice halved). Octave 11.1: render_cache 4/4, load_perf 5/5, 13 regression tests green (batch segfault = known teardown flake, isolated runs clean). DEFERRED to live MATLAB: TestDashboardEngine, TestFastSenseWidgetUpdate, TestDashboardSerializerRoundTrip suites. | 2026-06-10 | 3b7535ea | — | [260610-ov3-optimize-data-loading-speed-in-populated](./quick/260610-ov3-optimize-data-loading-speed-in-populated/) |
| 260624-nvf | Fix StatusWidget crash when bound to a MonitorTag via 'Tag' (route monitor-kind Tag to the Threshold/monitor path; unblocks README dashboard quickstart) | 2026-06-24 | 14b3d529 | Verified | [260624-nvf-fix-statuswidget-crash-when-bound-to-a-m](./quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/) |

## Progress Bar

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
quick_id: 260624-nvf
title: Fix StatusWidget crash when bound to a MonitorTag via Tag
status: complete
date: 2026-06-24
---

# Quick Task 260624-nvf — Fix StatusWidget crash when bound to a MonitorTag via Tag

## Problem

`addWidget('status', 'Tag', monitorTag)` crashed at render with
`Unrecognized method, property, or field 'Y' for class 'MonitorTag'`
(`libs/Dashboard/StatusWidget.m` `refresh`). `DashboardWidget.Sensor` is a
backward-compat **alias for `Tag`**, so a `'Tag'`-bound MonitorTag is read back
through `obj.Sensor`, and the Sensor branch assumes a SensorTag-like `.Y` value
series. MonitorTags are 0/1 alarm signals with no `.Y`. The README
"Build a dashboard" quickstart binds `status` to an alarm MonitorTag and hit this.

## Approach

StatusWidget already supports MonitorTags through the **Threshold/monitor path**
(`deriveStatusFromMonitorTag_`, which reads `obj.Threshold.getXY()`), previously
reachable only when the monitor was passed via the `Threshold` property. Reroute a
monitor-kind Tag to `Threshold` in the constructor so `refresh()` (and the label,
asciiRender, and toStruct paths) use that existing handling. The sibling
`MultiStatusWidget` already supports Tag-bound MonitorTags — this brings StatusWidget
to parity.

## Tasks

1. `libs/Dashboard/StatusWidget.m` — constructor: after Threshold-key resolution,
if no `Threshold` is set and the bound Tag is monitor-kind
(`thresholdIsMonitorKind_(obj.Sensor)`), move it to `Threshold` and clear `Sensor`.
- verify: `TestStatusWidget` passes; README dashboard repro renders without error.
- done: a monitor-bound status widget renders and `CurrentStatus` reflects the
monitor's latest 0/1 sample ('violation' / 'ok').
2. `tests/suite/TestStatusWidget.m` — add `testRefreshWithMonitorTag` covering the
violation (last sample 1) and ok (last sample 0) cases.

## Constraints

Pure MATLAB, toolbox-free; backward compatible (SensorTag-bound and Threshold-bound
status widgets unchanged); pure-MATLAB MEX fallback intact; break no existing test.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
quick_id: 260624-nvf
title: Fix StatusWidget crash when bound to a MonitorTag via Tag
status: complete
date: 2026-06-24
commit: 14b3d529
---

# Quick Task 260624-nvf — Summary

**Status:** complete

## What changed

- `libs/Dashboard/StatusWidget.m` (constructor): if no `Threshold` is set and the
bound Tag (`obj.Sensor`, the alias for `obj.Tag`) is monitor-kind, reroute it to
`obj.Threshold` and clear `obj.Sensor`. `refresh()` then takes the existing
`deriveStatusFromMonitorTag_` path instead of the SensorTag-only `obj.Sensor.Y`
access that crashed on a MonitorTag.
- `tests/suite/TestStatusWidget.m`: added `testRefreshWithMonitorTag` — binds a
MonitorTag via `'Tag'`, renders into an offscreen figure, asserts no render error
and `CurrentStatus` == `'violation'` (last sample > 0.5) / `'ok'` (<= 0.5).

## Verification (fresh `matlab -batch`, `restoredefaultpath`, this worktree)

- **RED (before fix):** `testRefreshWithMonitorTag` errored with
`Unrecognized ... 'Y' for class 'MonitorTag'` at `StatusWidget/refresh:119`;
the other 10 TestStatusWidget tests passed.
- **GREEN (after fix):** `TestStatusWidget` + `TestMultiStatusWidget` +
`TestMultiStatusWidgetTag` → **23/23**.
- **Regression:** `TestDashboardSerializerRoundTrip` + `TestDashboardBugFixes` +
`TestDashboardPreview` + `TestInfoTooltip` → **68/68**.
- **End-to-end:** the README "Build a dashboard" snippet
(`addWidget('status','Tag',alarm,...)` + `d.render()`) renders all 4 widgets,
no error.

## Backward compatibility

SensorTag-bound (kind `sensor` → reroute guard false → unchanged) and Threshold-bound
(`obj.Threshold` already set → guard skips) status widgets are unchanged. Only the
previously-crashing Tag-bound MonitorTag case changes behaviour.

## Notes

- Code commit: `14b3d529`.
- Unblocks the README dashboard quickstart; the companion `'Label'` → `'Title'` doc
fix landed earlier in `c0f0d949`.
- Originated from the `/first-run-check` loop (task_693ced52).
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,17 @@ Everything in FastSense — sensors, machine states, alarms, derived signals —
Tags carry their own metadata (units, criticality, labels) and live in a shared **`TagRegistry`** so every part of the system — plots, dashboards, event detection, the web bridge — speaks the same language.

```matlab
t = linspace(0, 100, 1e5); % your sensor's timestamps
pressure_data = 50 + 8*sin(t/5) + randn(size(t)); % ...and its readings

press = SensorTag('press_a', 'Name', 'Chamber Pressure', 'Units', 'bar');
press.updateData(t, pressure_data);

% Alarm whenever pressure > 55 bar
alarm = MonitorTag('press_high', press, @(x, y) y > 55);

TagRegistry.register(press);
TagRegistry.register(alarm);
TagRegistry.register('press_a', press);
TagRegistry.register('press_high', alarm);

fp = FastSense();
fp.addTag(press);
Expand All @@ -89,9 +92,9 @@ Compose monitoring dashboards from widgets on a 24-column grid. The same Tags dr
d = DashboardEngine('Process Monitor');
d.Theme = 'dark';
d.addWidget('fastsense', 'Position', [1 1 16 8], 'Tag', press);
d.addWidget('number', 'Position', [17 1 8 4], 'Tag', press, 'Label', 'Pressure');
d.addWidget('gauge', 'Position', [17 5 8 4], 'Tag', press, 'Label', 'Live');
d.addWidget('status', 'Position', [1 9 24 2], 'Tag', alarm, 'Label', 'Alarm');
d.addWidget('number', 'Position', [17 1 8 4], 'Tag', press, 'Title', 'Pressure');
d.addWidget('gauge', 'Position', [17 5 8 4], 'Tag', press, 'Title', 'Live');
d.addWidget('status', 'Position', [1 9 24 2], 'Tag', alarm, 'Title', 'Alarm');
d.render();

d.save('process.json'); % JSON-persist
Expand Down
4 changes: 2 additions & 2 deletions examples/01-basics/example_basic.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@
fp2.addLine(x2, y2, 'DisplayName', 'Exponential Growth');
fp2.addThreshold(100, 'Direction', 'upper', 'ShowViolations', true, ...
'Label', 'Warning');
fp2.render();

% Switch Y axis to log scale (can be called before or after render).
% Setting it before render() lets the axis autoscale directly in log space.
% Try fp2.setScale('YScale', 'linear') to toggle back.
fp2.setScale('YScale', 'log');
fp2.render();
title(fp2.hAxes, 'setScale — Logarithmic Y Axis');
fprintf('setScale() demo: Y axis switched to log scale.\n');

Expand Down
9 changes: 9 additions & 0 deletions libs/Dashboard/StatusWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
obj.Threshold = [];
end
end
% A MonitorTag bound via 'Tag' (or its 'Sensor' alias) is a 0/1
% alarm signal, not a value series, so route it through the
% Threshold/monitor path — refresh() then uses
% deriveStatusFromMonitorTag_ instead of the SensorTag-only
% obj.Sensor.Y access (which errors on a MonitorTag).
if isempty(obj.Threshold) && thresholdIsMonitorKind_(obj.Sensor)
obj.Threshold = obj.Sensor;
obj.Sensor = [];
end
% Mutual exclusivity: Threshold wins (per D-08)
if ~isempty(obj.Threshold) && ~isempty(obj.Sensor)
obj.Sensor = [];
Expand Down
27 changes: 27 additions & 0 deletions tests/suite/TestStatusWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,33 @@ function testRefreshWithTag(testCase)
'No threshold rules means status should be ok');
end

function testRefreshWithMonitorTag(testCase)
%% A MonitorTag bound via 'Tag' renders without error and maps
% its latest 0/1 sample to violation/ok. Regression: MonitorTag
% has no .Y, which the Sensor-value path assumed (#task_693ced52).
sHi = SensorTag('mw_press_hi', 'Name', 'Pressure Hi', 'Units', 'bar');
sHi.updateData([1 2 3], [50 52 60]); % last 60
monHi = MonitorTag('mw_mon_hi', sHi, @(x, y) y > 55); % last -> 1

wHi = StatusWidget('Tag', monHi, 'Title', 'Alarm');
hFig = figure('Visible', 'off');
testCase.addTeardown(@() close(hFig));
hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]);
testCase.verifyWarningFree(@() wHi.render(hp));
testCase.verifyEqual(wHi.CurrentStatus, 'violation', ...
'MonitorTag with last sample > 0.5 should read as violation');

sLo = SensorTag('mw_press_lo', 'Name', 'Pressure Lo', 'Units', 'bar');
sLo.updateData([1 2 3], [50 51 52]); % last 52
monLo = MonitorTag('mw_mon_lo', sLo, @(x, y) y > 55); % last -> 0

wLo = StatusWidget('Tag', monLo, 'Title', 'Alarm');
hp2 = uipanel('Parent', hFig, 'Position', [0 0 1 1]);
testCase.verifyWarningFree(@() wLo.render(hp2));
testCase.verifyEqual(wLo.CurrentStatus, 'ok', ...
'MonitorTag with last sample <= 0.5 should read as ok');
end

function testToStruct(testCase)
%% Serialization includes type, title, position, and source
s = SensorTag('V-100', 'Name', 'Valve');
Expand Down
14 changes: 10 additions & 4 deletions wiki/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,31 @@
## Setup

1. Clone or download the repository
2. In MATLAB/Octave, navigate to the FastPlot directory
2. In MATLAB/Octave, navigate to the FastSense directory
3. Run install:
```matlab
install;
```

This adds the library paths:
- `libs/FastPlot` — core plotting engine
- `libs/SensorThreshold` — sensor and threshold system
- `libs/FastSense` — core plotting engine
- `libs/SensorThreshold` — sensor, tag, and threshold system
- `libs/EventDetection` — event detection and viewer
- `libs/Dashboard` — dashboard engine and widgets
- `libs/WebBridge` — TCP server for web-based visualization
- `libs/FastSenseCompanion` — companion navigator app
- `libs/PlantLog` — plant-log entry storage
- `libs/Concurrency` — file-locking helpers for live/multi-process use
- `libs/Help` — in-app wiki browser

It also adds `examples/`, `benchmarks/`, and `tests/` to the path, so example and benchmark scripts can be run by name.

## MEX Compilation (Optional)

For maximum performance, compile the C MEX accelerators:

```matlab
cd libs/FastPlot
cd libs/FastSense
build_mex();
```

Expand Down
2 changes: 1 addition & 1 deletion wiki/_Sidebar.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
**FastPlot Wiki**
**FastSense Wiki**

- [[Home]]
- [[Installation]]
Expand Down
Loading