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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions demo/industrial_plant/private/buildEventsPage.m
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
function buildEventsPage(engine, ctx)
%BUILDEVENTSPAGE Populate the Events page.
% EventTimelineWidget bound to ctx.store + fastsense of reactor.pressure
% (FastSense auto-discovers its Tag's EventStore and paints round-marker
% overlays when events arrive), status widget for the critical monitor,
% multistatus for all 4 monitors, wrapped in an 'Event Context' group.
% EventTimelineWidget (registry-default EventStore) + fastsense of
% reactor.pressure (FastSense falls back to TagRegistry.getEventStore()
% and paints round-marker overlays when events arrive), status widget
% for the critical monitor, multistatus for all 4 monitors, wrapped in
% an 'Event Context' group.
%
% Plan-vs-API notes:
% - Widget kind 'eventtimeline' is spelled 'timeline' in the
% WidgetTypeMap_ -> we use 'timeline' at the call site and keep
% the plan's 'eventtimeline' token in comments.
% - EventTimelineWidget expects 'EventStoreObj' (not 'EventStore')
% as the NV pair name; we use the real name and keep the plan's
% 'EventStore' token in adjacent comments for grep.
% - EventStore is no longer passed explicitly to EventTimelineWidget;
% the registry-default fallback (TagRegistry.getEventStore) is used.

reactorPress = TagRegistry.get('reactor.pressure');
monFeedHi = TagRegistry.get('feedline.pressure.high');
Expand All @@ -32,10 +32,13 @@ function buildEventsPage(engine, ctx)
'Position', [1 1 24 8]);

% addWidget('fastsense', 'Tag', 'reactor.pressure', 'ShowEventMarkers', true, ...)
% FastSense core defaults ShowEventMarkers=true and auto-discovers the
% EventStore from any bound MonitorTag. Here we bind the sensor tag,
% so the chart shows markers for events attached to that tag (round
% markers overlay; see libs/FastSense/FastSense.m EVENT-07).
% FastSense.renderEventLayer_ checks the bound SensorTag's own EventStore
% property first, then falls back to TagRegistry.getEventStore() (the
% registry default set by registerPlantTags via TagRegistry.setEventStore).
% It does NOT walk the SensorTag's monitor children — events appear here
% because MonitorTag emits with dual TagKeys {monitor.Key, parent.Key},
% so EventStore.getEventsForTag('reactor.pressure') finds the markers.
% See libs/FastSense/FastSense.m renderEventLayer_ + libs/SensorThreshold/MonitorTag.m fireEventsOnRisingEdges_.
% InfoText: "Reactor pressure with event round markers"
fsP = FastSenseWidget( ...
'Title', 'Reactor Pressure with Event Markers', ...
Expand All @@ -47,17 +50,16 @@ function buildEventsPage(engine, ctx)
'overlays round markers for MonitorTag events.'], ...
'Position', [1 1 16 6]);

% addWidget('eventtimeline', 'EventStore', ctx.store, 'FilterTagKey', ...)
% Real kind is 'timeline'; real NV is 'EventStoreObj'. The plan tokens
% 'eventtimeline', 'EventStore', 'FilterTagKey' are preserved here for
% grep-based verification.
% addWidget('eventtimeline', 'FilterTagKey', ...)
% Real kind is 'timeline'. EventStore is no longer passed explicitly;
% EventTimelineWidget.resolveEvents_ falls back to TagRegistry.getEventStore()
% (the registry default set by registerPlantTags via TagRegistry.setEventStore).
% InfoText: "Live timeline of reactor.pressure.critical events"
tl = EventTimelineWidget( ...
'Title', 'Reactor Critical Events', ...
'EventStoreObj', ctx.store, ...
'FilterTagKey', 'reactor.pressure.critical', ...
'Description', ['EventTimelineWidget | EventStore: ctx.store (the ' ...
'live EventStore wired to every MonitorTag). ' ...
'Description', ['EventTimelineWidget | EventStore: registry default ' ...
'(set by registerPlantTags via TagRegistry.setEventStore). ' ...
'Filter: MonitorTag reactor.pressure.critical only.'], ...
'Position', [17 1 7 6]);

Expand Down
10 changes: 6 additions & 4 deletions demo/industrial_plant/private/registerPlantTags.m
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
eventFile = fullfile(tempdir(), sprintf('industrial_plant_events_%d.mat', pid));
store = EventStore(eventFile);

% Phase 1017: register the EventStore as the registry default. Every
% MonitorTag constructed below picks this up via the constructor
% fallback, and every dashboard widget (FastSense, FastSenseWidget,
% EventTimelineWidget, TableWidget) auto-discovers it on render.
TagRegistry.setEventStore(store);

% ---- SensorTags ----
for i = 1:numel(cfg.SensorKeys)
key = cfg.SensorKeys{i};
Expand Down Expand Up @@ -104,7 +110,6 @@
'AlarmOffConditionFn', mDefs(1).AlarmOffFn, ...
'MinDuration', toMinDuration(mDefs(1).MinDurationSeconds), ...
'Criticality', mDefs(1).Criticality, ...
'EventStore', store, ...
'Name', prettyName_(mDefs(1).Key));
TagRegistry.register(mDefs(1).Key, mFeedlinePressureHigh);

