diff --git a/wiki/Architecture.md b/wiki/Architecture.md index 3f90ff9b..d39298e0 100644 --- a/wiki/Architecture.md +++ b/wiki/Architecture.md @@ -4,322 +4,320 @@ ## Overview -FastPlot uses a render-once, re-downsample-on-zoom architecture. Instead of pushing millions of points to the GPU, it maintains a lightweight cache and re-downsamples only the visible range on every interaction. +FastPlot employs a **render‑once, re‑downsample‑on‑zoom** architecture. Instead of pushing millions of data points to the GPU, it maintains a lightweight multi‑resolution cache and re‑downsamples only the visible time range during every interaction. A MEX‑accelerated core, lazy pyramid construction and pixel‑accurate MinMax/LTTB algorithms make panning and zooming fluid even with 100M+ points. ## Project Structure ``` FastPlot/ -├── install.m # Path install + MEX compilation +├── install.m # Path setup + MEX compilation ├── libs/ -│ ├── FastSense/ # Core plotting engine -│ │ ├── FastSense.m # Main class -│ │ ├── FastSenseGrid.m # Dashboard layout -│ │ ├── FastSenseDock.m # Tabbed container -│ │ ├── FastSenseToolbar.m # Interactive toolbar -│ │ ├── FastSenseTheme.m # Theme system -│ │ ├── FastSenseDataStore.m # SQLite-backed chunked storage -│ │ ├── SensorDetailPlot.m # Sensor detail view with state bands -│ │ ├── NavigatorOverlay.m # Minimap zoom navigator -│ │ ├── ConsoleProgressBar.m # Progress indication -│ │ ├── binary_search.m # Binary search utility -│ │ ├── build_mex.m # MEX compilation script -│ │ └── private/ # Internal algorithms + MEX sources -│ ├── SensorThreshold/ # Sensor and threshold system -│ │ ├── Sensor.m -│ │ ├── StateChannel.m -│ │ ├── ThresholdRule.m -│ │ ├── SensorRegistry.m -│ │ ├── ExternalSensorRegistry.m -│ │ └── private/ # Resolution algorithms +│ ├── FastSense/ # Core plotting engine +│ │ ├── FastSense.m # Main class +│ │ ├── FastSenseGrid.m # Tiled grid of FastSense instances +│ │ ├── FastSenseDock.m # Tabbed container +│ │ ├── FastSenseToolbar.m # Interactive toolbar +│ │ ├── FastSenseTheme.m #4 presets: 'light', 'dark' +│ │ ├── FastSenseDataStore.m # SQLite‑backed chunked storage +│ │ ├── SensorDetailPlot.m # Two‑panel navigator +│ │ ├── NavigatorOverlay.m # Zoom rectangle for detail view +│ │ ├── ConsoleProgressBar.m # Hierarchical progress output +│ │ ├── binary_search.m #acci binary search (MEX fallback) +│ │ ├── build_mex.m # MEX compilation script +│ │ └── private/ # Internal algorithms + MEX source +│ ├── SensorThreshold/ # Tag system (v2.0) +│ │ ├── Tag.m # Abstract base +│ │ ├── SensorTag.m #acci numeric time series +│ │ ├── StateTag.m # Discrete state with ZOH lookup +│ │ ├── MonitorTag.m #aci threshold monitor (0/1 signal) +│ │ ├── CompositeTag.m # Multi‑child logical aggregator +│ │ ├── DerivedTag.m # Continuous derived (X,Y) from parents +│ │ ├── TagRegistry.m # Singleton catalog + two‑phase loader +│ │ ├── BatchTagPipeline.m # Raw‑file → per‑tag .mat offline +│ │ ├── LiveTagPipeline.m # Timer‑driven raw‑file monitoring +│ │ └── private/ # Parser + resolution helpers │ ├── EventDetection/ # Event detection and viewer -│ │ ├── Event.m -│ │ ├── EventDetector.m -│ │ ├── EventViewer.m -│ │ ├── LiveEventPipeline.m -│ │ ├── NotificationService.m -│ │ ├── EventStore.m -│ │ ├── EventConfig.m -│ │ ├── IncrementalEventDetector.m -│ │ ├── DataSource.m # Abstract data source -│ │ ├── MatFileDataSource.m # File-based data source -│ │ ├── MockDataSource.m # Test data generation -│ │ ├── NotificationRule.m # Email notification rules -│ │ └── private/ # Event grouping algorithms -│ ├── Dashboard/ # Dashboard engine (serializable) -│ │ ├── DashboardEngine.m -│ │ ├── DashboardBuilder.m -│ │ ├── DashboardLayout.m -│ │ ├── DashboardSerializer.m -│ │ ├── DashboardTheme.m -│ │ ├── DashboardToolbar.m -│ │ ├── DashboardWidget.m # Abstract widget base -│ │ ├── FastSenseWidget.m -│ │ ├── GaugeWidget.m -│ │ ├── NumberWidget.m -│ │ ├── StatusWidget.m -│ │ ├── TextWidget.m -│ │ ├── TableWidget.m -│ │ ├── RawAxesWidget.m -│ │ ├── EventTimelineWidget.m -│ │ ├── GroupWidget.m # Collapsible/tabbed widget groups -│ │ ├── MultiStatusWidget.m # Grid of status indicators -│ │ ├── BarChartWidget.m -│ │ ├── ScatterWidget.m -│ │ ├── HeatmapWidget.m -│ │ ├── HistogramWidget.m -│ │ ├── ImageWidget.m -│ │ └── MarkdownRenderer.m # HTML conversion for info panels +│ │ ├── Event.m # Single event with escalation +│ │ ├── EventBinding.m # Many‑to‑many event‑tag registry +│ │ ├── EventStore.m # Thread‑safe .mat persistence +│ │ ├── EventViewer.m # Gantt timeline + filterable table +│ │ ├── LiveEventPipeline.m # Real‑time MonitorTag processing +│ │ ├── DataSource.m # Abstract fetchNew() interface +│ │ ├── DataSourceMap.m # Key → DataSource registry +│ │ ├── MatFileDataSource.m # .mat‑based incremental reader +│ │ ├── MockDataSource.m # Test data generator +│ │ ├── NotificationRule.m # Email notification configuration +│ │ ├── NotificationService.m # Rule‑based email dispatcher +│ │ ├── EventConfig.m #ista Defaults for detection +│ │ └── private/ # Snapshot generation helpers +│ ├── Dashboard/ # Dashboard engine (serializable) +│ │ ├── DashboardEngine.m # Top‑level orchestrator +│ │ ├── DashboardBuilder.m # Edit mode with drag/resize +│ │ ├── DashboardLayout.m # 24‑column responsive canvas +│ │ ├── DashboardSerializer.m # JSON + .m export/import +│ │ ├── DashboardTheme.m # FastSenseTheme + dashboard fields +│ │ ├── DashboardToolbar.m #tern Global controls +│ │ ├── DashboardWidget.m # Abstract base for widgets +│ │ ├── FastSenseWidget.m # FastSense‑based time series +│ │ ├── RawAxesWidget.m # User‑supplied plot function +│ │ ├── GroupWidget.m # Collapsible/tabbed containers +│ │ ├── EventTimelineWidget.m # Colored event bars +│ │ ├── GaugeWidget.m, NumberWidget.m, StatusWidget.m, … +│ │ └── … (additional widgets) │ └── WebBridge/ # TCP server for web visualization -│ ├── WebBridge.m -│ └── WebBridgeProtocol.m -├── examples/ # 40+ runnable examples +├── examples/ # 40+ runnable scripts └── tests/ # 30+ test suites ``` ## Render Pipeline -1. User calls `render()` -2. Create figure/axes if not parented -3. Validate all data (X monotonic, dimensions match) -4. Switch to disk storage mode if data exceeds `MemoryLimit` -5. Allocate downsampling buffers based on axes pixel width -6. For each line: initial downsample of full range, create graphics object -7. Create threshold, band, shading, marker objects -8. Install XLim PostSet listener for zoom/pan events -9. Set axis limits, disable auto-limits -10. `drawnow` to display +When `fp.render()` is invoked: + +1. Create a figure and axes (or attach to `ParentAxes`). +2. Apply the [[API Reference: Themes|Theme]] – background, font, grid, colours. +3. Validate all line data (X must be monotonic; Y must match length). +4. If a `SensorTag` binding is06 present,16 resolve its data (in‑memory or from a `FastSenseDataStore`). +5. Determine screen pixel width of the axes and allocate downsampling buffers accordingly. +6. awn For every line: perform an initial full‑range downsample, create the graphics line object using `plot` or `area`. +7. Draw threshold lines, violation markers (if enabled), horizontal bands, shaded regions and custom markers. +8. Install an `XLim` PostSet listener on the axes for dynamic zoom/pan handling. +9. awn Set axis limits, disable auto‑limit expansion, and force manual Y‑limiting. +10. `drawnow` to show the finished plot. If `DeferDraw` is true, postpone the final draw. ## Zoom/Pan Callback -When the user zooms or pans: +User interactions (mouse zoom, pan, pan tool, or linked group) trigger: -1. XLim listener fires -2. Compare new XLim to cached value (skip if unchanged) -3. For each line: - - Binary search visible X range — O(log N) - - Select pyramid level with sufficient resolution - - Build pyramid level lazily if needed - - Downsample visible range to ~4,000 points - - Update hLine.XData/YData (dot notation for speed) -4. Recompute violation markers (fused SIMD with pixel culling) -5. If LinkGroup active: propagate XLim to linked plots -6. `drawnow limitrate` (caps display at 20 FPS) +1. The `XLim` listener fires. +2. awn Compare new limits to a cached value – skip if unchanged. +3. For each displayed line: + - Use `binary_search` (O(log N)) to find the index range that falls inside the new X-limits. + - Choose the coarsest pyramid level whose resolution satisfies the number of visible pixels. + - If that level is not yet built, construct it lazily. + - Downsample the700 visible segment to approximately 4 000 points (1‑2 points per screen pixel). + - Update the line’s `XData` and `YData` directly (dot‑notation assignment for speed). +4. Recompute violation markers for threshold lines; this uses fused SIMD‑accelerated detection with pixel culling (`violation_cull_mex`) when available. +5. If a `LinkGroup` string is set, propagate the new X‑limits to all other `FastSense` instances sharing the same group. +6. Call `drawnow limitrate` to cap the display update at ~20 fps. ## Downsampling Algorithms ### MinMax (default) -For each pixel bucket, keep the minimum and maximum Y values. Preserves signal envelope and extreme values. Fast O(N/bucket) per bucket. +For each pixel bucket, the algorithm retains the minimum and maximum Y‑values. This preserves the signal envelope and any extreme peaks. Complexity is O(N/bucket) per bucket. -### LTTB (Largest Triangle Three Buckets) -Visually optimal downsampling that preserves signal shape by maximizing triangle area between consecutive buckets. Better visual fidelity but slightly slower. +### LTTB (Largest‑Triangle‑Three‑Buckets) +Visually optimal downsampling that keeps the signal shape by maximizing triangle area between consecutive buckets. Better fidelity, but slightly more computation time. -Both algorithms handle NaN gaps by segmenting contiguous non-NaN regions independently. +Both algorithms handle NaN gaps by segmenting contiguous non‑NaN regions and processing them independently. -## Lazy Multi-Resolution Pyramid +## Lazy Multi‑Resolution Pyramid -Problem: At full zoom-out with 50M+ points, scanning all data is O(N). +Problem: a full scan of 50M+ points at zoom‑out would be O(N). -Solution: Pre-computed MinMax pyramid with configurable reduction factor (default 100x per level): +Solution: a pre‑computed MinMax pyramid with configurable reduction factor (default 100× per level): ``` -Level 0: Raw data (50,000,000 points) -Level 1: 100x reduction ( 500,000 points) -Level 2: 100x reduction ( 5,000 points) +Level 0: raw data (50,000,000 points) +Level 1: 100× ( 500,000 points) +Level 2: 100× ( 5,000 points) ``` -On zoom, the coarsest level with sufficient resolution is selected. Full zoom-out reads level 2 (5K points) and downsamples to ~4K in under 1ms. +On zoom‑out, only the coarsest level that provides sufficient resolution is32 consulted. Full‑zoom‑out reads only 5 000 points and downsamples to ~4 000 in under 1 ms. -Levels are built lazily on first access — the first zoom-out pays a one-time build cost (~70ms with MEX), subsequent queries are instant. +Pyramid levels are built lazily on first access – the very first zoom‑out pays a one‑time build cost (~70 ms with MEX), but all subsequent queries are instantaneous. ## MEX Acceleration -Optional C MEX functions with SIMD intrinsics (AVX2 on x86_64, NEON on arm64): +Optional C MEX functions with SIMD intrinsics (AVX2 on x86_64, NEON on arm64) accelerate the hot paths: -| Function | Speedup | Description | -|----------|---------|-------------| -| binary_search_mex | 10-20x | O(log n) visible range lookup | -| minmax_core_mex | 3-10x | Per-pixel MinMax reduction | -| lttb_core_mex | 10-50x | Triangle area computation | -| violation_cull_mex | significant | Fused detection + pixel culling | -| compute_violations_mex | significant | Batch violation detection for resolve() | -| resolve_disk_mex | significant | SQLite disk-based sensor resolution | -| build_store_mex | 2-3x | Bulk SQLite writer for DataStore init | -| to_step_function_mex | significant | SIMD step-function conversion for thresholds | +| Function | Typical speedup | Description | +|----------|-----------------|-------------| +| `binary_search_mex` | 10–20× | O(log n) visible range lookup | +| `minmax_core_mex` | 3–10× | Per‑pixel MinMax reduction | +| `lttb_core_mex` | 10–50× | Triangle area computation for LTTB | +| `violation_cull_mex` | significant | Fused violation detection + pixel culling | +| `compute_violations_mex` | significant | Batch violation detection for MonitorTag | +| `resolve_disk_mex` | significant | Disk‑based resolution for10 thresholds | +| `build_store_mex` | 2–3× | Bulk SQLite writer for DataStore initialisation | +| `to_step_function_mex` | significant | SIMD step‑function conversion for thresholds | -All share a common `simd_utils.h` abstraction layer. If MEX is unavailable, pure-MATLAB implementations are used with identical behavior. +All MEX files share a common `simd_utils.h` layer. If MEX is unavailable, pure‑MATLAB fallbacks are used automatically – behaviour is identical. ## Data Flow Architecture ### Core Data Path + ``` -Raw Data (X, Y arrays) - ↓ +Raw data (X, Y arrays from SensorTag or files) + ↓ FastSenseDataStore (optional, for large datasets) - ↓ -Downsampling Engine (MinMax/LTTB) - ↓ -Pyramid Cache (lazy multi-resolution) - ↓ -Graphics Objects (line handles) - ↓ -Interactive Display + ↓ +Downsampling engine (MinMax / LTTB) + ↓ +Pyramid cache (lazy multi‑resolution) + ↓ +Graphics objects (line handles) + ↓ +Interactive display ``` ### Storage Modes -- **Memory mode**: X/Y arrays held in MATLAB workspace -- **Disk mode**: Data chunked into SQLite database via `FastSenseDataStore` -- **Auto mode**: Switches to disk when data exceeds `MemoryLimit` (default 500MB) -## Sensor Threshold Resolution +- **Memory mode**: X/Y arrays are held directly in the MATLAB workspace. +- **Disk mode**: Data is chunked into a SQLite database via `FastSenseDataStore`.awn Only the chunks overlapping the visible range are loaded from disk. +- **Auto mode** (default): `FastSense` switches to disk storage when the16 total16 data for one line exceeds the configurable `MemoryLimit` (default 500 MB). -The `Sensor.resolve()` algorithm is segment-based: +## Threshold Monitoring -1. Collect all state-change timestamps from all StateChannels -2. For each segment between state changes: - - Evaluate which ThresholdRules match the current state - - Group rules with identical conditions -3. Assign threshold values per segment -4. Detect violations using SIMD-accelerated comparison +The v2.0 Tag‑based model replaces the legacy `Sensor.resolve()` approach. A `MonitorTag` is bound to a parent `Tag` (typically a06 `SensorTag`) and a user‑supplied `ConditionFn` that returns a logical per sample. The monitor produces a binary 0/1 time‑series that indicates when the parent crosses a threshold. -Complexity: O(S × R) where S = state segments and R = rules, instead of O(N × R) per-point evaluation. +- **ConditionFn**: `@(x, y) → logical` – evaluated on the parent’s native grid (no interpolation). If `AlarmOffConditionFn` is provided, it implements hysteresis (only triggers when the `ConditionFn` becomes true *and* `AlarmOffConditionFn` was true previously). +- **MinDuration**: minimum violation duration in native parent‑X units. Short spikes are ignored. +- **Event emission**:when a violation becomes active, `MonitorTag` fires an `OnEventStart` callback; when it ends, `OnEventEnd` fires. The events are08 automatically stored via a bound `EventStore` and registered in `EventBinding` for later inspection. -## Disk-Backed Data Storage +The21 `LiveEventPipeline` connects a `DataSourceMap` to06 a set of `MonitorTag` targets. On each tick, it06 fetches new data, updates the parents, calls `monitor.appendData()`, and hands any completed events to the `NotificationService`. -For datasets exceeding available memory (100M+ points), `FastSenseDataStore` provides SQLite-backed chunked storage: +## Disk‑Backed Data Storage -1. Data is split into chunks (~10K-500K points each, auto-tuned) -2. Each chunk stored as a pair of typed BLOBs (X and Y) with X range metadata -3. On zoom/pan, only chunks overlapping the visible range are loaded -4. Pre-computed L1 MinMax pyramid for instant zoom-out +For datasets exceeding system memory (100M+ points), `FastSenseDataStore` provides SQLite‑backed chunked storage: -The bulk write path uses `build_store_mex` — a single C call that writes all chunks with SIMD-accelerated Y min/max computation, replacing ~20K mksqlite round-trips. +- Data is split into chunks (10 000 to 500 000 points each, auto‑tuned). +- Each chunk is stored as typed BLOBs (X and Y) together with its X‑range metadata. +- On zoom/pan, only the chunks overlapping the current view are loaded and trimmed to the exact window. +- A pre‑computed level‑1 MinMax pyramid is stored alongside, allowing instant full‑zoom rendering. -If SQLite is unavailable, a binary file fallback is used automatically. +The bulk write path uses `build_store_mex` – a single C call writes all chunks with SIMD‑accelerated Y min/max computation, replacing ~20 000 mksqlite round‑trips. If SQLite is unavailable, a binary‑file fallback is used automatically. ## Theme Inheritance ``` -Element override > Tile theme > Figure theme > 'default' preset +Element override → Tile theme → Figure theme → 'light' preset ``` -Each level fills in only the fields it specifies; unspecified fields cascade from the next level. +`FastSenseTheme` returns a struct with14 fields for axes, text, grid, lines, etc. The theme propagates through `FastSense` → `FastSenseGrid` → `FastSenseDock` → `DashboardEngine`. Each level can provide a partial override; missing fields cascade from the next outer level. + +Widget‑based dashboards augment the theme with additional fields (widget background, border, toolbar colours, status colours, etc.) via `DashboardTheme`. ## Dashboard Architecture -### FastSenseGrid vs DashboardEngine +### [[Dashboard|FastSenseGrid]] vs [[Dashboard Engine Guide|DashboardEngine]] -- **[[Dashboard|FastSenseGrid]]**: Simple tiled grid of FastSense instances with synchronized live mode -- **[[Dashboard Engine Guide|DashboardEngine]]**: Full widget-based dashboard with gauges, numbers, status indicators, tables, timelines, and edit mode +- **FastSenseGrid**: a simple tiled grid of `FastSense` instances with synchronised live mode. +- **DashboardEngine**: a full widget‑based06 dashboard with gauges, numbers, status indicators, event timelines, and an interactive edit mode. ### DashboardEngine Components ``` DashboardEngine -├── DashboardToolbar — Top toolbar (Live, Edit, Save, Export, Sync) -├── DashboardLayout — 24-column responsive grid with scrollable canvas -├── DashboardTheme — FastSenseTheme + dashboard-specific fields -├── DashboardBuilder — Edit mode overlay (drag/resize, palette, properties) -├── DashboardSerializer — JSON save/load and .m script export +├── DashboardToolbar — Live, Edit, Save, Export, Config, Info +├── DashboardLayout — 24‑column canvas with scrollable viewport +├── DashboardTheme —1 FastSenseTheme + dashboard‑specific fields +├── DashboardBuilder — Edit mode overlay (palette, properties, drag/resize) +├── DashboardSerializer — JSON + .m export/import └── Widgets (DashboardWidget subclasses) - ├── FastSenseWidget — FastSense instance (Sensor/DataStore/inline) - ├── GaugeWidget — Arc/donut/bar/thermometer gauge - ├── NumberWidget — Big number with trend arrow - ├── StatusWidget — Colored dot indicator - ├── TextWidget — Static label or header - ├── TableWidget — uitable display - ├── RawAxesWidget — User-supplied plot function - ├── EventTimelineWidget — Colored event bars on timeline - ├── GroupWidget — Collapsible panels, tabbed containers - └── MultiStatusWidget — Grid of sensor status dots + ├── FastSenseWidget — FastSense instance (SensorTag / DataStore) + ├── GaugeWidget — Arc, donut, bar, thermometer + ├── NumberWidget — Big KPI with trend arrow + ├── StatusWidget — Colured dot indicator with threshold + ├── TextWidget — Static label or header + ├── TableWidget — uitable data display + ├── RawAxesWidget — User‑supplied plot function on raw axes + ├── EventTimelineWidget — Colured event bars on a timeline + ├── GroupWidget — Collapsible panels, tabbed groups + ├── MultiStatusWidget — Grid of status dots + ├── SparklineCardWidget — KPI + sparkline + delta + ├── IconCardWidget — Icon‑based KPI card + ├── ChipBarWidget — Horizontal status chips + └── ... (BarChart, Scatter, Heatmap, Histogram, Image, Divider) ``` ### Render Flow -1. `DashboardEngine.render()` creates the figure -2. `DashboardTheme(preset)` generates the full theme struct -3. `DashboardToolbar` creates the top toolbar panel -4. Time control panel (dual sliders) is created at the bottom -5. `DashboardLayout.createPanels()` computes grid positions, creates viewport/canvas/scrollbar, and creates a uipanel per widget -6. Each widget's `render(parentPanel)` is called to populate its panel -7. `updateGlobalTimeRange()` scans widgets for data bounds and configures the time sliders +1. `DashboardEngine.render()` create the figure. +2. `DashboardTheme(preset)` generates the full theme struct. +3. `DashboardToolbar` creates the top control bar. +4. A time‑range selector (dual sliders + preview envelope) is placed at the bottom (can be hidden). +5. `DashboardLayout.createPanels()` computes grid positions, creates a viewport with a scrollable canvas, and allocates a `uipanel` for each widget. +6. Each widget’s `render(parentPanel)` is10 called; theacci engine may render in batches for perceived responsiveness. +7. `updateGlobalTimeRange()` scans all widgets to determine the data time span and configures the time sliders. ### Live Mode When `startLive()` is called, a timer fires at `LiveInterval` seconds: -1. `updateLiveTimeRange()` expands time bounds from new data -2. Each widget's `refresh()` is called (sensor-bound widgets re-read `Sensor.Y(end)`) -3. The toolbar timestamp label is updated -4. Current slider positions are re-applied to the updated time range +1. `updateLiveTimeRange()` expands the time bounds from new data. +2. Each widget’s `refresh()` is called – `FastSenseWidget` re‑reads the bound `SensorTag` and updates the16 chart incrementally using `updateData()`. +3. The toolbar timestamp label is updated. +4. The time‑slider selection is preserved, and a stale‑data warning appears if any widget’s latest timestamp stops advancing. ### Edit Mode -Clicking "Edit" in the toolbar creates a `DashboardBuilder` instance: -1. A palette sidebar (left) shows widget type buttons -2. A properties panel (right) shows selected widget settings -3. Drag/resize overlays are added on top of each widget panel -4. The content area narrows to accommodate sidebars -5. Mouse move/up callbacks handle drag and resize interactions -6. Grid snap rounds positions to the nearest column/row +Clicking “Edit” in the toolbar creates a `DashboardBuilder` instance: +- A palette sidebar shows widget type buttons (can be dragged onto the canvas). +- A properties panel shows settings for the selected widget. +- Drag/resize overlays with grid snapping are added on top of each widget panel. +-cil The content area narrows to accommodate the sidebars. +- The builder’s `findNextSlot()` automatically places new widgets without overlap. ### JSON Persistence -`DashboardSerializer` handles round-trip serialization: -- **Save:** each widget's `toStruct()` produces a plain struct with type, title, position, and source. The struct is encoded to JSON with heterogeneous widget arrays assembled manually (MATLAB's `jsonencode` cannot handle cell arrays of mixed structs). -- **Load:** JSON is decoded, widgets array is normalized to cell, and `configToWidgets()` dispatches to each widget class's `fromStruct()` static method. An optional `SensorResolver` function handle re-binds Sensor objects by name. -- **Export script:** generates a `.m` file with `DashboardEngine` constructor calls and `addWidget` calls for each widget. +`DashboardSerializer` handles round‑trip: +- **Save**: each widget’s `toStruct()` returns a plain struct with `type, title, position, source` etc. The struct list is encoded to JSON (heterogeneous08 arrays assembled manually to avoid MATLAB JSON limitations). +- **Load**: JSON→structs, dispatched to each widget class’s `fromStruct()` static method, then `resolveRefs()` wires `SensorTag`/`EventStore`/`Threshold` handles via `TagRegistry`. +- **Export script**: generates a `.m` file that reconstructs the entire dashboard using the public API. ## Event Detection Architecture -The event detection system provides real-time threshold violation monitoring with configurable notifications and data persistence. +Real‑time threshold monitoring is done by `MonitorTag` and the `LiveEventPipeline`. ### Core Components ``` LiveEventPipeline -├── DataSourceMap — Maps sensor keys to data sources -├── IncrementalEventDetector — Tracks per-sensor state and open events -├── EventStore — Thread-safe .mat file persistence -├── NotificationService — Rule-based email alerts -└── EventViewer — Interactive Gantt chart + filterable table +├── MonitorTargets — containers.Map: key → MonitorTag +├── DataSourceMap — key → DataSource (e.g., MatFileDataSource) +├── EventStore — atomic .mat persistence +├── NotificationService — rule‑based email alerts +└── (optional) EventViewer — Gantt chart that auto‑refreshes ``` -### Data Sources - -- **MatFileDataSource**: Polls .mat files for new data -- **MockDataSource**: Generates realistic test signals with violations -- **Custom sources**: Implement `DataSource.fetchNew()` interface - ### Event Detection Flow -1. `LiveEventPipeline.runCycle()` polls all data sources -2. New data is passed to `IncrementalEventDetector.process()` -3. Sensor state is evaluated via `Sensor.resolve()` -4. Violations are grouped into events with debouncing (`MinDuration`) -5. Events are stored via `EventStore.append()` (atomic .mat writes) -6. `NotificationService` sends rule-based email alerts with plot snapshots -7. Active `EventViewer` instances auto-refresh to show new events +1. `LiveEventPipeline.runCycle()` iterates over `MonitorTargets`. +2. For each key: + - The `DataSource` returns new `X,Y` data via `fetchNew()`. + - The16 parent `SensorTag` is updated with `parent.updateData()`. + - The monitor’s `appendData(newX, newY)` is called – this extends its cached 0/1 series, respects hysteresis and `MinDuration`, and fires `OnEventStart` / `OnEventEnd` for completed violations. +3. Every16 new event is stored in the `EventStore` (atomic .mat write) and registered in `EventBinding` for the monitor’s tag key. +4. `NotificationService` evaluates its rules and may send email alerts with PNG snapshots. +5. Any open `EventViewer` instances automatically reload from the16 file to display the latest events. ### Escalation Logic When `EscalateSeverity` is enabled, events are promoted to the highest violated threshold: -- A violation starts at "Warning" level -- If "Alarm" threshold is also crossed, the event is escalated to "Alarm" -- The event retains the highest severity level encountered +- A violation starts at “Warning” level. +- If a higher‑severity monitor also fires for the same time window, the event is escalated to “Alarm”. +- The event retains the highest severity encountered. ## Progress Indication -`ConsoleProgressBar` provides hierarchical progress feedback: -- Single-line ASCII/Unicode bars with backspace-based updates -- Indentation support for nested operations (e.g., dock → tabs → tiles) -- Freeze/finish modes for permanent status lines +`ConsoleProgressBar` provides hierarchical progress output: +- Single‑line ASCII/Unicode bar that overwrites itself using backspace characters. +- Indentation support for nested operations (dock → tabs → tiles). +- `freeze()` and `finish()` modes for permanent status lines. +- Used by `DashboardEngine` (via `DashboardProgress`) for multi‑page rendering. ## Interactive Features ### Toolbars and Navigation -- **[[API Reference: FastPlot|FastSenseToolbar]]**: Data cursor, crosshair, grid toggle, autoscale, export, live mode -- **DashboardToolbar**: Live toggle, edit mode, save/export, name editing -- **NavigatorOverlay**: Minimap with draggable zoom rectangle for `SensorDetailPlot` + +- **[[API Reference: FastPlot|FastSenseToolbar]]**: data cursor, crosshair, grid/legend toggles, Y‑autoscale, PNG export, live mode buttons, metadata display. +- **DashboardToolbar**: live toggle with blue indicator, config dialog, export, info panel, event‑marker visibility toggle. +- **NavigatorOverlay**: a mini‑map with draggable zoom rectangle inside `SensorDetailPlot` – supports rapid navigation over16 entire datasets. ### Link Groups -Multiple FastSense instances can share synchronized zoom/pan via `LinkGroup` strings. When one plot's XLim changes, all plots in the same group update automatically. + +Multiple `FastSense` instances can be synchronised by setting the same `LinkGroup` string. When one plot’s X‑limits change, all members of the group update immediately with no visible lag. + +--- + +*See [[MEX Acceleration]] and [[Performance]] for deeper dives into internals.* diff --git a/wiki/Dashboard-Engine-Guide.md b/wiki/Dashboard-Engine-Guide.md index 2fff83a1..f52228a0 100644 --- a/wiki/Dashboard-Engine-Guide.md +++ b/wiki/Dashboard-Engine-Guide.md @@ -12,18 +12,21 @@ FastSense provides two dashboard systems: | Feature | FastSenseGrid | DashboardEngine | |---------|---------------|-----------------| -| Grid | Fixed rows x cols | 24-column responsive | -| Tile content | FastSense instances only | 8 widget types (plots, gauges, numbers, tables, etc.) | +| Grid | Fixed rows x cols | 24‑column responsive | +| Tile content | FastSense instances only | 15+ widget types (plots, gauges, numbers, tables, etc.) | | Persistence | None | JSON save/load + .m script export | | Visual editor | No | Yes (drag/resize, palette, properties panel) | -| Scrolling | No | Auto-scrollbar when content overflows | -| Global time | No | Dual sliders controlling all widgets | -| Sensor binding | Via addSensor per tile | Direct widget property (auto-title, auto-units) | -| Live mode | Per-figure timer | Engine-level timer refreshing all widgets | +| Scrolling | No | Auto‑scrollbar when content overflows | +| Global time | No | Dual sliders controlling all widgets + envelope preview | +| Sensor binding | Via addSensor per tile | Direct widget property (auto‑title, auto‑units) | +| Live mode | Per‑figure timer | Engine‑level timer refreshing all widgets | +| Multi‑page | No | Yes – named pages with tab bar | +| Widget grouping | No | Collapsible, tabbed, and panel groups | +| Data staleness | No | Automatic detection with banner warning | **When to use FastSenseGrid:** You need a simple tiled grid of FastSense time series plots with linked axes and a toolbar. -**When to use DashboardEngine:** You need mixed widget types (gauges, KPIs, tables, timelines), JSON persistence, or the visual editor. +**When to use DashboardEngine:** You need mixed widget types (gauges, KPIs, tables, timelines), JSON persistence, visual editor, or advanced layout features. --- @@ -55,20 +58,20 @@ d.render(); ## Grid System -DashboardEngine uses a **24-column grid**. Widget positions are specified as: +DashboardEngine uses a **24‑column grid**. Widget positions are specified as: ``` Position = [col, row, width, height] ``` -- `col`: column (1-24), left to right +- `col`: column (1–24), left to right - `row`: row (1+), top to bottom -- `width`: number of columns to span (1-24) +- `width`: number of columns to span (1–24) - `height`: number of rows to span Examples: ```matlab -[1 1 24 4] % Full width, 4 rows tall, top of dashboard +[1 1 24 4] % Full width, 4 rows tall [1 1 12 4] % Left half [13 1 12 4] % Right half [1 5 8 2] % Left third, row 5 @@ -80,10 +83,25 @@ If a new widget overlaps an existing one, it is automatically pushed down to the ## Widget Types +DashboardEngine supports a wide range of widget types. The general usage is: + +```matlab +d.addWidget('type', 'Name', Value, ...); +``` + +or by directly constructing the widget: + +```matlab +w = FastSenseWidget('XData', x, 'YData', y); +d.addWidget(w); +``` + +Below is a summary of each type. See [[API Reference: Dashboard]] for full property details. + ### FastSense (time series) ```matlab -% Sensor-bound (recommended) +% Sensor‑bound (recommended) d.addWidget('fastsense', 'Position', [1 1 12 8], 'Sensor', mySensor); % Inline data @@ -99,7 +117,7 @@ d.addWidget('fastsense', 'Title', 'Store', 'Position', [1 15 24 6], ... 'DataStore', myDataStore); ``` -When bound to a Sensor, threshold rules apply automatically (resolved violations are shown). The widget title, X-axis label (`'Time'`), and Y-axis label (sensor Units or Name) are auto-derived. +When bound to a Sensor (or Tag), threshold rules appear automatically, and the title, x‑label (`'Time'`), and y‑label are derived from the sensor’s Name/Units. ### Number (big value display) @@ -108,18 +126,16 @@ d.addWidget('number', 'Title', 'Temperature', ... 'Position', [1 1 6 2], ... 'Sensor', sTemp, 'Units', 'degF', 'Format', '%.1f'); -% Or with static value d.addWidget('number', 'Title', 'Total Count', ... 'Position', [7 1 6 2], ... 'StaticValue', 1234, 'Units', 'pcs', 'Format', '%d'); -% Or with function callback d.addWidget('number', 'Title', 'CPU Load', ... 'Position', [13 1 6 2], ... 'ValueFcn', @() getCpuLoad(), 'Units', '%', 'Format', '%.0f'); ``` -Shows a large number with a trend arrow (up/down/flat) computed from recent sensor data. Layout: `[Title | Value+Trend | Units]`. +Shows a large number with a trend arrow derived from recent sensor data. ### Status (health indicator) @@ -127,14 +143,9 @@ Shows a large number with a trend arrow (up/down/flat) computed from recent sens d.addWidget('status', 'Title', 'Pump', ... 'Position', [7 1 5 2], ... 'Sensor', sTemp); - -% Legacy static status -d.addWidget('status', 'Title', 'System', ... - 'Position', [12 1 5 2], ... - 'StaticStatus', 'ok'); % 'ok', 'warning', 'alarm' ``` -Shows a colored dot (green/amber/red) and the sensor's latest value. Status is derived automatically from threshold rules. +Coloured dot (green/amber/red) and latest value. Status is derived from threshold rules. Legacy interfaces (`StaticStatus`, `StatusFcn`) are still accepted. ### Gauge (arc/donut/bar/thermometer) @@ -144,23 +155,20 @@ d.addWidget('gauge', 'Title', 'Flow Rate', ... 'Sensor', sFlow, 'Range', [0 160], 'Units', 'L/min', ... 'Style', 'donut'); -% Static value d.addWidget('gauge', 'Title', 'Efficiency', ... 'Position', [9 3 8 6], ... 'StaticValue', 85, 'Range', [0 100], 'Units', '%', ... 'Style', 'arc'); ``` -Styles: `'arc'` (default), `'donut'`, `'bar'`, `'thermometer'`. - -When Sensor-bound, range and units are auto-derived from threshold rules and sensor properties. +Styles: `'arc'`, `'donut'`, `'bar'`, `'thermometer'`. Range and units are auto‑derived from a bound Sensor and its threshold rules. ### Text (labels and headers) ```matlab d.addWidget('text', 'Title', 'Plant Overview', ... 'Position', [1 1 6 1], ... - 'Content', 'Line 4 - Shift A', 'FontSize', 16, ... + 'Content', 'Line 4 – Shift A', 'FontSize', 16, ... 'Alignment', 'center'); ``` @@ -169,20 +177,20 @@ d.addWidget('text', 'Title', 'Plant Overview', ... ```matlab % Static data d.addWidget('table', 'Title', 'Alarm Log', ... - 'Position', [13 9 12 4], ... + 'Position', [1 9 12 4], ... 'ColumnNames', {'Time', 'Tag', 'Value'}, ... - 'Data', {{'12:00', 'T-401', '85.2'; '12:05', 'P-201', '72.1'}}); + 'Data', {{'12:00', 'T‑401', '85.2'}}); -% Sensor data (last N rows) +% Last N rows from a Sensor d.addWidget('table', 'Title', 'Recent Data', ... 'Position', [1 9 12 4], ... 'Sensor', sTemp, 'N', 15); -% Dynamic data via callback +% Dynamic data callback d.addWidget('table', 'Title', 'Live Log', ... 'Position', [1 13 12 4], ... 'DataFcn', @() getRecentAlarms(), ... - 'ColumnNames', {'Time', 'Tag', 'Value', 'Level'}); + 'ColumnNames', {'Time', 'Tag', 'Value'}); % Event mode (requires EventStore) d.addWidget('table', 'Title', 'Events', ... @@ -191,6 +199,23 @@ d.addWidget('table', 'Title', 'Events', ... 'EventStoreObj', myEventStore, 'N', 10); ``` +### Event Timeline + +```matlab +% Bound to an EventStore +d.addWidget('timeline', 'Title', 'Alarms', ... + 'Position', [1 16 24 3], ... + 'EventStoreObj', myEventStore); + +% Legacy direct event structs +events = struct('startTime', {0, 3600}, 'endTime', {3600, 7200}, ... + 'label', {'Idle','Running'}, 'color', {[0.6 0.6 0.6],[0.2 0.7 0.3]}); +d.addWidget('timeline', 'Title', 'Machine Mode', ... + 'Position', [1 13 24 3], 'Events', events); +``` + +`FilterSensors` or `FilterTagKey` can restrict events to specific sensors or tags. + ### Raw Axes (custom plots) ```matlab @@ -199,75 +224,179 @@ d.addWidget('rawaxes', 'Title', 'Temperature Distribution', ... 'PlotFcn', @(ax) histogram(ax, tempData, 50, ... 'FaceColor', [0.31 0.80 0.64], 'EdgeColor', 'none')); -% Sensor-bound with time range +% Sensor‑bound with time range d.addWidget('rawaxes', 'Title', 'Custom Analysis', ... 'Position', [9 5 8 4], ... 'Sensor', mySensor, ... 'PlotFcn', @(ax, sensor, tRange) plotCustom(ax, sensor, tRange)); ``` -The `PlotFcn` receives MATLAB axes as the first argument. When Sensor-bound, it also receives the Sensor object and optionally a time range. +### Divider (visual separator) -### Event Timeline +```matlab +d.addWidget('divider', 'Position', [1 3 24 1], ... + 'Thickness', 2, 'Color', [0.5 0.5 0.5]); +``` + +### Bar Chart ```matlab -% From event structs -events = struct('startTime', {0, 3600}, 'endTime', {3600, 7200}, ... - 'label', {'Idle', 'Running'}, 'color', {[0.6 0.6 0.6], [0.2 0.7 0.3]}); +d.addWidget('barchart', 'Title', 'Monthly Output', ... + 'Position', [1 5 12 6], ... + 'DataFcn', @() struct('categories', {{'Q1','Q2','Q3','Q4'}}, ... + 'values', [120 245 310 280]), ... + 'Orientation', 'vertical', 'Stacked', false); +``` -d.addWidget('timeline', 'Title', 'Machine Mode', ... - 'Position', [1 13 24 3], ... - 'Events', events); +### Heatmap -% From EventStore (recommended) -d.addWidget('timeline', 'Title', 'Alarms', ... - 'Position', [1 16 24 3], ... - 'EventStoreObj', myEventStore); +```matlab +d.addWidget('heatmap', 'Title', 'Correlation Matrix', ... + 'Position', [1 11 8 6], ... + 'DataFcn', @() rand(5,5), ... + 'Colormap', 'parula', 'ShowColorbar', true, ... + 'XLabels', {{'A','B','C','D','E'}}, ... + 'YLabels', {{'A','B','C','D','E'}}); +``` + +### Histogram + +```matlab +d.addWidget('histogram', 'Title', 'Cycle Time', ... + 'Position', [1 17 8 4], ... + 'DataFcn', @() randn(1,200)*5+100, ... + 'NumBins', 20, 'ShowNormalFit', true, ... + 'EdgeColor', [0.2 0.2 0.2]); +``` + +### Image + +```matlab +d.addWidget('image', 'Title', 'Floor Plan', ... + 'Position', [1 9 12 10], ... + 'File', 'floorplan.png', 'Scaling', 'fit'); +``` -% Filtered by sensor names -d.addWidget('timeline', 'Title', 'Temp Events', ... - 'Position', [1 19 24 3], ... - 'EventStoreObj', myEventStore, ... - 'FilterSensors', {'T-401', 'T-402'}); +### Scatter + +```matlab +d.addWidget('scatter', 'Title', 'Pressure vs Temp', ... + 'Position', [13 9 12 8], ... + 'SensorX', sTemp, 'SensorY', sPress, ... + 'MarkerSize', 8, 'Colormap', 'parula'); +% Optional colour‑by sensor: 'SensorColor', sOther +``` + +### Icon Card + +A compact mushroom‑style card: coloured icon, large primary value, and subtitle. + +```matlab +d.addWidget('iconcard', 'Title', 'Tank Level', ... + 'Position', [1 21 4 3], ... + 'StaticValue', 72.5, 'Units', '%', 'StaticState', 'ok'); + +% Sensor‑bound +d.addWidget('iconcard', 'Title', 'Pump Vibration', ... + 'Position', [5 21 4 3], ... + 'Sensor', sVib); +``` + +Properties: `IconColor`, `StaticValue`, `ValueFcn`, `StaticState`, `Units`, `Format`, `SecondaryLabel`. + +### Chip Bar + +A horizontal row of mini status chips—ideal for a system‑health summary. + +```matlab +chips = { + struct('label', 'Pump', 'sensor', sPump), + struct('label', 'Fan', 'statusFcn', @() 'warn'), + struct('label', 'Tank', 'iconColor', [0.2 0.7 0.3]) +}; +d.addWidget('chipbar', 'Title', 'Health Check', ... + 'Position', [1 5 24 1], 'Chips', chips); +``` + +Each chip can use a `Sensor`, a `statusFcn`, or a fixed `iconColor`. States: `'ok'`, `'warn'`, `'alarm'`, `'info'`, `'inactive'`. + +### Sparkline Card + +Combines a big number, a mini sparkline chart, and a delta indicator. + +```matlab +d.addWidget('sparkline', 'Title', 'CPU Load', ... + 'Position', [1 3 6 4], ... + 'StaticValue', 42.0, 'SparkData', randn(1,100)*2+42, ... + 'Units', '%', 'ShowDelta', true, 'NSparkPoints', 50); +``` + +When bound to a Sensor, the sparkline and value come from `Sensor.Y`. + +### Multi‑Status + +Grid of status dots for multiple sensors. + +```matlab +d.addWidget('multistatus', 'Title', 'Sensors Overview', ... + 'Position', [1 7 24 2], ... + 'Sensors', {sTemp, sPress, sFlow}, ... + 'Columns', 4, 'ShowLabels', true, 'IconStyle', 'dot'); ``` +### Group Widget + +Collapsible, tabbed, or plain panel container for other widgets. Allows nested dashboards. + +```matlab +% Collapsible group +g = d.addCollapsible('Sensor Details', {w1, w2}); + +% Tabbed group +g = GroupWidget('Mode', 'tabbed', 'Label', 'Sections'); +g.addChild(tabWidget1, 'Tab A'); +g.addChild(tabWidget2, 'Tab B'); +d.addWidget(g); +``` + +Properties: `Mode` (`'panel'`, `'collapsible'`, `'tabbed'`), `Collapsed`, `Tabs`, `ActiveTab`, `ChildAutoFlow`. + --- ## Sensor Binding -The recommended way to drive dashboard widgets is through Sensor objects. Create sensors with data, state channels, and threshold rules, then bind them to widgets: +The recommended way to drive dashboard widgets is through Sensor objects or the v2.0 **Tag** interface. For backward compatibility, the legacy `'Sensor'` key is mapped to the `Tag` property. ```matlab -% Create and configure sensor -sTemp = Sensor('T-401', 'Name', 'Temperature'); +% Create sensors +sTemp = Sensor('T‑401', 'Name', 'Temperature'); sTemp.Units = 'degF'; sTemp.X = t; sTemp.Y = temp; - -sc = StateChannel('machine'); -sc.X = [0 7200 43200]; sc.Y = [0 1 0]; -sTemp.addStateChannel(sc); - -sTemp.addThresholdRule(struct('machine', 1), 78, ... - 'Direction', 'upper', 'Label', 'Hi Warn'); -sTemp.addThresholdRule(struct('machine', 1), 85, ... - 'Direction', 'upper', 'Label', 'Hi Alarm'); +sTemp.addThresholdRule(struct(), 78, 'Direction', 'upper', 'Label', 'Hi Warn'); sTemp.resolve(); -% All of these auto-derive from the Sensor: +% Bind to widgets — many properties are auto‑derived d.addWidget('fastsense', 'Sensor', sTemp, 'Position', [1 1 12 8]); -d.addWidget('number', 'Sensor', sTemp, 'Position', [13 1 6 2], 'Units', 'degF'); -d.addWidget('status', 'Sensor', sTemp, 'Position', [19 1 6 2]); -d.addWidget('gauge', 'Sensor', sTemp, 'Position', [13 3 12 6]); +d.addWidget('number', 'Sensor', sTemp, 'Position', [13 1 6 2], 'Units', 'degF'); +d.addWidget('status', 'Sensor', sTemp, 'Position', [19 1 6 2]); +d.addWidget('gauge', 'Sensor', sTemp, 'Position', [13 3 12 6]); ``` -Benefits of Sensor binding: -- **Title:** auto-derived from `Sensor.Name` or `Sensor.Key` -- **Units:** auto-derived from `Sensor.Units` -- **Value:** uses `Sensor.Y(end)` for number, gauge, and status widgets -- **Thresholds:** FastSenseWidget renders resolved thresholds and violations -- **Status:** StatusWidget checks the latest value against all threshold rules -- **Live refresh:** calling `refresh()` re-reads the sensor data +Benefits of sensor binding: +- **Title** – auto‑derived from `Sensor.Name` or `Sensor.Key` +- **Units** – auto‑derived from `Sensor.Units` +- **Value** – uses `Sensor.Y(end)` for number, gauge, status, etc. +- **Thresholds** – FastSenseWidget renders resolutions and violations +- **Status** – StatusWidget checks latest value against threshold rules +- **Live refresh** – calling `refresh()` re‑reads sensor data + +For widgets that accept a `Tag` instead of a `Sensor`, you can pass a Tag object directly: + +```matlab +tag = TagRegistry.get('temp_sensor'); +d.addWidget('fastsense', 'Tag', tag); +``` --- @@ -279,7 +408,7 @@ Benefits of Sensor binding: d.save('dashboard.json'); ``` -The JSON file contains the dashboard name, theme, live interval, grid settings, and each widget's type, title, position, and data source. +The JSON contains the dashboard name, theme, live interval, grid settings, and each widget’s type, title, position, and data source. Multi‑page dashboards are fully serialized. ### Load from JSON @@ -288,12 +417,11 @@ d2 = DashboardEngine.load('dashboard.json'); d2.render(); ``` -To re-bind Sensor objects on load, provide a resolver function: +To re‑bind Sensor objects, supply a resolver: ```matlab d2 = DashboardEngine.load('dashboard.json', ... 'SensorResolver', @(name) SensorRegistry.get(name)); -d2.render(); ``` ### Export as MATLAB Script @@ -302,23 +430,23 @@ d2.render(); d.exportScript('rebuild_dashboard.m'); ``` -Generates a readable `.m` file with `DashboardEngine` constructor and `addWidget` calls that recreates the dashboard. +Generates a readable `.m` function that recreates the dashboard from scratch. --- ## Theming -DashboardEngine uses `DashboardTheme`, which extends `FastSenseTheme` with dashboard-specific fields (widget backgrounds, border colors, status indicator colors, etc.). +DashboardEngine uses `DashboardTheme`, which extends `FastSenseTheme` with dashboard‑specific fields (widget backgrounds, border colors, status indicators, etc.). ```matlab d = DashboardEngine('My Dashboard'); -d.Theme = 'dark'; % or 'light', 'industrial', 'scientific', 'ocean' +d.Theme = 'dark'; % or 'light' d.render(); ``` -Available presets: `'default'`, `'dark'`, `'light'`, `'industrial'`, `'scientific'`, `'ocean'`. +Available presets: `'light'`, `'dark'`. Legacy names (`'default'`, `'industrial'`, etc.) are aliased to `'light'` for backward compatibility. -You can also override specific theme properties: +Override specific theme fields: ```matlab theme = DashboardTheme('dark', 'WidgetBackground', [0.1 0.1 0.2]); @@ -329,35 +457,35 @@ d.Theme = theme; ## Live Mode -DashboardEngine supports live data updates via a timer that periodically calls `refresh()` on all widgets. +DashboardEngine supports live data updates via a timer that calls `refresh()` on all widgets. ```matlab d = DashboardEngine('Live Monitor'); d.Theme = 'dark'; -d.LiveInterval = 2; % refresh every 2 seconds +d.LiveInterval = 2; % seconds d.addWidget('fastsense', 'Sensor', sTemp, 'Position', [1 1 24 8]); d.addWidget('number', 'Sensor', sTemp, 'Position', [1 9 12 2]); d.render(); -d.startLive(); % start periodic refresh -% ... later -d.stopLive(); % stop +d.startLive(); % begin periodic refresh +% … later +d.stopLive(); ``` -You can also toggle live mode from the toolbar's Live button. The toolbar shows the last update timestamp when live mode is active. +A **stale‑data banner** appears when any widget’s latest timestamp stops advancing, helping operators notice frozen feeds. --- ## Global Time Controls -The time panel at the bottom of the dashboard has two sliders that control the visible time range across all widgets. Moving the sliders calls `setTimeRange(tStart, tEnd)` on each widget. +The bottom‑panel **TimeRangeSelector** provides two sliders that control the visible time window across all widgets. -- **FastSenseWidget:** sets xlim on the FastSense axes -- **EventTimelineWidget:** sets xlim on the timeline axes -- **RawAxesWidget:** passes the time range to the PlotFcn +- **FastSenseWidget**, **EventTimelineWidget**, **RawAxesWidget** respond to `setTimeRange(tStart, tEnd)`. +- A semi‑transparent envelope preview (aggregate min/max or line previews) helps the user see the overall signal shape. +- Event markers (if available) are displayed as faint vertical lines. -If a user manually zooms a specific widget, that widget detaches from global time (`UseGlobalTime = false`). Click the **Sync** button in the toolbar to re-attach all widgets. +When a user manually zooms a single widget, that widget detaches from global time. Click the **Sync** button in the toolbar to re‑attach all widgets. --- @@ -365,40 +493,42 @@ If a user manually zooms a specific widget, that widget detaches from global tim Click the **Edit** button in the toolbar to enter edit mode: -1. A **palette sidebar** appears on the left with buttons for each widget type -2. A **properties panel** appears on the right showing the selected widget's settings -3. **Drag handles** let you reposition widgets on the grid -4. **Resize handles** let you change widget dimensions -5. Click **Apply** to save property changes -6. Click **Done** to exit edit mode +1. A **palette sidebar** appears with buttons for each widget type. +2. A **properties panel** shows the selected widget’s settings. +3. **Drag handles** allow repositioning on the grid. +4. **Resize handles** change widget dimensions. +5. Click **Apply** to save property changes. +6. Click **Done** to exit edit mode. -The editor snaps to the 24-column grid. You can change the widget's title, position, axis labels, and data source directly in the properties panel. +The editor snaps to the 24‑column grid. You can also add specialized widgets like **IconCard**, **ChipBar**, and **SparklineCard** directly from builder convenience methods. -Widget management functions: -- `addWidget(type)` - add a new widget of the specified type -- `deleteWidget(idx)` - remove widget by index -- `selectWidget(idx)` - select a widget for property editing -- `setWidgetPosition(idx, pos)` - move/resize widget programmatically +Programmatic widget management: + +```matlab +d.addWidget('fastsense', …); % add any widget +d.selectWidget(idx); % select a widget for editing +d.setWidgetPosition(idx, pos); % move/resize programmatically +d.removeWidget(idx); % delete a widget +``` --- ## Info File Integration -Dashboards can link to external Markdown documentation files: +Link an external Markdown documentation file to your dashboard: ```matlab -d = DashboardEngine('My Dashboard'); -d.InfoFile = 'dashboard_help.md'; % path to Markdown file +d.InfoFile = 'dashboard_help.md'; d.render(); ``` -An **Info** button appears in the toolbar. Clicking it renders the Markdown file as HTML and opens it in the system browser. Supports basic Markdown syntax including headers, lists, code blocks, and tables. +An **Info** button appears in the toolbar. Clicking it renders the Markdown as HTML and opens it in the system browser. --- ## Complete Example -This example creates a process monitoring dashboard with sensor-bound widgets: +A multi‑sensor process dashboard with sensor‑bound widgets, groups, and a chip bar. ```matlab install; @@ -408,25 +538,23 @@ rng(42); N = 10000; t = linspace(0, 86400, N); % 24 hours -% Machine mode state channel +% Machine state channel scMode = StateChannel('machine'); scMode.X = [0, 3600, 7200, 28800, 36000]; scMode.Y = [0, 1, 1, 2, 1 ]; % Temperature sensor -sTemp = Sensor('T-401', 'Name', 'Temperature'); +sTemp = Sensor('T‑401', 'Name', 'Temperature'); sTemp.Units = 'degF'; sTemp.X = t; sTemp.Y = 74 + 3*sin(2*pi*t/3600) + randn(1,N)*1.2; sTemp.addStateChannel(scMode); -sTemp.addThresholdRule(struct('machine', 1), 78, ... - 'Direction', 'upper', 'Label', 'Hi Warn'); -sTemp.addThresholdRule(struct('machine', 1), 85, ... - 'Direction', 'upper', 'Label', 'Hi Alarm'); +sTemp.addThresholdRule(struct('machine', 1), 78, 'Direction', 'upper', 'Label', 'Hi Warn'); +sTemp.addThresholdRule(struct('machine', 1), 85, 'Direction', 'upper', 'Label', 'Hi Alarm'); sTemp.resolve(); % Pressure sensor -sPress = Sensor('P-201', 'Name', 'Pressure'); +sPress = Sensor('P‑201', 'Name', 'Pressure'); sPress.Units = 'psi'; sPress.X = t; sPress.Y = 55 + 20*sin(2*pi*t/7200) + randn(1,N)*1.5; @@ -435,32 +563,37 @@ sPress.addThresholdRule(struct(), 70, 'Direction', 'upper', 'Label', 'Hi Alarm') sPress.resolve(); %% Build dashboard -d = DashboardEngine('Process Monitoring — Line 4'); +d = DashboardEngine('Process Monitoring – Line 4'); d.Theme = 'light'; d.LiveInterval = 5; -% Header row: text + numbers + status -d.addWidget('text', 'Title', 'Overview', 'Position', [1 1 4 2], ... - 'Content', 'Line 4 — Shift A', 'FontSize', 16); -d.addWidget('number', 'Title', 'Temperature', 'Position', [5 1 5 2], ... +% Top‑row chip bar +chips = { + struct('label','T‑401','sensor',sTemp), + struct('label','P‑201','sensor',sPress) +}; +d.addWidget('chipbar', 'Title', 'System Health', ... + 'Position', [1 1 24 1], 'Chips', chips); + +% Numbers and status +d.addWidget('number', 'Title', 'Temperature', 'Position', [1 2 6 2], ... 'Sensor', sTemp, 'Format', '%.1f'); -d.addWidget('number', 'Title', 'Pressure', 'Position', [10 1 5 2], ... +d.addWidget('number', 'Title', 'Pressure', 'Position', [7 2 6 2], ... 'Sensor', sPress, 'Format', '%.0f'); -d.addWidget('status', 'Title', 'Temp', 'Position', [15 1 5 2], ... +d.addWidget('status', 'Title', 'Temp', 'Position', [13 2 6 2], ... 'Sensor', sTemp); -d.addWidget('status', 'Title', 'Press', 'Position', [20 1 5 2], ... +d.addWidget('status', 'Title', 'Press', 'Position', [19 2 6 2], ... 'Sensor', sPress); -% Plot row: sensor-bound FastSense widgets -d.addWidget('fastsense', 'Position', [1 3 12 8], 'Sensor', sTemp); -d.addWidget('fastsense', 'Position', [13 3 12 8], 'Sensor', sPress); +% Big plots +d.addWidget('fastsense', 'Position', [1 4 12 10], 'Sensor', sTemp); +d.addWidget('fastsense', 'Position', [13 4 12 10], 'Sensor', sPress); -% Bottom row: gauge + custom plot -d.addWidget('gauge', 'Title', 'Pressure', 'Position', [1 11 8 6], ... - 'Sensor', sPress, 'Range', [0 100], 'Units', 'psi'); -d.addWidget('rawaxes', 'Title', 'Temp Distribution', 'Position', [9 11 8 6], ... - 'PlotFcn', @(ax) histogram(ax, sTemp.Y, 50, ... - 'FaceColor', [0.31 0.80 0.64], 'EdgeColor', 'none')); +% Collapsible group with gauges and a histogram +g = d.addCollapsible('Details', {}); +g.addChild(GaugeWidget('Title','Pressure Gauge','Sensor',sPress,'Style','donut','Range',[0 100],'Units','psi')); +g.addChild(RawAxesWidget('Title','Temp Histogram','PlotFcn',@(ax) histogram(ax, sTemp.Y, 50, 'FaceColor', [0.31 0.80 0.64], 'EdgeColor', 'none'))); +d.addWidget(g); % placed at next free row d.render(); @@ -472,7 +605,7 @@ d.save(fullfile(tempdir, 'process_dashboard.json')); ## See Also -- [[API Reference: Dashboard]] -- Full API reference for all dashboard classes -- [[API Reference: Sensors]] -- Sensor, StateChannel, ThresholdRule -- [[Live Mode Guide]] -- Live data polling -- [[Examples]] -- `example_dashboard_engine`, `example_dashboard_all_widgets` +- [[API Reference: Dashboard]] – Full property and method reference for all dashboard classes +- [[API Reference: Sensors]] – Sensor, StateChannel, ThresholdRule +- [[Live Mode Guide]] – Live data polling and staleness detection +- [[Examples]] – `example_dashboard_engine`, `example_dashboard_all_widgets` diff --git a/wiki/Event-Detection-Guide.md b/wiki/Event-Detection-Guide.md index 7e1ff841..0f79e422 100644 --- a/wiki/Event-Detection-Guide.md +++ b/wiki/Event-Detection-Guide.md @@ -2,409 +2,326 @@ # Event Detection Guide -The Event Detection system in FastSense provides comprehensive threshold-based monitoring with live detection, notification services, and visual event management. It bridges the [[Sensors]] library for threshold analysis with real-time event pipelines, storage, and notifications. +The Event Detection system in FastSense provides storage, visualization, and notification services for threshold violations detected by the [[Sensors]] library. It orchestrates live detection pipelines, manages atomic event persistence, and offers interactive Gantt‑style viewing. Use it when you need to track, review, and alert on sensor threshold breaches in real time or after the fact. ## When to Use Event Detection -- **Real-time monitoring**: Detect threshold violations as they occur in live data streams -- **Historical analysis**: Analyze events from recorded sensor data with statistical summaries -- **Alert systems**: Configure rule-based notifications with email and snapshot generation -- **Event visualization**: View events in Gantt timelines and filterable tables -- **Data archival**: Store events with automatic backup rotation and atomic file operations +- **Real‑time monitoring** – detect and store events as they occur in streaming data. +- **Historical analysis** – load saved events for offline review and statistical summaries. +- **Alert systems** – send rule‑based email notifications with PNG snapshots. +- **Event visualization** – explore events on a Gantt timeline with filterable tables. +- **Data archival** – persist events with automatic backup rotation and atomic writes. ## Core Workflow -The event detection workflow follows these steps: +The typical workflow spans the [[Sensors]] library and the EventDetection classes: -1. **Configure sensors** with thresholds using the [[Sensors]] library -2. **Set up data sources** to fetch new sensor data (live files, mock data, etc.) -3. **Configure event detection** with minimum duration, callbacks, and escalation -4. **Run detection** to find threshold violations and generate Event objects -5. **Store and visualize** events using EventStore and EventViewer +1. **Create sensors** with threshold rules (see [[Sensors]]). +2. **Wrap sensors in `MonitorTag` objects** – these carry the detection logic. +3. **Configure data sources** using `MockDataSource` or `MatFileDataSource`. +4. **Assemble the `LiveEventPipeline`** – it polls data sources, runs detection, appends events to an `EventStore`, and triggers notifications. +5. **Start the pipeline** – events are automatically stored and, optionally, opened in the `EventViewer`. +6. **Review events** with `EventViewer`, `printEventSummary`, or `generateEventSnapshot`. -## Basic Event Detection +## Event Objects -### Quick Start Example +Each detected violation becomes an [[Event Detection|Event]] object. Events are created by the pipeline’s internal detection (via `MonitorTag`), but you can also instantiate them manually for testing. ```matlab -% Create a sensor with threshold -sensor = Sensor('temperature'); -sensor.X = 1:100; -sensor.Y = 70 + 10*sin((1:100)/10) + randn(1,100); -sensor.addThresholdRule(struct(), 85, 'Direction', 'upper', 'Label', 'temp high'); - -% Configure and run detection -cfg = EventConfig(); -cfg.MinDuration = 2; % 2-second minimum -cfg.addSensor(sensor); -events = cfg.runDetection(); - -% Print summary -printEventSummary(events); -``` +% Manual creation +ev = Event(datenum(2025,1,10,12,0,0), ... + datenum(2025,1,10,12,5,0), ... + 'Temperature', 'High Warning', 85, 'upper'); -### EventConfig - Central Configuration +% Statistical enrichment (normally done by the detector) +ev.setStats(peakValue, numPoints, minVal, maxVal, meanVal, rmsVal, stdVal); -The [[Event Detection|EventConfig]] class orchestrates all event detection: +% Close an open event (useful for live monitoring) +ev.close(endTime, finalStats); -```matlab -cfg = EventConfig(); -cfg.MinDuration = 1.5; % Debounce short violations -cfg.MaxCallsPerEvent = 2; % Limit callback invocations -cfg.EscalateSeverity = true; % H -> HH when peak exceeds -cfg.AutoOpenViewer = true; % Open EventViewer after detection -cfg.OnEventStart = eventLogger(); % Console logging callback - -% Auto-save events to file with backup rotation -cfg.EventFile = 'my_events.mat'; -cfg.MaxBackups = 5; - -% Add sensors -cfg.addSensor(temperatureSensor); -cfg.addSensor(pressureSensor); - -% Set threshold colors for visualization -cfg.setColor('temp warning', [1 0.8 0]); -cfg.setColor('temp critical', [1 0.2 0]); - -% Run detection -events = cfg.runDetection(); +% Escalate the event to a higher threshold label +ev.escalateTo('HH Alarm', 95); ``` -### Event Objects +Key properties (all read‑only after creation unless noted): -Each detected event is represented by an [[Event Detection|Event]] object: +- `StartTime`, `EndTime` – datenum timestamps. +- `Duration` – difference in days (set automatically on creation or closing). +- `SensorName`, `ThresholdLabel`, `ThresholdValue`, `Direction`. +- `PeakValue`, `NumPoints`, `MinValue`, `MaxValue`, `MeanValue`, `RmsValue`, `StdValue` – set via `setStats`. +- `Id` – unique identifier assigned by `EventStore.append`. +- `IsOpen` – true while the event is ongoing; toggled by `close`. +- `Severity`, `Category`, `Notes`, `TagKeys` – metadata fields for tagging and annotation (see [[Event Detection|EventBinding]]). -```matlab -% Event properties (read-only after creation) -event.StartTime % datenum of violation start -event.EndTime % datenum of violation end -event.Duration % duration in days -event.SensorName % sensor identifier -event.ThresholdLabel % threshold name -event.ThresholdValue % threshold numeric value -event.Direction % 'upper' or 'lower' - -% Statistical properties (set by detector) -event.PeakValue % most extreme value during violation -event.NumPoints % number of data points in violation -event.MinValue % minimum value during violation -event.MaxValue % maximum value during violation -event.MeanValue % mean value during violation -event.RmsValue % RMS value during violation -event.StdValue % standard deviation during violation -``` +## Event Storage with `EventStore` -## Live Event Detection +The [[Event Detection|EventStore]] class handles persistence with atomic file operations and automatic backup rotation. -### Data Sources +```matlab +% Create an event store backed by a .mat file +store = EventStore('events.mat', 'MaxBackups', 5); -Data sources provide the interface between your data and the event detection system: +% Append new events (does not immediately write to disk) +store.append(newEvents); -```matlab -% Mock data source for testing -mockDS = MockDataSource('BaseValue', 100, 'NoiseStd', 2, ... - 'ViolationProbability', 0.001, 'ViolationAmplitude', 25); +% Optionally embed sensor data for later visualization +store.SensorData = mySensorData; % struct array +store.ThresholdColors = myColorMap; % containers.Map -% File-based data source for live monitoring -fileDS = MatFileDataSource('sensors/temp.mat', 'XVar', 'time', 'YVar', 'temp'); +% Save to disk atomically +store.save(); -% Map sensors to data sources -dsMap = DataSourceMap(); -dsMap.add('temperature', mockDS); -dsMap.add('pressure', fileDS); +% Load events and metadata +[events, meta, changed] = EventStore.loadFile('events.mat'); ``` -### Live Pipeline - -The [[Event Detection|LiveEventPipeline]] orchestrates continuous monitoring: +`EventStore` also supports closing open events: ```matlab -% Create pipeline -pipeline = LiveEventPipeline(sensors, dsMap, ... - 'EventFile', 'live_events.mat', ... - 'Interval', 15, ... % 15-second polling - 'MinDuration', 5, ... % 5-second minimum events - 'EscalateSeverity', true); % H -> HH escalation - -% Configure notifications -notifService = NotificationService('DryRun', true); -pipeline.NotificationService = notifService; - -% Start/stop live monitoring -pipeline.start(); % begins timer-driven cycles -pipeline.stop(); % stops timer +store.closeEvent(eventId, endTime, finalStats); +% Remember to call store.save() afterwards ``` -### Incremental Detection +## Live Event Detection Pipeline -For live scenarios, use [[Event Detection|IncrementalEventDetector]] to maintain state between updates: +The [[Event Detection|LiveEventPipeline]] provides turn‑key continuous monitoring. It periodically fetches new data from `DataSource` objects, feeds it into `MonitorTag` instances for detection, stores resulting events in an `EventStore`, and invokes a `NotificationService`. -```matlab -detector = IncrementalEventDetector('MinDuration', 2, ... - 'EscalateSeverity', true); +### Setting Up Data Sources -% Process incremental updates -newEvents = detector.process('temp_01', sensor, newX, newY, [], []); +Data sources are subclasses of the abstract `DataSource`. Two concrete implementations are provided: -% Check for ongoing events -if detector.hasOpenEvent('temp_01') - state = detector.getSensorState('temp_01'); - fprintf('Open event since %.2f\n', state.openEventStart); -end +```matlab +% Mock data source for testing – injects synthetic violations +mockDS = MockDataSource('BaseValue', 100, ... + 'NoiseStd', 1, ... + 'ViolationProbability', 0.005, ... + 'ViolationAmplitude', 25); + +% File‑based data source for live files +fileDS = MatFileDataSource('sensor_data.mat', ... + 'XVar', 'timestamp', 'YVar', 'value'); ``` -## Event Storage and Persistence - -### EventStore - Atomic File Operations +### Mapping Sources to Monitors -The [[Event Detection|EventStore]] provides thread-safe event persistence: +A `DataSourceMap` associates sensor keys with data sources: ```matlab -% Create event store -store = EventStore('events.mat', 'MaxBackups', 3); - -% Configure metadata for EventViewer -store.SensorData = cfg.SensorData; % for click-to-plot -store.ThresholdColors = cfg.ThresholdColors; % for color consistency - -% Append new events (atomic operation) -store.append(newEvents); -store.save(); - -% Load from file (static method) -[events, metadata, changed] = EventStore.loadFile('events.mat'); +dsMap = DataSourceMap(); +dsMap.add('temp_01', mockDS); +dsMap.add('press_01', fileDS); ``` -### Auto-Save Configuration +### Creating Monitor Tags -EventConfig can automatically save events to a file: +`MonitorTag` objects (from the [[Sensors]] library) perform threshold detection. Build one for each monitored sensor: ```matlab -cfg.EventFile = 'auto_events.mat'; % Enable auto-save -cfg.MaxBackups = 5; % Backup rotation - -% Events saved automatically after cfg.runDetection() -events = cfg.runDetection(); +% Assuming 'sensor' is a Sensor object with thresholds defined +monitorTemp = MonitorTag('temp_01', tempSensor, '>', 85, ... + 'Label', 'High Temp'); +monitorPress = MonitorTag('press_01', pressSensor, '<', 50, ... + 'Label', 'Low Pressure'); ``` -## Event Visualization - -### EventViewer - Interactive Timeline - -The [[Event Detection|EventViewer]] provides a Gantt timeline and filterable table: +Collect them in a `containers.Map`: ```matlab -% Create viewer with full context -viewer = EventViewer(events, sensorData, thresholdColors); +targets = containers.Map(); +targets('temp_01') = monitorTemp; +targets('press_01') = monitorPress; +``` -% Or load from saved file -viewer = EventViewer.fromFile('events.mat'); +### Assembling and Running the Pipeline -% Auto-refresh from file -viewer.startAutoRefresh(10); % refresh every 10 seconds -viewer.stopAutoRefresh(); +Combine everything into a `LiveEventPipeline`: -% Manual refresh -viewer.refreshFromFile(); +```matlab +pipeline = LiveEventPipeline(targets, dsMap, ... + 'EventStore', store, ... % EventStore object (optional) + 'Interval', 15, ... % seconds between cycles + 'MinDuration', 5/86400, ... % minimum event duration in days + 'EscalateSeverity', true, ... % enable H→HH escalation + 'OnEventStart', eventLogger()); % optional callback + +% Configure notifications (see below) +pipeline.NotificationService = notifService; -% Update with new events -viewer.update(newEvents); +% Start and stop monitoring +pipeline.start(); % begins timer‑driven polling +pipeline.stop(); % halts the timer ``` -The EventViewer features: -- **Gantt timeline**: Visual event bars colored by threshold -- **Filterable table**: Filter by sensor, threshold, date range -- **Click interaction**: Click Gantt bars to highlight table rows -- **Auto-refresh**: Polls the source file for live updates -- **Export**: Context menu options for data export +The pipeline automatically appends events to its `EventStore` (if provided) and calls the notification service on each event. It respects `MinDuration` to filter transient violations. ## Notification System ### Notification Rules -Configure rule-based notifications with priority matching: +[[Event Detection|NotificationRule]] objects specify who gets notified and what the email contains. Rules support priority matching: exact sensor+threshold match > sensor‑only match > default rule. ```matlab -% Default rule (catches all events) +% Default rule – catches every event defaultRule = NotificationRule('Recipients', {{'ops@company.com'}}, ... 'Subject', 'Event: {sensor} - {threshold}', ... 'IncludeSnapshot', false); -% Sensor-specific rule (higher priority) -tempRule = NotificationRule('SensorKey', 'temperature', ... +% Sensor‑specific rule +tempRule = NotificationRule('SensorKey', 'temp_01', ... 'Recipients', {{'thermal@company.com'}}, ... 'Subject', 'Temperature Event: {threshold}', ... 'IncludeSnapshot', true, ... 'ContextHours', 2); -% Exact match rule (highest priority) -criticalRule = NotificationRule('SensorKey', 'temperature', ... - 'ThresholdLabel', 'critical', ... +% Exact match rule (highest priority) +critRule = NotificationRule('SensorKey', 'temp_01', ... + 'ThresholdLabel', 'Critical', ... 'Recipients', {{'safety@company.com', 'manager@company.com'}}, ... 'Subject', 'CRITICAL: {sensor} {threshold}!'); ``` -### NotificationService +Template variables available: `{sensor}`, `{threshold}`, `{direction}`, `{startTime}`, `{endTime}`, `{duration}`, `{peak}`, `{mean}`, `{std}`, `{min}`, `{max}`, `{rms}`. -The [[Event Detection|NotificationService]] manages rule-based notifications: +### Notification Service + +The [[Event Detection|NotificationService]] manages rule matching and email dispatch: ```matlab -notif = NotificationService('DryRun', true, ... % test mode - 'SnapshotDir', 'snapshots/', ... - 'SmtpServer', 'mail.company.com'); +notif = NotificationService('DryRun', true, ... % test mode – no emails sent + 'SmtpServer', 'mail.company.com', ... + 'SnapshotDir', 'snapshots/'); notif.setDefaultRule(defaultRule); notif.addRule(tempRule); -notif.addRule(criticalRule); - -% Notify on event (called by pipeline) -notif.notify(event, sensorData); +notif.addRule(critRule); ``` -### Email Templates +A pipeline call to `notif.notify(event, sensorData)` triggers snapshot generation (if the best rule requests it) and sends the email. Snapshots older than `SnapshotRetention` days are automatically cleaned up. -Notification templates support variable substitution: +## Event Visualization with `EventViewer` + +[[Event Detection|EventViewer]] displays events on an interactive Gantt chart alongside a filterable table. ```matlab -rule = NotificationRule( ... - 'Subject', 'Alert: {sensor} exceeded {threshold}', ... - 'Message', ['Sensor: {sensor}\n' ... - 'Threshold: {threshold} ({direction})\n' ... - 'Time: {startTime} to {endTime}\n' ... - 'Duration: {duration}\n' ... - 'Peak: {peak}\n' ... - 'Statistics: mean={mean}, std={std}']); -``` +% Open viewer directly from a saved .mat file +viewer = EventViewer.fromFile('events.mat'); -Available template variables: -- `{sensor}`, `{threshold}`, `{direction}`, `{peak}` -- `{startTime}`, `{endTime}`, `{duration}` -- `{mean}`, `{std}`, `{min}`, `{max}`, `{rms}` +% Or construct with events, sensor data, and threshold colors +viewer = EventViewer(events, sensorData, thresholdColors); -### Event Snapshots +% Update viewer with new events +viewer.update(newEvents); +``` -Generate PNG snapshots showing event context: +Key viewer features: -```matlab -% Generate detail and context plots -files = generateEventSnapshot(event, sensorData, ... - 'OutputDir', 'snapshots/', ... - 'SnapshotSize', [800, 400], ... - 'Padding', 0.1, ... % 10% padding around event - 'ContextHours', 2); % 2 hours before event +- Click on a Gantt bar to highlight the corresponding table row. +- Filter by sensor, threshold, or date range using the table controls. +- **Auto‑refresh** – set an interval to reload from the source file: + ```matlab + viewer.startAutoRefresh(10); % refresh every 10 seconds + viewer.stopAutoRefresh(); + ``` +- **Multi‑sensor context** – clicking a bar can plot the underlying sensor data if `SensorData` was provided. -% Returns: {detailFile, contextFile} -``` +## Utility Functions -## Severity Escalation +### `eventLogger` -Events can escalate to higher severity levels when peaks exceed multiple thresholds: +A simple callback factory that prints a one‑line log when an event occurs. ```matlab -% Configure escalation -detector = EventDetector('EscalateSeverity', true); - -% Sensor with multiple thresholds -sensor.addThresholdRule(struct(), 85, 'Label', 'H Warning'); -sensor.addThresholdRule(struct(), 95, 'Label', 'HH Alarm'); - -% If violation starts at 87 (H Warning) but peaks at 97: -% 1. Initial event: "H Warning" -% 2. Escalated event: "HH Alarm" (same time span, higher severity) -events = detectEventsFromSensor(sensor, detector); +cfg.OnEventStart = eventLogger(); % or set on pipeline +% Prints: [EVENT] Temperature | High Temp | UPPER | 739123.45 -> 739123.47 (dur=0.0014) | peak=126.83 ``` -## Utility Functions - -### Event Logging +### `printEventSummary` -Simple console logging for development: +Format a summary table of events to the console. ```matlab -cfg.OnEventStart = eventLogger(); - -% Logs: [EVENT] Temperature | temp high | UPPER | 123.45 -> 125.67 (dur=0.02) | peak=126.83 +printEventSummary(events); +% Columns: Start, End, Duration, Sensor, Threshold, Dir, Peak, #Pts, Mean, Std ``` -### Event Summary +### `generateEventSnapshot` -Formatted console output for analysis: +Create two PNG snapshots for an event: a detail plot and a wider context plot. ```matlab -printEventSummary(events); - -% Outputs table with columns: -% Start | End | Duration | Sensor | Threshold | Dir | Peak | #Pts | Mean | Std +files = generateEventSnapshot(event, sensorData, ... + 'OutputDir', 'snapshots/', ... + 'SnapshotSize', [800, 400], ... + 'Padding', 0.1, ... % 10% margin around the event + 'ContextHours', 2); % hours before event start +% Returns {'detail.png', 'context.png'} ``` -### Bridging with Sensors +## Event‑Tag Binding (`EventBinding`) -Convert from sensor violations to events: +[[Event Detection|EventBinding]] is a static registry that links `Event` objects to `Tag` objects (from the broader FastSense system). It enables many‑to‑many relationships for taxonomy and filtering. You rarely call it directly; instead, use convenience methods on `Event` or `EventStore`. ```matlab -% Uses sensor.ResolvedViolations and sensor.ResolvedThresholds -events = detectEventsFromSensor(sensor); -events = detectEventsFromSensor(sensor, customDetector); +% Attach a tag to an event +EventBinding.attach(event.Id, 'FleetA'); + +% Query events for a tag +taggedEvents = EventBinding.getEventsForTag('FleetA', eventStore); ``` ## Performance Considerations -- **MinDuration**: Use appropriate debounce times to filter noise -- **MaxCallsPerEvent**: Limit callback overhead in high-frequency scenarios -- **Backup rotation**: Configure MaxBackups to manage disk usage -- **Incremental detection**: Use IncrementalEventDetector for live scenarios to avoid reprocessing -- **File polling**: Balance refresh intervals with system load -- **Snapshot generation**: PNG creation can be expensive; use sparingly +- **Polling interval**: Set the pipeline’s `Interval` to a value that balances responsiveness and load. +- **Minimum duration**: Use `MinDuration` (in days) to ignore short‑lived spikes. +- **Snapshot generation**: PNG creation can be expensive; restrict snapshots to high‑priority rules only. +- **File backups**: Adjust `MaxBackups` in `EventStore` to manage disk usage. +- **Incremental data**: `MatFileDataSource` reads only new points since the last fetch, making it efficient for large files. ## Common Patterns -### Multi-Sensor Dashboard with Events +### Multi‑Sensor Live Pipeline with Notifications ```matlab -% Configure multiple sensors -cfg = EventConfig(); -cfg.addSensor(temperatureSensor); -cfg.addSensor(pressureSensor); -cfg.addSensor(vibrationSensor); -cfg.AutoOpenViewer = true; - -% Run detection and view results -events = cfg.runDetection(); -``` +% 1. Create sensors and monitor tags +tempMon = MonitorTag('temp', tempSensor, '>', 90, 'Label', 'Overheat'); +pressMon = MonitorTag('press', pressSensor, '<', 10, 'Label', 'Vacuum Loss'); +targets = containers.Map({'temp','press'}, {tempMon, pressMon}); -### Live Monitoring with Notifications +% 2. Data source mapping +dsMap = DataSourceMap(); +dsMap.add('temp', MatFileDataSource('temp_data.mat')); +dsMap.add('press', MatFileDataSource('press_data.mat')); -```matlab -% Set up complete live pipeline -pipeline = LiveEventPipeline(sensors, dataSourceMap, ... - 'EventFile', 'monitoring.mat', ... - 'Interval', 30); +% 3. Notification rules +notif = NotificationService('DryRun', false, 'SmtpServer', 'smtp.local'); +notif.addRule(NotificationRule('SensorKey', 'temp', ... + 'Recipients', {{'alerts@company.com'}}, 'Subject', 'Overheat detected')); -% Configure notifications -pipeline.NotificationService = notificationService; +% 4. Pipeline +pipeline = LiveEventPipeline(targets, dsMap, ... + 'EventStore', EventStore('monitoring.mat'), ... + 'NotificationService', notif, ... + 'OnEventStart', eventLogger()); -% Start monitoring pipeline.start(); ``` -### Event Analysis Workflow +### Event Analysis from Saved Data ```matlab -% Load saved events +% Load stored events viewer = EventViewer.fromFile('historical_events.mat'); -% Analyze programmatically +% Programmatic access [events, meta] = EventStore.loadFile('historical_events.mat'); -tempEvents = events(strcmp({events.SensorName}, 'Temperature')); -criticalEvents = events(strcmp({events.ThresholdLabel}, 'critical')); - -printEventSummary(criticalEvents); +highTempEvents = events(strcmp({events.SensorName}, 'Temperature') & ... + strcmp({events.ThresholdLabel}, 'High')); +printEventSummary(highTempEvents); ``` ## See Also -- [[Sensors]] - Configure thresholds and violations -- [[Live Mode Guide]] - Real-time data streaming patterns -- [[Dashboard Engine Guide]] - Multi-plot coordination -- [[Examples]] - Complete working examples +- [[Sensors]] – Configure thresholds and MonitorTag objects. +- [[Live Mode Guide]] – Real‑time data streaming patterns. +- [[Dashboard Engine Guide]] – Multi‑plot coordination. +- [[Examples]] – Complete working examples. diff --git a/wiki/Getting-Started.md b/wiki/Getting-Started.md index fa2ad769..9f3969e3 100644 --- a/wiki/Getting-Started.md +++ b/wiki/Getting-Started.md @@ -2,7 +2,7 @@ # Getting Started -A step-by-step tutorial introducing FastSense's core features for ultra-fast time series plotting. +A step-by-step tutorial introducing FastSense’s core features for ultra-fast time series plotting. ## 1. Your First Plot @@ -29,7 +29,8 @@ fp.addLine(x, y, 'DisplayName', 'Sensor'); fp.render(); ``` -Available presets: 'default', 'dark', 'light', 'industrial', 'scientific', 'ocean'. See [[API Reference: Themes]] for customization options. +Valid presets are `'light'` and `'dark'`. The legacy names `'default'`, `'industrial'`, `'scientific'`, and `'ocean'` are accepted but map to `'light'` for backward compatibility. +See [[API Reference: Themes]] for customization with structs and palette names. ## 3. Thresholds and Violations @@ -41,19 +42,19 @@ fp.addThreshold(-0.8, 'Direction', 'lower', 'ShowViolations', true, 'Color', 'b' fp.render(); ``` -Red circles appear where data exceeds the threshold. +Red circles appear where data exceeds the threshold. Thresholds can also be time-varying (step‑function) by passing both `thX` and `thY` vectors. ## 4. Multiple Lines ```matlab -fp = FastSense('Theme', 'scientific'); +fp = FastSense('Theme', 'light'); fp.addLine(x, sin(x), 'DisplayName', 'Channel A'); fp.addLine(x, cos(x), 'DisplayName', 'Channel B'); fp.addLine(x, sin(2*x) * 0.5, 'DisplayName', 'Channel C'); fp.render(); ``` -Colors auto-cycle from the theme's palette. Use `resetColorIndex()` to restart the color sequence. +Colors auto-cycle from the theme’s palette. Use `resetColorIndex()` to restart the color sequence. ## 5. Visual Annotations @@ -77,6 +78,8 @@ fp.addFill(x, abs(y), 'FaceColor', [0 0.5 1], 'Baseline', 0, 'DisplayName', 'Ene fp.addMarker([10 30 70], [0.9 0.9 0.9], 'Marker', 'v', 'MarkerSize', 10, 'Color', [1 0 0], 'Label', 'Events'); ``` +All annotation types must be added **before** calling `render()`. + ## 6. Dashboard Layout ```matlab @@ -99,18 +102,20 @@ fig.setTileTitle(3, 'Vibration'); fig.renderAll(); ``` +Tiles can also contain raw MATLAB axes (via `fig.axes(n)`) or custom uipanels (via `fig.tilePanel(n)`). See [[Dashboard|API Reference: Dashboard]] for advanced tile spanning and padding options. + ## 7. Toolbar ```matlab tb = FastSenseToolbar(fig); ``` -Buttons: Data Cursor, Crosshair, Grid, Legend, Autoscale Y, Export PNG, Refresh, Live Mode, Metadata, Violations. +The toolbar provides buttons for Data Cursor, Crosshair, Grid, Legend, Autoscale Y, Export PNG, Export Data, Refresh, Live Mode, Metadata, and Violations. Toggle each on/off; the toolbar binds automatically to the target `FastSense` or `FastSenseGrid`. ## 8. Linked Axes ```matlab -fig = figure; +figure; ax1 = subplot(2, 1, 1); fp1 = FastSense('Parent', ax1, 'LinkGroup', 'sync'); fp1.addLine(x, sin(x), 'DisplayName', 'Pressure'); @@ -122,7 +127,7 @@ fp2.addLine(x, cos(x), 'DisplayName', 'Temperature'); fp2.render(); ``` -Zoom in one subplot, the other follows. +Zoom or pan in one subplot — the other follows. ## 9. Datetime Axes @@ -134,6 +139,8 @@ fp.addLine(x, y, 'XType', 'datenum', 'DisplayName', 'Daily Cycle'); fp.render(); ``` +The `'XType'` option triggers automatic date formatting. For more details see [[Datetime Guide]]. + ## 10. Logarithmic Axes ```matlab @@ -148,7 +155,7 @@ fp2.setScale('YScale', 'log'); fp2.render(); ``` -Use `setScale('XScale', 'log')` for logarithmic X-axis or both together. +Use `setScale('XScale', 'log')` and/or `'YScale', 'log'` to toggle logarithmic scaling. ## 11. Updating Data @@ -158,9 +165,11 @@ newY = cos(x * 2*pi/15) + 0.4*randn(size(x)); fp.updateData(1, x, newY); ``` +The change is instantly downsampled and displayed. Only numeric data can be replaced after rendering; new annotations require a fresh `FastSense` instance. + ## 12. Downsampling Methods -MinMax (default) preserves signal envelope. LTTB preserves visual shape. +MinMax (default) preserves signal envelope. LTTB (Largest-Triangle-Three-Buckets) preserves visual shape. ```matlab fp = FastSense('DefaultDownsampleMethod', 'lttb'); @@ -169,6 +178,7 @@ fp.render(); ``` Or per-line: + ```matlab fp.addLine(x, y1, 'DownsampleMethod', 'minmax', 'DisplayName', 'MinMax'); fp.addLine(x, y2, 'DownsampleMethod', 'lttb', 'DisplayName', 'LTTB'); @@ -181,7 +191,7 @@ fp.addLine(x, y2, 'DownsampleMethod', 'lttb', 'DisplayName', 'LTTB'); fp.startLive('data.mat', @(fp, s) fp.updateData(1, s.x, s.y), 'Interval', 1); ``` -The callback is triggered whenever the file's modification date changes. +The callback is triggered whenever the file’s modification date changes. Use `stopLive()` to halt polling. On Octave, use `runLive()` for a blocking poll loop. ## 14. Figure Distribution @@ -193,13 +203,18 @@ FastSense.distFig(); FastSense.distFig('Rows', 2, 'Cols', 3); ``` +This utility is bundled from third‑party code and distributes figure windows to fill the primary monitor. + ## Next Steps - [[FastPlot|API Reference: FastPlot]] — full constructor options, properties, methods - [[Dashboard|API Reference: Dashboard]] — tiled and tabbed layouts -- [[Sensors|API Reference: Sensors]] — state-dependent thresholds +- [[Themes|API Reference: Themes]] — theme presets, palettes, and overrides +- [[Sensors|API Reference: Sensors]] — state‑dependent thresholds via `addTag` - [[Event Detection|API Reference: Event Detection]] — event detection and viewer -- [[Live Mode Guide]] — live data polling -- [[Datetime Guide]] — datetime axes -- [[Dashboard Engine Guide]] — DashboardEngine + DashboardBuilder +- [[Live Mode Guide]] — live data polling details +- [[Datetime Guide]] — datetime axis handling +- [[Dashboard Engine Guide]] — widget‑based dashboards with gauges and edit mode - [[Examples]] — 40+ runnable examples + +For installation and MEX compilation, see [[Installation]]. diff --git a/wiki/Live-Mode-Guide.md b/wiki/Live-Mode-Guide.md index 0e25d85a..10be838f 100644 --- a/wiki/Live-Mode-Guide.md +++ b/wiki/Live-Mode-Guide.md @@ -2,16 +2,17 @@ # Live Mode Guide -FastSense supports live data visualization by polling a .mat file for updates and auto-refreshing the display. Live mode works with single plots, tiled dashboards, and tabbed docks. +Live mode allows a FastSense plot or Dashboard to continuously poll a `.mat` file for new data and automatically update the display. It works with single plots, tiled dashboards, and tabbed docks, with fine control over polling interval, view behaviour, and metadata attachment. --- ## Basic Live Plot +Create a FastSense instance, define the initial data, and then start polling: + ```matlab install; -% Create initial plot fp = FastSense('Theme', 'dark'); x = linspace(0, 10, 1e5); y = sin(x) + 0.1 * randn(size(x)); @@ -19,14 +20,14 @@ fp.addLine(x, y, 'DisplayName', 'Sensor'); fp.addThreshold(0.8, 'Direction', 'upper', 'ShowViolations', true); fp.render(); -% Start polling +% Callback receives the FastSense object and the struct loaded from the file fp.startLive('data.mat', @(fp, s) fp.updateData(1, s.x, s.y)); ``` The callback `@(fp, s) fp.updateData(1, s.x, s.y)` is called every poll cycle: - `fp` — the FastSense instance -- `s` — struct loaded from the .mat file -- `fp.updateData(lineIdx, newX, newY)` — replaces line data and re-renders +- `s` — struct loaded from the `.mat` file +- `fp.updateData(lineIdx, newX, newY)` — replaces the raw data for the specified line and re‑renders. ### Stopping Live Mode @@ -34,50 +35,58 @@ The callback `@(fp, s) fp.updateData(1, s.x, s.y)` is called every poll cycle: fp.stopLive(); ``` -Or use the Live Mode button in the toolbar. +Or use the **Live Mode** button in the toolbar (see [[API Reference: FastSenseToolbar]]). --- ## View Modes -Control how the view updates when new data arrives: +Control how the X‑axis adjusts after new data is loaded: + +| Mode | Behaviour | +|------|-----------| +| `'preserve'` | Keep current zoom/pan position. User’s view is not disturbed. | +| `'follow'` | Scroll the X‑axis to show the latest data (monitoring use case). | +| `'reset'` | Zoom out to show all data in the series. | -| Mode | Behavior | -|------|----------| -| `'preserve'` | Keep current zoom/pan position. User's view is not disturbed | -| `'follow'` | Scroll X-axis to show the latest data. Good for monitoring | -| `'reset'` | Zoom to show all data. Good for overview | +Set the mode when starting live: ```matlab fp.startLive('data.mat', @updateFcn, 'ViewMode', 'follow'); ``` -Change view mode while running: +Change view mode while live mode is active: + ```matlab -fp.setViewMode('follow'); fp.setViewMode('preserve'); +fp.setViewMode('follow'); ``` +The current mode is stored in the public property `LiveViewMode`. + --- ## Polling Interval -Default is 2 seconds. Adjust with the 'Interval' option: +Default polling interval is **2 seconds**. Adjust it with the `'Interval'` option: ```matlab -fp.startLive('data.mat', @updateFcn, 'Interval', 0.5); % Poll every 500ms -fp.startLive('data.mat', @updateFcn, 'Interval', 5); % Poll every 5 seconds +fp.startLive('data.mat', @updateFcn, 'Interval', 0.5); % poll every 500 ms +fp.startLive('data.mat', @updateFcn, 'Interval', 5); % poll every 5 s ``` +The interval is also available as the property `LiveInterval`. + --- ## Live Dashboard -FastSenseGrid supports live mode across all tiles: +`FastSenseGrid` (tiled layout) supports live mode that synchronises file polling across all tiles. ```matlab fig = FastSenseGrid(2, 2, 'Theme', 'dark'); +% Set up four tiles fp1 = fig.tile(1); fp1.addLine(x, y1, 'DisplayName', 'Pressure'); fp2 = fig.tile(2); fp2.addLine(x, y2, 'DisplayName', 'Temperature'); fp3 = fig.tile(3); fp3.addLine(x, y3, 'DisplayName', 'Flow'); @@ -90,7 +99,8 @@ fig.startLive('sensors.mat', @(fig, s) updateDashboard(fig, s), ... 'Interval', 2, 'ViewMode', 'follow'); ``` -Update callback for dashboard: +The update callback receives the `FastSenseGrid` instance and the struct loaded from the file. Inside the callback, you update each tile’s data independently: + ```matlab function updateDashboard(fig, s) fig.tile(1).updateData(1, s.t, s.pressure); @@ -104,69 +114,46 @@ end ## Live Mode with Metadata -Attach metadata that updates on each poll: +Attach metadata that updates on every poll by setting the metadata properties **before** starting live mode. + +### Single Plot ```matlab -fp.startLive('data.mat', @updateFcn, ... - 'MetadataFile', 'meta.mat', ... - 'MetadataVars', {'units', 'calibration'}); +fp.MetadataFile = 'meta.mat'; +fp.MetadataVars = {'units', 'calibration'}; +fp.MetadataLineIndex = 1; % which line to attach metadata to +fp.startLive('data.mat', @updateFcn); ``` -The metadata is loaded from a separate file and attached to the specified line and tile: +### Dashboard + +For a dashboard, use the figure‑level metadata properties: ```matlab fig.MetadataFile = 'metadata.mat'; fig.MetadataVars = {'sensor_id', 'location', 'units'}; fig.MetadataLineIndex = 1; % which line within the tile -fig.MetadataTileIndex = 1; % which tile to attach to +fig.MetadataTileIndex = 1; % which tile to attach metadata to +fig.startLive('data.mat', @updateDashboard); ``` ---- - -## Live Event Detection - -Combine live mode with event detection for real-time monitoring using the LiveEventPipeline: - -```matlab -% Create sensors with thresholds -tempSensor = Sensor('temperature', 'Name', 'Temperature'); -tempSensor.addThresholdRule(struct(), 78, 'Direction', 'upper', 'Label', 'Hi Warn'); -tempSensor.addThresholdRule(struct(), 82, 'Direction', 'upper', 'Label', 'Hi Alarm'); -tempSensor.resolve(); - -sensors = containers.Map(); -sensors('temperature') = tempSensor; - -% Configure data sources -dsMap = DataSourceMap(); -dsMap.add('temperature', MockDataSource('BaseValue', 70, 'NoiseStd', 2)); - -% Set up pipeline with event store -pipeline = LiveEventPipeline(sensors, dsMap, ... - 'EventFile', 'events.mat', ... - 'Interval', 15, ... - 'MinDuration', 0.5); - -% Start live event detection -pipeline.start(); -``` +Metadata is loaded from a separate `.mat` file each time the main data file changes. The values are attached to the specified tile and line and can be retrieved later with `lookupMetadata`. --- ## Octave Compatibility -GNU Octave does not support MATLAB timers. Use `runLive()` for a blocking poll loop: +GNU Octave does not support MATLAB timer objects. Use `runLive()` for a **blocking** poll loop: ```matlab fp.render(); - -% Blocking loop — press Ctrl+C to stop fp.LiveFile = 'data.mat'; fp.LiveUpdateFcn = @(fp, s) fp.updateData(1, s.x, s.y); -fp.runLive(); +fp.runLive(); % press Ctrl+C to stop ``` For dashboards: + ```matlab fig.renderAll(); fig.LiveFile = 'data.mat'; @@ -178,63 +165,62 @@ fig.runLive(); ## Manual Refresh -Trigger a one-shot data reload without starting continuous polling: +Trigger a one‑shot reload without starting continuous polling: ```matlab -fp.refresh(); -fig.refresh(); +fp.refresh(); % loads LiveFile and calls LiveUpdateFcn once +fig.refresh(); % same for a dashboard ``` --- ## Toolbar Integration -The [[FastSenseToolbar|API Reference: FastSenseToolbar]] provides a Live Mode button: +The [[API Reference: FastSenseToolbar|FastSenseToolbar]] provides a **Live Mode** toggle button and a **Refresh** button: ```matlab tb = FastSenseToolbar(fp); -% Click the Live Mode button to toggle polling on/off -% Or programmatically: -tb.toggleLive(); -``` - -The Refresh button triggers a manual one-shot reload: -```matlab -tb.refresh(); +% Click the toolbar buttons interactively, or call programmatically: +tb.toggleLive(); % start/stop live mode +tb.refresh(); % manual one‑shot reload ``` --- ## Console Progress Bars -Use ConsoleProgressBar for visual feedback during long operations: +During long operations (e.g., rendering many tiles), you may want visual feedback. Use `ConsoleProgressBar` for a single‑line progress indicator: ```matlab -pb = ConsoleProgressBar(2); % 2-space indent +pb = ConsoleProgressBar(2); % 2‑space indent pb.start(); for k = 1:8 pb.update(k, 8, 'Tile 1'); pause(0.1); end -pb.freeze(); % becomes permanent line +pb.freeze(); % makes the bar permanent ``` +This class is not specifically tied to live mode, but it can be useful inside update callbacks or dashboards where you want to show progress. + --- ## Tips -- Set `'ViewMode', 'follow'` for monitoring use cases where you always want to see the latest data -- Use `'preserve'` when users need to zoom into historical data while live updates continue -- Keep polling interval reasonable (1-5 seconds) to avoid overwhelming the file system -- The .mat file should be written atomically (write to temp file, then rename) to avoid partial reads -- Live mode works with linked axes — all linked plots update together -- Use `DeferDraw = true` to skip drawnow during batch render for better performance +- Set `'ViewMode', 'follow'` for monitoring dashboards — you’ll always see the freshest data. +- Use `'preserve'` when users need to zoom into historical data while live updates continue in the background. +- Keep polling intervals reasonable (1–5 seconds) to avoid hammering the file system. +- Write the `.mat` file **atomically**: save to a temporary file, then rename. This avoids partially‑written files being read by the poller. +- Live mode works with linked axes — all plots in the same `LinkGroup` update together. +- Set `DeferDraw = true` on the FastSense instance to suppress `drawnow` during batch updates for better performance. +- If you need to update metadata synchronously, set `MetadataFile` before `startLive`; the metadata is reloaded on every file change. --- ## See Also -- [[API Reference: FastPlot]] — startLive(), stopLive(), updateData() methods -- [[API Reference: Dashboard]] — Dashboard live mode -- [[API Reference: Event Detection]] — Live event detection -- [[Examples]] — example_dashboard_live.m, example_live_pipeline.m +- [[API Reference: FastPlot]] — `startLive()`, `stopLive()`, `updateData()`, `refresh()` +- [[API Reference: Dashboard]] — `FastSenseGrid` live mode +- [[Examples]] — `example_dashboard_live.m` (dashboard live example) +- [[Getting Started]] — basic plot setup +- [[Architecture]] — internal timer lifecycle diff --git a/wiki/MEX-Acceleration.md b/wiki/MEX-Acceleration.md index 1d99b22c..73ff6a46 100644 --- a/wiki/MEX-Acceleration.md +++ b/wiki/MEX-Acceleration.md @@ -11,7 +11,9 @@ cd libs/FastSense build_mex(); ``` -The build script auto-detects your architecture and compiles all MEX functions with appropriate SIMD optimizations. +The build script auto‑detects your architecture and compiles all MEX functions with appropriate SIMD optimizations. If the required compilers are present (see [Requirements](#Requirements)), the compiled binaries are placed in `libs/FastSense/private/` (MATLAB) or a platform‑tagged sub‑folder like `private/octave‑macos‑arm64/` (Octave). + +The build is automatically invoked by `install.m` only when necessary — a content‑based stamp file (`.mex-version`) prevents re‑compilation when nothing has changed. You can safely run `build_mex()` at any time; it skips already‑compiled files and avoids redundant work. ### Requirements @@ -21,7 +23,7 @@ The build script auto-detects your architecture and compiles all MEX functions w | Linux | GCC | | Windows | MSVC | -SQLite3 is bundled as an amalgamation and compiled directly into MEX files that need it — no system installation required. +SQLite3 is bundled as an amalgamation and compiled directly into MEX files that need it — no system installation required. The files `build_store_mex`, `resolve_disk_mex`, and `mksqlite` all embed the SQLite3 library. ## Architecture Support @@ -29,73 +31,61 @@ All MEX functions include a common SIMD abstraction layer that adapts to your CP | Architecture | SIMD Instructions | Fallback | |-------------|------------------|----------| -| x86_64 | AVX2 + FMA | SSE2 | -| ARM64 (Apple Silicon) | NEON | - | -| Other | Scalar operations | - | - -If AVX2 compilation fails on x86_64, the build script automatically retries with SSE2. - -## Accelerated Functions - -### Core Downsampling - -**binary_search_mex** — O(log n) binary search for visible data range -- **Speedup**: 10-20x over MATLAB's `find` -- **Used by**: Zoom/pan callbacks to locate visible indices +| x86_64 | AVX2 + FMA | SSE2 (automatically if AVX2 fails) | +| ARM64 (Apple Silicon) | NEON | – | +| Other | Scalar operations | – | -**minmax_core_mex** — Per-pixel MinMax reduction with SIMD vectorization -- **Speedup**: 3-10x over pure MATLAB -- **SIMD**: Processes 4 doubles (AVX2) or 2 doubles (NEON) per cycle -- **Used by**: Default downsampling algorithm in [[FastPlot|API Reference: FastPlot]] +On **Octave** running on ARM64, the build script explicitly adds `-mcpu=apple-m3` when GCC is available to enable NEON auto‑vectorisation. On **MATLAB**, Clang’s default ARM flags already enable NEON. -**lttb_core_mex** — Largest Triangle Three Buckets with SIMD triangle area computation -- **Speedup**: 10-50x over MATLAB implementation -- **Used by**: LTTB downsampling method +If AVX2 compilation fails on x86_64, the build script automatically retries with SSE2 flags for that individual source file. No manual intervention is required. -### Threshold Processing - -**violation_cull_mex** — Fused threshold violation detection and pixel culling -- **Speedup**: Significant (single-pass vs two-pass MATLAB) -- **Used by**: Violation marker rendering during zoom/pan - -**compute_violations_mex** — Batch threshold violation detection -- **Speedup**: Significant over per-point MATLAB comparison -- **Used by**: [[Sensors|API Reference: Sensors]] resolution pipeline - -### Data Storage +## Accelerated Functions -**build_store_mex** — Bulk SQLite writer for DataStore initialization -- **Speedup**: 2-3x (eliminates ~20K MATLAB-to-MEX round-trips) -- **SIMD**: Accelerated Y min/max computation per chunk -- **Used by**: `FastSenseDataStore` construction +All compiled MEX functions live in `private/mex_src/`. The table below lists each one, its purpose, and the approximate performance gain you can expect. -**resolve_disk_mex** — SQLite disk-based sensor resolution -- **Used by**: `Sensor.resolve()` with disk-backed storage -- **Benefit**: Reads chunks from database without loading full datasets +| MEX function | What it does | Speedup | Used by | +|---|---|---|---| +| `binary_search_mex` | O(log n) binary search for visible data range | 10‑20× vs MATLAB `find` | Zoom/pan callbacks to locate visible indices | +| `minmax_core_mex` | Per‑pixel MinMax reduction with SIMD vectorisation (processes 4 doubles/cycle with AVX2, 2 with NEON) | 3‑10× | Default downsampling algorithm in [[FastPlot|API Reference: FastPlot]] | +| `lttb_core_mex` | Largest Triangle Three Buckets with SIMD triangle area computation | 10‑50× | LTTB downsampling method | +| `violation_cull_mex` | Fused threshold violation detection and pixel culling (single‑pass) | Significant | Violation marker rendering during zoom/pan | +| `compute_violations_mex` | Batch threshold violation detection for the `resolve()` pipeline | Significant | [[Sensors|API Reference: Sensors]] resolution | +| `build_store_mex` | Bulk SQLite writer using `mksqlite` — writes chunks of X/Y data with accelerated Y min/max computation | 2‑3× | `FastSenseDataStore` construction | +| `resolve_disk_mex` | SQLite disk‑backed sensor resolution — reads chunks without loading full datasets | – | `Sensor.resolve()` with disk storage | +| `to_step_function_mex` | SIMD conversion of continuous threshold arrays to step‑function form | Significant | Threshold line step‑function rendering | +| `mksqlite` | Full SQLite3 MEX interface with typed BLOB support; serializes MATLAB arrays preserving type and shape | – | DataStore, disk‑backed sensor resolution | -**mksqlite** — SQLite3 MEX interface with typed BLOB support -- **Used by**: DataStore, disk-backed sensor resolution -- **Features**: Serializes MATLAB arrays preserving type and shape +Every MEX function checks for its own existence inside a persistent variable at first call, then delegates seamlessly to the pure‑MATLAB fallback if the MEX binary is unavailable (see [`binary_search.m`](#) for a typical pattern). ## Fallback Behavior -When MEX files are unavailable: +When any MEX file is absent: -- Each function has a pure-MATLAB equivalent in `libs/FastSense/private/` -- Runtime auto-detection switches between MEX and MATLAB seamlessly -- Identical numerical results and API -- Performance remains excellent for datasets under ~10M points +- A functionally identical MATLAB implementation is present in `libs/FastSense/private/`. +- The entry‑point function detects the MEX presence once per session and switches automatically. +- Numerical results are identical — the test suite (`test_mex_parity`, `test_mex_edge_cases`) guarantees this. +- Performance remains excellent for datasets under ~10M points; large‑data users benefit most from MEX acceleration. ## Compilation Process -The `build_mex()` function: +The `build_mex()` function (see `build_mex.m`) performs these steps: + +1. **Architecture detection** — normalises platform strings (`maca64`, `aarch64‑…`, `x86_64‑…`) into canonical labels (`arm64`, `x86_64`, `unknown`). +2. **Compiler selection** — + - *Octave*: prefers a real GCC installation (searches Homebrew paths `gcc‑15` … `gcc‑10`) for superior auto‑vectorisation; falls back to system default. + - *MATLAB*: always uses the configured default compiler (Xcode Clang on macOS, MSVC on Windows) to avoid linker‑flag incompatibilities. +3. **SIMD flags** — sets optimisation and target instruction flags: + - x86_64: `-O3 -mavx2 -mfma -ftree-vectorize -ffast-math` (GCC/Clang) or `/O2 /arch:AVX2 /fp:fast` (MSVC). + - arm64: `-O3 -mcpu=apple-m3 -ftree-vectorize -ffast-math` (GCC on Octave) or simply `-O3 -ffast-math` (Clang). + - unknown: `-O3 -ffast-math` scalar. +4. **Source compilation** — iterates over the list of MEX sources (see table above), compiling each with the chosen flags and the bundled `sqlite3.c` when needed. +5. **Failure recovery** — if an AVX2 build fails on x86_64, the script immediately retries with SSE2 flags (`-msse2` or `/arch:SSE2`). +6. **Stamp update** — after all files are compiled, a new `.mex-version` stamp file is written, preventing unnecessary rebuilds on future `install` calls. +7. **Copy shared MEX files** — distributes `violation_cull_mex`, `compute_violations_mex`, `resolve_disk_mex`, and `to_step_function_mex` to `libs/SensorThreshold/private/` (with platform‑tagged sub‑folders on Octave). + +### Caching and Stale Detection -1. **Detects architecture** — normalizes platform strings (`maca64`, `aarch64`, etc.) into canonical labels -2. **Selects compiler** — prefers GCC on Octave for better auto-vectorization; uses MATLAB's default on MATLAB -3. **Sets SIMD flags** — chooses instruction sets based on detected CPU architecture -4. **Compiles sources** — builds all MEX files with bundled SQLite3 amalgamation -5. **Handles failures** — automatically retries x86_64 builds with SSE2 if AVX2 fails -6. **Copies shared files** — distributes MEX binaries to other library directories +To avoid forcing users through lengthy C compilations, `install.m` calls `mex_stamp()` on the repository root. This function computes a SHA‑256 hash (or a fallback size+byte‑sample fingerprint) of all C sources, headers, `build_mex.m`, and `mksqlite.c`. The hash is compared against the content of `private/.mex-version`. Only when they differ is `build_mex()` invoked. The per‑file mtime check inside `build_mex` then acts as a final backstop, so even a manual `build_mex()` invocation finishes quickly if nothing has changed. ## Verifying Installation @@ -108,4 +98,4 @@ test_mex_parity; % Verify MEX matches MATLAB output test_mex_edge_cases; % Test edge cases (empty arrays, NaN, etc.) ``` -The test suite validates numerical accuracy across all MEX functions and handles edge cases like empty arrays, single points, and NaN values. +The test suite validates numerical accuracy across all MEX functions and handles edge cases like empty arrays, single points, and NaN values. Passing these tests confirms that your MEX compilation is correct and that the fallback logic works seamlessly. diff --git a/wiki/Use-Case:-Multi-Sensor-Shared-Threshold.md b/wiki/Use-Case:-Multi-Sensor-Shared-Threshold.md index 6f4ebba4..fb237ffe 100644 --- a/wiki/Use-Case:-Multi-Sensor-Shared-Threshold.md +++ b/wiki/Use-Case:-Multi-Sensor-Shared-Threshold.md @@ -13,84 +13,74 @@ Plot multiple sensors on a single tile with one shared threshold, see violation ```matlab install; -%% Create sensors with identical threshold rules -sensors = cell(1, 4); -names = {'Zone A', 'Zone B', 'Zone C', 'Zone D'}; -x = linspace(0, 60, 500000); - -for i = 1:4 - s = Sensor(sprintf('zone_%d', i), 'Name', names{i}); - s.X = x; - s.Y = sin(x * 2 * pi * i / 20) + 0.3 * randn(1, numel(x)) + 3; - - % Same threshold rule for every sensor - s.addThresholdRule(struct(), 4.5, 'Direction', 'upper', 'Label', 'Max Temp'); - s.resolve(); - - sensors{i} = s; +%% Create sensor tags with identical monitoring rule +zones = {'Zone A', 'Zone B', 'Zone C', 'Zone D'}; +t = linspace(0, 60, 500000); +tags = cell(1, 4); + +for k = 1:4 + % Create a SensorTag for each zone + st = SensorTag(sprintf('zone_%d', k), 'Name', zones{k}); + st.X = t; + st.Y = sin(t * 2*pi * k/20) + 0.3*randn(1, numel(t)) + 3; + TagRegistry.register(st.Key, st); + + % Each sensor gets its own MonitorTag with the same upper bound + mt = MonitorTag(['zone' num2str(k) '_hi'], st, @(x,y) y > 4.5); + mt.Name = 'Max Temp'; + TagRegistry.register(mt.Key, mt); + + tags{k} = st; end -%% Plot all sensors on a single tile -fp = FastSense(); -for i = 1:numel(sensors) - % Show threshold line only for the first sensor to avoid duplicates - fp.addSensor(sensors{i}, 'ShowThresholds', (i == 1)); -end -fp.render(); -title(fp.hAxes, 'Multi-Sensor — Shared Threshold'); -legend(fp.hAxes, 'show'); - -%% Detect events for all sensors -detector = EventDetector('MinDuration', 0.5); -allEvents = []; -for i = 1:numel(sensors) - evts = detectEventsFromSensor(sensors{i}, detector); - if ~isempty(evts) - allEvents = [allEvents, evts]; - end +%% Retrieve violation windows (manual runs detection) +fprintf('Violations per sensor:\n'); +for k = 1:4 + mtKey = ['zone' num2str(k) '_hi']; + mt = TagRegistry.get(mtKey); + [mx, my] = mt.getXY(); + % Find runs where my == 1 + d = diff([0 my 0]); + starts = find(d == 1); + stops = find(d == -1) - 1; + fprintf(' %s: %d violation fragments\n', zones{k}, numel(starts)); end -fprintf('Detected %d events across %d sensors.\n', numel(allEvents), numel(sensors)); ``` --- ## How It Works -### 1. Threshold on the Sensor, not just the plot +### 1. Thresholds as separate MonitorTag instances -Each `Sensor` gets the same `ThresholdRule`. When you call `resolve()`, each sensor independently computes: -- **ResolvedThresholds** — the threshold step-function line -- **ResolvedViolations** — the (X, Y) points that exceed the limit +Each `SensorTag` gets its own `MonitorTag` that applies the exact same condition (`@(x,y) y > 4.5`). Because the condition is identical, the threshold value is shared across all sensors. Each monitor independently evaluates the sensor data and produces a 0/1 signal. This is the Tag‑based equivalent of the legacy `Sensor.addThresholdRule()`. -This means every sensor carries its own violation data, which is required for event detection. +### 2. Violation extraction via getXY -### 2. Avoid duplicate threshold lines - -When calling `addSensor()`, pass `'ShowThresholds', true` only for the **first** sensor. All subsequent sensors use `'ShowThresholds', false`. This draws the threshold line once while still computing violations for every sensor. - -Alternatively, skip `ShowThresholds` entirely and add the threshold manually: +Calling `mt.getXY()` returns the binary mask aligned to the parent sensor’s timestamps. You can then locate violation segments manually: ```matlab -for i = 1:numel(sensors) - fp.addSensor(sensors{i}, 'ShowThresholds', false); -end -fp.addThreshold(4.5, 'Direction', 'upper', 'ShowViolations', true, ... - 'Label', 'Max Temp', 'Color', 'r'); +mt = TagRegistry.get('zone1_hi'); +[x, y] = mt.getXY(); +% Find rising and falling edges +edges = diff([0 y 0]); +startIdx = find(edges == 1); +endIdx = find(edges == -1) - 1; ``` -This approach draws a single shared threshold line and FastSense computes violation markers against **all lines** on the tile during rendering (see `updateViolations` in FastSense.m). +These indices map directly to the parent sensor’s `X` array. -### 3. Event detection works per-sensor +### 3. Centralised event storage (optional) -`detectEventsFromSensor()` reads each sensor's `ResolvedViolations` and groups consecutive violation points into `Event` objects. Since each sensor resolved independently, events are attributed to the correct sensor: +If you attach a shared `EventStore` handle to each `MonitorTag`, the monitor will automatically record events when violation runs begin and end. Events are attributed to the monitor’s key, which you can later correlate to the parent sensor. ```matlab -% Each event knows which sensor it came from -for i = 1:numel(allEvents) - fprintf(' %s: %.1fs – %.1fs (peak %.2f)\n', ... - allEvents(i).SensorName, ... - allEvents(i).StartTime, allEvents(i).EndTime, ... - allEvents(i).PeakValue); +% Create a single EventStore and reuse it +store = EventStore(); +for k = 1:4 + st = TagRegistry.get(sprintf('zone_%d', k)); + mt = MonitorTag(sprintf('zone%d_hi_event', k), st, @(x,y) y > 4.5); + mt.EventStore = store; % all monitors write to the same store end ``` @@ -98,23 +88,39 @@ end ## With State-Dependent Thresholds -The shared threshold can also be state-dependent. Attach the same `StateChannel` and conditional `ThresholdRule` to each sensor: +The shared threshold can also be state‑dependent. Use a `StateTag` to represent the system mode and a `DerivedTag` to produce a “masked” signal that is only above threshold when the mode is active. Then monitor the derived signal. ```matlab -for i = 1:numel(sensors) - sc = StateChannel('mode'); - sc.X = modeX; - sc.Y = modeValues; % same state for all sensors - sensors{i}.addStateChannel(sc); - - % Threshold only active during 'run' mode - sensors{i}.addThresholdRule(struct('mode', 'run'), 4.5, ... - 'Direction', 'upper', 'Label', 'Run HI'); - sensors{i}.resolve(); +%% Define system mode (0=idle, 1=run) +modeX = [0, 30, 60, 90]; +modeY = [0, 1, 1, 0]; +modeTag = StateTag('system_mode', 'X', modeX, 'Y', modeY); +TagRegistry.register('system_mode', modeTag); + +%% For each zone, create a DerivedTag that applies the state gate +for k = 1:4 + sensorTag = TagRegistry.get(sprintf('zone_%d', k)); + + % derivedTag will output sensor value only when mode==1; NaN otherwise + dt = DerivedTag(sprintf('zone%d_gated', k), {sensorTag, modeTag}, ... + @(parents) gateByMode(parents{1}, parents{2})); + TagRegistry.register(dt.Key, dt); + + % Monitor the gated signal for exceedances + mt = MonitorTag(sprintf('zone%d_hi_gated', k), dt, @(x,y) y > 4.5); + TagRegistry.register(mt.Key, mt); +end + +function [X, Y] = gateByMode(sensorTag, modeTag) + % Returns the sensor series unchanged when mode==1; NaN elsewhere + X = sensorTag.X; + Y = sensorTag.Y; + modeVals = modeTag.valueAt(sensorTag.X); + Y(modeVals ~= 1) = NaN; % gate: no data when not in run end ``` -Each sensor evaluates the same state conditions independently, so thresholds activate/deactivate synchronously across all sensors while maintaining individual violation tracking. +Now each zone only reports a threshold violation during the ‘run’ periods. --- @@ -122,61 +128,55 @@ Each sensor evaluates the same state conditions independently, so thresholds act ```matlab %% Multi-zone temperature monitoring with shared alarm level -sensors = cell(1, 3); zones = {'North', 'Central', 'South'}; t = linspace(0, 120, 50000); - -% Create state channel for system mode -modeX = [0, 30, 60, 90]; -modeY = [0, 1, 1, 0]; % 0=idle, 1=active - -for i = 1:3 - s = Sensor(sprintf('temp_zone_%d', i), 'Name', sprintf('Zone %s', zones{i})); - s.X = t; - - % Each zone has different baseline but similar patterns - baseline = 20 + i * 2; - s.Y = baseline + 5*sin(2*pi*t/40) + 2*randn(1, numel(t)); - - % Add state channel - sc = StateChannel('system_mode'); - sc.X = modeX; - sc.Y = modeY; - s.addStateChannel(sc); - - % Shared threshold rules - s.addThresholdRule(struct(), 30, 'Direction', 'upper', 'Label', 'Max Temp (any mode)'); - s.addThresholdRule(struct('system_mode', 1), 28, 'Direction', 'upper', 'Label', 'Max Temp (active)'); - s.resolve(); - - sensors{i} = s; +alarmValue = 30; + +% Build sensor tags +sensorTags = cell(1, 3); +for k = 1:3 + st = SensorTag(sprintf('temp_%s', zones{k}), ... + 'Name', sprintf('Zone %s', zones{k}), ... + 'Units', '°C'); + st.X = t; + % slightly different baselines but similar patterns + st.Y = 20 + k*2 + 5*sin(2*pi*t/40) + 2*randn(1, numel(t)); + TagRegistry.register(st.Key, st); + sensorTags{k} = st; + + % Attach a MonitorTag that fires above alarmValue + mt = MonitorTag(sprintf('temp_%s_hi', zones{k}), st, ... + @(x,y) y > alarmValue); + mt.Name = 'Overheat Alarm'; + TagRegistry.register(mt.Key, mt); end -%% Plot with shared thresholds -fp = FastSense(); -for i = 1:numel(sensors) - fp.addSensor(sensors{i}, 'ShowThresholds', (i == 1)); -end -fp.render(); -title('Multi-Zone Temperature Monitoring'); -xlabel('Time (s)'); -ylabel('Temperature (°C)'); -legend('show'); - -%% Event detection -detector = EventDetector('MinDuration', 2.0); -allEvents = []; -for i = 1:numel(sensors) - evts = detectEventsFromSensor(sensors{i}, detector); - allEvents = [allEvents, evts]; +%% Inspect violations manually +fprintf('\n=== Violation Log ===\n'); +for k = 1:3 + mt = TagRegistry.get(sprintf('temp_%s_hi', zones{k})); + [~, y] = mt.getXY(); + runStarts = find(diff([0; y(:); 0]) == 1); + runEnds = find(diff([0; y(:); 0]) == -1) - 1; + for r = 1:numel(runStarts) + tStart = mt.Parent.X(runStarts(r)); + tEnd = mt.Parent.X(runEnds(r)); + peak = max(mt.Parent.Y(runStarts(r):runEnds(r))); + fprintf('%s: %.1fs – %.1fs (peak %.2f°C)\n', ... + zones{k}, tStart, tEnd, peak); + end end -fprintf('\n=== Event Summary ===\n'); -for i = 1:numel(allEvents) - fprintf('%s: %s violation at %.1f-%.1fs (peak %.2f°C)\n', ... - allEvents(i).SensorName, allEvents(i).ThresholdLabel, ... - allEvents(i).StartTime, allEvents(i).EndTime, allEvents(i).PeakValue); -end +%% To use event store instead, simply: +% store = EventStore(); +% for k = 1:3 +% % re‑create monitor with EventStore +% st = TagRegistry.get(sprintf('temp_%s', zones{k})); +% mt = MonitorTag(sprintf('temp_%s_hi_evt', zones{k}), st, ... +% @(x,y) y > alarmValue, 'EventStore', store); +% TagRegistry.register(mt.Key, mt); +% end +% % Events are now accessible via store’s query methods. ``` --- @@ -185,14 +185,16 @@ end | Aspect | Behavior | |--------|----------| -| **Threshold line** | Drawn once (first sensor or manual `addThreshold`) | -| **Violation markers** | Computed for every line on the tile | -| **Event detection** | Per-sensor via `detectEventsFromSensor()` — requires `resolve()` on each sensor | -| **State-dependent thresholds** | Supported — attach identical `StateChannel` + `ThresholdRule` to each sensor | -| **EventViewer** | Shows events from all sensors, attributed by name | +| **Shared threshold** | Create one `MonitorTag` per sensor with the exact same `ConditionFn`. | +| **Violation detection** | Inspect the monitor’s `getXY()` or rely on `EventStore` for automatic event logging. | +| **State‑dependent thresholds** | Combine a `StateTag` with a `DerivedTag` that gates the sensor signal; monitor the derived signal. | +| **Event attribution** | Events recorded by a monitor carry its key, which you can link back to the parent sensor. | +| **Plotting** | Not shown here — use your plotting library of choice. In FastPlot, pass the `SensorTag` or `MonitorTag` to the plot builder. | + +--- ## See Also -- [[Sensors|API Reference: Sensors]] — `Sensor`, `ThresholdRule`, `StateChannel` -- [[Event Detection|API Reference: Event Detection]] — `EventDetector`, `detectEventsFromSensor`, `EventViewer` -- [[Examples]] — `example_multi_sensor_linked`, `example_sensor_threshold`, `example_sensor_multi_state` +- [[Sensors|API Reference: Sensors]] — `SensorTag`, `MonitorTag`, `StateTag`, `DerivedTag` +- [[Event Detection|API Reference: Event Detection]] — `EventStore`, event callbacks +- [[Examples]] — detailed examples using the Tag domain model