diff --git a/CLAUDE.md b/CLAUDE.md index 7ebbe710..511525c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,13 +61,13 @@ An upgrade to FastSense's existing dashboard engine adding nested layout organiz - `uvicorn[standard] >= 0.24` - `websockets >= 12.0` - `numpy >= 1.24` -- `anthropic` (dev/scripts dependency, NOT in main dependencies — used only by `scripts/generate_wiki.py`) +- `openai` (dev/scripts dependency, NOT in main dependencies — used by `scripts/generate_wiki.py` to call the OpenRouter API) - GitHub Actions - CI/CD (tests, MEX build, benchmarks, wiki generation, release) - Codecov - test coverage reporting (MATLAB runs only; token via secret) ## Configuration - `FASTSENSE_SKIP_BUILD=1` - skip MEX compilation in CI when MEX binaries are cached - `FASTSENSE_RESULTS_FILE` - path for Octave test result output in CI -- `ANTHROPIC_API_KEY` - required only for `scripts/generate_wiki.py` (wiki auto-generation) +- `OPENROUTER_API_KEY` - required only for `scripts/generate_wiki.py` (wiki auto-generation via OpenRouter) - `miss_hit.cfg` - MISS_HIT linter/style/metric configuration (project root) - `bridge/python/pyproject.toml` - Python bridge package config - ARM64: `-O3 -ffast-math` (Clang/MATLAB) or `-O3 -mcpu=apple-m3 -ftree-vectorize -ffast-math` (GCC/Octave) diff --git a/scripts/generate_api_docs.py b/scripts/generate_api_docs.py index 6d1c8f87..85197030 100644 --- a/scripts/generate_api_docs.py +++ b/scripts/generate_api_docs.py @@ -637,75 +637,58 @@ def _escape_md_table(text: str) -> str: # Page definitions # --------------------------------------------------------------------------- -# Each page: (output filename, page title, library dir, class order) +# Each page: (output filename, page title, library dir, anchors). +# +# `anchors` is an ORDERING HINT, not an allow-list: for a library page, EVERY +# classdef discovered in the directory is documented regardless of whether it +# is listed here (see generate_page). Anchors float the headline classes to the +# top; remaining classes are appended alphabetically. An anchor that no longer +# exists is silently skipped, so this list can never drop or duplicate a real +# class — it only affects ordering. Keep it short (the few most important +# classes). This is what makes the generator immune to class renames. +# +# Exception: the special page with library dir None (Utilities) uses `anchors` +# as an explicit cross-library class selector — only the named classes appear. +# +# Page names track the published wiki and _Sidebar.md (project brand: +# "FastPlot"), which intentionally differs from the underlying class names +# (e.g. the FastSense class is documented on the "FastPlot" page). PAGES = [ ( - "API-Reference:-FastSense.md", - "API Reference: FastSense", + "API-Reference:-FastPlot.md", + "API Reference: FastPlot", "FastSense", - [ - "FastSense", - "FastSenseFigure", - "FastSenseDock", - "FastSenseToolbar", - "FastSenseTheme", - "FastSenseDataStore", - "NavigatorOverlay", - "SensorDetailPlot", - ], + ["FastSense", "FastSenseDataStore", "FastSenseGrid", "SensorDetailPlot"], ), ( "API-Reference:-Dashboard.md", "API Reference: Dashboard", "Dashboard", [ - "DashboardEngine", - "DashboardBuilder", - "DashboardWidget", - "FastSenseWidget", - "GaugeWidget", - "NumberWidget", - "StatusWidget", - "TextWidget", - "TableWidget", - "RawAxesWidget", - "EventTimelineWidget", - "DashboardSerializer", - "DashboardLayout", - "DashboardTheme", - "DashboardToolbar", + "DashboardEngine", "DashboardBuilder", "DashboardWidget", + "DashboardLayout", "DashboardPage", "DashboardSerializer", ], ), ( "API-Reference:-Sensors.md", "API Reference: Sensors", "SensorThreshold", - ["Sensor", "StateChannel", "ThresholdRule", "SensorRegistry"], + [ + "Tag", "SensorTag", "MonitorTag", "CompositeTag", "DerivedTag", + "StateTag", "TagRegistry", + ], ), ( "API-Reference:-Event-Detection.md", "API Reference: Event Detection", "EventDetection", - [ - "EventDetector", - "IncrementalEventDetector", - "Event", - "EventConfig", - "EventStore", - "EventViewer", - "LiveEventPipeline", - "NotificationService", - "NotificationRule", - "DataSource", - "MatFileDataSource", - "DataSourceMap", - ], + ["Event", "EventStore", "EventViewer", "LiveEventPipeline"], ), ( "API-Reference:-Utilities.md", "API Reference: Utilities", - None, # special: pulls from multiple dirs - ["ConsoleProgressBar", "FastSenseDefaults"], + None, # special: explicit cross-library class list (anchors IS the selector) + ["ConsoleProgressBar"], ), ] @@ -727,18 +710,24 @@ def collect_classes(lib_dir: Path) -> dict: return classes -def generate_page(filename, title, classes_by_name, class_order): - """Generate a wiki page for the given classes.""" +def generate_page(filename, title, classes_by_name, anchors): + """Generate a wiki page documenting every class in classes_by_name. + + `anchors` lists headline class names to emit first, in order; every + remaining class is appended alphabetically. Anchor names absent from + classes_by_name are skipped — completeness comes from discovery, never + from the anchor list, so a stale anchor cannot drop a real class. + """ parts = [AUTO_GENERATED_NOTICE, f"# {title}\n"] written = set() - for name in class_order: + for name in anchors: if name in classes_by_name: parts.append(format_class_markdown(classes_by_name[name])) parts.append("---\n") written.add(name) - # Append any classes found but not in the explicit order + # Append any discovered classes not already emitted via anchors. for name, cls in sorted(classes_by_name.items()): if name not in written: parts.append(format_class_markdown(cls)) @@ -761,8 +750,47 @@ def generate_page(filename, title, classes_by_name, class_order): # Main # --------------------------------------------------------------------------- +# Library directories scanned for classdefs. Order is cosmetic (parse logging). +SCANNED_LIBS = ["FastSense", "Dashboard", "SensorThreshold", "EventDetection", "WebBridge"] + +# Scanned for cross-page class lookup but intentionally given no page of their +# own. Suppresses the "undocumented library" warning below. +UNDOCUMENTED_OK = {"WebBridge"} + + +def prune_stale_api_pages(produced): + """Delete API-Reference pages this generator owns but no longer produces. + + Safety: only files matching ``API-Reference:-*.md`` that carry THIS + script's auto-generated banner are eligible. Manually-maintained pages + (e.g. API-Reference:-Themes.md, which documents a function rather than a + classdef) carry no banner and are never touched; nor are pages owned by + generate_wiki.py, which use a different banner. This makes orphans left by + a page rename self-healing instead of silently shipping stale content. + """ + banner_marker = "by scripts/generate_api_docs.py" + for page in sorted(WIKI_DIR.glob("API-Reference:-*.md")): + if page.name in produced: + continue + head = page.read_text(encoding="utf-8", errors="replace")[:300] + if banner_marker in head: + page.unlink() + print(f" -> Pruned stale auto-generated page: {page.relative_to(PROJECT_ROOT)}") + + +def warn_undocumented_libs(lib_classes, documented_libs): + """Warn if a scanned library has classdefs but no page maps to it.""" + for lib_name, parsed in lib_classes.items(): + if parsed and lib_name not in documented_libs and lib_name not in UNDOCUMENTED_OK: + print( + f" WARNING: libs/{lib_name}/ has {len(parsed)} class(es) but no " + f"API-Reference page maps to it — add an entry to PAGES.", + file=sys.stderr, + ) + + def main(): - print(f"FastSense API Doc Generator") + print("FastPlot API Doc Generator") print(f"Project root: {PROJECT_ROOT}") print(f"Libs dir: {LIBS_DIR}") print(f"Wiki dir: {WIKI_DIR}") @@ -778,7 +806,7 @@ def main(): all_classes = {} # name -> MatlabClass lib_classes = {} # lib_name -> {name: MatlabClass} - for lib_name in ["FastSense", "Dashboard", "SensorThreshold", "EventDetection", "WebBridge"]: + for lib_name in SCANNED_LIBS: lib_dir = LIBS_DIR / lib_name print(f"[{lib_name}]") parsed = collect_classes(lib_dir) @@ -788,18 +816,29 @@ def main(): # Generate pages print("Generating wiki pages:") - for filename, title, lib_name, class_order in PAGES: + produced = set() + documented_libs = set() + for filename, title, lib_name, anchors in PAGES: if lib_name is None: - # Utilities page: pull from all_classes + # Special page: anchors doubles as an explicit cross-library class + # selector — only the named classes are documented here. subset = { name: all_classes[name] - for name in class_order + for name in anchors if name in all_classes } else: + # Library page: document every classdef discovered in the directory. subset = lib_classes.get(lib_name, {}) + documented_libs.add(lib_name) + + generate_page(filename, title, subset, anchors) + produced.add(filename) - generate_page(filename, title, subset, class_order) + # Remove pages we used to generate but no longer do (e.g. after a rename), + # then flag any library that has classes but no page mapping (inverse drift). + prune_stale_api_pages(produced) + warn_undocumented_libs(lib_classes, documented_libs) print() print("Done. Generated API reference pages in wiki/.") diff --git a/wiki/API-Reference:-Dashboard.md b/wiki/API-Reference:-Dashboard.md index 4ede41ea..655d7ce1 100644 --- a/wiki/API-Reference:-Dashboard.md +++ b/wiki/API-Reference:-Dashboard.md @@ -693,521 +693,218 @@ ASCIIRENDER Return ASCII representation of this widget. --- -## `FastSenseWidget` --- Dashboard widget wrapping a FastSense instance. - -> Inherits from: `DashboardWidget` - -Supports data binding modes: - Tag: w = FastSenseWidget('Tag', tagObj) - DataStore: w = FastSenseWidget('DataStore', dsObj) - Inline: w = FastSenseWidget('XData', x, 'YData', y) - File: w = FastSenseWidget('File', 'path.mat', 'XVar', 'x', 'YVar', 'y') - -### Constructor - -```matlab -obj = FastSenseWidget(varargin) -``` - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| DataStoreObj | `[]` | | -| XData | `[]` | | -| YData | `[]` | | -| File | `''` | | -| XVar | `''` | | -| YVar | `''` | | -| Thresholds | `'auto'` | | -| XLabel | `''` | X-axis label (auto-set from Sensor if empty) | -| YLabel | `''` | Y-axis label (auto-set from Sensor if empty) | -| YLimits | `[]` | Fixed Y-axis range [min max]; empty = auto-scale | -| ShowThresholdLabels | `false` | show inline name labels on threshold lines | -| ShowEventMarkers | `false` | Phase 1012 — toggle event round-marker overlay | -| EventStore | `[]` | Phase 1012 — EventStore handle forwarded to inner FastSense | -| ShowPlantLog | `false` | Phase 1032 PLOG-VIZ-03 — opt-in per-widget plant-log vertical-line overlay | -| LiveViewMode | `'preserve'` | | -| YLimitMode | `'auto-visible'` | | - -### Methods - -#### `render(obj, parentPanel)` - -#### `refresh(obj)` - -Re-render Tag-bound widgets so updated data shows. -Uses incremental updateData() path when tag identity is unchanged -(PERF2-01); falls back to full teardown/rebuild on first render, -tag swap, or error. Zoom state (xlim) is preserved in both paths. - -#### `update(obj)` - -UPDATE Incrementally update Tag data without full axes rebuild. - Uses FastSenseObj.updateData() to replace data and re-downsample, - avoiding the expensive delete/recreate cycle of refresh(). - Falls back to refresh() if FastSenseObj is not in a renderable state. - (260513-ovt) Per-tick Y autoscale removed from this path so - Live mode never silently mutates the user's Y view. - -#### `setEventMarkersVisible(obj, tf)` - -SETEVENTMARKERSVISIBLE Pass-through to FastSense event-marker toggle. - No-op when no FastSense instance exists yet (pre-render). - When rendered, delegates to FastSense.setShowEventMarkers - which re-draws the overlay in place without disturbing - zoom state or live refresh cadence. - -#### `setPlantLogMarkers(obj, times, entries)` - -SETPLANTLOGMARKERS Draw or clear per-widget plant-log vertical lines. - Phase 1032 PLOG-VIZ-04. Draws one xline per finite timestamp - on the widget's inner FastSense axes (Tag = 'WidgetPlantLogMarker', - 1 px solid line with theme.MarkerPlantLog color, default - [0 0 0]). Empty / no-arg input clears every existing marker - via tag-based delete. Non-finite timestamps are silently - dropped (mirrors TimeRangeSelector.setPlantLogMarkers shape). - -#### `setPlantLogXLimListenerForEngine_(obj, lis)` - -#### `setShowPlantLog(obj, tf, engine)` - -SETSHOWPLANTLOG Toggle the per-widget plant-log overlay (Phase 1032 PLOG-VIZ-03). - tf — boolean; true enables overlay + attaches XLim listener, - false disables overlay + tears down listener + clears markers. - engine — DashboardEngine handle; required so refresh + listener - wiring can route through engine.refreshPlantLogOverlayForWidget_ - and engine.attachPlantLogXLimListener_. - -#### `setYLimitMode(obj, mode)` - -SETYLIMITMODE Set the Y-axis rescale strategy and re-fit if rendered. - mode is one of: - 'auto-visible' - rescale to data inside the current X window - 'auto-all' - rescale to all data the bound Tag exposes - 'locked' - freeze YLim; no further rescale on tick/refresh - -#### `autoScaleY_(obj, y)` - -AUTOSCALEY_ Rescale the Y axis to cover current data + thresholds. - FastSense locks YLim to manual mode at first render, so new - samples outside the initial range would fall off the chart. - This helper recomputes the Y extent every tick (including any - threshold values so MonitorTag lines stay visible) and updates - the axes. Skipped when: - - the widget has a user-pinned YLimits NV-pair, or - - the user manually zoomed Y via mouse (UserZoomedY), or - - the dashboard's Follow toggle is engaged - (FastSenseObj.LiveViewMode == 'follow') — Follow is an - explicit user intent to track the data tail in X only and - keep the rest of the view (including Y) frozen. (260513-ovt) - - YLimitMode == 'locked' — the user explicitly froze Y limits - via the L button on the WidgetButtonBar (260513-sfp). - -#### `onYLimChanged(obj)` - -ONYLIMCHANGED Detach widget from automatic Y rescale after user zoom. - Fired by the YLim PostSet listener. When the YLim change came - from inside autoScaleY_ (IsSettingYLim==true) we ignore it; any - other source — mouse scroll, drag, zoom toolbar, programmatic - ylim() from user code — counts as a manual override and - latches UserZoomedY so live ticks stop fighting the user. - -#### `setTimeRange(obj, tStart, tEnd)` - -#### `onXLimChanged(obj)` - -If xlim changed by user zoom/pan (not by setTimeRange), -detach this widget from global time. - -#### `[tMin, tMax] = getTimeRange(obj)` - -Return cached min/max in O(1). Cache is kept up to date by -updateTimeRangeCache() which is called from render/refresh/update. - -#### `series = getPreviewSeries(obj, nBuckets)` - -GETPREVIEWSERIES Per-bucket min/max preview for the dashboard envelope. - series = getPreviewSeries(obj, nBuckets) returns a struct with - fields xCenters, yMin, yMax — each a 1xnBucketsEff row vector; - yMin and yMax are normalized into [0,1] across the widget's own - current y-range. Returns [] only when no data is bound or when - the sample count is genuinely too sparse (<4) to downsample. - -#### `t = getEventTimes(obj)` - -GETEVENTTIMES Event start times for the dashboard time-slider markers. - Looks up events in this priority order: - 1. obj.EventStore (widget-level — the modern attachment point) - 2. obj.FastSenseObj.EventStore (legacy: events on inner FastSense) - 3. obj.FastSenseObj.Events / .EventTimes (defensive: extra hooks) - -#### `m = getEventMarkers(obj)` - -GETEVENTMARKERS Per-event time + severity + color for slider markers. - m = getEventMarkers(obj) returns a struct array with fields: - m(k).Time — numeric timestamp (StartTime) - m(k).Severity — numeric severity in {1,2,3} (default 1 if absent) - m(k).Color — 1x3 RGB triplet from severityColor(theme, sev) - -#### `invalidatePreviewCache_(obj)` - -INVALIDATEPREVIEWCACHE_ Clear PreviewCache_ so getPreviewSeries recomputes. - Called from refresh() / update() / rebuildForTag_() whenever - the underlying data may have changed. Cheap (no graphics). - -#### `t = getType(~)` - -#### `lines = asciiRender(obj, width, height)` - -#### `s = toStruct(obj)` - -### Static Methods - -#### `FastSenseWidget.obj = fromStruct(s)` - ---- - -## `GaugeWidget` --- Gauge widget with arc, donut, bar, and thermometer styles. - -> Inherits from: `DashboardWidget` - -w = GaugeWidget('Title', 'Pressure', 'ValueFcn', @() getPressure(), ... - 'Range', [0 100], 'Units', 'bar'); - w = GaugeWidget('Sensor', mySensor, 'Style', 'donut'); - w = GaugeWidget('Threshold', t, 'StaticValue', 50); - -### Constructor - -```matlab -obj = GaugeWidget(varargin) -``` - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| ValueFcn | `[]` | | -| Range | `[]` | Empty default for auto-derivation cascade | -| Units | `''` | | -| StaticValue | `[]` | | -| Style | `'arc'` | 'arc', 'donut', 'bar', 'thermometer' | -| Threshold | `[]` | Threshold object or registry key string (per D-01) | - -### Methods - -#### `render(obj, parentPanel)` - -#### `refresh(obj)` - -#### `t = getType(~)` - -#### `lines = asciiRender(obj, width, height)` - -#### `s = toStruct(obj)` - -### Static Methods - -#### `GaugeWidget.obj = fromStruct(s)` - ---- - -## `NumberWidget` --- Dashboard widget showing a big number with label and trend. - -> Inherits from: `DashboardWidget` +## `DashboardLayout` --- Manages 24-column responsive grid positioning. -w = NumberWidget('Title', 'Temp', 'ValueFcn', @() readTemp(), 'Units', 'degC'); +> Inherits from: `handle` - ValueFcn returns either: - - A scalar (displayed as-is) - - A struct with fields: value, unit, trend ('up'/'down'/'flat') +Converts widget grid positions [col, row, width, height] to normalized + canvas coordinates [x, y, w, h]. Handles overlap resolution, row + calculation, and scrollable canvas when content exceeds the viewport. ### Constructor ```matlab -obj = NumberWidget(varargin) +obj = DashboardLayout(varargin) ``` ### Properties | Property | Default | Description | |----------|---------|-------------| -| ValueFcn | `[]` | function_handle returning scalar or struct | -| Units | `''` | unit label string | -| Format | `'%.1f'` | sprintf format for value | -| StaticValue | `[]` | fixed value (no callback needed) | +| Columns | `24` | | +| TotalRows | `4` | | +| ContentArea | `[0 0 1 1]` | | +| Padding | `[0 0 0 0]` | | +| GapH | `0` | | +| GapV | `0` | | +| RowHeight | `0.22` | | +| ScrollbarWidth | `0.015` | | +| OnScrollCallback | `[]` | function handle: @(topRow, bottomRow) | +| DetachCallback | `[]` | function handle: @(widget) — set by DashboardEngine | +| CreateEventCallback | `[]` | function handle: @(widget) — set by DashboardEngine | +| VisibleRows | `[1 Inf]` | [topRow bottomRow] currently visible | +| EngineRef | `[]` | Phase 1032 PLOG-VIZ-05 — back-reference to DashboardEngine for chrome callbacks (addPlantLogToggle) | +| hFigure | `[]` | Figure handle for popup dismiss callbacks | +| hInfoPopup | `[]` | Handle to active info popup uipanel (at most one) | ### Methods -#### `render(obj, parentPanel)` - -#### `refresh(obj)` - -#### `t = getType(~)` - -#### `lines = asciiRender(obj, width, height)` - -#### `s = toStruct(obj)` - -### Static Methods - -#### `NumberWidget.obj = fromStruct(s)` - ---- - -## `StatusWidget` --- Colored dot indicator with sensor value. - -> Inherits from: `DashboardWidget` - -Sensor-first: - w = StatusWidget('Sensor', sensorObj); - - Threshold-bound (no Sensor required): - w = StatusWidget('Title', 'Temp', 'Threshold', t, 'Value', 85); - w = StatusWidget('Title', 'Temp', 'Threshold', 'temp_hi', 'ValueFcn', @getTemp); - - Legacy (still supported): - w = StatusWidget('Title', 'Pump 1', 'StatusFcn', @() 'ok'); - -### Constructor - -```matlab -obj = StatusWidget(varargin) -``` - -### Properties +#### `cr = canvasRatio(obj)` -| Property | Default | Description | -|----------|---------|-------------| -| StatusFcn | `[]` | function_handle returning 'ok'/'warning'/'alarm' (legacy) | -| StaticStatus | `''` | fixed status string (legacy) | -| Threshold | `[]` | Threshold object or registry key string (per D-01) | -| Value | `[]` | Scalar numeric value for threshold comparison (per D-03) | -| ValueFcn | `[]` | Function handle returning scalar value (per D-03, D-09) | +CANVASRATIO Ratio of canvas height to viewport height. + Returns 1 when content fits, >1 when scrolling is needed. -### Methods +#### `pos = computePosition(obj, gridPos)` -#### `render(obj, parentPanel)` +COMPUTEPOSITION Convert grid position to canvas-normalized coords. -#### `refresh(obj)` +#### `[stepW, stepH, cellW, cellH] = canvasStepSizes(obj)` -#### `t = getType(~)` +CANVASSTEPSIZES Grid step sizes in canvas-normalized coords. -#### `lines = asciiRender(obj, width, height)` +#### `[dx_c, dy_c] = figureToCanvasDelta(obj, dx_fig, dy_fig)` -#### `s = toStruct(obj)` +FIGURETOCANVASDELTA Convert figure-normalized deltas to canvas deltas. -### Static Methods +#### `maxRow = calculateMaxRow(obj, widgets)` -#### `StatusWidget.obj = fromStruct(s)` +#### `tf = overlaps(obj, posA, posB)` ---- +#### `newPos = resolveOverlap(obj, pos, existingPositions)` -## `TextWidget` --- Static text label or section header. +#### `ensureViewport(obj, hFigure, theme)` -> Inherits from: `DashboardWidget` +ENSUREVIEWPORT Create viewport/canvas/scrollbar only if they do not exist yet. + Idempotent: if the viewport handle is already valid, returns immediately + without deleting or recreating anything. On the first call the viewport, + canvas, and (if needed) scrollbar are created and TotalRows is reset to 0 + so that subsequent additive allocatePanels calls accumulate row counts. -w = TextWidget('Title', 'Section A', 'Content', 'Sensor overview'); +#### `resetViewport(obj)` -### Constructor +RESETVIEWPORT Destroy the current viewport so the next ensureViewport call rebuilds it. + Use when a full layout rebuild is required (e.g. single-page reflow). -```matlab -obj = TextWidget(varargin) -``` +#### `allocatePanels(obj, hFigure, widgets, theme)` -### Properties +ALLOCATEPANELS Create placeholder panels for widgets (additive; no viewport destruction). + Calls ensureViewport (idempotent) to guarantee hViewport/hCanvas exist, then + accumulates TotalRows and appends widget panels to the shared canvas. + Multiple calls for different page-widget sets are safe: earlier panels survive. +Ensure viewport exists (idempotent — no-op if already live) -| Property | Default | Description | -|----------|---------|-------------| -| Content | `''` | body text | -| FontSize | `0` | 0 = use theme default | -| Alignment | `'left'` | 'left', 'center', 'right' | +#### `realizeWidget(obj, widget)` -### Methods +REALIZEWIDGET Render a single widget into its pre-allocated panel. + Creates the chrome (full-width WidgetButtonBar + WidgetContentPanel + sub-panel below the bar) BEFORE calling widget.render so the + widget's own graphics children (titles, axes, status text, group + headers) land in the visible content area, never under the bar. -#### `render(obj, parentPanel)` +#### `createPanels(obj, hFigure, widgets, theme)` -#### `refresh(~)` +CREATEPANELS Create and render all widget panels (legacy path). -Static widget — nothing to refresh +#### `reflow(obj, hFigure, widgets, theme)` -#### `t = getType(~)` +Re-run layout after dynamic changes (e.g., group collapse/expand). +Tears down and recreates all panels, calling render() on each widget. -#### `lines = asciiRender(obj, width, height)` +#### `onScroll(obj, val)` -#### `s = toStruct(obj)` +ONSCROLL Adjust canvas position from scrollbar value. + val=1 shows top, val=0 shows bottom. -### Static Methods +#### `rows = computeVisibleRows(obj, scrollVal)` -#### `TextWidget.obj = fromStruct(s)` +COMPUTEVISIBLEROWS Derive visible row range from scroll position. ---- +#### `vis = isWidgetVisible(obj, gridPos, buffer)` -## `TableWidget` --- Tabular data display using uitable. +ISWIDGETVISIBLE Check if widget rows overlap visible range + buffer. -> Inherits from: `DashboardWidget` +#### `openInfoPopup(obj, widget, theme)` -w = TableWidget('Title', 'Sensor Data', 'DataFcn', @() getData()); - w = TableWidget('Title', 'Static', 'Data', {{'A',1;'B',2}}, ... - 'ColumnNames', {'Name','Value'}); - w = TableWidget('Sensor', sensorObj); % last N data rows - w = TableWidget('Sensor', sensorObj, 'Mode', 'events', 'EventStoreObj', store); +OPENINFOPOPUP Open a modal figure window showing widget Description. -### Constructor +#### `closeInfoPopup(obj)` -```matlab -obj = TableWidget(varargin) -``` +CLOSEINFOPOPUP Close and delete the active info popup panel. -### Properties +#### `onFigureClickForDismiss(obj)` -| Property | Default | Description | -|----------|---------|-------------| -| DataFcn | `[]` | | -| Data | `{}` | | -| ColumnNames | `{}` | | -| Mode | `'data'` | 'data' or 'events' | -| N | `10` | number of rows to display | -| EventStoreObj | `[]` | EventStore for event mode | +ONFIGURECLICKFORDISMISS Dismiss popup if click was outside the popup panel. -### Methods +#### `onKeyPressForDismiss(obj, eventData)` -#### `render(obj, parentPanel)` +ONKEYPRESSFORDISMISS Dismiss popup when Escape is pressed. -#### `refresh(obj)` +#### `addPlantLogToggle(obj, widget, engine)` -#### `t = getType(~)` +ADDPLANTLOGTOGGLE Add the per-widget plant-log overlay toggle (Phase 1032 PLOG-VIZ-05). + The toggle is always created (Decision B: always render, disable + when no store); clicking it calls + widget.setShowPlantLog(~widget.ShowPlantLog, engine). + The engine handle is captured by the callback closure. -#### `lines = asciiRender(obj, width, height)` +#### `onPlantLogTogglePressed_(obj, src, widget, engine)` -#### `s = toStruct(obj)` +ONPLANTLOGTOGGLEPRESSED_ Toggle button callback — wraps setShowPlantLog with try/catch (Phase 1032 PLOG-VIZ-05). + Programmatic force-call paths (tests, automation) need a + software-level guard for Enable='off' because uicontrols only + honor Enable natively for user-driven mouse clicks. ### Static Methods -#### `TableWidget.obj = fromStruct(s)` - ---- - -## `RawAxesWidget` --- User-supplied plot function on raw MATLAB axes. - -> Inherits from: `DashboardWidget` - -w = RawAxesWidget('Title', 'Histogram', ... - 'PlotFcn', @(ax) histogram(ax, randn(1,1000))); - - When bound to a Sensor, the PlotFcn receives (ax, sensor) or - (ax, sensor, timeRange) depending on its nargin. - -### Constructor - -```matlab -obj = RawAxesWidget(varargin) -``` - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| PlotFcn | `[]` | @(ax) or @(ax, sensor[, tRange]) or @(ax, tRange) | -| DataRangeFcn | `[]` | @() returning [tMin tMax] for global time range detection | - -### Methods - -#### `render(obj, parentPanel)` - -#### `refresh(obj)` - -#### `setTimeRange(obj, tStart, tEnd)` - -#### `[tMin, tMax] = getTimeRange(obj)` +#### `DashboardLayout.reflowChrome_(hCell, barH, inset)` -#### `t = getType(~)` +REFLOWCHROME_ SizeChangedFcn handler — re-anchor the WidgetButtonBar + AND resize the WidgetContentPanel after the parent cell panel + resizes. Public so tests can drive a deterministic resize without + relying on SizeChangedFcn firing under -batch. + No-op when the cell has been deleted or chrome isn't there yet. -#### `lines = asciiRender(obj, width, height)` +#### `DashboardLayout.bg = chooseYLimitActiveBg_(theme)` -#### `s = toStruct(obj)` +CHOOSEYLIMITACTIVEBG_ Pick the highlight color for the active YLimit button. + Tries PressedBg / SelectedBg / AccentColor in order, falling + back to ToolbarBackground brightened by 0.15 per channel + (capped at 1) when none are present. No new theme fields are + introduced by 260513-sfp; future themes can opt into a + dedicated PressedBg token without touching layout code. -### Static Methods +#### `DashboardLayout.syncYLimitButtonsState_(bar, mode)` -#### `RawAxesWidget.obj = fromStruct(s)` +SYNCYLIMITBUTTONSSTATE_ Visually highlight the YLimit button matching mode. + The active button's BackgroundColor becomes the value stashed on + bar.UserData.YLimitActiveBg by addYLimitButtons_; the other two + revert to the theme's ToolbarBackground. Tolerates missing + buttons (no-op if the bar's UserData was never primed). ---- +#### `DashboardLayout.reflowButtonBar_(hCell, barH, inset)` -## `EventTimelineWidget` --- Displays events as colored bars on a timeline. +REFLOWBUTTONBAR_ Deprecated alias — forwards to reflowChrome_. + Kept temporarily for any external callers that still reference + the m52-era name. -> Inherits from: `DashboardWidget` +--- -Preferred: bind to an EventStore from the event detection system: - w = EventTimelineWidget('Title', 'Events', 'EventStoreObj', store); +## `DashboardPage` --- Named page container within a multi-page dashboard. - Legacy (still supported for backwards compatibility): - w = EventTimelineWidget('Title', 'Events', 'EventFcn', @() getEvents()); - w = EventTimelineWidget('Title', 'Events', 'Events', eventArray); +> Inherits from: `handle` - Events must be a struct array with fields: - startTime, endTime, label, color (optional) +Each DashboardPage holds a list of widgets to be rendered when the + page is active. DashboardEngine maintains a Pages cell array of + DashboardPage objects and routes addWidget() to the active page. ### Constructor ```matlab -obj = EventTimelineWidget(varargin) +obj = DashboardPage(name) ``` +DASHBOARDPAGE Construct a named page container. + pg = DashboardPage() creates page with Name = '' + pg = DashboardPage('Name') creates page with given Name + ### Properties | Property | Default | Description | |----------|---------|-------------| -| EventStoreObj | `[]` | EventStore handle — primary data source | -| Events | `[]` | struct array of events (legacy) | -| EventFcn | `[]` | function_handle returning events (legacy) | -| FilterSensors | `{}` | Cell array of Sensor names to filter | -| FilterTagKey | `''` | Tag-key filter (MONITOR-05 carrier: SensorName OR ThresholdLabel match) | -| ColorSource | `'event'` | 'event' or 'theme' | +| Name | `''` | | +| Widgets | `{}` | | ### Methods -#### `render(obj, parentPanel)` - -#### `setTimeRange(obj, tStart, tEnd)` - -#### `[tMin, tMax] = getTimeRange(obj)` - -#### `t = getEventTimes(obj)` - -GETEVENTTIMES Event start times from resolveEvents (override). - Mirrors the same filtering pipeline the widget uses to draw - bars, so the time-slider overlay always matches what the - widget itself renders. - -#### `m = getEventMarkers(obj)` - -GETEVENTMARKERS Per-event time + severity + color for slider markers. - m = getEventMarkers(obj) returns a struct array with fields: - m(k).Time — numeric timestamp (startTime) - m(k).Severity — numeric severity (default 1 if absent) - m(k).Color — 1x3 RGB triplet from severityColor(theme, sev) - -#### `refresh(obj)` - -#### `t = getType(~)` +#### `w = addWidget(obj, w)` -#### `lines = asciiRender(obj, width, height)` +ADDWIDGET Append widget w to the Widgets list. + pg.addWidget(w) appends w to obj.Widgets. #### `s = toStruct(obj)` -#### `evts = resolveEvents(obj)` - -RESOLVEEVENTS Get events from the best available source. - Priority: EventStoreObj > TagRegistry default > EventFcn > Events - (static / Event objects). When FilterTagKey is set AND an - EventStore is bound (explicit or registry-default), events are - pulled via EventStore.getEventsForTag(tagKey) using the dual-key - pattern from Phase 1010 + the registry-default fallback from - Phase 1017. - -### Static Methods - -#### `EventTimelineWidget.obj = fromStruct(s)` +TOSTRUCT Serialize the page to a struct with name and widgets fields. + s = pg.toStruct() returns s.name (char) and s.widgets (cell). --- @@ -1288,179 +985,228 @@ EMITCHILDWIDGET Emit .m constructor lines for a child widget. --- -## `DashboardLayout` --- Manages 24-column responsive grid positioning. +## `BarChartWidget` -> Inherits from: `handle` +> Inherits from: `DashboardWidget` -Converts widget grid positions [col, row, width, height] to normalized - canvas coordinates [x, y, w, h]. Handles overlap resolution, row - calculation, and scrollable canvas when content exceeds the viewport. +### Constructor + +```matlab +obj = BarChartWidget(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| DataFcn | `[]` | @() struct('categories',{},'values',[]) | +| Orientation | `'vertical'` | 'vertical' or 'horizontal' | +| Stacked | `false` | | + +### Methods + +#### `render(obj, parentPanel)` + +#### `refresh(obj)` + +#### `t = getType(~)` + +#### `lines = asciiRender(obj, width, height)` + +#### `s = toStruct(obj)` + +### Static Methods + +#### `BarChartWidget.obj = fromStruct(s)` + +--- + +## `ChipBarWidget` --- Horizontal row of mini status chips for system health summary. + +> Inherits from: `DashboardWidget` + +Displays N colored circle icons with labels in a compact horizontal strip. + Designed as a dense multi-sensor status overview at a glance. ### Constructor ```matlab -obj = DashboardLayout(varargin) +obj = ChipBarWidget(varargin) ``` +CHIPBARWIDGET Construct a ChipBarWidget with optional name-value pairs. + ### Properties | Property | Default | Description | |----------|---------|-------------| -| Columns | `24` | | -| TotalRows | `4` | | -| ContentArea | `[0 0 1 1]` | | -| Padding | `[0 0 0 0]` | | -| GapH | `0` | | -| GapV | `0` | | -| RowHeight | `0.22` | | -| ScrollbarWidth | `0.015` | | -| OnScrollCallback | `[]` | function handle: @(topRow, bottomRow) | -| DetachCallback | `[]` | function handle: @(widget) — set by DashboardEngine | -| CreateEventCallback | `[]` | function handle: @(widget) — set by DashboardEngine | -| VisibleRows | `[1 Inf]` | [topRow bottomRow] currently visible | -| EngineRef | `[]` | Phase 1032 PLOG-VIZ-05 — back-reference to DashboardEngine for chrome callbacks (addPlantLogToggle) | -| hFigure | `[]` | Figure handle for popup dismiss callbacks | -| hInfoPopup | `[]` | Handle to active info popup uipanel (at most one) | +| Chips | `{}` | Cell array of chip structs (label, statusFcn, sensor, iconColor) | ### Methods -#### `cr = canvasRatio(obj)` +#### `render(obj, parentPanel)` -CANVASRATIO Ratio of canvas height to viewport height. - Returns 1 when content fits, >1 when scrolling is needed. +RENDER Draw all chips in a single shared axes inside parentPanel. +Re-entrancy guard: parenting an axes to parentPanel and +toggling its Units below can synchronously fire the panel's +SizeChangedFcn -> relayout_ -> render. Without this lock the +nested call deletes the axes the outer render is populating +and the outer render then crashes on text(obj.hAx, ...). -#### `pos = computePosition(obj, gridPos)` +#### `refresh(obj)` -COMPUTEPOSITION Convert grid position to canvas-normalized coords. +REFRESH Update chip circle colors from statusFcn or sensor state. -#### `[stepW, stepH, cellW, cellH] = canvasStepSizes(obj)` +#### `t = getType(~)` -CANVASSTEPSIZES Grid step sizes in canvas-normalized coords. +GETTYPE Return widget type string. -#### `[dx_c, dy_c] = figureToCanvasDelta(obj, dx_fig, dy_fig)` +#### `s = toStruct(obj)` -FIGURETOCANVASDELTA Convert figure-normalized deltas to canvas deltas. +TOSTRUCT Serialize widget to struct for JSON export. -#### `maxRow = calculateMaxRow(obj, widgets)` +### Static Methods -#### `tf = overlaps(obj, posA, posB)` +#### `ChipBarWidget.obj = fromStruct(s)` -#### `newPos = resolveOverlap(obj, pos, existingPositions)` +FROMSTRUCT Reconstruct ChipBarWidget from a saved struct. -#### `ensureViewport(obj, hFigure, theme)` +--- -ENSUREVIEWPORT Create viewport/canvas/scrollbar only if they do not exist yet. - Idempotent: if the viewport handle is already valid, returns immediately - without deleting or recreating anything. On the first call the viewport, - canvas, and (if needed) scrollbar are created and TotalRows is reset to 0 - so that subsequent additive allocatePanels calls accumulate row counts. +## `CreateEventDialog` --- Modal dialog to create a manual annotation Event (260513-snt). -#### `resetViewport(obj)` +> Inherits from: `handle` -RESETVIEWPORT Destroy the current viewport so the next ensureViewport call rebuilds it. - Use when a full layout rebuild is required (e.g. single-page reflow). +d = CreateEventDialog(fastSenseWidget, dashboardEngine) -#### `allocatePanels(obj, hFigure, widgets, theme)` + Opens a modal figure pre-filled with the widget's current X view as + the event time range and the widget's bound Tag.Key as the tag + binding. On Save: appends an Event to engine.EventStore, registers + per-tag EventBinding entries, calls EventStore.save() and finally + engine.notifyEventsChanged() so EventTimelineWidget + + FastSenseWidget instances and the slider's event-marker overlay + refresh. -ALLOCATEPANELS Create placeholder panels for widgets (additive; no viewport destruction). - Calls ensureViewport (idempotent) to guarantee hViewport/hCanvas exist, then - accumulates TotalRows and appends widget panels to the shared canvas. - Multiple calls for different page-widget sets are safe: earlier panels survive. -Ensure viewport exists (idempotent — no-op if already live) + The dialog mirrors DashboardConfigDialog's pattern: classical + figure (NOT uifigure) with WindowStyle='modal', styled from the + engine's theme. All UI callbacks are wrapped in try/catch with + non-blocking errordlg so a bad input never tears down the dialog. -#### `realizeWidget(obj, widget)` + Properties (SetAccess = private): + Widget - bound FastSenseWidget + Engine - bound DashboardEngine + hFigure - modal figure handle -REALIZEWIDGET Render a single widget into its pre-allocated panel. - Creates the chrome (full-width WidgetButtonBar + WidgetContentPanel - sub-panel below the bar) BEFORE calling widget.render so the - widget's own graphics children (titles, axes, status text, group - headers) land in the visible content area, never under the bar. + Methods (public): + onSave - validate, persist, notify, close dialog on success + onCancel - close dialog without writing + delete - destructor, tears down figure -#### `createPanels(obj, hFigure, widgets, theme)` + Methods (Static, public): + persistEventStatic(engine, tStart, tEnd, label, sev, cat, notes, + keys, primaryName) - mock-friendly persistence + seam used by Task-3 tests; instance persistEvent_ delegates here. -CREATEPANELS Create and render all widget panels (legacy path). + Errors raised (all namespaced): + CreateEventDialog:invalidWidget - widget is not a FastSenseWidget + CreateEventDialog:invalidEngine - engine is not a DashboardEngine + CreateEventDialog:noStore - engine.EventStore is empty + CreateEventDialog:invalidTimeRange - EndTime < StartTime (or + not finite) + CreateEventDialog:emptyLabel - Label is empty after trim -#### `reflow(obj, hFigure, widgets, theme)` +### Constructor -Re-run layout after dynamic changes (e.g., group collapse/expand). -Tears down and recreates all panels, calling render() on each widget. +```matlab +obj = CreateEventDialog(widget, engine) +``` + +CREATEEVENTDIALOG Construct + show modal dialog. + +### Methods + +#### `onSave(obj, ~, ~)` + +ONSAVE Validate inputs, persist Event, refresh dashboard, close dialog. + Wraps the full pipeline in try/catch so any throw surfaces + via errordlg without tearing the dialog down — the user + can correct input and Save again. On success: deletes + the modal figure. + +#### `onCancel(obj, ~, ~)` -#### `onScroll(obj, val)` +ONCANCEL Close the dialog without writing. -ONSCROLL Adjust canvas position from scrollbar value. - val=1 shows top, val=0 shows bottom. +### Static Methods -#### `rows = computeVisibleRows(obj, scrollVal)` +#### `CreateEventDialog.persistEventStatic(engine, tStart, tEnd, label, sev, cat, notes, keys, primaryName)` -COMPUTEVISIBLEROWS Derive visible row range from scroll position. +PERSISTEVENTSTATIC Persist a manual annotation Event into engine.EventStore (260513-snt). + Public static seam called by the instance persistEvent_ + wrapper AND directly by Task-3 tests. Keeping the + write-side logic free of any figure handles makes it + trivially unit-testable. -#### `vis = isWidgetVisible(obj, gridPos, buffer)` +--- -ISWIDGETVISIBLE Check if widget rows overlap visible range + buffer. +## `DashboardConfigDialog` --- Config editor for a DashboardEngine. -#### `openInfoPopup(obj, widget, theme)` +> Inherits from: `handle` -OPENINFOPOPUP Open a modal figure window showing widget Description. +Opens a figure listing every public DashboardEngine property with + an editable control. Apply writes values back to the engine and + propagates visible changes (figure title, theme re-render, live + timer restart). Close dismisses without additional changes. -#### `closeInfoPopup(obj)` + Enum-like properties get a popup menu: + Theme — {'light', 'dark'} + ProgressMode — {'auto', 'on', 'off'} + Numeric properties get a numeric edit control. Everything else + gets a plain text edit. -CLOSEINFOPOPUP Close and delete the active info popup panel. + Usage (usually invoked by the toolbar Config button): + dlg = DashboardConfigDialog(engine); + % ...user edits fields, clicks Apply/Close... -#### `onFigureClickForDismiss(obj)` +### Constructor -ONFIGURECLICKFORDISMISS Dismiss popup if click was outside the popup panel. +```matlab +obj = DashboardConfigDialog(engine) +``` -#### `onKeyPressForDismiss(obj, eventData)` +### Methods -ONKEYPRESSFORDISMISS Dismiss popup when Escape is pressed. +#### `close(obj)` -#### `addPlantLogToggle(obj, widget, engine)` +CLOSE Destroy the dialog figure. -ADDPLANTLOGTOGGLE Add the per-widget plant-log overlay toggle (Phase 1032 PLOG-VIZ-05). - The toggle is always created (Decision B: always render, disable - when no store); clicking it calls - widget.setShowPlantLog(~widget.ShowPlantLog, engine). - The engine handle is captured by the callback closure. +#### `apply(obj)` -#### `onPlantLogTogglePressed_(obj, src, widget, engine)` +APPLY Write all control values back to the engine and propagate. -ONPLANTLOGTOGGLEPRESSED_ Toggle button callback — wraps setShowPlantLog with try/catch (Phase 1032 PLOG-VIZ-05). - Programmatic force-call paths (tests, automation) need a - software-level guard for Enable='off' because uicontrols only - honor Enable natively for user-driven mouse clicks. +--- -### Static Methods +## `DashboardProgress` --- Progress-bar helper for DashboardEngine render passes. -#### `DashboardLayout.reflowChrome_(hCell, barH, inset)` +> Inherits from: `handle` -REFLOWCHROME_ SizeChangedFcn handler — re-anchor the WidgetButtonBar - AND resize the WidgetContentPanel after the parent cell panel - resizes. Public so tests can drive a deterministic resize without - relying on SizeChangedFcn firing under -batch. - No-op when the cell has been deleted or chrome isn't there yet. +Emits a self-updating progress line to stdout as widgets are realized + during DashboardEngine.render() / rerenderWidgets(), and a final + summary line on completion. -#### `DashboardLayout.bg = chooseYLimitActiveBg_(theme)` + Silent outside interactive sessions so test / CI output stays clean. -CHOOSEYLIMITACTIVEBG_ Pick the highlight color for the active YLimit button. - Tries PressedBg / SelectedBg / AccentColor in order, falling - back to ToolbarBackground brightened by 0.15 per channel - (capped at 1) when none are present. No new theme fields are - introduced by 260513-sfp; future themes can opt into a - dedicated PressedBg token without touching layout code. +### Constructor -#### `DashboardLayout.syncYLimitButtonsState_(bar, mode)` +```matlab +obj = DashboardProgress(name, totalWidgets, totalPages, mode) +``` -SYNCYLIMITBUTTONSSTATE_ Visually highlight the YLimit button matching mode. - The active button's BackgroundColor becomes the value stashed on - bar.UserData.YLimitActiveBg by addYLimitButtons_; the other two - revert to the theme's ToolbarBackground. Tolerates missing - buttons (no-op if the bar's UserData was never primed). +### Methods -#### `DashboardLayout.reflowButtonBar_(hCell, barH, inset)` +#### `tick(obj, widget, pageIdx, pageName)` -REFLOWBUTTONBAR_ Deprecated alias — forwards to reflowChrome_. - Kept temporarily for any external callers that still reference - the m52-era name. +#### `finish(obj)` --- @@ -1592,369 +1338,402 @@ GETCONTENTAREA Compute the widget content area in normalized units. --- -## `BarChartWidget` +## `DetachedMirror` --- Standalone live-mirrored widget window for DashboardEngine. -> Inherits from: `DashboardWidget` +> Inherits from: `handle` -### Constructor +DetachedMirror wraps a cloned DashboardWidget in a standalone MATLAB + figure window. The clone is produced via toStruct/fromStruct with post- + clone live-reference restoration for FastSenseWidget and RawAxesWidget. -```matlab -obj = BarChartWidget(varargin) -``` + The mirror is NOT a DashboardWidget subclass — it wraps one. It belongs + to DashboardEngine.DetachedMirrors and is ticked by the engine's existing + LiveTimer via the engine's onLiveTick() loop. -### Properties + Usage (called internally by DashboardEngine.detachWidget()): + theme = DashboardTheme(obj.Theme); + cb = @() obj.removeDetached(mirror); + mirror = DetachedMirror(originalWidget, theme, cb); -| Property | Default | Description | -|----------|---------|-------------| -| DataFcn | `[]` | @() struct('categories',{},'values',[]) | -| Orientation | `'vertical'` | 'vertical' or 'horizontal' | -| Stacked | `false` | | + Properties (SetAccess = private): + hFigure — standalone MATLAB figure window handle + hPanel — full-figure uipanel that hosts the cloned widget + Widget — cloned DashboardWidget instance + RemoveCallback — @() called by onFigureClose() before delete(hFigure) -### Methods +### Constructor -#### `render(obj, parentPanel)` +```matlab +obj = DetachedMirror(originalWidget, themeStruct, removeCallback) +``` -#### `refresh(obj)` +DETACHEDMIRROR Create a detached live-mirror window for originalWidget. -#### `t = getType(~)` +### Methods -#### `lines = asciiRender(obj, width, height)` +#### `tick(obj)` -#### `s = toStruct(obj)` +TICK Refresh the cloned widget; no-op if figure is stale. -### Static Methods +#### `result = isStale(obj)` -#### `BarChartWidget.obj = fromStruct(s)` +ISSTALE Return true when the mirror's figure has been closed or destroyed. --- -## `ChipBarWidget` --- Horizontal row of mini status chips for system health summary. +## `DividerWidget` --- Horizontal divider line for visual section separation. > Inherits from: `DashboardWidget` -Displays N colored circle icons with labels in a compact horizontal strip. - Designed as a dense multi-sensor status overview at a glance. +DividerWidget renders a horizontal colored line using the theme's + WidgetBorderColor (or a custom Color override). It is a static widget + with no data binding. ### Constructor ```matlab -obj = ChipBarWidget(varargin) +obj = DividerWidget(varargin) ``` -CHIPBARWIDGET Construct a ChipBarWidget with optional name-value pairs. +DIVIDERWIDGET Construct a DividerWidget. + obj = DividerWidget() creates with defaults. + obj = DividerWidget('Thickness', 2, 'Color', [1 0 0]) sets props. ### Properties | Property | Default | Description | |----------|---------|-------------| -| Chips | `{}` | Cell array of chip structs (label, statusFcn, sensor, iconColor) | +| Thickness | `1` | Relative line thickness (1=thin, 2=medium, 3=thick) | +| Color | `[]` | RGB override; empty = use theme WidgetBorderColor | ### Methods #### `render(obj, parentPanel)` -RENDER Draw all chips in a single shared axes inside parentPanel. -Re-entrancy guard: parenting an axes to parentPanel and -toggling its Units below can synchronously fire the panel's -SizeChangedFcn -> relayout_ -> render. Without this lock the -nested call deletes the axes the outer render is populating -and the outer render then crashes on text(obj.hAx, ...). +RENDER Create the divider line inside parentPanel. + render(obj, parentPanel) creates a uipanel that acts as a + horizontal colored line centered vertically in the panel. -#### `refresh(obj)` +#### `refresh(~)` -REFRESH Update chip circle colors from statusFcn or sensor state. +REFRESH No-op for static widget. #### `t = getType(~)` GETTYPE Return widget type string. +#### `lines = asciiRender(obj, width, height)` + +ASCIIRENDER Return ASCII representation of the divider. + First line is a row of dashes; remaining lines are blank. + #### `s = toStruct(obj)` -TOSTRUCT Serialize widget to struct for JSON export. +TOSTRUCT Serialize to struct. + Omits 'thickness' at default (1) and 'color' when empty. ### Static Methods -#### `ChipBarWidget.obj = fromStruct(s)` +#### `DividerWidget.obj = fromStruct(s)` -FROMSTRUCT Reconstruct ChipBarWidget from a saved struct. +FROMSTRUCT Reconstruct DividerWidget from serialized struct. --- -## `CreateEventDialog` --- Modal dialog to create a manual annotation Event (260513-snt). - -> Inherits from: `handle` - -d = CreateEventDialog(fastSenseWidget, dashboardEngine) - - Opens a modal figure pre-filled with the widget's current X view as - the event time range and the widget's bound Tag.Key as the tag - binding. On Save: appends an Event to engine.EventStore, registers - per-tag EventBinding entries, calls EventStore.save() and finally - engine.notifyEventsChanged() so EventTimelineWidget + - FastSenseWidget instances and the slider's event-marker overlay - refresh. - - The dialog mirrors DashboardConfigDialog's pattern: classical - figure (NOT uifigure) with WindowStyle='modal', styled from the - engine's theme. All UI callbacks are wrapped in try/catch with - non-blocking errordlg so a bad input never tears down the dialog. +## `EventTimelineWidget` --- Displays events as colored bars on a timeline. - Properties (SetAccess = private): - Widget - bound FastSenseWidget - Engine - bound DashboardEngine - hFigure - modal figure handle +> Inherits from: `DashboardWidget` - Methods (public): - onSave - validate, persist, notify, close dialog on success - onCancel - close dialog without writing - delete - destructor, tears down figure +Preferred: bind to an EventStore from the event detection system: + w = EventTimelineWidget('Title', 'Events', 'EventStoreObj', store); - Methods (Static, public): - persistEventStatic(engine, tStart, tEnd, label, sev, cat, notes, - keys, primaryName) - mock-friendly persistence - seam used by Task-3 tests; instance persistEvent_ delegates here. + Legacy (still supported for backwards compatibility): + w = EventTimelineWidget('Title', 'Events', 'EventFcn', @() getEvents()); + w = EventTimelineWidget('Title', 'Events', 'Events', eventArray); - Errors raised (all namespaced): - CreateEventDialog:invalidWidget - widget is not a FastSenseWidget - CreateEventDialog:invalidEngine - engine is not a DashboardEngine - CreateEventDialog:noStore - engine.EventStore is empty - CreateEventDialog:invalidTimeRange - EndTime < StartTime (or - not finite) - CreateEventDialog:emptyLabel - Label is empty after trim + Events must be a struct array with fields: + startTime, endTime, label, color (optional) ### Constructor ```matlab -obj = CreateEventDialog(widget, engine) +obj = EventTimelineWidget(varargin) ``` -CREATEEVENTDIALOG Construct + show modal dialog. - -### Methods - -#### `onSave(obj, ~, ~)` - -ONSAVE Validate inputs, persist Event, refresh dashboard, close dialog. - Wraps the full pipeline in try/catch so any throw surfaces - via errordlg without tearing the dialog down — the user - can correct input and Save again. On success: deletes - the modal figure. - -#### `onCancel(obj, ~, ~)` +### Properties -ONCANCEL Close the dialog without writing. +| Property | Default | Description | +|----------|---------|-------------| +| EventStoreObj | `[]` | EventStore handle — primary data source | +| Events | `[]` | struct array of events (legacy) | +| EventFcn | `[]` | function_handle returning events (legacy) | +| FilterSensors | `{}` | Cell array of Sensor names to filter | +| FilterTagKey | `''` | Tag-key filter (MONITOR-05 carrier: SensorName OR ThresholdLabel match) | +| ColorSource | `'event'` | 'event' or 'theme' | -### Static Methods +### Methods -#### `CreateEventDialog.persistEventStatic(engine, tStart, tEnd, label, sev, cat, notes, keys, primaryName)` +#### `render(obj, parentPanel)` -PERSISTEVENTSTATIC Persist a manual annotation Event into engine.EventStore (260513-snt). - Public static seam called by the instance persistEvent_ - wrapper AND directly by Task-3 tests. Keeping the - write-side logic free of any figure handles makes it - trivially unit-testable. +#### `setTimeRange(obj, tStart, tEnd)` ---- +#### `[tMin, tMax] = getTimeRange(obj)` -## `DashboardConfigDialog` --- Config editor for a DashboardEngine. +#### `t = getEventTimes(obj)` -> Inherits from: `handle` +GETEVENTTIMES Event start times from resolveEvents (override). + Mirrors the same filtering pipeline the widget uses to draw + bars, so the time-slider overlay always matches what the + widget itself renders. -Opens a figure listing every public DashboardEngine property with - an editable control. Apply writes values back to the engine and - propagates visible changes (figure title, theme re-render, live - timer restart). Close dismisses without additional changes. +#### `m = getEventMarkers(obj)` - Enum-like properties get a popup menu: - Theme — {'light', 'dark'} - ProgressMode — {'auto', 'on', 'off'} - Numeric properties get a numeric edit control. Everything else - gets a plain text edit. +GETEVENTMARKERS Per-event time + severity + color for slider markers. + m = getEventMarkers(obj) returns a struct array with fields: + m(k).Time — numeric timestamp (startTime) + m(k).Severity — numeric severity (default 1 if absent) + m(k).Color — 1x3 RGB triplet from severityColor(theme, sev) - Usage (usually invoked by the toolbar Config button): - dlg = DashboardConfigDialog(engine); - % ...user edits fields, clicks Apply/Close... +#### `refresh(obj)` -### Constructor +#### `t = getType(~)` -```matlab -obj = DashboardConfigDialog(engine) -``` +#### `lines = asciiRender(obj, width, height)` -### Methods +#### `s = toStruct(obj)` -#### `close(obj)` +#### `evts = resolveEvents(obj)` -CLOSE Destroy the dialog figure. +RESOLVEEVENTS Get events from the best available source. + Priority: EventStoreObj > TagRegistry default > EventFcn > Events + (static / Event objects). When FilterTagKey is set AND an + EventStore is bound (explicit or registry-default), events are + pulled via EventStore.getEventsForTag(tagKey) using the dual-key + pattern from Phase 1010 + the registry-default fallback from + Phase 1017. -#### `apply(obj)` +### Static Methods -APPLY Write all control values back to the engine and propagate. +#### `EventTimelineWidget.obj = fromStruct(s)` --- -## `DashboardPage` --- Named page container within a multi-page dashboard. +## `FastSenseWidget` --- Dashboard widget wrapping a FastSense instance. -> Inherits from: `handle` +> Inherits from: `DashboardWidget` -Each DashboardPage holds a list of widgets to be rendered when the - page is active. DashboardEngine maintains a Pages cell array of - DashboardPage objects and routes addWidget() to the active page. +Supports data binding modes: + Tag: w = FastSenseWidget('Tag', tagObj) + DataStore: w = FastSenseWidget('DataStore', dsObj) + Inline: w = FastSenseWidget('XData', x, 'YData', y) + File: w = FastSenseWidget('File', 'path.mat', 'XVar', 'x', 'YVar', 'y') ### Constructor ```matlab -obj = DashboardPage(name) +obj = FastSenseWidget(varargin) ``` -DASHBOARDPAGE Construct a named page container. - pg = DashboardPage() creates page with Name = '' - pg = DashboardPage('Name') creates page with given Name - ### Properties | Property | Default | Description | |----------|---------|-------------| -| Name | `''` | | -| Widgets | `{}` | | +| DataStoreObj | `[]` | | +| XData | `[]` | | +| YData | `[]` | | +| File | `''` | | +| XVar | `''` | | +| YVar | `''` | | +| Thresholds | `'auto'` | | +| XLabel | `''` | X-axis label (auto-set from Sensor if empty) | +| YLabel | `''` | Y-axis label (auto-set from Sensor if empty) | +| YLimits | `[]` | Fixed Y-axis range [min max]; empty = auto-scale | +| ShowThresholdLabels | `false` | show inline name labels on threshold lines | +| ShowEventMarkers | `false` | Phase 1012 — toggle event round-marker overlay | +| EventStore | `[]` | Phase 1012 — EventStore handle forwarded to inner FastSense | +| ShowPlantLog | `false` | Phase 1032 PLOG-VIZ-03 — opt-in per-widget plant-log vertical-line overlay | +| LiveViewMode | `'preserve'` | | +| YLimitMode | `'auto-visible'` | | ### Methods -#### `w = addWidget(obj, w)` +#### `render(obj, parentPanel)` -ADDWIDGET Append widget w to the Widgets list. - pg.addWidget(w) appends w to obj.Widgets. +#### `refresh(obj)` -#### `s = toStruct(obj)` +Re-render Tag-bound widgets so updated data shows. +Uses incremental updateData() path when tag identity is unchanged +(PERF2-01); falls back to full teardown/rebuild on first render, +tag swap, or error. Zoom state (xlim) is preserved in both paths. -TOSTRUCT Serialize the page to a struct with name and widgets fields. - s = pg.toStruct() returns s.name (char) and s.widgets (cell). +#### `update(obj)` ---- +UPDATE Incrementally update Tag data without full axes rebuild. + Uses FastSenseObj.updateData() to replace data and re-downsample, + avoiding the expensive delete/recreate cycle of refresh(). + Falls back to refresh() if FastSenseObj is not in a renderable state. + (260513-ovt) Per-tick Y autoscale removed from this path so + Live mode never silently mutates the user's Y view. -## `DashboardProgress` --- Progress-bar helper for DashboardEngine render passes. +#### `setEventMarkersVisible(obj, tf)` -> Inherits from: `handle` +SETEVENTMARKERSVISIBLE Pass-through to FastSense event-marker toggle. + No-op when no FastSense instance exists yet (pre-render). + When rendered, delegates to FastSense.setShowEventMarkers + which re-draws the overlay in place without disturbing + zoom state or live refresh cadence. -Emits a self-updating progress line to stdout as widgets are realized - during DashboardEngine.render() / rerenderWidgets(), and a final - summary line on completion. +#### `setPlantLogMarkers(obj, times, entries)` - Silent outside interactive sessions so test / CI output stays clean. +SETPLANTLOGMARKERS Draw or clear per-widget plant-log vertical lines. + Phase 1032 PLOG-VIZ-04. Draws one xline per finite timestamp + on the widget's inner FastSense axes (Tag = 'WidgetPlantLogMarker', + 1 px solid line with theme.MarkerPlantLog color, default + [0 0 0]). Empty / no-arg input clears every existing marker + via tag-based delete. Non-finite timestamps are silently + dropped (mirrors TimeRangeSelector.setPlantLogMarkers shape). -### Constructor +#### `setPlantLogXLimListenerForEngine_(obj, lis)` -```matlab -obj = DashboardProgress(name, totalWidgets, totalPages, mode) -``` +#### `setShowPlantLog(obj, tf, engine)` -### Methods +SETSHOWPLANTLOG Toggle the per-widget plant-log overlay (Phase 1032 PLOG-VIZ-03). + tf — boolean; true enables overlay + attaches XLim listener, + false disables overlay + tears down listener + clears markers. + engine — DashboardEngine handle; required so refresh + listener + wiring can route through engine.refreshPlantLogOverlayForWidget_ + and engine.attachPlantLogXLimListener_. -#### `tick(obj, widget, pageIdx, pageName)` +#### `setYLimitMode(obj, mode)` -#### `finish(obj)` +SETYLIMITMODE Set the Y-axis rescale strategy and re-fit if rendered. + mode is one of: + 'auto-visible' - rescale to data inside the current X window + 'auto-all' - rescale to all data the bound Tag exposes + 'locked' - freeze YLim; no further rescale on tick/refresh ---- +#### `autoScaleY_(obj, y)` -## `DetachedMirror` --- Standalone live-mirrored widget window for DashboardEngine. +AUTOSCALEY_ Rescale the Y axis to cover current data + thresholds. + FastSense locks YLim to manual mode at first render, so new + samples outside the initial range would fall off the chart. + This helper recomputes the Y extent every tick (including any + threshold values so MonitorTag lines stay visible) and updates + the axes. Skipped when: + - the widget has a user-pinned YLimits NV-pair, or + - the user manually zoomed Y via mouse (UserZoomedY), or + - the dashboard's Follow toggle is engaged + (FastSenseObj.LiveViewMode == 'follow') — Follow is an + explicit user intent to track the data tail in X only and + keep the rest of the view (including Y) frozen. (260513-ovt) + - YLimitMode == 'locked' — the user explicitly froze Y limits + via the L button on the WidgetButtonBar (260513-sfp). -> Inherits from: `handle` +#### `onYLimChanged(obj)` -DetachedMirror wraps a cloned DashboardWidget in a standalone MATLAB - figure window. The clone is produced via toStruct/fromStruct with post- - clone live-reference restoration for FastSenseWidget and RawAxesWidget. +ONYLIMCHANGED Detach widget from automatic Y rescale after user zoom. + Fired by the YLim PostSet listener. When the YLim change came + from inside autoScaleY_ (IsSettingYLim==true) we ignore it; any + other source — mouse scroll, drag, zoom toolbar, programmatic + ylim() from user code — counts as a manual override and + latches UserZoomedY so live ticks stop fighting the user. - The mirror is NOT a DashboardWidget subclass — it wraps one. It belongs - to DashboardEngine.DetachedMirrors and is ticked by the engine's existing - LiveTimer via the engine's onLiveTick() loop. +#### `setTimeRange(obj, tStart, tEnd)` - Usage (called internally by DashboardEngine.detachWidget()): - theme = DashboardTheme(obj.Theme); - cb = @() obj.removeDetached(mirror); - mirror = DetachedMirror(originalWidget, theme, cb); +#### `onXLimChanged(obj)` - Properties (SetAccess = private): - hFigure — standalone MATLAB figure window handle - hPanel — full-figure uipanel that hosts the cloned widget - Widget — cloned DashboardWidget instance - RemoveCallback — @() called by onFigureClose() before delete(hFigure) +If xlim changed by user zoom/pan (not by setTimeRange), +detach this widget from global time. -### Constructor +#### `[tMin, tMax] = getTimeRange(obj)` -```matlab -obj = DetachedMirror(originalWidget, themeStruct, removeCallback) -``` +Return cached min/max in O(1). Cache is kept up to date by +updateTimeRangeCache() which is called from render/refresh/update. -DETACHEDMIRROR Create a detached live-mirror window for originalWidget. +#### `series = getPreviewSeries(obj, nBuckets)` -### Methods +GETPREVIEWSERIES Per-bucket min/max preview for the dashboard envelope. + series = getPreviewSeries(obj, nBuckets) returns a struct with + fields xCenters, yMin, yMax — each a 1xnBucketsEff row vector; + yMin and yMax are normalized into [0,1] across the widget's own + current y-range. Returns [] only when no data is bound or when + the sample count is genuinely too sparse (<4) to downsample. -#### `tick(obj)` +#### `t = getEventTimes(obj)` -TICK Refresh the cloned widget; no-op if figure is stale. +GETEVENTTIMES Event start times for the dashboard time-slider markers. + Looks up events in this priority order: + 1. obj.EventStore (widget-level — the modern attachment point) + 2. obj.FastSenseObj.EventStore (legacy: events on inner FastSense) + 3. obj.FastSenseObj.Events / .EventTimes (defensive: extra hooks) -#### `result = isStale(obj)` +#### `m = getEventMarkers(obj)` -ISSTALE Return true when the mirror's figure has been closed or destroyed. +GETEVENTMARKERS Per-event time + severity + color for slider markers. + m = getEventMarkers(obj) returns a struct array with fields: + m(k).Time — numeric timestamp (StartTime) + m(k).Severity — numeric severity in {1,2,3} (default 1 if absent) + m(k).Color — 1x3 RGB triplet from severityColor(theme, sev) + +#### `invalidatePreviewCache_(obj)` + +INVALIDATEPREVIEWCACHE_ Clear PreviewCache_ so getPreviewSeries recomputes. + Called from refresh() / update() / rebuildForTag_() whenever + the underlying data may have changed. Cheap (no graphics). + +#### `t = getType(~)` + +#### `lines = asciiRender(obj, width, height)` + +#### `s = toStruct(obj)` + +### Static Methods + +#### `FastSenseWidget.obj = fromStruct(s)` --- -## `DividerWidget` --- Horizontal divider line for visual section separation. +## `GaugeWidget` --- Gauge widget with arc, donut, bar, and thermometer styles. > Inherits from: `DashboardWidget` -DividerWidget renders a horizontal colored line using the theme's - WidgetBorderColor (or a custom Color override). It is a static widget - with no data binding. +w = GaugeWidget('Title', 'Pressure', 'ValueFcn', @() getPressure(), ... + 'Range', [0 100], 'Units', 'bar'); + w = GaugeWidget('Sensor', mySensor, 'Style', 'donut'); + w = GaugeWidget('Threshold', t, 'StaticValue', 50); ### Constructor ```matlab -obj = DividerWidget(varargin) +obj = GaugeWidget(varargin) ``` -DIVIDERWIDGET Construct a DividerWidget. - obj = DividerWidget() creates with defaults. - obj = DividerWidget('Thickness', 2, 'Color', [1 0 0]) sets props. - ### Properties | Property | Default | Description | |----------|---------|-------------| -| Thickness | `1` | Relative line thickness (1=thin, 2=medium, 3=thick) | -| Color | `[]` | RGB override; empty = use theme WidgetBorderColor | +| ValueFcn | `[]` | | +| Range | `[]` | Empty default for auto-derivation cascade | +| Units | `''` | | +| StaticValue | `[]` | | +| Style | `'arc'` | 'arc', 'donut', 'bar', 'thermometer' | +| Threshold | `[]` | Threshold object or registry key string (per D-01) | ### Methods #### `render(obj, parentPanel)` -RENDER Create the divider line inside parentPanel. - render(obj, parentPanel) creates a uipanel that acts as a - horizontal colored line centered vertically in the panel. - -#### `refresh(~)` - -REFRESH No-op for static widget. +#### `refresh(obj)` #### `t = getType(~)` -GETTYPE Return widget type string. - #### `lines = asciiRender(obj, width, height)` -ASCIIRENDER Return ASCII representation of the divider. - First line is a row of dashes; remaining lines are blank. - #### `s = toStruct(obj)` -TOSTRUCT Serialize to struct. - Omits 'thickness' at default (1) and 'color' when empty. - ### Static Methods -#### `DividerWidget.obj = fromStruct(s)` - -FROMSTRUCT Reconstruct DividerWidget from serialized struct. +#### `GaugeWidget.obj = fromStruct(s)` --- @@ -2253,6 +2032,94 @@ Fully override — does not use base Sensor property --- +## `NumberWidget` --- Dashboard widget showing a big number with label and trend. + +> Inherits from: `DashboardWidget` + +w = NumberWidget('Title', 'Temp', 'ValueFcn', @() readTemp(), 'Units', 'degC'); + + ValueFcn returns either: + - A scalar (displayed as-is) + - A struct with fields: value, unit, trend ('up'/'down'/'flat') + +### Constructor + +```matlab +obj = NumberWidget(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| ValueFcn | `[]` | function_handle returning scalar or struct | +| Units | `''` | unit label string | +| Format | `'%.1f'` | sprintf format for value | +| StaticValue | `[]` | fixed value (no callback needed) | + +### Methods + +#### `render(obj, parentPanel)` + +#### `refresh(obj)` + +#### `t = getType(~)` + +#### `lines = asciiRender(obj, width, height)` + +#### `s = toStruct(obj)` + +### Static Methods + +#### `NumberWidget.obj = fromStruct(s)` + +--- + +## `RawAxesWidget` --- User-supplied plot function on raw MATLAB axes. + +> Inherits from: `DashboardWidget` + +w = RawAxesWidget('Title', 'Histogram', ... + 'PlotFcn', @(ax) histogram(ax, randn(1,1000))); + + When bound to a Sensor, the PlotFcn receives (ax, sensor) or + (ax, sensor, timeRange) depending on its nargin. + +### Constructor + +```matlab +obj = RawAxesWidget(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| PlotFcn | `[]` | @(ax) or @(ax, sensor[, tRange]) or @(ax, tRange) | +| DataRangeFcn | `[]` | @() returning [tMin tMax] for global time range detection | + +### Methods + +#### `render(obj, parentPanel)` + +#### `refresh(obj)` + +#### `setTimeRange(obj, tStart, tEnd)` + +#### `[tMin, tMax] = getTimeRange(obj)` + +#### `t = getType(~)` + +#### `lines = asciiRender(obj, width, height)` + +#### `s = toStruct(obj)` + +### Static Methods + +#### `RawAxesWidget.obj = fromStruct(s)` + +--- + ## `ScatterWidget` > Inherits from: `DashboardWidget` @@ -2368,6 +2235,139 @@ FROMSTRUCT Deserialize a SparklineCardWidget from a struct. --- +## `StatusWidget` --- Colored dot indicator with sensor value. + +> Inherits from: `DashboardWidget` + +Sensor-first: + w = StatusWidget('Sensor', sensorObj); + + Threshold-bound (no Sensor required): + w = StatusWidget('Title', 'Temp', 'Threshold', t, 'Value', 85); + w = StatusWidget('Title', 'Temp', 'Threshold', 'temp_hi', 'ValueFcn', @getTemp); + + Legacy (still supported): + w = StatusWidget('Title', 'Pump 1', 'StatusFcn', @() 'ok'); + +### Constructor + +```matlab +obj = StatusWidget(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| StatusFcn | `[]` | function_handle returning 'ok'/'warning'/'alarm' (legacy) | +| StaticStatus | `''` | fixed status string (legacy) | +| Threshold | `[]` | Threshold object or registry key string (per D-01) | +| Value | `[]` | Scalar numeric value for threshold comparison (per D-03) | +| ValueFcn | `[]` | Function handle returning scalar value (per D-03, D-09) | + +### Methods + +#### `render(obj, parentPanel)` + +#### `refresh(obj)` + +#### `t = getType(~)` + +#### `lines = asciiRender(obj, width, height)` + +#### `s = toStruct(obj)` + +### Static Methods + +#### `StatusWidget.obj = fromStruct(s)` + +--- + +## `TableWidget` --- Tabular data display using uitable. + +> Inherits from: `DashboardWidget` + +w = TableWidget('Title', 'Sensor Data', 'DataFcn', @() getData()); + w = TableWidget('Title', 'Static', 'Data', {{'A',1;'B',2}}, ... + 'ColumnNames', {'Name','Value'}); + w = TableWidget('Sensor', sensorObj); % last N data rows + w = TableWidget('Sensor', sensorObj, 'Mode', 'events', 'EventStoreObj', store); + +### Constructor + +```matlab +obj = TableWidget(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| DataFcn | `[]` | | +| Data | `{}` | | +| ColumnNames | `{}` | | +| Mode | `'data'` | 'data' or 'events' | +| N | `10` | number of rows to display | +| EventStoreObj | `[]` | EventStore for event mode | + +### Methods + +#### `render(obj, parentPanel)` + +#### `refresh(obj)` + +#### `t = getType(~)` + +#### `lines = asciiRender(obj, width, height)` + +#### `s = toStruct(obj)` + +### Static Methods + +#### `TableWidget.obj = fromStruct(s)` + +--- + +## `TextWidget` --- Static text label or section header. + +> Inherits from: `DashboardWidget` + +w = TextWidget('Title', 'Section A', 'Content', 'Sensor overview'); + +### Constructor + +```matlab +obj = TextWidget(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| Content | `''` | body text | +| FontSize | `0` | 0 = use theme default | +| Alignment | `'left'` | 'left', 'center', 'right' | + +### Methods + +#### `render(obj, parentPanel)` + +#### `refresh(~)` + +Static widget — nothing to refresh + +#### `t = getType(~)` + +#### `lines = asciiRender(obj, width, height)` + +#### `s = toStruct(obj)` + +### Static Methods + +#### `TextWidget.obj = fromStruct(s)` + +--- + ## `TimeRangeSelector` --- Single-window time-range selector with data-preview envelope. > Inherits from: `handle` diff --git a/wiki/API-Reference:-Event-Detection.md b/wiki/API-Reference:-Event-Detection.md index 3992ba08..09f950ab 100644 --- a/wiki/API-Reference:-Event-Detection.md +++ b/wiki/API-Reference:-Event-Detection.md @@ -338,81 +338,6 @@ RUNCYCLE Execute one poll cycle synchronously (exposed for tests + timer callbac --- -## `NotificationService` --- Rule-based email notifications with event snapshots. - -> Inherits from: `handle` - -### Constructor - -```matlab -obj = NotificationService(varargin) -``` - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| Rules | `[]` | | -| DefaultRule | `[]` | | -| Enabled | `true` | | -| DryRun | `false` | | -| SnapshotDir | `''` | | -| SnapshotRetention | `7` | days | -| SmtpServer | `''` | | -| SmtpPort | `25` | | -| SmtpUser | `''` | | -| SmtpPassword | `''` | | -| FromAddress | `'fastsense@noreply.com'` | | -| NotificationCount | `0` | | - -### Methods - -#### `addRule(obj, rule)` - -#### `setDefaultRule(obj, rule)` - -#### `rule = findBestRule(obj, event)` - -#### `notify(obj, event, sensorData)` - -#### `cleanupSnapshots(obj)` - ---- - -## `NotificationRule` --- Configures notification for sensor/threshold events. - -> Inherits from: `handle` - -### Constructor - -```matlab -obj = NotificationRule(varargin) -``` - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| SensorKey | `''` | | -| ThresholdLabel | `''` | | -| Recipients | `{{}}` | | -| Subject | `'Event: {sensor} - {threshold}'` | | -| Message | `'{sensor} exceeded {threshold} ({direction}) at {startTime}. Peak: {peak}'` | | -| IncludeSnapshot | `true` | | -| ContextHours | `2` | | -| SnapshotPadding | `0.1` | | -| SnapshotSize | `[800, 400]` | | - -### Methods - -#### `score = matches(obj, event)` - -Returns match score: 3=sensor+threshold, 2=sensor, 1=default, 0=no match - -#### `txt = fillTemplate(~, template, event)` - ---- - ## `DataSource` --- Abstract interface for fetching new sensor data. > Inherits from: `handle` @@ -434,32 +359,6 @@ Subclasses must implement fetchNew() which returns a struct: --- -## `MatFileDataSource` --- Reads sensor data from a continuously-updated .mat file. - -> Inherits from: `DataSource` - -### Constructor - -```matlab -obj = MatFileDataSource(filePath, varargin) -``` - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| FilePath | `''` | | -| XVar | `'X'` | | -| YVar | `'Y'` | | -| StateXVar | `''` | | -| StateYVar | `''` | | - -### Methods - -#### `result = fetchNew(obj)` - ---- - ## `DataSourceMap` --- Maps sensor keys to DataSource instances. > Inherits from: `handle` @@ -518,6 +417,32 @@ CLEAR Reset all bindings in both forward and reverse indexes. --- +## `MatFileDataSource` --- Reads sensor data from a continuously-updated .mat file. + +> Inherits from: `DataSource` + +### Constructor + +```matlab +obj = MatFileDataSource(filePath, varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| FilePath | `''` | | +| XVar | `'X'` | | +| YVar | `'Y'` | | +| StateXVar | `''` | | +| StateYVar | `''` | | + +### Methods + +#### `result = fetchNew(obj)` + +--- + ## `MockDataSource` --- Generates realistic industrial sensor signals for testing. > Inherits from: `DataSource` @@ -549,3 +474,78 @@ obj = MockDataSource(varargin) #### `result = fetchNew(obj)` +--- + +## `NotificationRule` --- Configures notification for sensor/threshold events. + +> Inherits from: `handle` + +### Constructor + +```matlab +obj = NotificationRule(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| SensorKey | `''` | | +| ThresholdLabel | `''` | | +| Recipients | `{{}}` | | +| Subject | `'Event: {sensor} - {threshold}'` | | +| Message | `'{sensor} exceeded {threshold} ({direction}) at {startTime}. Peak: {peak}'` | | +| IncludeSnapshot | `true` | | +| ContextHours | `2` | | +| SnapshotPadding | `0.1` | | +| SnapshotSize | `[800, 400]` | | + +### Methods + +#### `score = matches(obj, event)` + +Returns match score: 3=sensor+threshold, 2=sensor, 1=default, 0=no match + +#### `txt = fillTemplate(~, template, event)` + +--- + +## `NotificationService` --- Rule-based email notifications with event snapshots. + +> Inherits from: `handle` + +### Constructor + +```matlab +obj = NotificationService(varargin) +``` + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| Rules | `[]` | | +| DefaultRule | `[]` | | +| Enabled | `true` | | +| DryRun | `false` | | +| SnapshotDir | `''` | | +| SnapshotRetention | `7` | days | +| SmtpServer | `''` | | +| SmtpPort | `25` | | +| SmtpUser | `''` | | +| SmtpPassword | `''` | | +| FromAddress | `'fastsense@noreply.com'` | | +| NotificationCount | `0` | | + +### Methods + +#### `addRule(obj, rule)` + +#### `setDefaultRule(obj, rule)` + +#### `rule = findBestRule(obj, event)` + +#### `notify(obj, event, sensorData)` + +#### `cleanupSnapshots(obj)` + diff --git a/wiki/API-Reference:-FastPlot.md b/wiki/API-Reference:-FastPlot.md index e2429df5..0acc1b65 100644 --- a/wiki/API-Reference:-FastPlot.md +++ b/wiki/API-Reference:-FastPlot.md @@ -45,6 +45,18 @@ FASTSENSE Construct a FastSense instance. | XScale | `'linear'` | 'linear' or 'log' — X axis scale | | YScale | `'linear'` | 'linear' or 'log' — Y axis scale | | ViolationsVisible | `true` | global toggle for violation markers | +| ShowThresholdLabels | `false` | show inline name labels on threshold lines | +| ShowEventMarkers | `true` | toggle event round-marker overlay (EVENT-07) | +| EventStore | `[]` | EventStore handle for event overlay queries | +| HoverCrosshair | `true` | enable hover crosshair + multi-line datatip (set false to disable; see HoverCrosshair.m) | +| IsEventPicking_ | `false` | event-pick mode active flag (260513-v69) | +| EventPickT1_ | `[]` | first-click x coordinate | +| EventPickEngine_ | `[]` | DashboardEngine handle (needed for persist + store) | +| PrevAxesBDFcn_ | `[]` | saved hAxes.ButtonDownFcn during pick mode | +| PrevFigKPFcn_ | `[]` | saved figure WindowKeyPressFcn during pick mode | +| EventPickPatch_ | `[]` | patch handle (Tag='EventPickRegion') for shaded region during pick (260513-voo) | +| PrevFigWBMFcn_ | `[]` | saved figure WindowButtonMotionFcn during pick mode (260513-voo) | +| EventPickModalListener_ | `[]` | event.listener on hEventDetails_ ObjectBeingDestroyed (260513-voo) | | MinPointsForDownsample | `5000` | below this, plot raw data | | DownsampleFactor | `2` | points per pixel (min + max) | | PyramidReduction | `100` | reduction factor per pyramid level | @@ -84,14 +96,6 @@ ADDLINE Add a data line to the plot. fp.ADDLINE(x, y, 'DownsampleMethod', 'lttb') uses the Largest-Triangle-Three-Buckets algorithm instead of MinMax. -#### `addSensor(obj, sensor, varargin)` - -ADDSENSOR Add a resolved Sensor's data and thresholds to the plot. - fp.ADDSENSOR(s) adds the sensor's X/Y data as a line and - all resolved thresholds with violation markers enabled. - fp.ADDSENSOR(s, 'ShowThresholds', false) adds only the data - line, suppressing threshold overlay. - #### `addThreshold(obj, varargin)` ADDTHRESHOLD Add a threshold line (scalar or time-varying). @@ -118,6 +122,15 @@ ADDMARKER Add custom event markers at specific positions. fp.ADDMARKER(x, y, 'Marker', 'v', 'MarkerSize', 8, 'Color', [1 0 0]) plots red downward-pointing triangles of size 8. +#### `setShowEventMarkers(obj, tf)` + +SETSHOWEVENTMARKERS Toggle event-marker overlay post-render. + fp.SETSHOWEVENTMARKERS(true|false) flips ShowEventMarkers and + either deletes existing markers (tf=false) or re-runs + renderEventLayer_ (tf=true) so markers appear/disappear in + place without a full re-render. No-op if not yet rendered; + the next render() honours the new flag automatically. + #### `addShaded(obj, x, y1, y2, varargin)` ADDSHADED Add a shaded region between two curves. @@ -134,6 +147,23 @@ ADDFILL Add an area fill from a line to a baseline. fp.ADDFILL(x, y, 'Baseline', -1, 'FaceColor', [0 0.5 1]) fills between y and y=-1 with a custom color. +#### `addTag(obj, tag, varargin)` + +ADDTAG Polymorphic dispatch — route a Tag to the correct render path. + fp.ADDTAG(sensorTag) — routes to addLine via tag.getXY + fp.ADDTAG(stateTag) — routes to a staircase line (numeric Y) + fp.ADDTAG(monitorTag) — routes to addLine via tag.getXY (0/1 binary series) + fp.ADDTAG(compositeTag) — routes to addLine via tag.getXY (aggregated 0/1 or 0..1 series) + fp.ADDTAG(derivedTag) — routes to addLine via tag.getXY (continuous derived series) + +#### `addStateTagAsStaircase_(obj, tag, varargin)` + +ADDSTATETAGASSTAIRCASE_ Render a numeric StateTag as a stepped line. + Private helper (name ends in _) invoked by addTag for the + 'state' kind. Expands (X, Y) pairs into an interleaved + 2N-1 staircase and delegates to addLine. Cellstr Y is not + supported in Phase 1005 (deferred). + #### `render(obj, progressBar)` RENDER Create the plot with all configured lines and annotations. @@ -175,6 +205,17 @@ STARTLIVE Start live mode — poll a .mat file for changes. fp.STARTLIVE(filepath, updateFcn, 'Interval', 2) fp.STARTLIVE(filepath, updateFcn, 'ViewMode', 'follow') +#### `setXLimQuiet(obj, tStart, tEnd)` + +SETXLIMQUIET Set XLim without triggering XLimMode listener overhead. + fp.SETXLIMQUIET(tStart, tEnd) is a performance-optimised + alternative to calling xlim(ax, [tStart tEnd]) from external + callers (e.g. FastSenseWidget.setTimeRange). The plain + xlim() call fires the XLimMode PostSet listener every time, + which routes to onXLimModeChanged -> scheduleDeferredXLimCheck + and creates a new 10 ms one-shot timer per call. That timer + overhead adds ~4 ms per FastSenseWidget per live tick. + #### `stopLive(obj)` STOPLIVE Stop live mode polling. @@ -196,6 +237,17 @@ SETVIEWMODE Change the live view mode at runtime. fp.SETVIEWMODE(mode) sets the LiveViewMode property, which controls how the X-axis adjusts when new data arrives. +#### `snapToTail(obj)` + +SNAPTOTAIL Slide XLim window so its right edge sits just past the data tail. + fp.SNAPTOTAIL() does a one-shot "jump to now" — finds the + maximum X across all lines, then sets XLim to + [xMax - currentWindowWidth + pad, xMax + pad] where pad = + 2% of the current window width. The small right-edge + padding leaves visual breathing room between the latest + data point and the chart's right border so the line tail + doesn't get clipped against the axes frame. + #### `runLive(obj)` RUNLIVE Blocking poll loop for live mode (Octave compatibility). @@ -237,6 +289,19 @@ OPENLOUPE Open a standalone enlarged copy of this tile. by [+30, -30] pixels from the source figure, and receives its own FastSenseToolbar. +#### `exportData(obj, filepath, format)` + +EXPORTDATA Export raw line and threshold data as CSV or MAT. + EXPORTDATA(obj, filepath, format) writes all raw line and + threshold data from the plot to the file at filepath. + +#### `refreshEventLayer(obj)` + +REFRESHEVENTLAYER Public thin wrapper — rebuild the event marker layer. + Calls the private renderEventLayer_ so external consumers + (e.g. FastSenseWidget.refresh()) can trigger a marker rebuild + without exposing the implementation method directly. + #### `n = lineNumPoints(obj, i)` LINENUMPOINTS Return total point count for line i. @@ -245,8 +310,160 @@ LINENUMPOINTS Return total point count for line i. LINEXRANGE Return X endpoints for line i. +#### `onEventMarkerClick_(obj, src, ~)` + +ONEVENTMARKERCLICK_ ButtonDownFcn dispatcher for event markers. + Hidden public so TestFastSenseEventClick can call it for direct + dispatch of the click -> details-popup path. Branches on figure + SelectionType: 'normal' -> openEventDetails_; 'alt' (right-click) + -> builds/shows uicontextmenu with quick-nav actions. + +#### `openEventDetails_(obj, ev)` + +OPENEVENTDETAILS_ Open a separate floating figure with event fields. + Phase 1012 refit: standalone figure (OS-native drag/close), light + theme with standard font, read-only field list on top and an + editable Notes box at the bottom. Saving the notes mutates + ev.Notes (handle persists across the MATLAB session) and calls + EventStore.save() when a FilePath is configured (disk persistence). + +#### `fitDetailsTableColumns_(~, hTable)` + +FITDETAILSTABLECOLUMNS_ Split the uitable width ~1:2 between + Field and Value columns based on the parent FIGURE's + current pixel width. Deriving from the figure rather than + reading the table's own Position avoids a race where the + table layout hasn't settled when SizeChangedFcn fires. + +#### `saveEventNotes_(obj, ev, hNotesControl)` + +SAVEEVENTNOTES_ Commit the Notes textarea to ev.Notes and persist. + Mutates the Event handle (in-session persistence) and calls + obj.EventStore.save() when available so notes survive MATLAB + restarts. Updates the status label to confirm. + +#### `closeEventDetails_(obj)` + +CLOSEEVENTDETAILS_ Dismiss the popup figure. + +#### `onKeyPressForDetailsDismiss_(obj, eventData)` + +ONKEYPRESSFORDETAILSDISMISS_ Close popup on ESC key. + +#### `startEventPick_(obj, engine)` + +STARTEVENTPICK_ Enter two-click event-pick mode (260513-v69). + Toggle-cancels if already active. Saves axes ButtonDownFcn + and figure WindowKeyPressFcn, installs our handlers, draws + hint. WindowButtonMotionFcn is never touched so + HoverCrosshair stays fully functional. + +#### `cancelEventPick_(obj)` + +CANCELEVENTPICK_ Exit pick mode + cleanup. Idempotent. Delegates to onEventDetailsClosed_ (260513-voo). + Preserves the v69 Test 7 contract: silent no-op when not + picking AND no patch alive (axes children count unchanged). + +#### `onPickClick_(obj, ~, ~)` + +ONPICKCLICK_ Axes ButtonDownFcn during pick mode. Right-click cancels. + +#### `onPickKey_(obj, src, evt)` + +ONPICKKEY_ Figure WindowKeyPressFcn during pick. ESC cancels; chain otherwise. + +#### `completeEventPick_(obj, tStart, tEnd)` + +COMPLETEEVENTPICK_ Sort, persist, hand off to openEventDetails_, cleanup. + +#### `drawPickHint_(obj, str)` + +DRAWPICKHINT_ Draw the EventPickHint text annotation in obj.hAxes. + +#### `updatePickHint_(obj, str)` + +UPDATEPICKHINT_ Mutate an existing EventPickHint's String, fallback redraws. + +#### `drawPickLine_(obj, x)` + +DRAWPICKLINE_ Draw a single orange vertical EventPickLine at x. + +#### `onPickMotion_(obj, src, evt)` + +ONPICKMOTION_ Chained figure WindowButtonMotionFcn during pick mode (260513-voo). + FIRST forward to the saved handler so HoverCrosshair keeps + working. THEN, while in the post-click-1 pre-click-2 sub- + state, update the shaded patch XData to track the cursor. + Wrapped in try/catch so our chained handler never breaks + HoverCrosshair downstream. + +#### `onPickMotion_FromX_(obj, cx)` + +ONPICKMOTION_FROMX_ Update patch geometry to span [EventPickT1_, cx] x current YLim (260513-voo). + Pulled out of onPickMotion_ so tests can drive geometry + updates deterministically without having to mutate + CurrentPoint (which is read-only on MATLAB). + +#### `createPickPatch_(obj, x)` + +CREATEPICKPATCH_ Create the EventPickRegion patch at zero width (260513-voo). + FaceColor is read from the just-drawn EventPickLine (SSOT) + with fallback to the canonical [1.0 0.55 0.0] orange. The + patch is pushed to the back of axes Children so the lines + and plotted signal stay in front. HitTest='off' + + PickableParts='none' so click 2 reaches the axes + underneath this patch. + +#### `finalizePickPatch_(obj, tStart, tEnd)` + +FINALIZEPICKPATCH_ Snap the patch to sorted [tStart, tEnd] x current YLim (260513-voo). + +#### `c = pickLineColor_(obj)` + +PICKLINECOLOR_ Resolve patch FaceColor from a live EventPickLine, fallback orange. + +#### `restorePickCallbacks_(obj)` + +RESTOREPICKCALLBACKS_ Restore axes BDF + figure KP + figure WBM to pre-pick values (260513-voo). + +#### `onEventDetailsClosed_(obj)` + +ONEVENTDETAILSCLOSED_ Unified pick-mode cleanup (260513-voo). + Idempotent. Called from three paths: + - addlistener on hEventDetails_ ObjectBeingDestroyed (modal close) + - cancelEventPick_ (toggle / ESC / right-click) + - completeEventPick_ catch fallback when modal couldn't open + First-line guard returns silently when nothing is in flight. + +#### `tbl = buildEventFieldsTable_(~, ev)` + +BUILDEVENTFIELDSTABLE_ Nx2 cell array for the uitable in the + details popup. Columns are {Field, Value}. Empty statistics + rows are skipped. Section separators use a blank-label row + with a bullet '·' value to maintain visual grouping without + relying on cell-level styling (not portable across MATLAB + versions). + +#### `txt = formatEventFields_(~, ev)` + +FORMATEVENTFIELDS_ Produce a grouped, readable listing of event fields. + Sections: TIMING / STATISTICS / CLASSIFICATION / TAGS / THRESHOLD. + Empty-valued statistics rows are hidden (they carry no + information and clutter the popup). IsOpen=true displays + "Open" for EndTime and Duration so the test contract in + TestFastSenseEventClick.testFormatEventFieldsShowsOpenForOpenEvent + still holds. + +#### `s = formatSection(header, rows, labelWidth)` + ### Static Methods +#### `FastSense.fp = plot(x, y, varargin)` + +PLOT One-liner convenience for quick plotting. + FastSense.plot(x, y) + FastSense.plot(x, y, 'DisplayName', 'Signal', 'Theme', 'dark') + #### `FastSense.resetDefaults()` RESETDEFAULTS Force reload of FastSenseDefaults on next use. @@ -265,6 +482,149 @@ DISTFIG Distribute figure windows across the screen. --- +## `FastSenseDataStore` --- SQLite-backed data storage for large time series. + +> Inherits from: `handle` + +Stores X/Y data in a temporary SQLite database via mksqlite using + chunked typed BLOBs for fast bulk insert and range-based retrieval. + This avoids loading full datasets into MATLAB memory, preventing + out-of-memory errors on Windows and memory-constrained systems. + + Data is split into chunks of ~100K points. Each chunk is stored as + a pair of typed BLOBs (X and Y arrays) with the chunk's X range + indexed for fast overlap queries. On zoom/pan, only the chunks + overlapping the visible range are loaded, then trimmed to the exact + view window. + + Additional data columns (cell, char, string, categorical, logical, + or any numeric type) can be attached via addColumn / getColumn. + + Requires mksqlite. If not available, falls back to binary file + storage (extra columns require mksqlite). + +### Constructor + +```matlab +obj = FastSenseDataStore(x, y) +``` + +FASTSENSEDATASTORE Create a disk-backed store from X/Y arrays. + +### Methods + +#### `[xOut, yOut] = getRange(obj, xMin, xMax)` + +GETRANGE Read data within an X range (with one-point padding). + +#### `[xOut, yOut] = readSlice(obj, startIdx, endIdx)` + +READSLICE Read a contiguous slice of data by row index. + +#### `addColumn(obj, name, data)` + +ADDCOLUMN Store an extra data column alongside X/Y. + Categorical arrays auto-convert to codes+categories struct. + String arrays auto-convert to cell of char. + +#### `data = getColumnRange(obj, name, xMin, xMax)` + +GETCOLUMNRANGE Read a column's data within an X range. + Converts the X range to a point-offset range using chunk + metadata (no x_data BLOB fetch), then delegates to slice. + +#### `data = getColumnSlice(obj, name, startIdx, endIdx)` + +GETCOLUMNSLICE Read a column slice by point index range. + +#### `names = listColumns(obj)` + +LISTCOLUMNS Return names of all stored extra columns. + +#### `idx = findIndex(obj, xVal, side)` + +FINDINDEX Binary search for a global point index by X value. + idx = ds.findIndex(xVal, 'left') returns the first index + where X(idx) >= xVal. idx = ds.findIndex(xVal, 'right') + returns the last index where X(idx) <= xVal. + +#### `[violX, violY] = findViolations(obj, startIdx, endIdx, threshold, isUpper)` + +FINDVIOLATIONS Find violation points using chunk-level Y filtering. + [vx, vy] = ds.findViolations(lo, hi, thresh, true) finds all + points in [lo, hi] where Y > thresh (upper violation). + [vx, vy] = ds.findViolations(lo, hi, thresh, false) finds + points where Y < thresh (lower violation). + +#### `enableWAL(obj)` + +ENABLEWAL Switch database to WAL journal mode for concurrent reads. + +#### `disableWAL(obj)` + +DISABLEWAL Revert database to DELETE journal mode. + +#### `storeResolved(obj, resolvedTh, resolvedViol)` + +STORERESOLVED Cache pre-computed resolve() results in SQLite. + ds.storeResolved(resolvedTh, resolvedViol) stores the + threshold and violation struct arrays produced by + Sensor.resolve() into the database for instant retrieval. + +#### `[resolvedTh, resolvedViol] = loadResolved(obj)` + +LOADRESOLVED Load pre-computed resolve() results from SQLite. + Returns empty arrays if no cached results exist. + +#### `clearResolved(obj)` + +CLEARRESOLVED Invalidate pre-computed resolve() cache. + +#### `storeMonitor(obj, key, X, Y, parentKey, parentNumPts, parentXMin, parentXMax)` + +STOREMONITOR Cache a MonitorTag's derived (X, Y) plus staleness quad. + ds.storeMonitor(key, X, Y, parentKey, parentNumPts, parentXMin, parentXMax) + upserts a monitors row. The quad (parent_key, num_points, + parent_xmin, parent_xmax) is stamped at write time and is + compared at load time by MonitorTag.cacheIsStale_. + +#### `[X, Y, meta] = loadMonitor(obj, key)` + +LOADMONITOR Retrieve cached MonitorTag (X, Y) + staleness metadata. + [X, Y, meta] = ds.loadMonitor(key) returns X=[] on miss. + Callers must verify freshness via the returned meta struct + (fields: parent_key, num_points, parent_xmin, parent_xmax, + computed_at). + +#### `clearMonitor(obj, key)` + +CLEARMONITOR Delete a cached MonitorTag row by key. + +#### `cleanup(obj)` + +CLEANUP Close the database and delete temp files. + +#### `ensureOpenForTest(obj)` + +ENSUREOPENFORTEST Test-only hook to force-reopen the DB handle. + Exposes the private ensureOpen() lifecycle helper so WAL-mode + tests can query journal_mode via mksqlite(DbId, ...) without + hitting MethodRestricted. Hidden (rather than narrower + Access = {?matlab.unittest.TestCase}) so Octave parsing + survives — Octave has no matlab.unittest. + +### Static Methods + +#### `FastSenseDataStore.c = toCategorical(s)` + +TOCATEGORICAL Convert a codes+categories struct back to categorical. + +#### `FastSenseDataStore.c = fromCategorical(data)` + +FROMCATEGORICAL Convert a MATLAB categorical to codes+categories struct. + +--- + ## `FastSenseGrid` --- Tiled layout manager for FastSense dashboards. > Inherits from: `handle` @@ -273,6 +633,9 @@ Creates a grid of FastSense tiles in a single figure window with configurable spacing, per-tile theme overrides, and tile spanning. Supports live mode that synchronizes file polling across all tiles. + For widget-based dashboards with gauges, numbers, status indicators, + and edit mode, see DashboardEngine. + fig = FastSenseGrid(rows, cols) fig = FastSenseGrid(rows, cols, 'Theme', 'dark') fig = FastSenseGrid(rows, cols, 'ParentFigure', hFig) @@ -347,24 +710,24 @@ SETTILETHEME Set per-tile theme overrides. struct for tile n. When the tile is created or re-themed, these overrides are merged on top of the figure-level theme. -#### `tileTitle(obj, n, str)` +#### `setTileTitle(obj, n, str)` -TILETITLE Set title for tile n. - fig.tileTitle(n, str) sets the axes title on tile n using +SETTILETITLE Set title for tile n. + fig.setTileTitle(n, str) sets the axes title on tile n using the figure theme's TitleFontSize and ForegroundColor. Can be called before or after render(). -#### `tileXLabel(obj, n, str)` +#### `setTileXLabel(obj, n, str)` -TILEXLABEL Set xlabel for tile n. - fig.tileXLabel(n, str) sets the X-axis label on tile n +SETTILEXLABEL Set xlabel for tile n. + fig.setTileXLabel(n, str) sets the X-axis label on tile n using the figure theme's ForegroundColor. Can be called before or after render(). -#### `tileYLabel(obj, n, str)` +#### `setTileYLabel(obj, n, str)` -TILEYLABEL Set ylabel for tile n. - fig.tileYLabel(n, str) sets the Y-axis label on tile n +SETTILEYLABEL Set ylabel for tile n. + fig.setTileYLabel(n, str) sets the Y-axis label on tile n using the figure theme's ForegroundColor. Can be called before or after render(). @@ -435,6 +798,105 @@ COMPUTETILEPOSITION Calculate normalized [x y w h] for tile n. --- +## `SensorDetailPlot` --- Two-panel sensor overview+detail plot with interactive navigator. + +> Inherits from: `handle` + +sdp = SensorDetailPlot(tag) + sdp = SensorDetailPlot(tag, Name, Value, ...) + + Name-Value Options: + 'Theme' - FastSense theme (default: 'default') + 'NavigatorHeight' - Fraction 0-1 for navigator (default: 0.20) + 'ShowThresholds' - Show thresholds in main plot (default: true) + 'ShowThresholdBands' - Show threshold bands in navigator (default: true) + 'Events' - EventStore or Event array (default: []) + 'ShowEventLabels' - Reserved, no effect (default: false) + 'Parent' - uipanel handle for embedding (default: []) + 'Title' - Plot title (default: tag.Name) + 'XType' - 'numeric' or 'datenum' (default: 'numeric') + +### Constructor + +```matlab +obj = SensorDetailPlot(tag, varargin) +``` + +Accept Tag (v2.0) only. +Tag class is the abstract base — uses isa(x, 'Tag'), NOT +isa-on-subclass-name (Pitfall 1). + +### Methods + +#### `render(obj)` + +#### `setZoomRange(obj, xMin, xMax)` + +#### `[xMin, xMax] = getZoomRange(obj)` + +--- + +## `ConsoleProgressBar` --- Single-line console progress bar with indentation. + +> Inherits from: `handle` + +A lightweight progress indicator that renders an ASCII/Unicode bar + on a single console line, overwriting itself on each update via + backspace characters. Supports optional leading indentation so + multiple bars can be stacked hierarchically. + + The typical lifecycle is: construct -> start -> update (loop) -> + freeze or finish. Calling freeze() prints a newline to make the + current state permanent, allowing a subsequent bar to render on a + fresh line below. Calling finish() sets progress to 100 % and + freezes automatically. + + On GNU Octave the bar uses ASCII characters (# and -). On MATLAB + it uses Unicode block characters for a smoother appearance. + +### Constructor + +```matlab +obj = ConsoleProgressBar(indent) +``` + +CONSOLEPROGRESSBAR Construct a progress bar instance. + pb = ConsoleProgressBar() creates a bar with no indentation. + +### Methods + +#### `start(obj)` + +START Initialize and render the progress bar for the first time. + pb.start() resets the frozen/started state and prints the + initial (empty) bar. Must be called before update() will + have any visible effect. + +#### `update(obj, current, total, label)` + +UPDATE Set progress counters and redraw the bar. + pb.update(current, total) updates the progress fraction + to current/total and redraws the bar in-place. + +#### `freeze(obj)` + +FREEZE Make the current bar state permanent by printing a newline. + pb.freeze() redraws the bar one final time, appends a + newline character, and sets IsFrozen to true. Subsequent + calls to update() are silently ignored. Use this when you + want the bar to remain visible while a new bar starts on + the next line. + +#### `finish(obj)` + +FINISH Set progress to 100 %, freeze, and mark the bar done. + pb.finish() fills the bar to completion, prints a newline + (if not already frozen), and sets IsStarted to false. This + is a convenience shortcut equivalent to calling + pb.update(total, total) followed by pb.freeze(). + +--- + ## `FastSenseDock` --- Tabbed container for multiple FastSenseGrid dashboards. > Inherits from: `handle` @@ -559,8 +1021,10 @@ Adds a uitoolbar with data cursor, crosshair, grid/legend toggles, Legend — toggle legend visibility Autoscale Y — fit Y-axis to visible data range Export PNG — save figure as PNG with file dialog + Export Data — save raw data as CSV or MAT with file dialog Refresh — manual one-shot data reload Live Mode — toggle automatic file polling + Follow — auto-pan X-axis to show the data tail Metadata — show/hide metadata in data cursor tooltips Violations — toggle violation marker visibility @@ -594,6 +1058,12 @@ EXPORTPNG Save figure as PNG image at 150 DPI. tb.exportPNG() — opens file dialog tb.exportPNG(filepath) — saves directly to path +#### `exportData(obj, filepath)` + +EXPORTDATA Export raw plot data as CSV or MAT file. + tb.exportData() — opens file dialog + tb.exportData(filepath) — saves directly (format from extension) + #### `setCrosshair(obj, on)` SETCROSSHAIR Enable or disable crosshair tracking mode. @@ -614,6 +1084,14 @@ REFRESH Trigger a manual data refresh. TOGGLELIVE Toggle live mode on/off. +#### `setFollow(obj, on)` + +SETFOLLOW Enable or disable Follow auto-pan-to-tail. + tb.setFollow(true) — set LiveViewMode='follow' and immediately + snap XLim to [x(end)-w, x(end)] if the + current XLim does not already include x(end). + tb.setFollow(false) — set LiveViewMode='preserve' (XLim unchanged). + #### `setMetadata(obj, on)` SETMETADATA Enable or disable metadata display in tooltips. @@ -641,6 +1119,13 @@ BUILDCURSORLABEL Build the text label for data cursor. SNAPTONEAREST Find the closest data point to a click position. [sx, sy, lineIdx] = tb.snapToNearest(fp, xClick, yClick) +#### `syncFollowState(obj)` + +SYNCFOLLOWSTATE Mirror target's LiveViewMode onto the Follow button. + Sets State='on' when LiveViewMode='follow', 'off' otherwise. + Sets Enable='off' when LiveViewMode is empty (no live wiring), + 'on' otherwise. Safe to call before/after rebind. + ### Static Methods #### `FastSenseToolbar.icon = makeIcon(name)` @@ -660,117 +1145,62 @@ FORMATX Format an X value based on XType. --- -## `FastSenseDataStore` --- SQLite-backed data storage for large time series. +## `HoverCrosshair` --- Hover-driven vertical crosshair + multi-line datatip for FastSense. > Inherits from: `handle` -Stores X/Y data in a temporary SQLite database via mksqlite using - chunked typed BLOBs for fast bulk insert and range-based retrieval. - This avoids loading full datasets into MATLAB memory, preventing - out-of-memory errors on Windows and memory-constrained systems. +hc = HOVERCROSSHAIR(fp) attaches a hover crosshair to a rendered + FastSense instance fp. While the mouse is over fp.hAxes, a vertical + line tracks the cursor's x position and a small datatip near the + cursor shows the formatted x value plus one row per visible line + (DisplayName + interpolated y at hovered x via binary_search). - Data is split into chunks of ~100K points. Each chunk is stored as - a pair of typed BLOBs (X and Y arrays) with the chunk's X range - indexed for fast overlap queries. On zoom/pan, only the chunks - overlapping the visible range are loaded, then trimmed to the exact - view window. + The handler is *chained*: any pre-existing WindowButtonMotionFcn + on the figure is preserved and invoked first on every motion event, + so this class coexists with other hover-driven features + (FastSenseToolbar crosshair toggle, NavigatorOverlay drag, etc.). - Additional data columns (cell, char, string, categorical, logical, - or any numeric type) can be attached via addColumn / getColumn. + Properties (read-only): + Target — FastSense instance + hFigure, hAxes — cached graphics handles + hLineV — vertical crosshair line + hTipBox — text annotation acting as datatip box - Requires mksqlite. If not available, falls back to binary file - storage (extra columns require mksqlite). + Methods: + onMove(xQuery) — update + show crosshair at xQuery (data coords) + onLeave() — hide crosshair + datatip + delete() — restore prior WindowButtonMotionFcn and clean up + + Coexistence with FastSenseToolbar: + The toolbar's setCrosshair() also swaps WindowButtonMotionFcn at + activation. Because we only chain whatever handler was installed + when our constructor ran, when the toolbar later overwrites the + callback our hover handler is temporarily detached (toolbar mode + wins). When the toolbar deactivates and restores its saved + callback (which is *our* chained handler), hover resumes + automatically. ### Constructor ```matlab -obj = FastSenseDataStore(x, y) +obj = HoverCrosshair(fp) ``` -FASTSENSEDATASTORE Create a disk-backed store from X/Y arrays. +HOVERCROSSHAIR Construct hover crosshair attached to a FastSense. + hc = HOVERCROSSHAIR(fp) requires fp to be a rendered FastSense + handle. Throws HoverCrosshair:invalidTarget if not. ### Methods -#### `[xOut, yOut] = getRange(obj, xMin, xMax)` - -GETRANGE Read data within an X range (with one-point padding). - -#### `[xOut, yOut] = readSlice(obj, startIdx, endIdx)` - -READSLICE Read a contiguous slice of data by row index. - -#### `addColumn(obj, name, data)` - -ADDCOLUMN Store an extra data column alongside X/Y. - Categorical arrays auto-convert to codes+categories struct. - String arrays auto-convert to cell of char. - -#### `data = getColumnRange(obj, name, xMin, xMax)` - -GETCOLUMNRANGE Read a column's data within an X range. - Converts the X range to a point-offset range using chunk - metadata (no x_data BLOB fetch), then delegates to slice. - -#### `data = getColumnSlice(obj, name, startIdx, endIdx)` - -GETCOLUMNSLICE Read a column slice by point index range. - -#### `names = listColumns(obj)` - -LISTCOLUMNS Return names of all stored extra columns. - -#### `idx = findIndex(obj, xVal, side)` - -FINDINDEX Binary search for a global point index by X value. - idx = ds.findIndex(xVal, 'left') returns the first index - where X(idx) >= xVal. idx = ds.findIndex(xVal, 'right') - returns the last index where X(idx) <= xVal. - -#### `[violX, violY] = findViolations(obj, startIdx, endIdx, threshold, isUpper)` - -FINDVIOLATIONS Find violation points using chunk-level Y filtering. - [vx, vy] = ds.findViolations(lo, hi, thresh, true) finds all - points in [lo, hi] where Y > thresh (upper violation). - [vx, vy] = ds.findViolations(lo, hi, thresh, false) finds - points where Y < thresh (lower violation). - -#### `enableWAL(obj)` - -ENABLEWAL Switch database to WAL journal mode for concurrent reads. - -#### `disableWAL(obj)` - -DISABLEWAL Revert database to DELETE journal mode. - -#### `storeResolved(obj, resolvedTh, resolvedViol)` - -STORERESOLVED Cache pre-computed resolve() results in SQLite. - ds.storeResolved(resolvedTh, resolvedViol) stores the - threshold and violation struct arrays produced by - Sensor.resolve() into the database for instant retrieval. - -#### `[resolvedTh, resolvedViol] = loadResolved(obj)` - -LOADRESOLVED Load pre-computed resolve() results from SQLite. - Returns empty arrays if no cached results exist. - -#### `clearResolved(obj)` - -CLEARRESOLVED Invalidate pre-computed resolve() cache. +#### `onMove(obj, xQuery)` -#### `cleanup(obj)` - -CLEANUP Close the database and delete temp files. - -### Static Methods +ONMOVE Update + show the crosshair at data x-coordinate xQuery. + Public so tests can drive motion deterministically without + needing real mouse input. -#### `FastSenseDataStore.c = toCategorical(s)` +#### `onLeave(obj)` -TOCATEGORICAL Convert a codes+categories struct back to categorical. - -#### `FastSenseDataStore.c = fromCategorical(data)` - -FROMCATEGORICAL Convert a MATLAB categorical to codes+categories struct. +ONLEAVE Hide the crosshair line + datatip. --- @@ -805,100 +1235,3 @@ obj = NavigatorOverlay(hAxes, varargin) Clamp to data limits ---- - -## `SensorDetailPlot` --- Two-panel sensor overview+detail plot with interactive navigator. - -> Inherits from: `handle` - -sdp = SensorDetailPlot(sensor) - sdp = SensorDetailPlot(sensor, Name, Value, ...) - - Name-Value Options: - 'Theme' - FastSense theme (default: 'default') - 'NavigatorHeight' - Fraction 0-1 for navigator (default: 0.20) - 'ShowThresholds' - Show thresholds in main plot (default: true) - 'ShowThresholdBands' - Show threshold bands in navigator (default: true) - 'Events' - EventStore or Event array (default: []) - 'ShowEventLabels' - Reserved, no effect (default: false) - 'Parent' - uipanel handle for embedding (default: []) - 'Title' - Plot title (default: sensor.Name) - 'XType' - 'numeric' or 'datenum' (default: 'numeric') - -### Constructor - -```matlab -obj = SensorDetailPlot(sensor, varargin) -``` - -Validate sensor - -### Methods - -#### `render(obj)` - -#### `setZoomRange(obj, xMin, xMax)` - -#### `[xMin, xMax] = getZoomRange(obj)` - ---- - -## `ConsoleProgressBar` --- Single-line console progress bar with indentation. - -> Inherits from: `handle` - -A lightweight progress indicator that renders an ASCII/Unicode bar - on a single console line, overwriting itself on each update via - backspace characters. Supports optional leading indentation so - multiple bars can be stacked hierarchically. - - The typical lifecycle is: construct -> start -> update (loop) -> - freeze or finish. Calling freeze() prints a newline to make the - current state permanent, allowing a subsequent bar to render on a - fresh line below. Calling finish() sets progress to 100 % and - freezes automatically. - - On GNU Octave the bar uses ASCII characters (# and -). On MATLAB - it uses Unicode block characters for a smoother appearance. - -### Constructor - -```matlab -obj = ConsoleProgressBar(indent) -``` - -CONSOLEPROGRESSBAR Construct a progress bar instance. - pb = ConsoleProgressBar() creates a bar with no indentation. - -### Methods - -#### `start(obj)` - -START Initialize and render the progress bar for the first time. - pb.start() resets the frozen/started state and prints the - initial (empty) bar. Must be called before update() will - have any visible effect. - -#### `update(obj, current, total, label)` - -UPDATE Set progress counters and redraw the bar. - pb.update(current, total) updates the progress fraction - to current/total and redraws the bar in-place. - -#### `freeze(obj)` - -FREEZE Make the current bar state permanent by printing a newline. - pb.freeze() redraws the bar one final time, appends a - newline character, and sets IsFrozen to true. Subsequent - calls to update() are silently ignored. Use this when you - want the bar to remain visible while a new bar starts on - the next line. - -#### `finish(obj)` - -FINISH Set progress to 100 %, freeze, and mark the bar done. - pb.finish() fills the bar to completion, prints a newline - (if not already frozen), and sets IsStarted to false. This - is a convenience shortcut equivalent to calling - pb.update(total, total) followed by pb.freeze(). - diff --git a/wiki/API-Reference:-FastSense.md b/wiki/API-Reference:-FastSense.md deleted file mode 100644 index 2ba3e454..00000000 --- a/wiki/API-Reference:-FastSense.md +++ /dev/null @@ -1,1237 +0,0 @@ - - -# API Reference: FastSense - -## `FastSense` --- Ultra-fast time series plotting with dynamic downsampling. - -> Inherits from: `handle` - -FastSense renders 1K to 100M data points with fluid zoom/pan by - dynamically downsampling data to screen resolution using MinMax or - LTTB algorithms. A multi-level pyramid cache provides instant - re-downsample on zoom without touching raw data. - -### Constructor - -```matlab -obj = FastSense(varargin) -``` - -FASTSENSE Construct a FastSense instance. - fp = FASTSENSE() creates a new FastSense with default settings. - fp = FASTSENSE('Parent', ax, 'Theme', 'dark') creates a plot - inside existing axes ax with the 'dark' theme. - fp = FASTSENSE('LinkGroup', 'g1', 'Verbose', true) creates a - plot that shares zoom/pan with other plots in group 'g1'. - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| ParentAxes | `[]` | axes handle, empty = create new | -| LinkGroup | `''` | string ID for linked zoom/pan | -| Theme | `[]` | theme struct (from FastSenseTheme) | -| Verbose | `false` | print diagnostics to console | -| LiveViewMode | `''` | 'preserve' \| 'follow' \| 'reset' (empty = no view mode applied) | -| LiveFile | `''` | path to .mat file for live mode | -| LiveUpdateFcn | `[]` | @(fp, data) callback for live updates | -| LiveIsActive | `false` | whether live polling is running | -| LiveInterval | `2.0` | poll interval in seconds | -| MetadataFile | `''` | path to separate .mat file for metadata | -| MetadataVars | `{}` | cell array of variable names to extract | -| MetadataLineIndex | `1` | which line index to attach metadata to | -| DeferDraw | `false` | skip drawnow during batch render | -| ShowProgress | `true` | show console progress bar during render | -| XScale | `'linear'` | 'linear' or 'log' — X axis scale | -| YScale | `'linear'` | 'linear' or 'log' — Y axis scale | -| ViolationsVisible | `true` | global toggle for violation markers | -| ShowThresholdLabels | `false` | show inline name labels on threshold lines | -| ShowEventMarkers | `true` | toggle event round-marker overlay (EVENT-07) | -| EventStore | `[]` | EventStore handle for event overlay queries | -| HoverCrosshair | `true` | enable hover crosshair + multi-line datatip (set false to disable; see HoverCrosshair.m) | -| IsEventPicking_ | `false` | event-pick mode active flag (260513-v69) | -| EventPickT1_ | `[]` | first-click x coordinate | -| EventPickEngine_ | `[]` | DashboardEngine handle (needed for persist + store) | -| PrevAxesBDFcn_ | `[]` | saved hAxes.ButtonDownFcn during pick mode | -| PrevFigKPFcn_ | `[]` | saved figure WindowKeyPressFcn during pick mode | -| EventPickPatch_ | `[]` | patch handle (Tag='EventPickRegion') for shaded region during pick (260513-voo) | -| PrevFigWBMFcn_ | `[]` | saved figure WindowButtonMotionFcn during pick mode (260513-voo) | -| EventPickModalListener_ | `[]` | event.listener on hEventDetails_ ObjectBeingDestroyed (260513-voo) | -| MinPointsForDownsample | `5000` | below this, plot raw data | -| DownsampleFactor | `2` | points per pixel (min + max) | -| PyramidReduction | `100` | reduction factor per pyramid level | -| DefaultDownsampleMethod | `'minmax'` | 'minmax' or 'lttb' | -| StorageMode | `'auto'` | 'auto', 'memory', or 'disk' | -| MemoryLimit | `500e6` | bytes; lines above this use disk (auto mode) | - -### Methods - -#### `resetColorIndex(obj)` - -RESETCOLORINDEX Reset the auto color cycling counter. - fp.RESETCOLORINDEX() resets the internal color counter to zero. - The next addLine() call without an explicit 'Color' option - will use the first color from the theme palette. - -#### `reapplyTheme(obj)` - -REAPPLYTHEME Re-apply the current Theme to axes and figure. - fp.REAPPLYTHEME() refreshes all visual properties (background, - foreground, grid, font, line widths) from the current Theme - struct. Call this after changing fp.Theme on an already-rendered - plot to update the display without re-rendering. - -#### `setScale(obj, varargin)` - -SETSCALE Set axis scale (linear or log) for X and/or Y. - fp.SETSCALE('YScale', 'log') switches Y axis to logarithmic. - fp.SETSCALE('XScale', 'log', 'YScale', 'linear') sets both. - -#### `addLine(obj, x, y, varargin)` - -ADDLINE Add a data line to the plot. - fp.ADDLINE(x, y) adds a line with auto-assigned color. - fp.ADDLINE(x, y, 'Color', 'r', 'DisplayName', 'Sensor1') - adds a red line labeled 'Sensor1' in the legend. - fp.ADDLINE(x, y, 'DownsampleMethod', 'lttb') uses the - Largest-Triangle-Three-Buckets algorithm instead of MinMax. - -#### `addThreshold(obj, varargin)` - -ADDTHRESHOLD Add a threshold line (scalar or time-varying). - fp.ADDTHRESHOLD(value) adds a constant horizontal threshold. - fp.ADDTHRESHOLD(value, 'Direction', 'upper', 'ShowViolations', true) - adds an upper threshold with violation markers at crossings. - fp.ADDTHRESHOLD(thX, thY, 'Direction', 'upper', 'ShowViolations', true) - adds a time-varying (step-function) threshold. - -#### `addBand(obj, yLow, yHigh, varargin)` - -ADDBAND Add a horizontal band fill (constant y bounds). - fp.ADDBAND(yLow, yHigh) adds a shaded horizontal band - spanning the full X range between yLow and yHigh, using - theme defaults for color and alpha. - fp.ADDBAND(yLow, yHigh, 'FaceColor', [1 0.9 0.9], 'FaceAlpha', 0.3) - uses custom color and transparency. - -#### `addMarker(obj, x, y, varargin)` - -ADDMARKER Add custom event markers at specific positions. - fp.ADDMARKER(x, y) plots marker symbols at the given (x,y) - positions using theme defaults. - fp.ADDMARKER(x, y, 'Marker', 'v', 'MarkerSize', 8, 'Color', [1 0 0]) - plots red downward-pointing triangles of size 8. - -#### `setShowEventMarkers(obj, tf)` - -SETSHOWEVENTMARKERS Toggle event-marker overlay post-render. - fp.SETSHOWEVENTMARKERS(true|false) flips ShowEventMarkers and - either deletes existing markers (tf=false) or re-runs - renderEventLayer_ (tf=true) so markers appear/disappear in - place without a full re-render. No-op if not yet rendered; - the next render() honours the new flag automatically. - -#### `addShaded(obj, x, y1, y2, varargin)` - -ADDSHADED Add a shaded region between two curves. - fp.ADDSHADED(x, y_upper, y_lower) fills the area between - y_upper and y_lower over the common X axis. - fp.ADDSHADED(x, y1, y2, 'FaceColor', [0 0 1], 'FaceAlpha', 0.2) - fills with blue at 20% opacity. - -#### `addFill(obj, x, y, varargin)` - -ADDFILL Add an area fill from a line to a baseline. - fp.ADDFILL(x, y) fills the area between y and a baseline - of zero using default shading colors. - fp.ADDFILL(x, y, 'Baseline', -1, 'FaceColor', [0 0.5 1]) - fills between y and y=-1 with a custom color. - -#### `addTag(obj, tag, varargin)` - -ADDTAG Polymorphic dispatch — route a Tag to the correct render path. - fp.ADDTAG(sensorTag) — routes to addLine via tag.getXY - fp.ADDTAG(stateTag) — routes to a staircase line (numeric Y) - fp.ADDTAG(monitorTag) — routes to addLine via tag.getXY (0/1 binary series) - fp.ADDTAG(compositeTag) — routes to addLine via tag.getXY (aggregated 0/1 or 0..1 series) - fp.ADDTAG(derivedTag) — routes to addLine via tag.getXY (continuous derived series) - -#### `addStateTagAsStaircase_(obj, tag, varargin)` - -ADDSTATETAGASSTAIRCASE_ Render a numeric StateTag as a stepped line. - Private helper (name ends in _) invoked by addTag for the - 'state' kind. Expands (X, Y) pairs into an interleaved - 2N-1 staircase and delegates to addLine. Cellstr Y is not - supported in Phase 1005 (deferred). - -#### `render(obj, progressBar)` - -RENDER Create the plot with all configured lines and annotations. - fp.RENDER() finalizes the plot and displays it. This method: - 1. Creates a figure and axes (or uses ParentAxes) - 2. Applies the Theme (background, grid, font) - 3. Renders bands and shaded regions (back layer) - 4. Downsamples all lines to screen pixel resolution - 5. Draws threshold lines and violation markers - 6. Draws custom markers (front layer) - 7. Sets axis limits with 5% padding - 8. Installs XLim/resize listeners for dynamic re-downsample - 9. Schedules async refinement for large datasets - 10. Registers in LinkGroup for synchronized zoom/pan - -#### `result = lookupMetadata(obj, lineIdx, xValue)` - -LOOKUPMETADATA Get active metadata at a given X value (forward-fill). - result = fp.LOOKUPMETADATA(lineIdx, xValue) returns a struct - containing the metadata values that were active at xValue, - using forward-fill (last-observation-carried-forward) logic. - -#### `updateData(obj, lineIdx, newX, newY, varargin)` - -UPDATEDATA Replace data for a line and re-downsample. - fp.UPDATEDATA(lineIdx, newX, newY) replaces the raw X/Y - data for the specified line and refreshes the display. - fp.UPDATEDATA(lineIdx, newX, newY, 'Metadata', meta) - also replaces the line's metadata struct. - fp.UPDATEDATA(lineIdx, newX, newY, 'SkipViewMode', true) - replaces data without applying LiveViewMode adjustments. - -#### `startLive(obj, filepath, updateFcn, varargin)` - -STARTLIVE Start live mode — poll a .mat file for changes. - fp.STARTLIVE(filepath, updateFcn) begins polling filepath - at the default interval. When the file's modification date - changes, it loads the .mat and calls updateFcn(fp, data). - fp.STARTLIVE(filepath, updateFcn, 'Interval', 2) - fp.STARTLIVE(filepath, updateFcn, 'ViewMode', 'follow') - -#### `setXLimQuiet(obj, tStart, tEnd)` - -SETXLIMQUIET Set XLim without triggering XLimMode listener overhead. - fp.SETXLIMQUIET(tStart, tEnd) is a performance-optimised - alternative to calling xlim(ax, [tStart tEnd]) from external - callers (e.g. FastSenseWidget.setTimeRange). The plain - xlim() call fires the XLimMode PostSet listener every time, - which routes to onXLimModeChanged -> scheduleDeferredXLimCheck - and creates a new 10 ms one-shot timer per call. That timer - overhead adds ~4 ms per FastSenseWidget per live tick. - -#### `stopLive(obj)` - -STOPLIVE Stop live mode polling. - fp.STOPLIVE() stops the live timer, cleans up the deferred - timer, and sets LiveIsActive to false. Safe to call even if - live mode is not active. Also stops the refinement timer. - -#### `refresh(obj)` - -REFRESH Manual one-shot reload from LiveFile. - fp.REFRESH() loads the current LiveFile, calls LiveUpdateFcn, - and updates the LiveFileDate timestamp. Also reloads the - MetadataFile if configured. Useful for triggering a manual - update without waiting for the live timer. - -#### `setViewMode(obj, mode)` - -SETVIEWMODE Change the live view mode at runtime. - fp.SETVIEWMODE(mode) sets the LiveViewMode property, which - controls how the X-axis adjusts when new data arrives. - -#### `snapToTail(obj)` - -SNAPTOTAIL Slide XLim window so its right edge sits just past the data tail. - fp.SNAPTOTAIL() does a one-shot "jump to now" — finds the - maximum X across all lines, then sets XLim to - [xMax - currentWindowWidth + pad, xMax + pad] where pad = - 2% of the current window width. The small right-edge - padding leaves visual breathing room between the latest - data point and the chart's right border so the line tail - doesn't get clipped against the axes frame. - -#### `runLive(obj)` - -RUNLIVE Blocking poll loop for live mode (Octave compatibility). - fp.RUNLIVE() enters a blocking loop that polls LiveFile for - changes at LiveInterval. This is required on Octave where - MATLAB timer objects are not available. - -#### `onLiveTimerPublic(obj)` - -ONLIVETIMERPUBLIC Public wrapper for testing live timer callback. - fp.ONLIVETIMERPUBLIC() delegates to the private onLiveTimer - method. Exists solely to allow unit tests to invoke the - timer callback directly without relying on real timers. - -#### `setLineMetadata(obj, lineIdx, meta)` - -SETLINEMETADATA Set metadata on a line after construction. - fp.SETLINEMETADATA(lineIdx, meta) attaches or replaces the - metadata struct on the specified line. Primarily used by - FastSenseGrid to attach metadata loaded from a separate - file after the plot has been rendered. - -#### `setViolationsVisible(obj, on)` - -SETVIOLATIONSVISIBLE Show or hide all violation markers. - fp.SETVIOLATIONSVISIBLE(true) shows violation markers on all - thresholds that have ShowViolations enabled, forcing a - recomputation from the currently displayed line data. - fp.SETVIOLATIONSVISIBLE(false) hides all violation markers - without recomputing them. - -#### `openLoupe(obj)` - -OPENLOUPE Open a standalone enlarged copy of this tile. - fp.OPENLOUPE() creates a new FastSense in a separate figure - containing deep copies of all lines, thresholds, bands, - shadings, and markers from the current plot. The new figure - preserves the current zoom state (XLim/YLim), is offset - by [+30, -30] pixels from the source figure, and receives - its own FastSenseToolbar. - -#### `exportData(obj, filepath, format)` - -EXPORTDATA Export raw line and threshold data as CSV or MAT. - EXPORTDATA(obj, filepath, format) writes all raw line and - threshold data from the plot to the file at filepath. - -#### `refreshEventLayer(obj)` - -REFRESHEVENTLAYER Public thin wrapper — rebuild the event marker layer. - Calls the private renderEventLayer_ so external consumers - (e.g. FastSenseWidget.refresh()) can trigger a marker rebuild - without exposing the implementation method directly. - -#### `n = lineNumPoints(obj, i)` - -LINENUMPOINTS Return total point count for line i. - -#### `[xMin, xMax] = lineXRange(obj, i)` - -LINEXRANGE Return X endpoints for line i. - -#### `onEventMarkerClick_(obj, src, ~)` - -ONEVENTMARKERCLICK_ ButtonDownFcn dispatcher for event markers. - Hidden public so TestFastSenseEventClick can call it for direct - dispatch of the click -> details-popup path. Branches on figure - SelectionType: 'normal' -> openEventDetails_; 'alt' (right-click) - -> builds/shows uicontextmenu with quick-nav actions. - -#### `openEventDetails_(obj, ev)` - -OPENEVENTDETAILS_ Open a separate floating figure with event fields. - Phase 1012 refit: standalone figure (OS-native drag/close), light - theme with standard font, read-only field list on top and an - editable Notes box at the bottom. Saving the notes mutates - ev.Notes (handle persists across the MATLAB session) and calls - EventStore.save() when a FilePath is configured (disk persistence). - -#### `fitDetailsTableColumns_(~, hTable)` - -FITDETAILSTABLECOLUMNS_ Split the uitable width ~1:2 between - Field and Value columns based on the parent FIGURE's - current pixel width. Deriving from the figure rather than - reading the table's own Position avoids a race where the - table layout hasn't settled when SizeChangedFcn fires. - -#### `saveEventNotes_(obj, ev, hNotesControl)` - -SAVEEVENTNOTES_ Commit the Notes textarea to ev.Notes and persist. - Mutates the Event handle (in-session persistence) and calls - obj.EventStore.save() when available so notes survive MATLAB - restarts. Updates the status label to confirm. - -#### `closeEventDetails_(obj)` - -CLOSEEVENTDETAILS_ Dismiss the popup figure. - -#### `onKeyPressForDetailsDismiss_(obj, eventData)` - -ONKEYPRESSFORDETAILSDISMISS_ Close popup on ESC key. - -#### `startEventPick_(obj, engine)` - -STARTEVENTPICK_ Enter two-click event-pick mode (260513-v69). - Toggle-cancels if already active. Saves axes ButtonDownFcn - and figure WindowKeyPressFcn, installs our handlers, draws - hint. WindowButtonMotionFcn is never touched so - HoverCrosshair stays fully functional. - -#### `cancelEventPick_(obj)` - -CANCELEVENTPICK_ Exit pick mode + cleanup. Idempotent. Delegates to onEventDetailsClosed_ (260513-voo). - Preserves the v69 Test 7 contract: silent no-op when not - picking AND no patch alive (axes children count unchanged). - -#### `onPickClick_(obj, ~, ~)` - -ONPICKCLICK_ Axes ButtonDownFcn during pick mode. Right-click cancels. - -#### `onPickKey_(obj, src, evt)` - -ONPICKKEY_ Figure WindowKeyPressFcn during pick. ESC cancels; chain otherwise. - -#### `completeEventPick_(obj, tStart, tEnd)` - -COMPLETEEVENTPICK_ Sort, persist, hand off to openEventDetails_, cleanup. - -#### `drawPickHint_(obj, str)` - -DRAWPICKHINT_ Draw the EventPickHint text annotation in obj.hAxes. - -#### `updatePickHint_(obj, str)` - -UPDATEPICKHINT_ Mutate an existing EventPickHint's String, fallback redraws. - -#### `drawPickLine_(obj, x)` - -DRAWPICKLINE_ Draw a single orange vertical EventPickLine at x. - -#### `onPickMotion_(obj, src, evt)` - -ONPICKMOTION_ Chained figure WindowButtonMotionFcn during pick mode (260513-voo). - FIRST forward to the saved handler so HoverCrosshair keeps - working. THEN, while in the post-click-1 pre-click-2 sub- - state, update the shaded patch XData to track the cursor. - Wrapped in try/catch so our chained handler never breaks - HoverCrosshair downstream. - -#### `onPickMotion_FromX_(obj, cx)` - -ONPICKMOTION_FROMX_ Update patch geometry to span [EventPickT1_, cx] x current YLim (260513-voo). - Pulled out of onPickMotion_ so tests can drive geometry - updates deterministically without having to mutate - CurrentPoint (which is read-only on MATLAB). - -#### `createPickPatch_(obj, x)` - -CREATEPICKPATCH_ Create the EventPickRegion patch at zero width (260513-voo). - FaceColor is read from the just-drawn EventPickLine (SSOT) - with fallback to the canonical [1.0 0.55 0.0] orange. The - patch is pushed to the back of axes Children so the lines - and plotted signal stay in front. HitTest='off' + - PickableParts='none' so click 2 reaches the axes - underneath this patch. - -#### `finalizePickPatch_(obj, tStart, tEnd)` - -FINALIZEPICKPATCH_ Snap the patch to sorted [tStart, tEnd] x current YLim (260513-voo). - -#### `c = pickLineColor_(obj)` - -PICKLINECOLOR_ Resolve patch FaceColor from a live EventPickLine, fallback orange. - -#### `restorePickCallbacks_(obj)` - -RESTOREPICKCALLBACKS_ Restore axes BDF + figure KP + figure WBM to pre-pick values (260513-voo). - -#### `onEventDetailsClosed_(obj)` - -ONEVENTDETAILSCLOSED_ Unified pick-mode cleanup (260513-voo). - Idempotent. Called from three paths: - - addlistener on hEventDetails_ ObjectBeingDestroyed (modal close) - - cancelEventPick_ (toggle / ESC / right-click) - - completeEventPick_ catch fallback when modal couldn't open - First-line guard returns silently when nothing is in flight. - -#### `tbl = buildEventFieldsTable_(~, ev)` - -BUILDEVENTFIELDSTABLE_ Nx2 cell array for the uitable in the - details popup. Columns are {Field, Value}. Empty statistics - rows are skipped. Section separators use a blank-label row - with a bullet '·' value to maintain visual grouping without - relying on cell-level styling (not portable across MATLAB - versions). - -#### `txt = formatEventFields_(~, ev)` - -FORMATEVENTFIELDS_ Produce a grouped, readable listing of event fields. - Sections: TIMING / STATISTICS / CLASSIFICATION / TAGS / THRESHOLD. - Empty-valued statistics rows are hidden (they carry no - information and clutter the popup). IsOpen=true displays - "Open" for EndTime and Duration so the test contract in - TestFastSenseEventClick.testFormatEventFieldsShowsOpenForOpenEvent - still holds. - -#### `s = formatSection(header, rows, labelWidth)` - -### Static Methods - -#### `FastSense.fp = plot(x, y, varargin)` - -PLOT One-liner convenience for quick plotting. - FastSense.plot(x, y) - FastSense.plot(x, y, 'DisplayName', 'Signal', 'Theme', 'dark') - -#### `FastSense.resetDefaults()` - -RESETDEFAULTS Force reload of FastSenseDefaults on next use. - FastSense.RESETDEFAULTS() clears the cached defaults struct - so the next FastSense constructor will re-read - FastSenseDefaults.m. Useful after editing the defaults file - in a running session. - -#### `FastSense.distFig(varargin)` - -DISTFIG Distribute figure windows across the screen. - FastSense.DISTFIG() auto-arranges all open figure windows - in a grid that fills the screen. Figures are sorted by - number and tiled left-to-right, top-to-bottom. - FastSense.DISTFIG('Rows', 2, 'Cols', 3) uses a 2-by-3 grid. - ---- - -## `FastSenseDock` --- Tabbed container for multiple FastSenseGrid dashboards. - -> Inherits from: `handle` - -Manages multiple FastSenseGrid instances as switchable tabs in a - single window. Each tab has its own panel, toolbar, close button, - and undock button. Tabs can be dynamically added, removed, or - popped out into standalone figures. - - dock = FastSenseDock() - dock = FastSenseDock('Theme', 'dark') - dock = FastSenseDock('Theme', 'dark', 'Name', 'My Dock') - -### Constructor - -```matlab -obj = FastSenseDock(varargin) -``` - -FASTSENSEDOCK Construct a tabbed dock container. - dock = FastSenseDock() - dock = FastSenseDock('Theme', 'dark', 'Name', 'My Dock') - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| Theme | `[]` | FastSenseTheme struct | -| hFigure | `[]` | shared figure handle | -| ShowProgress | `true` | show console progress bar during renderAll | -| TabBarHeight | `0.03` | normalized height of tab bar | -| MinTabWidth | `0.10` | minimum normalized width per tab | - -### Methods - -#### `addTab(obj, fig, name)` - -ADDTAB Register a FastSenseGrid as a tab. - dock.addTab(fig, name) adds a FastSenseGrid as a new tab - in the dock. The figure's ParentFigure and hFigure are - redirected to the dock's shared figure. A uipanel is created - for the tab's content, offset below the tab bar. - -#### `render(obj)` - -RENDER Render active tab, create tab bar, show first tab. - dock.render() renders only the first tab (lazy rendering), - creates tab bar buttons for all tabs, attaches a shared - FastSenseToolbar, selects tab 1, and makes the figure visible. - Subsequent tabs are rendered on-demand when selectTab is called. - -#### `renderAll(obj)` - -RENDERALL Eagerly render all tabs with hierarchical progress. - dock.renderAll() renders every tab upfront (not lazily). - Shows hierarchical console progress: tab headers + per-tile - progress bars. After all tabs are rendered, creates the tab - bar, shared toolbar, and selects tab 1. - -#### `selectTab(obj, n)` - -SELECTTAB Switch to tab n, rendering it lazily if needed. - dock.selectTab(n) hides the currently active tab, renders - tab n if it hasn't been rendered yet, rebinds the shared - toolbar to the new tab's FastSenseGrid, and shows tab n. - -#### `removeTab(obj, n)` - -REMOVETAB Close and remove tab n. - dock.removeTab(n) stops live mode on the tab, deletes its - panel and UI buttons, removes it from all internal arrays, - and rebuilds the tab bar. If the removed tab was active, the - nearest remaining tab is selected. If no tabs remain, the - toolbar is also deleted. - -#### `undockTab(obj, n)` - -UNDOCKTAB Pop tab n out into its own standalone figure. - dock.undockTab(n) creates a new standalone figure, stops - live mode, reparents all tile axes from the dock panel to - the new figure, recomputes tile positions for standalone - layout, creates a fresh FastSenseToolbar, and removes the - tab from the dock. The remaining dock tabs are reindexed - and the tab bar is rebuilt. - -#### `recomputeLayout(obj)` - -RECOMPUTELAYOUT Reposition tab, undock, and close buttons on resize. - dock.recomputeLayout() recalculates the normalized positions - of all tab, undock (^), and close (x) buttons based on the - current number of tabs. When the ideal tab width falls below - MinTabWidth, scroll arrows (< >) appear and only a subset of - tabs is shown. Called automatically on SizeChangedFcn and - after addTabButton/rebuildTabBar. - -#### `reapplyTheme(obj)` - -REAPPLYTHEME Re-apply theme to dock, tab bar, panels, and all tabs. - dock.reapplyTheme() updates the figure background, re-styles - all tab/undock/close buttons, updates panel backgrounds, and - propagates the theme to every tab's FastSenseGrid (calling - reapplyTheme on rendered figures). - ---- - -## `FastSenseToolbar` --- Interactive toolbar for FastSense and FastSenseGrid. - -> Inherits from: `handle` - -Adds a uitoolbar with data cursor, crosshair, grid/legend toggles, - Y-axis autoscale, PNG export, live mode controls, and metadata - display. Integrates with MATLAB's built-in datacursormode for - enhanced tooltips. - - tb = FastSenseToolbar(fp) — attach to a FastSense instance - tb = FastSenseToolbar(fig) — attach to a FastSenseGrid instance - - Toolbar buttons: - Data Cursor — click to snap to nearest data point, shows value - Crosshair — tracks mouse position with coordinate readout - Grid — toggle grid on/off (active axes or all) - Legend — toggle legend visibility - Autoscale Y — fit Y-axis to visible data range - Export PNG — save figure as PNG with file dialog - Export Data — save raw data as CSV or MAT with file dialog - Refresh — manual one-shot data reload - Live Mode — toggle automatic file polling - Follow — auto-pan X-axis to show the data tail - Metadata — show/hide metadata in data cursor tooltips - Violations — toggle violation marker visibility - -### Constructor - -```matlab -obj = FastSenseToolbar(target) -``` - -FASTSENSETOOLBAR Construct and attach a toolbar to a plot target. - tb = FastSenseToolbar(fp) — FastSense instance - tb = FastSenseToolbar(fig) — FastSenseGrid instance - -### Methods - -#### `toggleGrid(obj)` - -TOGGLEGRID Toggle grid visibility on all managed axes. - -#### `toggleLegend(obj)` - -TOGGLELEGEND Toggle legend visibility on all managed axes. - -#### `autoscaleY(obj)` - -AUTOSCALEY Fit Y-axis limits to visible data on all axes. - -#### `exportPNG(obj, filepath)` - -EXPORTPNG Save figure as PNG image at 150 DPI. - tb.exportPNG() — opens file dialog - tb.exportPNG(filepath) — saves directly to path - -#### `exportData(obj, filepath)` - -EXPORTDATA Export raw plot data as CSV or MAT file. - tb.exportData() — opens file dialog - tb.exportData(filepath) — saves directly (format from extension) - -#### `setCrosshair(obj, on)` - -SETCROSSHAIR Enable or disable crosshair tracking mode. - tb.setCrosshair(true) — activate crosshair, disable zoom - tb.setCrosshair(false) — deactivate, re-enable zoom - -#### `setCursor(obj, on)` - -SETCURSOR Enable or disable data cursor snap mode. - tb.setCursor(true) — activate cursor, disable zoom - tb.setCursor(false) — deactivate, re-enable zoom - -#### `refresh(obj)` - -REFRESH Trigger a manual data refresh. - -#### `toggleLive(obj)` - -TOGGLELIVE Toggle live mode on/off. - -#### `setFollow(obj, on)` - -SETFOLLOW Enable or disable Follow auto-pan-to-tail. - tb.setFollow(true) — set LiveViewMode='follow' and immediately - snap XLim to [x(end)-w, x(end)] if the - current XLim does not already include x(end). - tb.setFollow(false) — set LiveViewMode='preserve' (XLim unchanged). - -#### `setMetadata(obj, on)` - -SETMETADATA Enable or disable metadata display in tooltips. - tb.setMetadata(true) — show metadata fields in cursor - tb.setMetadata(false) — hide metadata - -#### `setViolationsVisible(obj, on)` - -SETVIOLATIONSVISIBLE Toggle violation markers on all tiles. - setViolationsVisible(obj, on) iterates over all managed - FastSense instances and calls setViolationsVisible(on) on - each, then syncs the toolbar toggle button state. - -#### `rebind(obj, target)` - -REBIND Switch toolbar to a new target without recreating HG objects. - tb.rebind(newTarget) - -#### `label = buildCursorLabel(obj, fp, sx, sy, lineIdx)` - -BUILDCURSORLABEL Build the text label for data cursor. - -#### `[sx, sy, lineIdx] = snapToNearest(~, fp, xClick, yClick)` - -SNAPTONEAREST Find the closest data point to a click position. - [sx, sy, lineIdx] = tb.snapToNearest(fp, xClick, yClick) - -#### `syncFollowState(obj)` - -SYNCFOLLOWSTATE Mirror target's LiveViewMode onto the Follow button. - Sets State='on' when LiveViewMode='follow', 'off' otherwise. - Sets Enable='off' when LiveViewMode is empty (no live wiring), - 'on' otherwise. Safe to call before/after rebind. - -### Static Methods - -#### `FastSenseToolbar.icon = makeIcon(name)` - -MAKEICON Generate a 16x16x3 RGB icon for toolbar buttons. - icon = FastSenseToolbar.makeIcon(name) - -#### `FastSenseToolbar.initIcons()` - -INITICONS Pre-warm the icon cache for all toolbar buttons. - -#### `FastSenseToolbar.s = formatX(xVal, xType)` - -FORMATX Format an X value based on XType. - s = FastSenseToolbar.formatX(xVal, 'datenum') - s = FastSenseToolbar.formatX(xVal, 'numeric') - ---- - -## `FastSenseDataStore` --- SQLite-backed data storage for large time series. - -> Inherits from: `handle` - -Stores X/Y data in a temporary SQLite database via mksqlite using - chunked typed BLOBs for fast bulk insert and range-based retrieval. - This avoids loading full datasets into MATLAB memory, preventing - out-of-memory errors on Windows and memory-constrained systems. - - Data is split into chunks of ~100K points. Each chunk is stored as - a pair of typed BLOBs (X and Y arrays) with the chunk's X range - indexed for fast overlap queries. On zoom/pan, only the chunks - overlapping the visible range are loaded, then trimmed to the exact - view window. - - Additional data columns (cell, char, string, categorical, logical, - or any numeric type) can be attached via addColumn / getColumn. - - Requires mksqlite. If not available, falls back to binary file - storage (extra columns require mksqlite). - -### Constructor - -```matlab -obj = FastSenseDataStore(x, y) -``` - -FASTSENSEDATASTORE Create a disk-backed store from X/Y arrays. - -### Methods - -#### `[xOut, yOut] = getRange(obj, xMin, xMax)` - -GETRANGE Read data within an X range (with one-point padding). - -#### `[xOut, yOut] = readSlice(obj, startIdx, endIdx)` - -READSLICE Read a contiguous slice of data by row index. - -#### `addColumn(obj, name, data)` - -ADDCOLUMN Store an extra data column alongside X/Y. - Categorical arrays auto-convert to codes+categories struct. - String arrays auto-convert to cell of char. - -#### `data = getColumnRange(obj, name, xMin, xMax)` - -GETCOLUMNRANGE Read a column's data within an X range. - Converts the X range to a point-offset range using chunk - metadata (no x_data BLOB fetch), then delegates to slice. - -#### `data = getColumnSlice(obj, name, startIdx, endIdx)` - -GETCOLUMNSLICE Read a column slice by point index range. - -#### `names = listColumns(obj)` - -LISTCOLUMNS Return names of all stored extra columns. - -#### `idx = findIndex(obj, xVal, side)` - -FINDINDEX Binary search for a global point index by X value. - idx = ds.findIndex(xVal, 'left') returns the first index - where X(idx) >= xVal. idx = ds.findIndex(xVal, 'right') - returns the last index where X(idx) <= xVal. - -#### `[violX, violY] = findViolations(obj, startIdx, endIdx, threshold, isUpper)` - -FINDVIOLATIONS Find violation points using chunk-level Y filtering. - [vx, vy] = ds.findViolations(lo, hi, thresh, true) finds all - points in [lo, hi] where Y > thresh (upper violation). - [vx, vy] = ds.findViolations(lo, hi, thresh, false) finds - points where Y < thresh (lower violation). - -#### `enableWAL(obj)` - -ENABLEWAL Switch database to WAL journal mode for concurrent reads. - -#### `disableWAL(obj)` - -DISABLEWAL Revert database to DELETE journal mode. - -#### `storeResolved(obj, resolvedTh, resolvedViol)` - -STORERESOLVED Cache pre-computed resolve() results in SQLite. - ds.storeResolved(resolvedTh, resolvedViol) stores the - threshold and violation struct arrays produced by - Sensor.resolve() into the database for instant retrieval. - -#### `[resolvedTh, resolvedViol] = loadResolved(obj)` - -LOADRESOLVED Load pre-computed resolve() results from SQLite. - Returns empty arrays if no cached results exist. - -#### `clearResolved(obj)` - -CLEARRESOLVED Invalidate pre-computed resolve() cache. - -#### `storeMonitor(obj, key, X, Y, parentKey, parentNumPts, parentXMin, parentXMax)` - -STOREMONITOR Cache a MonitorTag's derived (X, Y) plus staleness quad. - ds.storeMonitor(key, X, Y, parentKey, parentNumPts, parentXMin, parentXMax) - upserts a monitors row. The quad (parent_key, num_points, - parent_xmin, parent_xmax) is stamped at write time and is - compared at load time by MonitorTag.cacheIsStale_. - -#### `[X, Y, meta] = loadMonitor(obj, key)` - -LOADMONITOR Retrieve cached MonitorTag (X, Y) + staleness metadata. - [X, Y, meta] = ds.loadMonitor(key) returns X=[] on miss. - Callers must verify freshness via the returned meta struct - (fields: parent_key, num_points, parent_xmin, parent_xmax, - computed_at). - -#### `clearMonitor(obj, key)` - -CLEARMONITOR Delete a cached MonitorTag row by key. - -#### `cleanup(obj)` - -CLEANUP Close the database and delete temp files. - -#### `ensureOpenForTest(obj)` - -ENSUREOPENFORTEST Test-only hook to force-reopen the DB handle. - Exposes the private ensureOpen() lifecycle helper so WAL-mode - tests can query journal_mode via mksqlite(DbId, ...) without - hitting MethodRestricted. Hidden (rather than narrower - Access = {?matlab.unittest.TestCase}) so Octave parsing - survives — Octave has no matlab.unittest. - -### Static Methods - -#### `FastSenseDataStore.c = toCategorical(s)` - -TOCATEGORICAL Convert a codes+categories struct back to categorical. - -#### `FastSenseDataStore.c = fromCategorical(data)` - -FROMCATEGORICAL Convert a MATLAB categorical to codes+categories struct. - ---- - -## `NavigatorOverlay` --- Zoom rectangle, dimming, and drag interaction on navigator axes. - -> Inherits from: `handle` - -ov = NavigatorOverlay(hAxes) - - Properties (read-only): - hRegion, hDimLeft, hDimRight, hEdgeLeft, hEdgeRight — graphics handles - - Methods: - setRange(xMin, xMax) — update the visible region rectangle - delete() — clean up all handles and callbacks - -### Constructor - -```matlab -obj = NavigatorOverlay(hAxes, varargin) -``` - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| OnRangeChanged | | Callback: @(xMin, xMax) | - -### Methods - -#### `setRange(obj, xMin, xMax)` - -Clamp to data limits - ---- - -## `SensorDetailPlot` --- Two-panel sensor overview+detail plot with interactive navigator. - -> Inherits from: `handle` - -sdp = SensorDetailPlot(tag) - sdp = SensorDetailPlot(tag, Name, Value, ...) - - Name-Value Options: - 'Theme' - FastSense theme (default: 'default') - 'NavigatorHeight' - Fraction 0-1 for navigator (default: 0.20) - 'ShowThresholds' - Show thresholds in main plot (default: true) - 'ShowThresholdBands' - Show threshold bands in navigator (default: true) - 'Events' - EventStore or Event array (default: []) - 'ShowEventLabels' - Reserved, no effect (default: false) - 'Parent' - uipanel handle for embedding (default: []) - 'Title' - Plot title (default: tag.Name) - 'XType' - 'numeric' or 'datenum' (default: 'numeric') - -### Constructor - -```matlab -obj = SensorDetailPlot(tag, varargin) -``` - -Accept Tag (v2.0) only. -Tag class is the abstract base — uses isa(x, 'Tag'), NOT -isa-on-subclass-name (Pitfall 1). - -### Methods - -#### `render(obj)` - -#### `setZoomRange(obj, xMin, xMax)` - -#### `[xMin, xMax] = getZoomRange(obj)` - ---- - -## `ConsoleProgressBar` --- Single-line console progress bar with indentation. - -> Inherits from: `handle` - -A lightweight progress indicator that renders an ASCII/Unicode bar - on a single console line, overwriting itself on each update via - backspace characters. Supports optional leading indentation so - multiple bars can be stacked hierarchically. - - The typical lifecycle is: construct -> start -> update (loop) -> - freeze or finish. Calling freeze() prints a newline to make the - current state permanent, allowing a subsequent bar to render on a - fresh line below. Calling finish() sets progress to 100 % and - freezes automatically. - - On GNU Octave the bar uses ASCII characters (# and -). On MATLAB - it uses Unicode block characters for a smoother appearance. - -### Constructor - -```matlab -obj = ConsoleProgressBar(indent) -``` - -CONSOLEPROGRESSBAR Construct a progress bar instance. - pb = ConsoleProgressBar() creates a bar with no indentation. - -### Methods - -#### `start(obj)` - -START Initialize and render the progress bar for the first time. - pb.start() resets the frozen/started state and prints the - initial (empty) bar. Must be called before update() will - have any visible effect. - -#### `update(obj, current, total, label)` - -UPDATE Set progress counters and redraw the bar. - pb.update(current, total) updates the progress fraction - to current/total and redraws the bar in-place. - -#### `freeze(obj)` - -FREEZE Make the current bar state permanent by printing a newline. - pb.freeze() redraws the bar one final time, appends a - newline character, and sets IsFrozen to true. Subsequent - calls to update() are silently ignored. Use this when you - want the bar to remain visible while a new bar starts on - the next line. - -#### `finish(obj)` - -FINISH Set progress to 100 %, freeze, and mark the bar done. - pb.finish() fills the bar to completion, prints a newline - (if not already frozen), and sets IsStarted to false. This - is a convenience shortcut equivalent to calling - pb.update(total, total) followed by pb.freeze(). - ---- - -## `FastSenseGrid` --- Tiled layout manager for FastSense dashboards. - -> Inherits from: `handle` - -Creates a grid of FastSense tiles in a single figure window with - configurable spacing, per-tile theme overrides, and tile spanning. - Supports live mode that synchronizes file polling across all tiles. - - For widget-based dashboards with gauges, numbers, status indicators, - and edit mode, see DashboardEngine. - - fig = FastSenseGrid(rows, cols) - fig = FastSenseGrid(rows, cols, 'Theme', 'dark') - fig = FastSenseGrid(rows, cols, 'ParentFigure', hFig) - -### Constructor - -```matlab -obj = FastSenseGrid(rows, cols, varargin) -``` - -FASTSENSEGRID Construct a tiled dashboard. - fig = FastSenseGrid(rows, cols) - fig = FastSenseGrid(rows, cols, 'Theme', 'dark') - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| Grid | `[1 1]` | [rows, cols] | -| Theme | `[]` | FastSenseTheme struct | -| hFigure | `[]` | figure handle | -| ParentFigure | `[]` | external figure handle (skip figure creation) | -| ContentOffset | `[0 0 1 1]` | [left bottom width height] normalized content area | -| LiveViewMode | `''` | 'preserve' \| 'follow' \| 'reset' | -| LiveFile | `''` | path to .mat file | -| LiveUpdateFcn | `[]` | @(fig, data) callback | -| LiveIsActive | `false` | whether polling is running | -| LiveInterval | `2.0` | poll interval in seconds | -| MetadataFile | `''` | path to metadata .mat file | -| MetadataVars | `{}` | variable names to extract | -| MetadataLineIndex | `1` | line index within the tile | -| MetadataTileIndex | `1` | which tile to attach metadata to | -| ShowProgress | `true` | show console progress bar during renderAll | -| Padding | `[0.06 0.04 0.01 0.02]` | [left bottom right top] normalized | -| GapH | `0.03` | horizontal gap between tiles | -| GapV | `0.06` | vertical gap between tiles | - -### Methods - -#### `fp = tile(obj, n)` - -TILE Get or create the FastSense instance for tile n. - fp = fig.tile(n) returns the FastSense for tile n, creating - it (and its axes) on first access. Tile themes are merged - from the figure theme and any per-tile overrides. - -#### `ax = axes(obj, n)` - -AXES Get or create a raw MATLAB axes for tile n. - ax = fig.axes(n) returns a themed MATLAB axes handle at the - position for tile n. Use for non-FastSense plot types (bar, - scatter, histogram, stem, etc.). The axes gets theme colors - applied but no FastSense optimization. - -#### `hp = tilePanel(obj, n)` - -TILEPANEL Get or create a uipanel for tile n. - hp = fig.tilePanel(n) returns a uipanel handle at the - computed grid position for tile n. Use this to embed - composite widgets (e.g. SensorDetailPlot) into a tile. - -#### `setTileSpan(obj, n, span)` - -SETTILESPAN Set the row/column span for tile n. - fig.setTileSpan(n, span) configures tile n to occupy - multiple rows and/or columns in the grid layout. - -#### `setTileTheme(obj, n, themeOverrides)` - -SETTILETHEME Set per-tile theme overrides. - fig.setTileTheme(n, themeOverrides) stores a partial theme - struct for tile n. When the tile is created or re-themed, - these overrides are merged on top of the figure-level theme. - -#### `setTileTitle(obj, n, str)` - -SETTILETITLE Set title for tile n. - fig.setTileTitle(n, str) sets the axes title on tile n using - the figure theme's TitleFontSize and ForegroundColor. - Can be called before or after render(). - -#### `setTileXLabel(obj, n, str)` - -SETTILEXLABEL Set xlabel for tile n. - fig.setTileXLabel(n, str) sets the X-axis label on tile n - using the figure theme's ForegroundColor. - Can be called before or after render(). - -#### `setTileYLabel(obj, n, str)` - -SETTILEYLABEL Set ylabel for tile n. - fig.setTileYLabel(n, str) sets the Y-axis label on tile n - using the figure theme's ForegroundColor. - Can be called before or after render(). - -#### `renderAll(obj, parentProgressBar)` - -RENDERALL Render all tiles that haven't been rendered yet. - fig.renderAll() renders all tiles and makes the figure visible. - fig.renderAll(parentProgressBar) renders as a child of a - dock or parent progress context (skips figure show/drawnow). - -#### `render(obj)` - -RENDER Alias for renderAll. - fig.render() is a convenience alias for fig.renderAll(). - -#### `reapplyTheme(obj)` - -REAPPLYTHEME Re-apply theme to figure and all rendered tiles. - fig.reapplyTheme() updates the figure background and - propagates the current Theme to every rendered tile. - Per-tile theme overrides (from setTileTheme) are merged - on top of the figure-level theme before propagation. - -#### `startLive(obj, filepath, updateFcn, varargin)` - -STARTLIVE Start live mode on the dashboard. - fig.startLive(filepath, updateFcn) - fig.startLive(filepath, updateFcn, 'Interval', 1) - -#### `stopLive(obj)` - -STOPLIVE Stop live polling. - fig.stopLive() stops and deletes the internal timer, then - sets LiveIsActive to false. Safe to call when not active. - -#### `refresh(obj)` - -REFRESH Manual one-shot reload. - fig.refresh() loads the LiveFile, calls LiveUpdateFcn, - and reloads the metadata file if configured. Errors if - no live source has been configured via startLive(). - -#### `setViewMode(obj, mode)` - -SETVIEWMODE Set view mode on all tiles. - fig.setViewMode(mode) sets LiveViewMode on the figure and - propagates it to every non-empty tile. - -#### `runLive(obj)` - -RUNLIVE Blocking poll loop for live mode (Octave compatibility). - fig.runLive() enters a blocking while-loop that polls - LiveFile at LiveInterval. On MATLAB, this is a no-op if - the timer is already running. On Octave (which lacks the - timer object), this provides equivalent functionality. - The loop exits when LiveIsActive becomes false or the - figure is closed. An onCleanup guard calls stopLive(). - -#### `pos = computeTilePosition(obj, n)` - -COMPUTETILEPOSITION Calculate normalized [x y w h] for tile n. - pos = computeTilePosition(obj, n) computes the normalized - position vector for tile n, accounting for grid position, - Padding, GapH, GapV, tile spanning (TileSpans), and - ContentOffset. Tiles are numbered in row-major order with - top-left origin, then converted to MATLAB's bottom-left - coordinate system. - ---- - -## `HoverCrosshair` --- Hover-driven vertical crosshair + multi-line datatip for FastSense. - -> Inherits from: `handle` - -hc = HOVERCROSSHAIR(fp) attaches a hover crosshair to a rendered - FastSense instance fp. While the mouse is over fp.hAxes, a vertical - line tracks the cursor's x position and a small datatip near the - cursor shows the formatted x value plus one row per visible line - (DisplayName + interpolated y at hovered x via binary_search). - - The handler is *chained*: any pre-existing WindowButtonMotionFcn - on the figure is preserved and invoked first on every motion event, - so this class coexists with other hover-driven features - (FastSenseToolbar crosshair toggle, NavigatorOverlay drag, etc.). - - Properties (read-only): - Target — FastSense instance - hFigure, hAxes — cached graphics handles - hLineV — vertical crosshair line - hTipBox — text annotation acting as datatip box - - Methods: - onMove(xQuery) — update + show crosshair at xQuery (data coords) - onLeave() — hide crosshair + datatip - delete() — restore prior WindowButtonMotionFcn and clean up - - Coexistence with FastSenseToolbar: - The toolbar's setCrosshair() also swaps WindowButtonMotionFcn at - activation. Because we only chain whatever handler was installed - when our constructor ran, when the toolbar later overwrites the - callback our hover handler is temporarily detached (toolbar mode - wins). When the toolbar deactivates and restores its saved - callback (which is *our* chained handler), hover resumes - automatically. - -### Constructor - -```matlab -obj = HoverCrosshair(fp) -``` - -HOVERCROSSHAIR Construct hover crosshair attached to a FastSense. - hc = HOVERCROSSHAIR(fp) requires fp to be a rendered FastSense - handle. Throws HoverCrosshair:invalidTarget if not. - -### Methods - -#### `onMove(obj, xQuery)` - -ONMOVE Update + show the crosshair at data x-coordinate xQuery. - Public so tests can drive motion deterministically without - needing real mouse input. - -#### `onLeave(obj)` - -ONLEAVE Hide the crosshair line + datatip. - diff --git a/wiki/API-Reference:-Sensors.md b/wiki/API-Reference:-Sensors.md index 1118e932..e4c1b74b 100644 --- a/wiki/API-Reference:-Sensors.md +++ b/wiki/API-Reference:-Sensors.md @@ -2,512 +2,464 @@ # API Reference: Sensors -## `BatchTagPipeline` --- Synchronous raw-data -> per-tag .mat pipeline. +## `Tag` --- Abstract base for the unified Tag domain model. > Inherits from: `handle` -Enumerates TagRegistry for ingestable tags (SensorTag/StateTag - with a non-empty RawSource), de-duplicates file reads, parses - each raw file once, slices the requested column per tag, and - writes /.mat in the SensorTag.load shape. +Tag is the root of the v2.0 domain hierarchy. Subclasses + (SensorTag, StateTag, MonitorTag, CompositeTag) provide concrete + implementations of the six abstract-by-convention methods. - Batch semantics (D-12, D-15, D-18): - - OutputDir required at construction; auto-created if missing. - - run() returns a report struct; throws TagPipeline:ingestFailed - at end-of-run if any tag failed. - - Each tag's ingest is a try/catch boundary; one failing tag - does NOT abort the batch. + Tag uses the Octave-safe "throw-from-base" abstract pattern: + the base class provides stub methods that raise a notImplemented + error, and subclasses override with concrete implementations. + Do NOT use the Abstract-methods block pattern here — it has + divergent semantics between MATLAB and Octave (see DataSource.m + for the proven pattern used here). - Observability (Major-2 / revision-1): - - LastFileParseCount: public SetAccess=private property - recording the number of DISTINCT raw files parsed in the - most recent run(). Captured BEFORE the end-of-run cache - reset. Enables testFileCacheDedup to assert exact dedup - without wrapping readRawDelimited_ (blocked by MATLAB's - private-folder scoping). + Tag Properties (public): + Key — char: unique identifier (required, non-empty) + Name — char: human-readable name (defaults to Key) + Units — char: measurement unit + Description — char: free-text description + Labels — cellstr: cross-cutting classification (META-01) + Metadata — struct: open key-value bag (META-03) + Criticality — char enum: 'low'|'medium'|'high'|'safety' (META-04) + SourceRef — char: optional provenance string - Errors (namespaced under TagPipeline:*): - TagPipeline:invalidOutputDir -- OutputDir missing / empty - TagPipeline:cannotCreateOutputDir -- mkdir failed - TagPipeline:ingestFailed -- 1+ tags failed (end-of-run throw) - TagPipeline:unknownExtension -- file ext not .csv/.txt/.dat + Tag Methods (abstract-by-convention — subclass must implement): + getXY — return [X, Y] data vectors + valueAt(t) — return scalar value at time t + getTimeRange — return [tMin, tMax] + getKind — return kind string ('sensor'|'state'|'monitor'|'composite'|'mock') + toStruct — return serializable struct + fromStruct (Static) — reconstruct from struct + + Tag Methods (default hooks — override when needed): + resolveRefs(registry) — Pass-2 deserialization hook; default no-op ### Constructor ```matlab -obj = BatchTagPipeline(varargin) +obj = Tag(key, varargin) ``` -BATCHTAGPIPELINE Construct with required OutputDir NV-pair. - p = BatchTagPipeline('OutputDir', dir) - p = BatchTagPipeline('OutputDir', dir, 'Verbose', true) +TAG Construct a Tag with required key and optional name-value pairs. ### Properties | Property | Default | Description | |----------|---------|-------------| -| OutputDir | `''` | | -| Verbose | `false` | | +| Key | `''` | char: unique identifier | +| Name | `''` | char: human-readable name | +| Units | `''` | char: measurement unit | +| Description | `''` | char: free-text description | +| Labels | `{}` | cellstr: cross-cutting classification | +| Metadata | `struct()` | struct: open key-value bag | +| Criticality | `'medium'` | char enum: 'low'\|'medium'\|'high'\|'safety' | +| SourceRef | `''` | char: optional provenance string | +| EventStore | `[]` | EventStore handle; [] disables event convenience methods | ### Methods -#### `report = run(obj)` +#### `set()` -RUN Enumerate tags, ingest each, write per-tag .mat; throw at end if any failed. - Returns a report struct with fields: - succeeded - cellstr of tag keys that wrote OK - failed - struct array of failed tags (key, file, errorId, message) +SET.CRITICALITY Validate enum before assigning. -#### `setWriteFnForTesting_(obj, fn)` +#### `[X, Y] = getXY(obj)` -SETWRITEFNFORTESTING_ Internal-only DI seam for .mat write suppression. - Phase 1028 plan 02b: replace the default @writeTagMat_ with a - user-supplied function handle (e.g., a no-op for benchmark NoIO - measurement). Production callers MUST NOT use this — the - default cadence per D-12 is write-on-every-tick. +GETXY Return [X, Y] data vectors. Subclass must override. -#### `setFsCoalesceForTesting_(obj, tf)` +#### `v = valueAt(obj, t)` -SETFSCOALESCEFORTESTING_ Shape-parity setter mirroring LiveTagPipeline (plan 06). - Phase 1028 plan 06: BatchTagPipeline.run() does not currently - issue per-tag exist/dir/datenum syscalls (parsing happens via - parseOrCache_, which uses ext-based dispatch, not file stats), - so fs-stat coalescing is a no-op here. The setter exists for - symmetry with LiveTagPipeline so tests/bench scripts can - configure both pipelines uniformly. Hidden (D-10). +VALUEAT Return scalar value at time t. Subclass must override. -#### `setCoalesceActiveForTesting_(obj, tf)` +#### `[tMin, tMax] = getTimeRange(obj)` -SETCOALESCEACTIVEFORTESTING_ Shape-parity setter mirroring LiveTagPipeline (plan 05). - Phase 1028 plan 05: BatchTagPipeline.run() does not currently - accumulate a listener cascade (it writes 'overwrite' mode and - does not call tag.updateData()), so coalescing is a no-op - here. The setter exists for symmetry with LiveTagPipeline so - tests/bench scripts can configure both pipelines uniformly. - Hidden (D-10). +GETTIMERANGE Return [tMin, tMax] time bounds. Subclass must override. -#### `setCacheActiveForTesting_(obj, tf)` +#### `k = getKind(obj)` -SETCACHEACTIVEFORTESTING_ Internal-only setter for the prior-state cache. - Phase 1028 plan 02d: enable/disable the in-memory priorState_ cache. - Mirror of LiveTagPipeline.setCacheActiveForTesting_; production callers - MUST NOT use this — cache-on is the production default and is byte-for-byte - parity-tested against the cache-off path. Hidden so it does not appear in - tab-completion, doc(), or properties() listings (D-10). +GETKIND Return kind string. Subclass must override. ---- +#### `s = toStruct(obj)` -## `CompositeTag` --- Aggregate MonitorTag/CompositeTag children into a 0/1 derived series. +TOSTRUCT Return serializable struct. Subclass must override. -> Inherits from: `Tag` +#### `resolveRefs(obj, registry)` -CompositeTag < Tag -- a derived-signal Tag that aggregates 1..N - MonitorTag/CompositeTag children into a single 0/1 (or 0..1 - severity-pre-threshold) time series via k-way merge-sort ZOH - streaming (implemented in Plan 02; Plan 01 ships the core API only: - constructor, addChild cycle-DFS + type-guard + listener hookup, and - the 7-mode aggregator helper). +RESOLVEREFS Pass-2 hook for two-phase deserialization. + Default: no-op. CompositeTag (Phase 1008) will override to + wire up children by key. Leaf tags (Sensor/State/Monitor) + do not need references resolved. - Truth Table (binary 0/1 inputs; NaN = unknown): +#### `addManualEvent(obj, tStart, tEnd, label, message)` - AND: - | c1 | c2 | out | - | 0 | 0 | 0 | - | 0 | 1 | 0 | - | 1 | 1 | 1 | - | 0 | NaN | NaN | - | 1 | NaN | NaN | - | NaN | NaN | NaN | +ADDMANUALEVENT Create a manual annotation event bound to this tag. + tag.addManualEvent(tStart, tEnd, label, message) creates an Event + with Category = 'manual_annotation' and TagKeys = {obj.Key}, + appends to the bound EventStore, and registers in EventBinding. - OR: - | c1 | c2 | out | - | 0 | 0 | 0 | - | 0 | 1 | 1 | - | 1 | 1 | 1 | - | 0 | NaN | 0 | (other operand wins) - | 1 | NaN | 1 | (other operand wins) - | NaN | NaN | NaN | +#### `events = eventsAttached(obj)` - WORST: max(vals) ignoring NaN; all-NaN -> NaN. Matches - MATLAB `max([...], 'omitnan')` semantics. - COUNT: sum of (vals >= 0.5) ignoring NaN; then thresholded - by obj.Threshold to 0/1. - MAJORITY: #ones > (#non-NaN)/2 -> 1; all-NaN -> NaN. Strictly - binary 0/1 inputs for v2.0 (multi-state deferred). - SEVERITY: weighted avg (sum(w_i*v_i)/sum(w_i)) over non-NaN, - then thresholded by obj.Threshold to 0/1. All-NaN or - zero-weight -> NaN. - USER_FN: obj.UserFn(vals) -- caller handles NaN semantics. +EVENTSATTACHED Query events bound to this tag via EventBinding. + Returns Event array (possibly empty). This is a query, NOT a + stored property -- no Event handles on Tag (Pitfall 4). - Properties (public): - AggregateMode -- 'and'|'or'|'majority'|'count'|'worst'|'severity'|'user_fn' - UserFn -- function_handle; required when mode=='user_fn' - Threshold -- double; for COUNT/SEVERITY binarization (default 0.5) +#### `ll = getListeners_(obj)` - Methods (public): - addChild(tagOrKey, 'Weight', w) -- resolves string keys via TagRegistry; - cycle DFS (Key-equality per RESEARCH §7); - rejects SensorTag/StateTag - invalidate() / addListener(m) -- observer pattern (inherited shape) - getChildCount / getChildKeys -- read-only inspection probes - getChildWeights / isDirty -- read-only inspection probes - getChildAt(i) -- i-th child Tag handle (3-deep descent) - getKind() -- returns 'composite' +GETLISTENERS_ Default accessor returning empty cell (Phase 1028 plan 05). + Subclasses that maintain a listener cell (SensorTag, + StateTag, MonitorTag, CompositeTag, DerivedTag) override + this to expose their private `listeners_` property for + `Tag.invalidateBatch_` to walk. The Tag base returns {} — + abstract Tag has no listeners. - Methods (Tag contract -- Plan 02 merge-sort + serialization): - getXY() -- lazy-memoized union-of-timestamps grid via - RESEARCH §5 vectorized sort-based merge - (no set union, no linear interpolation; ALIGN-03) - valueAt(t) -- COMPOSITE-06 fast path; aggregates - child.valueAt(t) without materializing series - getTimeRange() -- [X(1), X(end)] of the aggregated grid - toStruct() -- serialize to {kind, key, ..., childkeys, - childweights, aggregatemode, threshold} - fromStruct(s) -- Static Pass-1 ctor; stashes ChildKeys_ for Pass-2 - resolveRefs(r) -- Pass-2 wiring; iterates ChildKeys_ and calls - obj.addChild(registry(k), 'Weight', w) per child +### Static Methods - Error IDs (locked): - CompositeTag:cycleDetected -- addChild would create cycle - (self or deeper via Key-equality DFS) - CompositeTag:invalidChildType -- child is not MonitorTag/CompositeTag - CompositeTag:invalidAggregateMode -- AggregateMode not in 7-mode list - CompositeTag:userFnRequired -- mode=='user_fn' but UserFn empty - CompositeTag:unknownOption -- constructor NV-pair unknown - CompositeTag:invalidListener -- addListener target lacks invalidate() - CompositeTag:dataMismatch -- fromStruct missing required .key - CompositeTag:unresolvedChild -- resolveRefs key not in registry - CompositeTag:indexOutOfBounds -- getChildAt index out of range +#### `Tag.obj = fromStruct(s)` - Cycle-detection note (RESEARCH §7 / Pitfall 3 Octave SIGILL): - CompositeTag EXPLICITLY creates listener cycles (addChild wires - composite as listener on child). Octave's `isequal`/`==` on - user-defined handles recurses through listener cells and hits - SIGILL. Use Key equality (`strcmp(a.Key, b.Key)`) for all handle - identity checks -- TagRegistry enforces globally-unique keys so - Key equality is semantically equivalent to handle equality within - a registry session AND Octave-safe. +FROMSTRUCT Reconstruct a Tag from a struct. Subclass must override. -### Constructor +#### `Tag.invalidateBatch_(tagSet)` -```matlab -obj = CompositeTag(key, aggregateMode, varargin) -``` +INVALIDATEBATCH_ Coalesced invalidation across many tags (Phase 1028 plan 05). -COMPOSITETAG Construct a CompositeTag with aggregation mode + Tag NV pairs. - c = CompositeTag(key) -- mode defaults to 'and' - c = CompositeTag(key, mode) -- mode in the 7-mode set - c = CompositeTag(key, mode, NV, NV, ...) -- Tag + CompositeTag NV pairs +--- -### Properties +## `SensorTag` --- Concrete Tag subclass for sensor time-series data. -| Property | Default | Description | -|----------|---------|-------------| -| AggregateMode | `'and'` | 'and'\|'or'\|'majority'\|'count'\|'worst'\|'severity'\|'user_fn' | -| UserFn | `[]` | function_handle; required for 'user_fn' | -| Threshold | `0.5` | for COUNT/SEVERITY binarization | +> Inherits from: `Tag` -### Methods +SensorTag is the primary sensor data carrier in the Tag-based domain + model. It stores time-series data (X, Y) directly and satisfies the + Tag contract (getXY, valueAt, getTimeRange, getKind='sensor', + toStruct, fromStruct). Data-role methods (load, toDisk, toMemory, + isOnDisk) operate on the inlined private properties. -#### `addChild(obj, tagOrKey, varargin)` + Properties (Dependent): DataStore -- read-only view of the disk store. -ADDCHILD Attach a MonitorTag/CompositeTag child with optional Weight. - addChild(tagHandle) -- handle path - addChild('keyString') -- registry-resolved path - addChild(tagOrKey, 'Weight', w) -- SEVERITY-mode weight (default 1.0) + Constructor accepts Tag universals (Name, Units, Description, + Labels, Metadata, Criticality, SourceRef), sensor extras (ID, + Source, MatFile, KeyName), and inline 'X'/'Y' data arrays. -#### `invalidate(obj)` +### Constructor -INVALIDATE Clear cache + mark dirty; cascade to downstream listeners. +```matlab +obj = SensorTag(key, varargin) +``` -#### `addListener(obj, m)` +SENSORTAG Construct a SensorTag with inlined data storage. + t = SensorTag(key) creates a SensorTag with the given key. -ADDLISTENER Register a listener notified when this composite invalidates. - Errors: CompositeTag:invalidListener if ~ismethod(m, 'invalidate'). +### Methods -#### `n = getChildCount(obj)` +#### `ds = get()` -GETCHILDCOUNT Return the number of attached children. +GET.DATASTORE Return the disk-backed DataStore (read-only view). -#### `keys = getChildKeys(obj)` +#### `v = get()` -GETCHILDKEYS Return a cellstr of child Keys (order preserved). +GET.X Read-only access to timestamps (backward-compat with legacy Sensor.X). -#### `w = getChildWeights(obj)` +#### `v = get()` -GETCHILDWEIGHTS Return a numeric row vector of child weights. +GET.Y Read-only access to values (backward-compat with legacy Sensor.Y). -#### `tf = isDirty(obj)` +#### `v = get()` -ISDIRTY Return whether the composite cache is stale. +GET.THRESHOLDS Always empty cell array (backward-compat stub). + Legacy Sensor class exposed a Thresholds cell array of + ThresholdRule handles. In the v2.0 Tag model, thresholds + are expressed as MonitorTag children bound via TagRegistry + — not as a nested collection on the sensor. Widgets that + still read .Thresholds (GaugeWidget, StatusWidget) see an + empty cell here and fall through to their "no thresholds" + branch. Consumers should migrate to the TagRegistry + + MonitorTag workflow for threshold behaviour. -#### `k = getKind(~)` +#### `r = get()` -GETKIND Return the literal kind identifier 'composite'. +GET.RAWSOURCE Return the raw-data source binding (read-only view). + Populated only for SensorTags whose 'RawSource' NV-pair was + set at construction. Consumed by BatchTagPipeline / + LiveTagPipeline to locate the raw file + column for this tag. -#### `[x, y] = getXY(obj)` +#### `[X, Y] = getXY(obj)` -GETXY Lazy-memoized union-of-timestamps grid via merge-sort streaming. - Aggregates every child's (X, Y) via the RESEARCH §5 - vectorized sort-based algorithm (no set-union, no linear - interpolation). Drops samples before `max(child.X(1))` - per ALIGN-03. Cache stays warm across calls; invalidate() - (cascade from any child) clears it. +GETXY Return X, Y by reference (zero-copy via COW). + MATLAB copy-on-write guarantees no memory allocation until + the caller mutates X or Y. #### `v = valueAt(obj, t)` -VALUEAT COMPOSITE-06 fast-path -- aggregate child.valueAt(t). - Iterates children and aggregates their instantaneous - scalar values; NEVER materializes the full series. Does - NOT increment recomputeCount_ and does NOT warm the cache. - At N=8 children, depth 3, log(M)=17 -> ~400 ops per call - (sub-microsecond vs. ~150ms for a full getXY). +VALUEAT Return Y at the last index where X <= t (ZOH, clamped). + Returns NaN on empty data. #### `[tMin, tMax] = getTimeRange(obj)` -GETTIMERANGE Return [X(1), X(end)] of the aggregated grid. - Warms the merge-sort cache if cold. Returns [NaN NaN] when - there are no children or any child has no data. +GETTIMERANGE Return [X(1), X(end)]. [NaN NaN] if empty. + +#### `k = getKind(obj)` + +GETKIND Return the literal kind identifier 'sensor'. #### `s = toStruct(obj)` -TOSTRUCT Serialize CompositeTag to a plain struct. - Emits {kind='composite', key, name, labels, metadata, - criticality, units, description, sourceref, aggregatemode, - threshold, childkeys, childweights}. UserFn is NOT - serialized (function handles cannot round-trip); consumers - must re-bind UserFn after loadFromStructs for 'user_fn' mode. - childkeys is double-wrapped (cell-in-cell) to survive the - MATLAB struct() cellstr-collapse idiom; fromStruct unwraps. +TOSTRUCT Serialize SensorTag state to a plain struct. + Tag universals at the top level; sensor-specific extras + nested under s.sensor (only when non-default) to keep the + struct compact. X/Y are INTENTIONALLY OMITTED -- runtime + data, not serialization state. -#### `resolveRefs(obj, registry)` +#### `load(obj, matFile)` -RESOLVEREFS Pass-2 hook -- wire stashed ChildKeys_ via addChild. - Called by TagRegistry.loadFromStructs (and local two-pass - loaders during Plan 02 tests). Re-uses the validated - addChild path so type guard + cycle DFS + listener hookup - all run on deserialized children. +LOAD Load sensor data from a .mat file. + t.load() uses the already-configured MatFile. + t.load(path) sets MatFile before loading. -#### `tag = getChildAt(obj, i)` +#### `toDisk(obj)` -GETCHILDAT Return the Tag handle of the i-th child (1-based). - Test-affordance API for 3-deep descent assertions - (Pitfall 8 round-trip). Not a mutation path -- child - insertion goes through addChild. +TODISK Move X/Y data to disk-backed FastSenseDataStore. + Clears X_ and Y_ from memory after transfer. + +#### `toMemory(obj)` + +TOMEMORY Load disk-backed data back into memory. + +#### `tf = isOnDisk(obj)` + +ISONDISK True if sensor data is stored on disk. + +#### `addListener(obj, m)` + +ADDLISTENER Register a listener notified on underlying data change. + Listener must implement an invalidate() method. Strong + reference -- caller manages lifecycle. + +#### `updateData(obj, X, Y)` + +UPDATEDATA Replace X/Y data and fire listeners. #### `ll = getListeners_(obj)` GETLISTENERS_ Internal accessor for Tag.invalidateBatch_ (Phase 1028 plan 05). Returns the private listeners_ cell. Hidden so it does not appear in tab-completion / doc(); not part of public API - (D-10). + (D-10). Mirrors getListeners_ on StateTag, MonitorTag, + CompositeTag, DerivedTag. ### Static Methods -#### `CompositeTag.out = aggregateForTesting(vals, weights, mode, userFn, threshold)` - -AGGREGATEFORTESTING Public test-probe wrapper over private aggregate_. - Exists SOLELY so suite/flat tests can exercise the truth - tables without materializing a full CompositeTag + children - graph. Not part of the stable public API -- consumers - should use getXY() / valueAt() instead (Plan 02). - -#### `CompositeTag.obj = fromStruct(s)` +#### `SensorTag.obj = fromStruct(s)` -FROMSTRUCT Pass-1 reconstruction from a toStruct output. - Constructs an empty-children CompositeTag and stashes - `ChildKeys_` + `ChildWeights_` for Pass-2 `resolveRefs` to - consume. UserFn is NOT restored -- consumers re-bind it - after loadFromStructs for 'user_fn' mode. +FROMSTRUCT Reconstruct SensorTag from a toStruct output. --- -## `DerivedTag` --- Continuous (X, Y) signal derived from N parent Tags via compute fn. +## `MonitorTag` --- Derived 0/1 binary time-series Tag — lazy-by-default, no persistence. > Inherits from: `Tag` -DerivedTag is the 5th concrete Tag class in the FastPlot Tag - hierarchy — the continuous-output counterpart to MonitorTag - (1 parent → 0/1) and CompositeTag (N children → 0/1). It produces - a full (X, Y) time series by applying a user-supplied compute - function (or compute object) to its parents' data. Output is - lazy-memoized on the first getXY() call and recomputed only when - invalidate() fires (directly or via a parent's DataChanged - listener notification — see addListener wiring in the constructor). +MonitorTag produces a binary alarm/ok signal by evaluating a + user-supplied ConditionFn against its Parent tag's (X, Y). Output + is cached on first read and recomputed only when invalidate() is + called (directly or via parent.updateData listener notification). - This Phase 1008-r2b implementation is lazy-by-default and in-memory - only — no DataStore persistence, no streaming appendData, no - debouncing. Future v2 features (Persist, appendData, MinDuration, - OnDataAvailable, multi-output, alignParentsZOH) are documented in - docs/DerivedTag-spec.md §11 (out of scope here). + This Phase 1006 implementation is lazy-by-default, no persistence — + no FastSense data store writes, no disk footprint. Opt-in persistence + arrives in Phase 1007 (MONITOR-09). - Lifecycle / cycle note (Pitfall 3 — Octave SIGILL): - Parents hold strong refs to DerivedTag via listeners_; DerivedTag - holds strong refs to Parents. This is intentional but creates a - handle cycle. ALL handle equality MUST use strcmp(a.Key, b.Key) - (TagRegistry guarantees globally-unique keys, so Key equality is - semantically equivalent to handle equality within a registry - session). Never use isequal/== on Tag handles — Octave SIGILLs - when recursing through listener cycles. + MONITOR-05 note: Phase 1006 (later plans) uses the existing Event + carrier fields SensorName = Parent.Key and ThresholdLabel = obj.Key. + Phase 1010 (EVENT-01) will migrate to a per-Tag keys field on Event. + Do NOT write a TagKeys field in this class — it does not exist on + Event yet (the carrier pattern uses SensorName + ThresholdLabel). - Properties (public): - Parents — 1×N cell of Tag handles (required at construction) - ComputeFn — function_handle @(parents)->[X,Y], OR a handle - object with a method [X,Y] = compute(obj, parents). - Detected via ismethod(compute, 'compute'). - MinDuration — scalar double; reserved for v2 debouncing (default 0) - EventStore — EventStore handle; inherited from Tag base + MONITOR-10: Only event-level callbacks (OnEventStart, OnEventEnd) + are supported. Per-sample callbacks are a documented anti-pattern + (PI-AF side-effect pitfall). This class MUST NOT expose keywords + whose shape is a per-sample callback. - Tag-contract methods: - getXY — lazy-memoized; recomputes on dirty - valueAt(t) — ZOH lookup into the cached (X, Y) via binary_search - getTimeRange — [X(1), X(end)] or [NaN NaN] if empty - getKind — returns 'derived' - toStruct — serialize state. Function-handle ComputeFn stores - a func2str string but cannot round-trip — see §3.6 - of the spec; the user must reattach the real handle - after fromStruct or invocation raises - DerivedTag:computeNotRehydrated. Object-form - ComputeFn stores class name + (optional) toStruct - state and DOES round-trip. - fromStruct — Static Pass-1 reconstruction; stashes parentkeys - in ParentKeys_ for Pass-2 resolveRefs. - resolveRefs — Pass-2: bind real Parents from the registry and - register self as listener on each. + ALIGN: operates directly on parent's native grid via parent.getXY(). + No interp1 linear ever — ZOH is the only legal alignment when + aggregating across parents (CompositeTag in a later phase will + re-assert this contract via valueAt-on-common-grid). - DerivedTag-specific methods: - invalidate — clear cache, mark dirty, cascade to listeners - addListener(l) — register a downstream listener - notifyListeners_ — internal observer fan-out + Lifecycle: MonitorTag holds a Parent handle; Parent holds a strong + reference to MonitorTag via its listeners_ cell. To dispose, + unregister the monitor via TagRegistry.unregister AND reset the + parent's listener cell (or construct a fresh parent). - Error IDs (locked — see SPEC §4): - DerivedTag:invalidParents parents empty or non-Tag - DerivedTag:invalidCompute compute not fn handle / no compute() - DerivedTag:unknownOption unrecognized NV key - DerivedTag:invalidListener addListener target lacks invalidate() - DerivedTag:computeReturnedNonNumeric compute result non-numeric - DerivedTag:computeShapeMismatch X, Y length mismatch - DerivedTag:dataMismatch fromStruct missing required fields - DerivedTag:unresolvedParent resolveRefs missing key in registry - DerivedTag:cycleDetected cyclic parent graph (direct or transitive) - DerivedTag:nonSerializableCompute toStruct on opaque non-fn / non-object compute - DerivedTag:computeNotRehydrated deserialized invoked without ComputeFn rehydration + Properties (public): + Parent — Tag handle (required at construction) + ConditionFn — function_handle @(x,y)->logical (required) + AlarmOffConditionFn — function_handle; [] means no hysteresis + MinDuration — native parent-X units; 0 disables debounce + EventStore — EventStore handle; [] disables event emission + OnEventStart — function_handle @(event); [] disables + OnEventEnd — function_handle @(event); [] disables + Persist — logical; when true, derived (X, Y) is + cached to DataStore via storeMonitor on + every recompute_()/appendData() and loaded + on first getXY() (staleness-checked via + quad-signature). Default false — the opt-in + default enforces Pitfall 2 cache-invalidation + discipline: consumers that do not opt in + pay zero disk cost. + DataStore — FastSenseDataStore handle; required when + Persist=true. Provides storeMonitor / + loadMonitor / clearMonitor back-end. - Cycle detection: - The constructor runs a depth-first traversal over the parents' - ancestry chain. If newKey appears anywhere in any parent's - parents (transitively), DerivedTag:cycleDetected is raised at - construction time. The DFS uses strcmp(a.Key, b.Key) — never - handle equality — for Octave compatibility (see Pitfall 3 above). + Methods (Tag contract): + getXY — lazy-memoized 0/1 vector on parent's grid + valueAt(t) — ZOH lookup into getXY cache + getTimeRange — [X(1), X(end)]; [NaN NaN] if empty + getKind — returns 'monitor' + toStruct — serialize (no function handles, no data) + fromStruct (Static) — Pass-1 reconstruction (dummy parent) + resolveRefs(registry)— Pass-2 wire Parent + register listener - Compute strategy contract: - 1. Function handle: signature [X, Y] = fn(parents) where parents - is the same 1×N cell array passed to the constructor. - 2. Object: handle class instance with method - [X, Y] = compute(obj, parents). Detected at construction via - ismethod(compute, 'compute'). For round-tripping through - toStruct/fromStruct, the class SHOULD also implement a - toStruct() instance method and a fromStruct(s) static method - (mirrors the Tag pattern). Otherwise default-construction is - attempted at deserialization time. + Methods (additional): + invalidate — clear cache + mark dirty + appendData(newX,newY) — Phase 1007 (MONITOR-08) streaming tail. + Extends cache incrementally; preserves + hysteresis FSM state and MinDuration + bookkeeping across the append boundary. + Falls back to full recompute_() when + the cache is dirty/empty (cold start). - Recompute pipeline: - 1. Dispatch on isa(ComputeFn, 'function_handle') vs. - isobject(ComputeFn) && ismethod(ComputeFn, 'compute'). - 2. Validate result: numeric X and Y, equal length. - 3. Reshape both to row vectors and store in cache_. - 4. Clear dirty_ flag. + Error IDs: + MonitorTag:invalidParent — parentTag not a Tag + MonitorTag:invalidCondition — conditionFn not a function_handle + MonitorTag:unknownOption — unknown NV key or dangling key + MonitorTag:dataMismatch — fromStruct missing required fields + MonitorTag:unresolvedParent — Pass-2 parent key not in registry + MonitorTag:invalidData — appendData numeric/length mismatch + MonitorTag:persistDataStoreRequired — Persist=true but DataStore empty + MonitorTag:emitEventBadKind — emitEvent_ called with kind not in {start,closed,end} + MonitorTag:eventLogReentrantSkip — (warning ID) cluster-mode emission skipped due to + re-entrant per-tag lock acquire (Plan 02 will handle) - Listener / observer: - The constructor calls parent.addListener(obj) for every parent - that exposes addListener (SensorTag, StateTag, MonitorTag, - CompositeTag, DerivedTag all qualify; MockTag does too in - tests). Subsequent parent.updateData(...) → parent.invalidate - fan-out → DerivedTag.invalidate → notifyListeners_ cascades to - any downstream MonitorTag/DerivedTag wrapping this one. This - mirrors MonitorTag's listener wiring exactly. + Deferred-notify (Pitfall 13 prevention): + OnEventStart / OnEventEnd callbacks are NOT invoked during the emission body. + They are queued on pendingNotify_ and flushed by flushPendingNotify_() AFTER + the emission loop completes, with inEmission_ = false. + Pre-refactor: listeners fired synchronously DURING EventStore.append. + Post-refactor: listeners fire immediately AFTER appendData/getXY returns, + but OUTSIDE the emission window. The "event was emitted" semantic is preserved; + only the timing changes from synchronous-during-append to post-emission-batch. - No DataChanged event in invalidate (Pitfall 5): - Cache invalidation does NOT fire `notify(obj, 'DataChanged')` — - only SensorTag.updateData and StateTag mutators do. DerivedTag - fires events implicitly via downstream consumers pulling getXY. - This avoids flap loops in deeply-chained derivation graphs. + Persistence (Phase 1007 MONITOR-09): + Opt-in via Persist=true + DataStore. Staleness detection uses a + quad-signature (parent_key, num_points, parent_xmin, parent_xmax) + stamped at write. Default-off preserves Pitfall 2 cache-invalidation + safety — consumers that do not opt in pay zero disk cost. ### Constructor ```matlab -obj = DerivedTag(key, parents, compute, varargin) +obj = MonitorTag(key, parentTag, conditionFn, varargin) ``` -DERIVEDTAG Construct a DerivedTag with N parents and a compute strategy. - d = DerivedTag(key, parents, compute) creates a DerivedTag - whose output is compute(parents) lazy-evaluated on first - getXY() and recomputed automatically when any parent's - updateData fires. +MONITORTAG Construct a MonitorTag. + m = MonitorTag(key, parentTag, conditionFn) creates a lazy + binary monitor whose output is conditionFn(parentTag.X, + parentTag.Y) aligned to parent's native grid. ### Properties | Property | Default | Description | |----------|---------|-------------| -| Parents | `{}` | 1×N cell of Tag handles (required at construction) | -| ComputeFn | `[]` | function_handle, OR object with compute() method | -| MinDuration | `0` | reserved for v2 debouncing; unused in v1 | +| Parent | | Tag handle (required) | +| ConditionFn | | function_handle @(x,y) -> logical (required) | +| AlarmOffConditionFn | `[]` | function_handle; [] means no hysteresis | +| MinDuration | `0` | native parent-X units; 0 disables debounce | +| OnEventStart | `[]` | function_handle @(event); [] disables callback | +| OnEventEnd | `[]` | function_handle @(event); [] disables callback | +| Persist | `false` | MONITOR-09 opt-in (Pitfall 2 default-off) | +| DataStore | `[]` | FastSenseDataStore handle; required when Persist=true | +| EventLog | `[]` | libs/Concurrency/EventLog.m handle; non-empty triggers cluster-mode emission | ### Methods -#### `[X, Y] = getXY(obj)` +#### `[x, y] = getXY(obj)` -GETXY Return lazy-memoized (X, Y) — recomputes on dirty. +GETXY Return lazy-memoized 0/1 vector aligned to parent's grid. + When Persist=true + DataStore bound, first attempts a disk + load via tryLoadFromDisk_ (quad-signature staleness check). + On miss or stale cache, falls through to recompute_() and + then persistIfEnabled_() writes the fresh row. #### `v = valueAt(obj, t)` -VALUEAT Right-biased ZOH lookup into the cached (X, Y). - Mirrors StateTag.valueAt structure exactly: scalar branch - uses a single binary_search call; vector branch loops one - binary_search per query. Returns NaN-filled output if the - compute returned an empty series. +VALUEAT ZOH lookup into the cached 0/1 series. + Returns NaN if parent has no data. #### `[tMin, tMax] = getTimeRange(obj)` -GETTIMERANGE Return [X(1), X(end)] from getXY; [NaN NaN] if empty. +GETTIMERANGE Return [X(1), X(end)]; [NaN NaN] if empty. -#### `k = getKind(~)` +#### `k = getKind(obj)` -GETKIND Return the literal kind identifier 'derived'. +GETKIND Return the kind identifier 'monitor'. #### `s = toStruct(obj)` -TOSTRUCT Serialize state to a plain struct. - Function-handle ComputeFn cannot round-trip cleanly - (closures, anonymous fns) — toStruct stores - s.computekind = 'function_handle' and s.computestr = - func2str(...). fromStruct leaves a sentinel that errors - with DerivedTag:computeNotRehydrated until the user - reattaches the real handle. +TOSTRUCT Serialize MonitorTag state to a plain struct. + Function handles are NOT serialized — consumers re-bind + ConditionFn / AlarmOffConditionFn / EventStore / callbacks + after loadFromStructs. The Parent handle is stored as its + Key string (parentkey); resolveRefs wires the real handle + in Pass 2 of the two-phase loader. #### `resolveRefs(obj, registry)` -RESOLVEREFS Pass-2 hook to bind Parents from registry by key. - Iterates ParentKeys_ (stashed by fromStruct), fetches each - real handle from the registry, registers self as a listener - on each, and clears ParentKeys_. Forces dirty_ = true so - the next getXY() recomputes against the real parent data. +RESOLVEREFS Pass-2 hook to wire Parent from registry by key. + Called by TagRegistry.loadFromStructs. On success: + - obj.Parent is swapped to the real registry entry + - obj registers itself as a listener on the real parent + - obj.invalidate() clears any stale cache + - obj.ParentKey_ is cleared (consumed) #### `invalidate(obj)` INVALIDATE Clear cache + mark dirty; cascade to downstream listeners. - Called automatically when a parent's listener fan-out - reaches this DerivedTag (parent.updateData → - parent.notifyListeners_ → invalidate). Also called - directly by user code when ComputeFn semantics change. + MonitorTag itself is observable: downstream MonitorTags + (recursive chains) register as listeners and are invalidated + here so that a root-parent update propagates through the + full derivation chain. -#### `addListener(obj, l)` +#### `addListener(obj, m)` -ADDLISTENER Register a downstream listener. - l must implement an invalidate() method (any Tag in the - FastPlot domain qualifies; struct/handle objects with a - bespoke invalidate also work). Listeners are held by - strong reference — caller manages lifecycle. +ADDLISTENER Register a listener notified when this monitor invalidates. + Enables recursive MonitorTag chains — an outer MonitorTag + that wraps an inner MonitorTag registers as the inner's + listener so that root-parent updates cascade through. + +#### `tf = getInEmission_(obj)` + +GETINMISSION_ Test accessor: return true while inside an emission body. + Exists ONLY for test observability (deferred-notify proof in + TestListenerCannotAcquireLock). The trailing underscore marks it as + an internal accessor not intended for production callers. + +#### `appendData(obj, newX, newY)` + +APPENDDATA Extend cached (X, Y) with new tail samples — no full recompute. + Preserves hysteresis FSM state and MinDuration bookkeeping + across the append boundary (MONITOR-08). Events fire only + for runs that COMPLETE (reach a falling edge) inside newX: + a run still open at the tail end is carried as state for + the next appendData call; a run that was already open at + the cache end and closes inside newX fires ONE event with + StartTime = the original (carried) start. + +#### `set()` + +#### `set()` + +#### `set()` #### `ll = getListeners_(obj)` @@ -518,358 +470,212 @@ GETLISTENERS_ Internal accessor for Tag.invalidateBatch_ (Phase 1028 plan 05). ### Static Methods -#### `DerivedTag.obj = fromStruct(s)` +#### `MonitorTag.obj = fromStruct(s)` -FROMSTRUCT Pass-1 reconstruction with sentinel parents + stashed keys. - Required fields: s.key (non-empty char), s.parentkeys - (cellstr of length ≥ 1). Pass-2 resolveRefs(registry) is - responsible for swapping in real Parent handles. +FROMSTRUCT Pass-1 reconstruction from a toStruct output. + The real Parent handle is wired in Pass 2 via resolveRefs. + ConditionFn / AlarmOffConditionFn / EventStore / callbacks + are NOT restored — consumers must re-bind these after load. --- -## `LiveTagPipeline` --- Timer-driven raw-data -> per-tag .mat pipeline. - -> Inherits from: `handle` - -Mirrors MatFileDataSource's modTime + lastIndex state machine - over raw text files. Does NOT subclass LiveEventPipeline (D-14) - -- borrows the timer ergonomics only. +## `CompositeTag` --- Aggregate MonitorTag/CompositeTag children into a 0/1 derived series. - Live semantics (D-13, D-14, D-18): - - Each tick re-enumerates TagRegistry, stats each tag's RawSource.file. - - Files with advanced mtime are re-parsed ONCE (per-tick file cache). - - New rows (lastIndex+1 : total) are appended to /.mat. - - Append uses load->concat->save (Pitfall 2 guard); the writer - never uses the dash-append flag of save (which would clobber - the existing `data` variable rather than merge its fields). - - Per-tag try/catch: one tag's failure does NOT abort the tick. - - tagState_ entries GC'd each tick for tags no longer eligible. +> Inherits from: `Tag` - Cluster mode (Phase 1030, Plan 02): - - Enabled by passing 'SharedRoot' NV-pair to constructor. - - All shared .mat writes routed through TagWriteCoordinator + - AtomicWriter for safe multi-process access (REQ CONC-01). - - Single-user mode (no SharedRoot) exercises ZERO Concurrency- - library code paths (Success Criterion 5 / byte-identical guarantee). - - BusyMode='drop' is forced in cluster mode (Pitfall 7). - - Timer period is jittered +-25% in cluster mode (Pitfall 11). - - Lock contention causes per-tag skip-and-defer, not whole-tick block. +CompositeTag < Tag -- a derived-signal Tag that aggregates 1..N + MonitorTag/CompositeTag children into a single 0/1 (or 0..1 + severity-pre-threshold) time series via k-way merge-sort ZOH + streaming (implemented in Plan 02; Plan 01 ships the core API only: + constructor, addChild cycle-DFS + type-guard + listener hookup, and + the 7-mode aggregator helper). - Observability (Major-2 / revision-1): - - LastFileParseCount: public SetAccess=private property recording the - number of DISTINCT files parsed in the most recent tick. Captured - BEFORE the per-tick tickCache goes out of scope. Mirrors - BatchTagPipeline's mechanism so tests can assert dedup behavior - via direct property read rather than wrapping readRawDelimited_. + Truth Table (binary 0/1 inputs; NaN = unknown): - Cluster-mode observability (Phase 1030 Plan 02): - - SkippedTickCount: public SetAccess=private; incremented on lock - contention or BusyMode='drop' skip. - - LastTickDurationSec: public SetAccess=private; wall-clock duration - of the last onTick_ invocation. - - LastLockContentionEvent: public SetAccess=private; most recent - contention event struct {tagKey, holder.{user, host, age}}. + AND: + | c1 | c2 | out | + | 0 | 0 | 0 | + | 0 | 1 | 0 | + | 1 | 1 | 1 | + | 0 | NaN | NaN | + | 1 | NaN | NaN | + | NaN | NaN | NaN | - Shares readRawDelimited_ / selectTimeAndValue_ / writeTagMat_ with - BatchTagPipeline -- single source of truth for parse + shape + write. + OR: + | c1 | c2 | out | + | 0 | 0 | 0 | + | 0 | 1 | 1 | + | 1 | 1 | 1 | + | 0 | NaN | 0 | (other operand wins) + | 1 | NaN | 1 | (other operand wins) + | NaN | NaN | NaN | -### Constructor + WORST: max(vals) ignoring NaN; all-NaN -> NaN. Matches + MATLAB `max([...], 'omitnan')` semantics. + COUNT: sum of (vals >= 0.5) ignoring NaN; then thresholded + by obj.Threshold to 0/1. + MAJORITY: #ones > (#non-NaN)/2 -> 1; all-NaN -> NaN. Strictly + binary 0/1 inputs for v2.0 (multi-state deferred). + SEVERITY: weighted avg (sum(w_i*v_i)/sum(w_i)) over non-NaN, + then thresholded by obj.Threshold to 0/1. All-NaN or + zero-weight -> NaN. + USER_FN: obj.UserFn(vals) -- caller handles NaN semantics. -```matlab -obj = LiveTagPipeline(varargin) -``` - -LIVETAGPIPELINE Construct with OutputDir (required) + options. - p = LiveTagPipeline('OutputDir', dir) - p = LiveTagPipeline('OutputDir', dir, 'Interval', 5, 'Verbose', true) - p = LiveTagPipeline('OutputDir', dir, 'ErrorFcn', @(ex) ...) - p = LiveTagPipeline('OutputDir', dir, 'SharedRoot', root) % cluster mode - p = LiveTagPipeline('OutputDir', dir, 'SharedRoot', root, 'LockTimeout', 10) - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| OutputDir | `''` | | -| Interval | `15` | seconds | -| Status | `'stopped'` | 'stopped' \| 'running' \| 'error' | -| ErrorFcn | `[]` | optional @(ex) callback for tick-level errors | -| Verbose | `false` | | - -### Methods - -#### `start(obj)` - -START Launch the polling timer and set Status='running'. - -#### `stop(obj)` - -STOP Halt the polling timer; mirrors the pattern used by the - live-event pipeline class in libs/EventDetection/. - Pitfall 8 -- guard with isvalid + try/catch so stop() - during an in-flight tick doesn't cascade errors. - -#### `tickOnce(obj)` - -TICKONCE Run one tick synchronously (exposed for tests). - Production callers use start()/stop(); tests call this - to avoid pausing for timer intervals. - -#### `n = get()` - -GET.TAGSTATECOUNT Dependent property exposing tagState_.Count. - RESEARCH Q3 observability -- lets tests verify that entries - for unregistered tags are GC'd between ticks. - -#### `setWriteFnForTesting_(obj, fn)` - -SETWRITEFNFORTESTING_ Internal-only DI seam for .mat write suppression. - Phase 1028 plan 02b: replace the default @writeTagMat_ with a - user-supplied function handle (e.g., a no-op for benchmark NoIO - measurement). Production callers MUST NOT use this — the - default cadence per D-12 is write-on-every-tick. - -#### `setFsCoalesceForTesting_(obj, tf)` + Properties (public): + AggregateMode -- 'and'|'or'|'majority'|'count'|'worst'|'severity'|'user_fn' + UserFn -- function_handle; required when mode=='user_fn' + Threshold -- double; for COUNT/SEVERITY binarization (default 0.5) -SETFSCOALESCEFORTESTING_ Internal-only setter for per-tick fs-stat coalescing. - Phase 1028 plan 06: enable/disable the per-tick coalesced - filesystem-stat lookup inside onTick_. When ON (production - default), onTick_ issues ONE dir(parentDir) call per unique - raw-source parent directory at tick start and stores the - resulting basename->struct map; processTag_ consults that map - instead of issuing per-tag exist/dir/datenum syscalls. When - OFF, the per-tag fallback path runs (one exist + one dir per - tag) — used by the benchmark to isolate the coalescing win. + Methods (public): + addChild(tagOrKey, 'Weight', w) -- resolves string keys via TagRegistry; + cycle DFS (Key-equality per RESEARCH §7); + rejects SensorTag/StateTag + invalidate() / addListener(m) -- observer pattern (inherited shape) + getChildCount / getChildKeys -- read-only inspection probes + getChildWeights / isDirty -- read-only inspection probes + getChildAt(i) -- i-th child Tag handle (3-deep descent) + getKind() -- returns 'composite' -#### `setCoalesceActiveForTesting_(obj, tf)` + Methods (Tag contract -- Plan 02 merge-sort + serialization): + getXY() -- lazy-memoized union-of-timestamps grid via + RESEARCH §5 vectorized sort-based merge + (no set union, no linear interpolation; ALIGN-03) + valueAt(t) -- COMPOSITE-06 fast path; aggregates + child.valueAt(t) without materializing series + getTimeRange() -- [X(1), X(end)] of the aggregated grid + toStruct() -- serialize to {kind, key, ..., childkeys, + childweights, aggregatemode, threshold} + fromStruct(s) -- Static Pass-1 ctor; stashes ChildKeys_ for Pass-2 + resolveRefs(r) -- Pass-2 wiring; iterates ChildKeys_ and calls + obj.addChild(registry(k), 'Weight', w) per child -SETCOALESCEACTIVEFORTESTING_ Internal-only setter for end-of-tick listener coalescing. - Phase 1028 plan 05: enable/disable the A1+A2 end-of-tick - Tag.invalidateBatch_(updatedSet) call inside onTick_. - Production callers MUST NOT use this — coalescing-on is the - production default (coalesceActive_ = true). The setter exists - so the bench can measure the coalesce-on vs coalesce-off - delta against the dominant `other` bucket (per-tag dispatch + - listener cascade — see Plan 02d VERIFICATION.md). Hidden so - it does not appear in tab-completion / doc() (D-10). Mirrors - the plan-02b setWriteFnForTesting_ and plan-02d - setCacheActiveForTesting_ patterns. + Error IDs (locked): + CompositeTag:cycleDetected -- addChild would create cycle + (self or deeper via Key-equality DFS) + CompositeTag:invalidChildType -- child is not MonitorTag/CompositeTag + CompositeTag:invalidAggregateMode -- AggregateMode not in 7-mode list + CompositeTag:userFnRequired -- mode=='user_fn' but UserFn empty + CompositeTag:unknownOption -- constructor NV-pair unknown + CompositeTag:invalidListener -- addListener target lacks invalidate() + CompositeTag:dataMismatch -- fromStruct missing required .key + CompositeTag:unresolvedChild -- resolveRefs key not in registry + CompositeTag:indexOutOfBounds -- getChildAt index out of range -#### `setCacheActiveForTesting_(obj, tf)` + Cycle-detection note (RESEARCH §7 / Pitfall 3 Octave SIGILL): + CompositeTag EXPLICITLY creates listener cycles (addChild wires + composite as listener on child). Octave's `isequal`/`==` on + user-defined handles recurses through listener cells and hits + SIGILL. Use Key equality (`strcmp(a.Key, b.Key)`) for all handle + identity checks -- TagRegistry enforces globally-unique keys so + Key equality is semantically equivalent to handle equality within + a registry session AND Octave-safe. -SETCACHEACTIVEFORTESTING_ Internal-only setter for the prior-state cache. - Phase 1028 plan 02d: enable/disable the in-memory priorState_ cache - used to skip the on-disk load() in writeTagMat_('append',...). - Production callers MUST NOT use this — the cache is the production - default (cacheActive_ = true) and is byte-for-byte parity-tested - against the cache-off path. Disabling it is a benchmark feature for - measuring the load()-only cost (see bench_tag_pipeline_1k --cache-off). +### Constructor ---- +```matlab +obj = CompositeTag(key, aggregateMode, varargin) +``` -## `MonitorTag` --- Derived 0/1 binary time-series Tag — lazy-by-default, no persistence. +COMPOSITETAG Construct a CompositeTag with aggregation mode + Tag NV pairs. + c = CompositeTag(key) -- mode defaults to 'and' + c = CompositeTag(key, mode) -- mode in the 7-mode set + c = CompositeTag(key, mode, NV, NV, ...) -- Tag + CompositeTag NV pairs -> Inherits from: `Tag` +### Properties -MonitorTag produces a binary alarm/ok signal by evaluating a - user-supplied ConditionFn against its Parent tag's (X, Y). Output - is cached on first read and recomputed only when invalidate() is - called (directly or via parent.updateData listener notification). +| Property | Default | Description | +|----------|---------|-------------| +| AggregateMode | `'and'` | 'and'\|'or'\|'majority'\|'count'\|'worst'\|'severity'\|'user_fn' | +| UserFn | `[]` | function_handle; required for 'user_fn' | +| Threshold | `0.5` | for COUNT/SEVERITY binarization | - This Phase 1006 implementation is lazy-by-default, no persistence — - no FastSense data store writes, no disk footprint. Opt-in persistence - arrives in Phase 1007 (MONITOR-09). +### Methods - MONITOR-05 note: Phase 1006 (later plans) uses the existing Event - carrier fields SensorName = Parent.Key and ThresholdLabel = obj.Key. - Phase 1010 (EVENT-01) will migrate to a per-Tag keys field on Event. - Do NOT write a TagKeys field in this class — it does not exist on - Event yet (the carrier pattern uses SensorName + ThresholdLabel). +#### `addChild(obj, tagOrKey, varargin)` - MONITOR-10: Only event-level callbacks (OnEventStart, OnEventEnd) - are supported. Per-sample callbacks are a documented anti-pattern - (PI-AF side-effect pitfall). This class MUST NOT expose keywords - whose shape is a per-sample callback. +ADDCHILD Attach a MonitorTag/CompositeTag child with optional Weight. + addChild(tagHandle) -- handle path + addChild('keyString') -- registry-resolved path + addChild(tagOrKey, 'Weight', w) -- SEVERITY-mode weight (default 1.0) - ALIGN: operates directly on parent's native grid via parent.getXY(). - No interp1 linear ever — ZOH is the only legal alignment when - aggregating across parents (CompositeTag in a later phase will - re-assert this contract via valueAt-on-common-grid). +#### `invalidate(obj)` - Lifecycle: MonitorTag holds a Parent handle; Parent holds a strong - reference to MonitorTag via its listeners_ cell. To dispose, - unregister the monitor via TagRegistry.unregister AND reset the - parent's listener cell (or construct a fresh parent). +INVALIDATE Clear cache + mark dirty; cascade to downstream listeners. - Properties (public): - Parent — Tag handle (required at construction) - ConditionFn — function_handle @(x,y)->logical (required) - AlarmOffConditionFn — function_handle; [] means no hysteresis - MinDuration — native parent-X units; 0 disables debounce - EventStore — EventStore handle; [] disables event emission - OnEventStart — function_handle @(event); [] disables - OnEventEnd — function_handle @(event); [] disables - Persist — logical; when true, derived (X, Y) is - cached to DataStore via storeMonitor on - every recompute_()/appendData() and loaded - on first getXY() (staleness-checked via - quad-signature). Default false — the opt-in - default enforces Pitfall 2 cache-invalidation - discipline: consumers that do not opt in - pay zero disk cost. - DataStore — FastSenseDataStore handle; required when - Persist=true. Provides storeMonitor / - loadMonitor / clearMonitor back-end. +#### `addListener(obj, m)` - Methods (Tag contract): - getXY — lazy-memoized 0/1 vector on parent's grid - valueAt(t) — ZOH lookup into getXY cache - getTimeRange — [X(1), X(end)]; [NaN NaN] if empty - getKind — returns 'monitor' - toStruct — serialize (no function handles, no data) - fromStruct (Static) — Pass-1 reconstruction (dummy parent) - resolveRefs(registry)— Pass-2 wire Parent + register listener +ADDLISTENER Register a listener notified when this composite invalidates. + Errors: CompositeTag:invalidListener if ~ismethod(m, 'invalidate'). - Methods (additional): - invalidate — clear cache + mark dirty - appendData(newX,newY) — Phase 1007 (MONITOR-08) streaming tail. - Extends cache incrementally; preserves - hysteresis FSM state and MinDuration - bookkeeping across the append boundary. - Falls back to full recompute_() when - the cache is dirty/empty (cold start). +#### `n = getChildCount(obj)` - Error IDs: - MonitorTag:invalidParent — parentTag not a Tag - MonitorTag:invalidCondition — conditionFn not a function_handle - MonitorTag:unknownOption — unknown NV key or dangling key - MonitorTag:dataMismatch — fromStruct missing required fields - MonitorTag:unresolvedParent — Pass-2 parent key not in registry - MonitorTag:invalidData — appendData numeric/length mismatch - MonitorTag:persistDataStoreRequired — Persist=true but DataStore empty - MonitorTag:emitEventBadKind — emitEvent_ called with kind not in {start,closed,end} - MonitorTag:eventLogReentrantSkip — (warning ID) cluster-mode emission skipped due to - re-entrant per-tag lock acquire (Plan 02 will handle) +GETCHILDCOUNT Return the number of attached children. - Deferred-notify (Pitfall 13 prevention): - OnEventStart / OnEventEnd callbacks are NOT invoked during the emission body. - They are queued on pendingNotify_ and flushed by flushPendingNotify_() AFTER - the emission loop completes, with inEmission_ = false. - Pre-refactor: listeners fired synchronously DURING EventStore.append. - Post-refactor: listeners fire immediately AFTER appendData/getXY returns, - but OUTSIDE the emission window. The "event was emitted" semantic is preserved; - only the timing changes from synchronous-during-append to post-emission-batch. +#### `keys = getChildKeys(obj)` - Persistence (Phase 1007 MONITOR-09): - Opt-in via Persist=true + DataStore. Staleness detection uses a - quad-signature (parent_key, num_points, parent_xmin, parent_xmax) - stamped at write. Default-off preserves Pitfall 2 cache-invalidation - safety — consumers that do not opt in pay zero disk cost. +GETCHILDKEYS Return a cellstr of child Keys (order preserved). -### Constructor +#### `w = getChildWeights(obj)` -```matlab -obj = MonitorTag(key, parentTag, conditionFn, varargin) -``` +GETCHILDWEIGHTS Return a numeric row vector of child weights. -MONITORTAG Construct a MonitorTag. - m = MonitorTag(key, parentTag, conditionFn) creates a lazy - binary monitor whose output is conditionFn(parentTag.X, - parentTag.Y) aligned to parent's native grid. +#### `tf = isDirty(obj)` -### Properties +ISDIRTY Return whether the composite cache is stale. -| Property | Default | Description | -|----------|---------|-------------| -| Parent | | Tag handle (required) | -| ConditionFn | | function_handle @(x,y) -> logical (required) | -| AlarmOffConditionFn | `[]` | function_handle; [] means no hysteresis | -| MinDuration | `0` | native parent-X units; 0 disables debounce | -| OnEventStart | `[]` | function_handle @(event); [] disables callback | -| OnEventEnd | `[]` | function_handle @(event); [] disables callback | -| Persist | `false` | MONITOR-09 opt-in (Pitfall 2 default-off) | -| DataStore | `[]` | FastSenseDataStore handle; required when Persist=true | -| EventLog | `[]` | libs/Concurrency/EventLog.m handle; non-empty triggers cluster-mode emission | +#### `k = getKind(~)` -### Methods +GETKIND Return the literal kind identifier 'composite'. #### `[x, y] = getXY(obj)` -GETXY Return lazy-memoized 0/1 vector aligned to parent's grid. - When Persist=true + DataStore bound, first attempts a disk - load via tryLoadFromDisk_ (quad-signature staleness check). - On miss or stale cache, falls through to recompute_() and - then persistIfEnabled_() writes the fresh row. +GETXY Lazy-memoized union-of-timestamps grid via merge-sort streaming. + Aggregates every child's (X, Y) via the RESEARCH §5 + vectorized sort-based algorithm (no set-union, no linear + interpolation). Drops samples before `max(child.X(1))` + per ALIGN-03. Cache stays warm across calls; invalidate() + (cascade from any child) clears it. #### `v = valueAt(obj, t)` -VALUEAT ZOH lookup into the cached 0/1 series. - Returns NaN if parent has no data. +VALUEAT COMPOSITE-06 fast-path -- aggregate child.valueAt(t). + Iterates children and aggregates their instantaneous + scalar values; NEVER materializes the full series. Does + NOT increment recomputeCount_ and does NOT warm the cache. + At N=8 children, depth 3, log(M)=17 -> ~400 ops per call + (sub-microsecond vs. ~150ms for a full getXY). #### `[tMin, tMax] = getTimeRange(obj)` -GETTIMERANGE Return [X(1), X(end)]; [NaN NaN] if empty. - -#### `k = getKind(obj)` - -GETKIND Return the kind identifier 'monitor'. +GETTIMERANGE Return [X(1), X(end)] of the aggregated grid. + Warms the merge-sort cache if cold. Returns [NaN NaN] when + there are no children or any child has no data. #### `s = toStruct(obj)` -TOSTRUCT Serialize MonitorTag state to a plain struct. - Function handles are NOT serialized — consumers re-bind - ConditionFn / AlarmOffConditionFn / EventStore / callbacks - after loadFromStructs. The Parent handle is stored as its - Key string (parentkey); resolveRefs wires the real handle - in Pass 2 of the two-phase loader. +TOSTRUCT Serialize CompositeTag to a plain struct. + Emits {kind='composite', key, name, labels, metadata, + criticality, units, description, sourceref, aggregatemode, + threshold, childkeys, childweights}. UserFn is NOT + serialized (function handles cannot round-trip); consumers + must re-bind UserFn after loadFromStructs for 'user_fn' mode. + childkeys is double-wrapped (cell-in-cell) to survive the + MATLAB struct() cellstr-collapse idiom; fromStruct unwraps. #### `resolveRefs(obj, registry)` -RESOLVEREFS Pass-2 hook to wire Parent from registry by key. - Called by TagRegistry.loadFromStructs. On success: - - obj.Parent is swapped to the real registry entry - - obj registers itself as a listener on the real parent - - obj.invalidate() clears any stale cache - - obj.ParentKey_ is cleared (consumed) - -#### `invalidate(obj)` - -INVALIDATE Clear cache + mark dirty; cascade to downstream listeners. - MonitorTag itself is observable: downstream MonitorTags - (recursive chains) register as listeners and are invalidated - here so that a root-parent update propagates through the - full derivation chain. - -#### `addListener(obj, m)` - -ADDLISTENER Register a listener notified when this monitor invalidates. - Enables recursive MonitorTag chains — an outer MonitorTag - that wraps an inner MonitorTag registers as the inner's - listener so that root-parent updates cascade through. - -#### `tf = getInEmission_(obj)` - -GETINMISSION_ Test accessor: return true while inside an emission body. - Exists ONLY for test observability (deferred-notify proof in - TestListenerCannotAcquireLock). The trailing underscore marks it as - an internal accessor not intended for production callers. - -#### `appendData(obj, newX, newY)` - -APPENDDATA Extend cached (X, Y) with new tail samples — no full recompute. - Preserves hysteresis FSM state and MinDuration bookkeeping - across the append boundary (MONITOR-08). Events fire only - for runs that COMPLETE (reach a falling edge) inside newX: - a run still open at the tail end is carried as state for - the next appendData call; a run that was already open at - the cache end and closes inside newX fires ONE event with - StartTime = the original (carried) start. - -#### `set()` +RESOLVEREFS Pass-2 hook -- wire stashed ChildKeys_ via addChild. + Called by TagRegistry.loadFromStructs (and local two-pass + loaders during Plan 02 tests). Re-uses the validated + addChild path so type guard + cycle DFS + listener hookup + all run on deserialized children. -#### `set()` +#### `tag = getChildAt(obj, i)` -#### `set()` +GETCHILDAT Return the Tag handle of the i-th child (1-based). + Test-affordance API for 3-deep descent assertions + (Pitfall 8 round-trip). Not a mutation path -- child + insertion goes through addChild. #### `ll = getListeners_(obj)` @@ -880,142 +686,226 @@ GETLISTENERS_ Internal accessor for Tag.invalidateBatch_ (Phase 1028 plan 05). ### Static Methods -#### `MonitorTag.obj = fromStruct(s)` +#### `CompositeTag.out = aggregateForTesting(vals, weights, mode, userFn, threshold)` + +AGGREGATEFORTESTING Public test-probe wrapper over private aggregate_. + Exists SOLELY so suite/flat tests can exercise the truth + tables without materializing a full CompositeTag + children + graph. Not part of the stable public API -- consumers + should use getXY() / valueAt() instead (Plan 02). + +#### `CompositeTag.obj = fromStruct(s)` FROMSTRUCT Pass-1 reconstruction from a toStruct output. - The real Parent handle is wired in Pass 2 via resolveRefs. - ConditionFn / AlarmOffConditionFn / EventStore / callbacks - are NOT restored — consumers must re-bind these after load. + Constructs an empty-children CompositeTag and stashes + `ChildKeys_` + `ChildWeights_` for Pass-2 `resolveRefs` to + consume. UserFn is NOT restored -- consumers re-bind it + after loadFromStructs for 'user_fn' mode. --- -## `SensorTag` --- Concrete Tag subclass for sensor time-series data. +## `DerivedTag` --- Continuous (X, Y) signal derived from N parent Tags via compute fn. > Inherits from: `Tag` -SensorTag is the primary sensor data carrier in the Tag-based domain - model. It stores time-series data (X, Y) directly and satisfies the - Tag contract (getXY, valueAt, getTimeRange, getKind='sensor', - toStruct, fromStruct). Data-role methods (load, toDisk, toMemory, - isOnDisk) operate on the inlined private properties. +DerivedTag is the 5th concrete Tag class in the FastPlot Tag + hierarchy — the continuous-output counterpart to MonitorTag + (1 parent → 0/1) and CompositeTag (N children → 0/1). It produces + a full (X, Y) time series by applying a user-supplied compute + function (or compute object) to its parents' data. Output is + lazy-memoized on the first getXY() call and recomputed only when + invalidate() fires (directly or via a parent's DataChanged + listener notification — see addListener wiring in the constructor). - Properties (Dependent): DataStore -- read-only view of the disk store. + This Phase 1008-r2b implementation is lazy-by-default and in-memory + only — no DataStore persistence, no streaming appendData, no + debouncing. Future v2 features (Persist, appendData, MinDuration, + OnDataAvailable, multi-output, alignParentsZOH) are documented in + docs/DerivedTag-spec.md §11 (out of scope here). + + Lifecycle / cycle note (Pitfall 3 — Octave SIGILL): + Parents hold strong refs to DerivedTag via listeners_; DerivedTag + holds strong refs to Parents. This is intentional but creates a + handle cycle. ALL handle equality MUST use strcmp(a.Key, b.Key) + (TagRegistry guarantees globally-unique keys, so Key equality is + semantically equivalent to handle equality within a registry + session). Never use isequal/== on Tag handles — Octave SIGILLs + when recursing through listener cycles. + + Properties (public): + Parents — 1×N cell of Tag handles (required at construction) + ComputeFn — function_handle @(parents)->[X,Y], OR a handle + object with a method [X,Y] = compute(obj, parents). + Detected via ismethod(compute, 'compute'). + MinDuration — scalar double; reserved for v2 debouncing (default 0) + EventStore — EventStore handle; inherited from Tag base + + Tag-contract methods: + getXY — lazy-memoized; recomputes on dirty + valueAt(t) — ZOH lookup into the cached (X, Y) via binary_search + getTimeRange — [X(1), X(end)] or [NaN NaN] if empty + getKind — returns 'derived' + toStruct — serialize state. Function-handle ComputeFn stores + a func2str string but cannot round-trip — see §3.6 + of the spec; the user must reattach the real handle + after fromStruct or invocation raises + DerivedTag:computeNotRehydrated. Object-form + ComputeFn stores class name + (optional) toStruct + state and DOES round-trip. + fromStruct — Static Pass-1 reconstruction; stashes parentkeys + in ParentKeys_ for Pass-2 resolveRefs. + resolveRefs — Pass-2: bind real Parents from the registry and + register self as listener on each. + + DerivedTag-specific methods: + invalidate — clear cache, mark dirty, cascade to listeners + addListener(l) — register a downstream listener + notifyListeners_ — internal observer fan-out + + Error IDs (locked — see SPEC §4): + DerivedTag:invalidParents parents empty or non-Tag + DerivedTag:invalidCompute compute not fn handle / no compute() + DerivedTag:unknownOption unrecognized NV key + DerivedTag:invalidListener addListener target lacks invalidate() + DerivedTag:computeReturnedNonNumeric compute result non-numeric + DerivedTag:computeShapeMismatch X, Y length mismatch + DerivedTag:dataMismatch fromStruct missing required fields + DerivedTag:unresolvedParent resolveRefs missing key in registry + DerivedTag:cycleDetected cyclic parent graph (direct or transitive) + DerivedTag:nonSerializableCompute toStruct on opaque non-fn / non-object compute + DerivedTag:computeNotRehydrated deserialized invoked without ComputeFn rehydration + + Cycle detection: + The constructor runs a depth-first traversal over the parents' + ancestry chain. If newKey appears anywhere in any parent's + parents (transitively), DerivedTag:cycleDetected is raised at + construction time. The DFS uses strcmp(a.Key, b.Key) — never + handle equality — for Octave compatibility (see Pitfall 3 above). + + Compute strategy contract: + 1. Function handle: signature [X, Y] = fn(parents) where parents + is the same 1×N cell array passed to the constructor. + 2. Object: handle class instance with method + [X, Y] = compute(obj, parents). Detected at construction via + ismethod(compute, 'compute'). For round-tripping through + toStruct/fromStruct, the class SHOULD also implement a + toStruct() instance method and a fromStruct(s) static method + (mirrors the Tag pattern). Otherwise default-construction is + attempted at deserialization time. + + Recompute pipeline: + 1. Dispatch on isa(ComputeFn, 'function_handle') vs. + isobject(ComputeFn) && ismethod(ComputeFn, 'compute'). + 2. Validate result: numeric X and Y, equal length. + 3. Reshape both to row vectors and store in cache_. + 4. Clear dirty_ flag. + + Listener / observer: + The constructor calls parent.addListener(obj) for every parent + that exposes addListener (SensorTag, StateTag, MonitorTag, + CompositeTag, DerivedTag all qualify; MockTag does too in + tests). Subsequent parent.updateData(...) → parent.invalidate + fan-out → DerivedTag.invalidate → notifyListeners_ cascades to + any downstream MonitorTag/DerivedTag wrapping this one. This + mirrors MonitorTag's listener wiring exactly. - Constructor accepts Tag universals (Name, Units, Description, - Labels, Metadata, Criticality, SourceRef), sensor extras (ID, - Source, MatFile, KeyName), and inline 'X'/'Y' data arrays. + No DataChanged event in invalidate (Pitfall 5): + Cache invalidation does NOT fire `notify(obj, 'DataChanged')` — + only SensorTag.updateData and StateTag mutators do. DerivedTag + fires events implicitly via downstream consumers pulling getXY. + This avoids flap loops in deeply-chained derivation graphs. ### Constructor ```matlab -obj = SensorTag(key, varargin) +obj = DerivedTag(key, parents, compute, varargin) ``` -SENSORTAG Construct a SensorTag with inlined data storage. - t = SensorTag(key) creates a SensorTag with the given key. - -### Methods - -#### `ds = get()` - -GET.DATASTORE Return the disk-backed DataStore (read-only view). - -#### `v = get()` - -GET.X Read-only access to timestamps (backward-compat with legacy Sensor.X). - -#### `v = get()` - -GET.Y Read-only access to values (backward-compat with legacy Sensor.Y). - -#### `v = get()` +DERIVEDTAG Construct a DerivedTag with N parents and a compute strategy. + d = DerivedTag(key, parents, compute) creates a DerivedTag + whose output is compute(parents) lazy-evaluated on first + getXY() and recomputed automatically when any parent's + updateData fires. -GET.THRESHOLDS Always empty cell array (backward-compat stub). - Legacy Sensor class exposed a Thresholds cell array of - ThresholdRule handles. In the v2.0 Tag model, thresholds - are expressed as MonitorTag children bound via TagRegistry - — not as a nested collection on the sensor. Widgets that - still read .Thresholds (GaugeWidget, StatusWidget) see an - empty cell here and fall through to their "no thresholds" - branch. Consumers should migrate to the TagRegistry + - MonitorTag workflow for threshold behaviour. +### Properties -#### `r = get()` +| Property | Default | Description | +|----------|---------|-------------| +| Parents | `{}` | 1×N cell of Tag handles (required at construction) | +| ComputeFn | `[]` | function_handle, OR object with compute() method | +| MinDuration | `0` | reserved for v2 debouncing; unused in v1 | -GET.RAWSOURCE Return the raw-data source binding (read-only view). - Populated only for SensorTags whose 'RawSource' NV-pair was - set at construction. Consumed by BatchTagPipeline / - LiveTagPipeline to locate the raw file + column for this tag. +### Methods #### `[X, Y] = getXY(obj)` -GETXY Return X, Y by reference (zero-copy via COW). - MATLAB copy-on-write guarantees no memory allocation until - the caller mutates X or Y. +GETXY Return lazy-memoized (X, Y) — recomputes on dirty. #### `v = valueAt(obj, t)` -VALUEAT Return Y at the last index where X <= t (ZOH, clamped). - Returns NaN on empty data. +VALUEAT Right-biased ZOH lookup into the cached (X, Y). + Mirrors StateTag.valueAt structure exactly: scalar branch + uses a single binary_search call; vector branch loops one + binary_search per query. Returns NaN-filled output if the + compute returned an empty series. #### `[tMin, tMax] = getTimeRange(obj)` -GETTIMERANGE Return [X(1), X(end)]. [NaN NaN] if empty. +GETTIMERANGE Return [X(1), X(end)] from getXY; [NaN NaN] if empty. -#### `k = getKind(obj)` +#### `k = getKind(~)` -GETKIND Return the literal kind identifier 'sensor'. +GETKIND Return the literal kind identifier 'derived'. #### `s = toStruct(obj)` -TOSTRUCT Serialize SensorTag state to a plain struct. - Tag universals at the top level; sensor-specific extras - nested under s.sensor (only when non-default) to keep the - struct compact. X/Y are INTENTIONALLY OMITTED -- runtime - data, not serialization state. - -#### `load(obj, matFile)` - -LOAD Load sensor data from a .mat file. - t.load() uses the already-configured MatFile. - t.load(path) sets MatFile before loading. - -#### `toDisk(obj)` - -TODISK Move X/Y data to disk-backed FastSenseDataStore. - Clears X_ and Y_ from memory after transfer. - -#### `toMemory(obj)` - -TOMEMORY Load disk-backed data back into memory. +TOSTRUCT Serialize state to a plain struct. + Function-handle ComputeFn cannot round-trip cleanly + (closures, anonymous fns) — toStruct stores + s.computekind = 'function_handle' and s.computestr = + func2str(...). fromStruct leaves a sentinel that errors + with DerivedTag:computeNotRehydrated until the user + reattaches the real handle. -#### `tf = isOnDisk(obj)` +#### `resolveRefs(obj, registry)` -ISONDISK True if sensor data is stored on disk. +RESOLVEREFS Pass-2 hook to bind Parents from registry by key. + Iterates ParentKeys_ (stashed by fromStruct), fetches each + real handle from the registry, registers self as a listener + on each, and clears ParentKeys_. Forces dirty_ = true so + the next getXY() recomputes against the real parent data. -#### `addListener(obj, m)` +#### `invalidate(obj)` -ADDLISTENER Register a listener notified on underlying data change. - Listener must implement an invalidate() method. Strong - reference -- caller manages lifecycle. +INVALIDATE Clear cache + mark dirty; cascade to downstream listeners. + Called automatically when a parent's listener fan-out + reaches this DerivedTag (parent.updateData → + parent.notifyListeners_ → invalidate). Also called + directly by user code when ComputeFn semantics change. -#### `updateData(obj, X, Y)` +#### `addListener(obj, l)` -UPDATEDATA Replace X/Y data and fire listeners. +ADDLISTENER Register a downstream listener. + l must implement an invalidate() method (any Tag in the + FastPlot domain qualifies; struct/handle objects with a + bespoke invalidate also work). Listeners are held by + strong reference — caller manages lifecycle. #### `ll = getListeners_(obj)` GETLISTENERS_ Internal accessor for Tag.invalidateBatch_ (Phase 1028 plan 05). Returns the private listeners_ cell. Hidden so it does not appear in tab-completion / doc(); not part of public API - (D-10). Mirrors getListeners_ on StateTag, MonitorTag, - CompositeTag, DerivedTag. + (D-10). ### Static Methods -#### `SensorTag.obj = fromStruct(s)` +#### `DerivedTag.obj = fromStruct(s)` -FROMSTRUCT Reconstruct SensorTag from a toStruct output. +FROMSTRUCT Pass-1 reconstruction with sentinel parents + stashed keys. + Required fields: s.key (non-empty char), s.parentkeys + (cellstr of length ≥ 1). Pass-2 resolveRefs(registry) is + responsible for swapping in real Parent handles. --- @@ -1112,147 +1002,22 @@ ADDLISTENER Register a listener notified on underlying data change. #### `updateData(obj, X, Y)` UPDATEDATA Replace public X/Y and fire listeners (MONITOR-04). - Additive API — does NOT touch constructor or getXY paths. - Any registered MonitorTag or other listener receives an - invalidate() call after the new data is installed. - -#### `ll = getListeners_(obj)` - -GETLISTENERS_ Internal accessor for Tag.invalidateBatch_ (Phase 1028 plan 05). - Returns the private listeners_ cell. Hidden so it does not - appear in tab-completion / doc(); not part of public API - (D-10). - -### Static Methods - -#### `StateTag.obj = fromStruct(s)` - -FROMSTRUCT Reconstruct StateTag from a toStruct output. - ---- - -## `Tag` --- Abstract base for the unified Tag domain model. - -> Inherits from: `handle` - -Tag is the root of the v2.0 domain hierarchy. Subclasses - (SensorTag, StateTag, MonitorTag, CompositeTag) provide concrete - implementations of the six abstract-by-convention methods. - - Tag uses the Octave-safe "throw-from-base" abstract pattern: - the base class provides stub methods that raise a notImplemented - error, and subclasses override with concrete implementations. - Do NOT use the Abstract-methods block pattern here — it has - divergent semantics between MATLAB and Octave (see DataSource.m - for the proven pattern used here). - - Tag Properties (public): - Key — char: unique identifier (required, non-empty) - Name — char: human-readable name (defaults to Key) - Units — char: measurement unit - Description — char: free-text description - Labels — cellstr: cross-cutting classification (META-01) - Metadata — struct: open key-value bag (META-03) - Criticality — char enum: 'low'|'medium'|'high'|'safety' (META-04) - SourceRef — char: optional provenance string - - Tag Methods (abstract-by-convention — subclass must implement): - getXY — return [X, Y] data vectors - valueAt(t) — return scalar value at time t - getTimeRange — return [tMin, tMax] - getKind — return kind string ('sensor'|'state'|'monitor'|'composite'|'mock') - toStruct — return serializable struct - fromStruct (Static) — reconstruct from struct - - Tag Methods (default hooks — override when needed): - resolveRefs(registry) — Pass-2 deserialization hook; default no-op - -### Constructor - -```matlab -obj = Tag(key, varargin) -``` - -TAG Construct a Tag with required key and optional name-value pairs. - -### Properties - -| Property | Default | Description | -|----------|---------|-------------| -| Key | `''` | char: unique identifier | -| Name | `''` | char: human-readable name | -| Units | `''` | char: measurement unit | -| Description | `''` | char: free-text description | -| Labels | `{}` | cellstr: cross-cutting classification | -| Metadata | `struct()` | struct: open key-value bag | -| Criticality | `'medium'` | char enum: 'low'\|'medium'\|'high'\|'safety' | -| SourceRef | `''` | char: optional provenance string | -| EventStore | `[]` | EventStore handle; [] disables event convenience methods | - -### Methods - -#### `set()` - -SET.CRITICALITY Validate enum before assigning. - -#### `[X, Y] = getXY(obj)` - -GETXY Return [X, Y] data vectors. Subclass must override. - -#### `v = valueAt(obj, t)` - -VALUEAT Return scalar value at time t. Subclass must override. - -#### `[tMin, tMax] = getTimeRange(obj)` - -GETTIMERANGE Return [tMin, tMax] time bounds. Subclass must override. - -#### `k = getKind(obj)` - -GETKIND Return kind string. Subclass must override. - -#### `s = toStruct(obj)` - -TOSTRUCT Return serializable struct. Subclass must override. - -#### `resolveRefs(obj, registry)` - -RESOLVEREFS Pass-2 hook for two-phase deserialization. - Default: no-op. CompositeTag (Phase 1008) will override to - wire up children by key. Leaf tags (Sensor/State/Monitor) - do not need references resolved. - -#### `addManualEvent(obj, tStart, tEnd, label, message)` - -ADDMANUALEVENT Create a manual annotation event bound to this tag. - tag.addManualEvent(tStart, tEnd, label, message) creates an Event - with Category = 'manual_annotation' and TagKeys = {obj.Key}, - appends to the bound EventStore, and registers in EventBinding. - -#### `events = eventsAttached(obj)` - -EVENTSATTACHED Query events bound to this tag via EventBinding. - Returns Event array (possibly empty). This is a query, NOT a - stored property -- no Event handles on Tag (Pitfall 4). + Additive API — does NOT touch constructor or getXY paths. + Any registered MonitorTag or other listener receives an + invalidate() call after the new data is installed. #### `ll = getListeners_(obj)` -GETLISTENERS_ Default accessor returning empty cell (Phase 1028 plan 05). - Subclasses that maintain a listener cell (SensorTag, - StateTag, MonitorTag, CompositeTag, DerivedTag) override - this to expose their private `listeners_` property for - `Tag.invalidateBatch_` to walk. The Tag base returns {} — - abstract Tag has no listeners. +GETLISTENERS_ Internal accessor for Tag.invalidateBatch_ (Phase 1028 plan 05). + Returns the private listeners_ cell. Hidden so it does not + appear in tab-completion / doc(); not part of public API + (D-10). ### Static Methods -#### `Tag.obj = fromStruct(s)` - -FROMSTRUCT Reconstruct a Tag from a struct. Subclass must override. - -#### `Tag.invalidateBatch_(tagSet)` +#### `StateTag.obj = fromStruct(s)` -INVALIDATEBATCH_ Coalesced invalidation across many tags (Phase 1028 plan 05). +FROMSTRUCT Reconstruct StateTag from a toStruct output. --- @@ -1374,3 +1139,238 @@ INSTANTIATEBYKIND Dispatch fromStruct based on s.kind. (tests). Phase 1005+ extends the switch for sensor, state, monitor, and composite kinds. +--- + +## `BatchTagPipeline` --- Synchronous raw-data -> per-tag .mat pipeline. + +> Inherits from: `handle` + +Enumerates TagRegistry for ingestable tags (SensorTag/StateTag + with a non-empty RawSource), de-duplicates file reads, parses + each raw file once, slices the requested column per tag, and + writes /.mat in the SensorTag.load shape. + + Batch semantics (D-12, D-15, D-18): + - OutputDir required at construction; auto-created if missing. + - run() returns a report struct; throws TagPipeline:ingestFailed + at end-of-run if any tag failed. + - Each tag's ingest is a try/catch boundary; one failing tag + does NOT abort the batch. + + Observability (Major-2 / revision-1): + - LastFileParseCount: public SetAccess=private property + recording the number of DISTINCT raw files parsed in the + most recent run(). Captured BEFORE the end-of-run cache + reset. Enables testFileCacheDedup to assert exact dedup + without wrapping readRawDelimited_ (blocked by MATLAB's + private-folder scoping). + + Errors (namespaced under TagPipeline:*): + TagPipeline:invalidOutputDir -- OutputDir missing / empty + TagPipeline:cannotCreateOutputDir -- mkdir failed + TagPipeline:ingestFailed -- 1+ tags failed (end-of-run throw) + TagPipeline:unknownExtension -- file ext not .csv/.txt/.dat + +### Constructor + +```matlab +obj = BatchTagPipeline(varargin) +``` + +BATCHTAGPIPELINE Construct with required OutputDir NV-pair. + p = BatchTagPipeline('OutputDir', dir) + p = BatchTagPipeline('OutputDir', dir, 'Verbose', true) + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| OutputDir | `''` | | +| Verbose | `false` | | + +### Methods + +#### `report = run(obj)` + +RUN Enumerate tags, ingest each, write per-tag .mat; throw at end if any failed. + Returns a report struct with fields: + succeeded - cellstr of tag keys that wrote OK + failed - struct array of failed tags (key, file, errorId, message) + +#### `setWriteFnForTesting_(obj, fn)` + +SETWRITEFNFORTESTING_ Internal-only DI seam for .mat write suppression. + Phase 1028 plan 02b: replace the default @writeTagMat_ with a + user-supplied function handle (e.g., a no-op for benchmark NoIO + measurement). Production callers MUST NOT use this — the + default cadence per D-12 is write-on-every-tick. + +#### `setFsCoalesceForTesting_(obj, tf)` + +SETFSCOALESCEFORTESTING_ Shape-parity setter mirroring LiveTagPipeline (plan 06). + Phase 1028 plan 06: BatchTagPipeline.run() does not currently + issue per-tag exist/dir/datenum syscalls (parsing happens via + parseOrCache_, which uses ext-based dispatch, not file stats), + so fs-stat coalescing is a no-op here. The setter exists for + symmetry with LiveTagPipeline so tests/bench scripts can + configure both pipelines uniformly. Hidden (D-10). + +#### `setCoalesceActiveForTesting_(obj, tf)` + +SETCOALESCEACTIVEFORTESTING_ Shape-parity setter mirroring LiveTagPipeline (plan 05). + Phase 1028 plan 05: BatchTagPipeline.run() does not currently + accumulate a listener cascade (it writes 'overwrite' mode and + does not call tag.updateData()), so coalescing is a no-op + here. The setter exists for symmetry with LiveTagPipeline so + tests/bench scripts can configure both pipelines uniformly. + Hidden (D-10). + +#### `setCacheActiveForTesting_(obj, tf)` + +SETCACHEACTIVEFORTESTING_ Internal-only setter for the prior-state cache. + Phase 1028 plan 02d: enable/disable the in-memory priorState_ cache. + Mirror of LiveTagPipeline.setCacheActiveForTesting_; production callers + MUST NOT use this — cache-on is the production default and is byte-for-byte + parity-tested against the cache-off path. Hidden so it does not appear in + tab-completion, doc(), or properties() listings (D-10). + +--- + +## `LiveTagPipeline` --- Timer-driven raw-data -> per-tag .mat pipeline. + +> Inherits from: `handle` + +Mirrors MatFileDataSource's modTime + lastIndex state machine + over raw text files. Does NOT subclass LiveEventPipeline (D-14) + -- borrows the timer ergonomics only. + + Live semantics (D-13, D-14, D-18): + - Each tick re-enumerates TagRegistry, stats each tag's RawSource.file. + - Files with advanced mtime are re-parsed ONCE (per-tick file cache). + - New rows (lastIndex+1 : total) are appended to /.mat. + - Append uses load->concat->save (Pitfall 2 guard); the writer + never uses the dash-append flag of save (which would clobber + the existing `data` variable rather than merge its fields). + - Per-tag try/catch: one tag's failure does NOT abort the tick. + - tagState_ entries GC'd each tick for tags no longer eligible. + + Cluster mode (Phase 1030, Plan 02): + - Enabled by passing 'SharedRoot' NV-pair to constructor. + - All shared .mat writes routed through TagWriteCoordinator + + AtomicWriter for safe multi-process access (REQ CONC-01). + - Single-user mode (no SharedRoot) exercises ZERO Concurrency- + library code paths (Success Criterion 5 / byte-identical guarantee). + - BusyMode='drop' is forced in cluster mode (Pitfall 7). + - Timer period is jittered +-25% in cluster mode (Pitfall 11). + - Lock contention causes per-tag skip-and-defer, not whole-tick block. + + Observability (Major-2 / revision-1): + - LastFileParseCount: public SetAccess=private property recording the + number of DISTINCT files parsed in the most recent tick. Captured + BEFORE the per-tick tickCache goes out of scope. Mirrors + BatchTagPipeline's mechanism so tests can assert dedup behavior + via direct property read rather than wrapping readRawDelimited_. + + Cluster-mode observability (Phase 1030 Plan 02): + - SkippedTickCount: public SetAccess=private; incremented on lock + contention or BusyMode='drop' skip. + - LastTickDurationSec: public SetAccess=private; wall-clock duration + of the last onTick_ invocation. + - LastLockContentionEvent: public SetAccess=private; most recent + contention event struct {tagKey, holder.{user, host, age}}. + + Shares readRawDelimited_ / selectTimeAndValue_ / writeTagMat_ with + BatchTagPipeline -- single source of truth for parse + shape + write. + +### Constructor + +```matlab +obj = LiveTagPipeline(varargin) +``` + +LIVETAGPIPELINE Construct with OutputDir (required) + options. + p = LiveTagPipeline('OutputDir', dir) + p = LiveTagPipeline('OutputDir', dir, 'Interval', 5, 'Verbose', true) + p = LiveTagPipeline('OutputDir', dir, 'ErrorFcn', @(ex) ...) + p = LiveTagPipeline('OutputDir', dir, 'SharedRoot', root) % cluster mode + p = LiveTagPipeline('OutputDir', dir, 'SharedRoot', root, 'LockTimeout', 10) + +### Properties + +| Property | Default | Description | +|----------|---------|-------------| +| OutputDir | `''` | | +| Interval | `15` | seconds | +| Status | `'stopped'` | 'stopped' \| 'running' \| 'error' | +| ErrorFcn | `[]` | optional @(ex) callback for tick-level errors | +| Verbose | `false` | | + +### Methods + +#### `start(obj)` + +START Launch the polling timer and set Status='running'. + +#### `stop(obj)` + +STOP Halt the polling timer; mirrors the pattern used by the + live-event pipeline class in libs/EventDetection/. + Pitfall 8 -- guard with isvalid + try/catch so stop() + during an in-flight tick doesn't cascade errors. + +#### `tickOnce(obj)` + +TICKONCE Run one tick synchronously (exposed for tests). + Production callers use start()/stop(); tests call this + to avoid pausing for timer intervals. + +#### `n = get()` + +GET.TAGSTATECOUNT Dependent property exposing tagState_.Count. + RESEARCH Q3 observability -- lets tests verify that entries + for unregistered tags are GC'd between ticks. + +#### `setWriteFnForTesting_(obj, fn)` + +SETWRITEFNFORTESTING_ Internal-only DI seam for .mat write suppression. + Phase 1028 plan 02b: replace the default @writeTagMat_ with a + user-supplied function handle (e.g., a no-op for benchmark NoIO + measurement). Production callers MUST NOT use this — the + default cadence per D-12 is write-on-every-tick. + +#### `setFsCoalesceForTesting_(obj, tf)` + +SETFSCOALESCEFORTESTING_ Internal-only setter for per-tick fs-stat coalescing. + Phase 1028 plan 06: enable/disable the per-tick coalesced + filesystem-stat lookup inside onTick_. When ON (production + default), onTick_ issues ONE dir(parentDir) call per unique + raw-source parent directory at tick start and stores the + resulting basename->struct map; processTag_ consults that map + instead of issuing per-tag exist/dir/datenum syscalls. When + OFF, the per-tag fallback path runs (one exist + one dir per + tag) — used by the benchmark to isolate the coalescing win. + +#### `setCoalesceActiveForTesting_(obj, tf)` + +SETCOALESCEACTIVEFORTESTING_ Internal-only setter for end-of-tick listener coalescing. + Phase 1028 plan 05: enable/disable the A1+A2 end-of-tick + Tag.invalidateBatch_(updatedSet) call inside onTick_. + Production callers MUST NOT use this — coalescing-on is the + production default (coalesceActive_ = true). The setter exists + so the bench can measure the coalesce-on vs coalesce-off + delta against the dominant `other` bucket (per-tag dispatch + + listener cascade — see Plan 02d VERIFICATION.md). Hidden so + it does not appear in tab-completion / doc() (D-10). Mirrors + the plan-02b setWriteFnForTesting_ and plan-02d + setCacheActiveForTesting_ patterns. + +#### `setCacheActiveForTesting_(obj, tf)` + +SETCACHEACTIVEFORTESTING_ Internal-only setter for the prior-state cache. + Phase 1028 plan 02d: enable/disable the in-memory priorState_ cache + used to skip the on-disk load() in writeTagMat_('append',...). + Production callers MUST NOT use this — the cache is the production + default (cacheActive_ = true) and is byte-for-byte parity-tested + against the cache-off path. Disabling it is a benchmark feature for + measuring the load()-only cost (see bench_tag_pipeline_1k --cache-off). +