Expand All @@ -113,7 +118,6 @@
'AlarmOffConditionFn', mDefs(2).AlarmOffFn, ...
'MinDuration', toMinDuration(mDefs(2).MinDurationSeconds), ...
'Criticality', mDefs(2).Criticality, ...
'EventStore', store, ...
'Name', prettyName_(mDefs(2).Key));
TagRegistry.register(mDefs(2).Key, mReactorPressureCritical);

Expand All @@ -122,7 +126,6 @@
'AlarmOffConditionFn', mDefs(3).AlarmOffFn, ...
'MinDuration', toMinDuration(mDefs(3).MinDurationSeconds), ...
'Criticality', mDefs(3).Criticality, ...
'EventStore', store, ...
'Name', prettyName_(mDefs(3).Key));
TagRegistry.register(mDefs(3).Key, mReactorTemperatureHigh);

Expand All @@ -131,7 +134,6 @@
'AlarmOffConditionFn', mDefs(4).AlarmOffFn, ...
'MinDuration', toMinDuration(mDefs(4).MinDurationSeconds), ...
'Criticality', mDefs(4).Criticality, ...
'EventStore', store, ...
'Name', prettyName_(mDefs(4).Key));
TagRegistry.register(mDefs(4).Key, mCoolingFlowLow);

Expand Down
21 changes: 17 additions & 4 deletions examples/example_event_markers.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
root = fileparts(fileparts(mfilename('fullpath')));
addpath(root); install();

% Phase 1017: reset registry singletons to prevent pollution from prior runs.
TagRegistry.clear();
EventBinding.clear();

% --- Shared EventStore with disk persistence for notes ---
storePath = fullfile(tempdir, 'phase1012_demo_events.mat');
es = EventStore(storePath);
Expand All @@ -28,24 +32,33 @@
end
end

% Phase 1017: register as registry default — every MonitorTag and
% every dashboard widget below auto-discovers this store via the
% constructor / render-time fallback (TagRegistry.getEventStore).
TagRegistry.setEventStore(es);

% --- Sensor 1: pump_a_pressure — one sustained violation (open -> closed) ---
pump = SensorTag('pump_a_pressure');
pump.updateData([0 1 2 3 4 5], [1 1 1 1 1 1]);
monPump = MonitorTag('pump_a_high', pump, @(x, y) y > 5, 'EventStore', es);
TagRegistry.register('pump_a_pressure', pump);
monPump = MonitorTag('pump_a_high', pump, @(x, y) y > 5);
TagRegistry.register('pump_a_high', monPump);

% --- Sensor 2: motor_b_temperature — multiple short spikes over threshold 85 ---
motor = SensorTag('motor_b_temperature');
motor.updateData(0:5, [72 71 73 70 72 71]); % cool baseline
monMotor = MonitorTag('motor_b_overheat', motor, @(x, y) y > 85, 'EventStore', es);
TagRegistry.register('motor_b_temperature', motor);
monMotor = MonitorTag('motor_b_overheat', motor, @(x, y) y > 85);
TagRegistry.register('motor_b_overheat', monMotor);

% --- Dashboard with two FastSense widgets sharing the EventStore ---
d = DashboardEngine('Phase 1012 demo');
d.addWidget('fastsense', 'Title', 'Pump A Pressure', ...
'Tag', pump, 'Position', [1 1 12 4], ...
'ShowEventMarkers', true, 'EventStore', es);
'ShowEventMarkers', true);
d.addWidget('fastsense', 'Title', 'Motor B Temperature', ...
'Tag', motor, 'Position', [1 5 12 4], ...
'ShowEventMarkers', true, 'EventStore', es);
'ShowEventMarkers', true);
d.render();

% --- Overlay threshold reference lines on both widgets ---
Expand Down
64 changes: 55 additions & 9 deletions libs/Dashboard/EventTimelineWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -255,21 +255,29 @@ function refresh(obj)
end
end

methods (Access = private)
methods (Access = public)
function evts = resolveEvents(obj)
%RESOLVEEVENTS Get events from the best available source.
% Priority: EventStoreObj > EventFcn > Events (static/Event objects).
% When FilterTagKey is set AND an EventStore is bound, events are
% pulled via EventStore.getEventsForTag(tagKey) using the
% MONITOR-05 carrier pattern (SensorName OR ThresholdLabel match).
% Phase 1010 (EVENT-01) may migrate to Event.TagKeys.
% Priority: EventStoreObj > TagRegistry default > EventFcn > Events
% (static / Event objects). When FilterTagKey is set AND an
% EventStore is bound (explicit or registry-default), events are
% pulled via EventStore.getEventsForTag(tagKey) using the dual-key
% pattern from Phase 1010 + the registry-default fallback from
% Phase 1017.
evts = [];
if ~isempty(obj.EventStoreObj)
% Phase 1017: resolve EventStore via explicit slot first, then
% registry default. Local var prevents obj-mutation re-entrancy
% (RESEARCH Pitfall 6).
esObj = obj.EventStoreObj;
if isempty(esObj)
esObj = TagRegistry.getEventStore();
end
if ~isempty(esObj)
if ~isempty(obj.FilterTagKey)
raw = obj.EventStoreObj.getEventsForTag(obj.FilterTagKey);
raw = esObj.getEventsForTag(obj.FilterTagKey);
evts = obj.eventObjectsToStructs(raw);
else
evts = obj.eventStoreToStructs();
evts = obj.eventStoreToStructsFrom_(esObj);
end
elseif ~isempty(obj.EventFcn)
evts = obj.EventFcn();
Expand All @@ -296,7 +304,9 @@ function refresh(obj)
evts = evts(mask);
end
end
end

methods (Access = private)
function evts = eventStoreToStructs(obj)
%EVENTSTORETOSTRUCTS Convert Event objects from EventStore to
% the struct format used for rendering (startTime, endTime, label, color).
Expand Down Expand Up @@ -330,6 +340,42 @@ function refresh(obj)
end
end

function evts = eventStoreToStructsFrom_(obj, esObj)
%EVENTSTORETOSTRUCTSFROM_ Phase 1017 variant of eventStoreToStructs.
% Same conversion logic, but reads from the supplied esObj rather
% than obj.EventStoreObj. Lets resolveEvents() use the registry-
% default store without temporarily mutating obj.EventStoreObj
% (avoids re-entrancy risk per RESEARCH Pitfall 6).
evts = struct('startTime', {}, 'endTime', {}, 'label', {}, 'color', {});
raw = esObj.getEvents();
if isempty(raw), return; end

theme = obj.getTheme();
alarmColor = theme.StatusAlarmColor;
warnColor = theme.StatusWarnColor;

for i = 1:numel(raw)
ev = raw(i);
lbl = ev.SensorName;
if ~isempty(ev.ThresholdLabel)
lbl = [ev.SensorName ' — ' ev.ThresholdLabel];
end
% Colour routing is driven by the numeric Severity field
% (1=ok/info, 2=warn, 3=alarm; see Event.m EVENT-04) with
% a ThresholdLabel keyword fallback for events authored
% before Severity existed.
clr = warnColor;
if isfield(ev, 'Severity') && ~isempty(ev.Severity) && ev.Severity >= 3
clr = alarmColor;
elseif ~isfield(ev, 'Severity') && ~isempty(ev.ThresholdLabel) && ...
~isempty(strfind(lower(ev.ThresholdLabel), 'alarm'))
clr = alarmColor;
end
evts(end+1) = struct('startTime', ev.StartTime, ...
'endTime', ev.EndTime, 'label', lbl, 'color', clr); %#ok<AGROW>
end
end

function evts = eventObjectsToStructs(obj, eventObjs)
%EVENTOBJECTSTOSTRUCTS Convert Event objects to rendering structs.
% Accepts an array of Event objects (or structs with StartTime/
Expand Down
22 changes: 18 additions & 4 deletions libs/Dashboard/FastSenseWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,18 @@ function render(obj, parentPanel)
% widget level). Otherwise we leave the inner FastSense's own
% properties untouched — preserving the Phase-1010 default-true
% behaviour for consumers that bypassed the widget API.
if obj.ShowEventMarkers || ~isempty(obj.EventStore)
% Phase 1017: resolve EventStore via registry default if no
% explicit per-widget handle was provided. This ensures the
% inner FastSense receives the registry-default store at
% render time even when ShowEventMarkers was not explicitly
% set true on the widget.
esForward = obj.EventStore;
if isempty(esForward)
esForward = TagRegistry.getEventStore();
end
if obj.ShowEventMarkers || ~isempty(esForward)
fp.ShowEventMarkers = obj.ShowEventMarkers;
fp.EventStore = obj.EventStore;
fp.EventStore = esForward;
end

% Slide the X window as new samples arrive on updateData().
Expand Down Expand Up @@ -729,9 +738,14 @@ function rebuildForTag_(obj)
obj.FastSenseObj = fp;
fp.ShowThresholdLabels = obj.ShowThresholdLabels;
% Phase 1012 — guarded forwarding (see render() comment above).
if obj.ShowEventMarkers || ~isempty(obj.EventStore)
% Phase 1017: resolve EventStore via registry default if not explicitly set.
esForward = obj.EventStore;
if isempty(esForward)
esForward = TagRegistry.getEventStore();
end
if obj.ShowEventMarkers || ~isempty(esForward)
fp.ShowEventMarkers = obj.ShowEventMarkers;
fp.EventStore = obj.EventStore;
fp.EventStore = esForward;
end
fp.addTag(obj.Tag);

Expand Down
44 changes: 26 additions & 18 deletions libs/Dashboard/TableWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -83,26 +83,34 @@ function refresh(obj)
if isempty(colNames)
colNames = {'Time', obj.Sensor.Name};
end
elseif strcmp(obj.Mode, 'events') && ~isempty(obj.EventStoreObj)
evts = obj.EventStoreObj.getEvents();
if ~isempty(evts)
sName = obj.Sensor.Name;
mask = arrayfun(@(e) contains(e.SensorName, sName), evts);
evts = evts(mask);
n = min(obj.N, numel(evts));
if n > 0
evts = evts(end-n+1:end);
data = cell(n, 4);
for i = 1:n
data{i,1} = datestr(evts(i).StartTime, 'HH:MM:SS');
data{i,2} = datestr(evts(i).EndTime, 'HH:MM:SS');
data{i,3} = evts(i).ThresholdLabel;
data{i,4} = sprintf('%.1fs', (evts(i).EndTime - evts(i).StartTime)*86400);
elseif strcmp(obj.Mode, 'events')
% Phase 1017: registry-default fallback. Local esObj prevents
% obj-mutation re-entrancy (RESEARCH Pitfall 6).
esObj = obj.EventStoreObj;
if isempty(esObj)
esObj = TagRegistry.getEventStore();
end
if ~isempty(esObj)
evts = esObj.getEvents();
if ~isempty(evts)
sName = obj.Sensor.Name;
mask = arrayfun(@(e) ~isempty(strfind(e.SensorName, sName)), evts);
evts = evts(mask);
n = min(obj.N, numel(evts));
if n > 0
evts = evts(end-n+1:end);
data = cell(n, 4);
for i = 1:n
data{i,1} = datestr(evts(i).StartTime, 'HH:MM:SS');
data{i,2} = datestr(evts(i).EndTime, 'HH:MM:SS');
data{i,3} = evts(i).ThresholdLabel;
data{i,4} = sprintf('%.1fs', (evts(i).EndTime - evts(i).StartTime)*86400);
end
end
end
end
if isempty(colNames)
colNames = {'Start', 'End', 'Label', 'Duration'};
if isempty(colNames)
colNames = {'Start', 'End', 'Label', 'Duration'};
end
end
end
elseif ~isempty(obj.DataFcn)
Expand Down
4 changes: 4 additions & 0 deletions libs/FastSense/FastSense.m
Original file line number Diff line number Diff line change
Expand Up @@ -2311,6 +2311,10 @@ function renderEventLayer_(obj)
end
end
end
% Phase 1017: registry-default fallback (tail of existing chain).
if isempty(es)
es = TagRegistry.getEventStore();
end
if isempty(es), return; end

% Delete old markers (idempotent rebuild)
Expand Down
9 changes: 9 additions & 0 deletions libs/SensorThreshold/MonitorTag.m
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@
end
end

% Phase 1017: registry-default fallback. If no explicit
% 'EventStore' NV-pair was provided, consult the registry
% default set via TagRegistry.setEventStore(store). Returns
% [] when no default has been set, preserving pre-1017
% behavior for users who never wired a registry default.
if isempty(obj.EventStore)
obj.EventStore = TagRegistry.getEventStore();
end

% MONITOR-09 Persist-pairing validation: Persist=true requires
% a DataStore handle, otherwise storeMonitor/loadMonitor have
% nowhere to go. Fail fast at construction rather than at first
Expand Down
Loading
Loading