Problem / motivation
MonitorTag already has a full staged alarm FSM — raw ConditionFn → optional hysteresis (AlarmOffConditionFn) → MinDuration on-delay debounce (applyDebounce_, libs/SensorThreshold/MonitorTag.m:708, which zeroes out ON-runs shorter than MinDuration). MinDuration filters short onset spikes so a brief excursion above threshold doesn't raise the alarm.
There is no complementary clear-side stage. The alarm de-asserts the instant the condition goes false — even for a single-sample dropout. So a signal hovering right at the threshold flaps the alarm off→on→off (alarm chatter). This is exactly the case that process-alarm practice (ISA-18.2 "delay-on-return") solves with an off-delay: MinDuration already covers the ON half; the OFF half is simply absent.
This is a direct "exists for onset, missing for clear" asymmetry, not a new subsystem.
Proposed feature
A new opt-in property and a symmetric debounce stage:
OffDelay = 0 — native parent-X units; 0 (default) = today's behavior.
- The alarm clears only after the condition has been continuously false for ≥
OffDelay; OFF-runs shorter than OffDelay are bridged back to ON.
% Signal noisy right at 50 — currently the alarm chatters every sample;
% with OffDelay it stays asserted through brief dropouts and clears once
% the signal is genuinely back below threshold for >= 2 X-units.
m = MonitorTag('press_hi', st, @(x, y) y > 50, ...
'MinDuration', 2, ... % existing on-delay (ignore brief spikes)
'OffDelay', 2); % NEW clear-side debounce (ignore brief drops)
Rough sketch
- Lib/class:
libs/SensorThreshold/MonitorTag.m only.
- Property:
OffDelay = 0 in the public properties block (next to MinDuration, MonitorTag.m:112), with a set.OffDelay validator mirroring set.MinDuration (MonitorTag.m:476).
- New private helper
applyClearDebounce_ — a near-mirror of applyDebounce_ (MonitorTag.m:708) that fills OFF-runs shorter than OffDelay with ON, reusing findRuns_, with a carry-in start argument for the streaming path (symmetric to ongoingRunStart_).
- Stage wiring: apply as a 4th stage after the
MinDuration on-delay in both recompute_ (Stage 3, MonitorTag.m:405) and the appendData streaming path (MonitorTag.m:643). Add one carry-in field to cache_ (mirrors lastHystState_/ongoingRunStart_) so streaming stays deterministic/replayable.
- Serialization: add
s.offdelay = obj.OffDelay; to toStruct (mirrors s.minduration, MonitorTag.m:293) and 'OffDelay', MonitorTag.fieldOr_(s, 'offdelay', 0) to fromStruct (mirrors MonitorTag.m:1163).
- Public API shape: name–value option
'OffDelay', <scalar >= 0> on the existing constructor; getXY() return contract unchanged (still a 0/1 vector on the parent grid).
Value
High for the target persona (a MATLAB sensor-analysis engineer watching a dashboard, not the live sample stream). A monitor bound to a StatusWidget/NumberWidget on a noisy channel currently chatters; OffDelay is the standard off-delay knob that stops it. It completes the two-sided debounce that every real alarm system carries, using machinery that already exists on the ON side.
Constraints check
- Toolbox-free: ✅ run-length ops on a 0/1 vector — same primitives as
applyDebounce_/findRuns_. No toolbox.
- Backward-compatible: ✅ new property defaults to
0 → byte-identical output to today. Serialized tags that predate the field → fieldOr_(..., 0) → keep working. getXY()/Tag contract unchanged; existing scripts unaffected.
- Pure MATLAB / Octave: ✅ no MEX, no new dependency; mirrors an existing pure-MATLAB helper.
- Widget/Tag contract: ✅ works entirely through the existing
MonitorTag/Tag interface.
Effort estimate
S. One class. One property + set validator, one private applyClearDebounce_ (near-mirror of the existing applyDebounce_), one cache_ carry-in field, two serialization lines, one class-based test (tests/suite/TestMonitorTag.m).
AI-proposed via /feature-scout — needs a human product decision before implementation.
Problem / motivation
MonitorTagalready has a full staged alarm FSM — rawConditionFn→ optional hysteresis (AlarmOffConditionFn) →MinDurationon-delay debounce (applyDebounce_,libs/SensorThreshold/MonitorTag.m:708, which zeroes out ON-runs shorter thanMinDuration).MinDurationfilters short onset spikes so a brief excursion above threshold doesn't raise the alarm.There is no complementary clear-side stage. The alarm de-asserts the instant the condition goes false — even for a single-sample dropout. So a signal hovering right at the threshold flaps the alarm off→on→off (alarm chatter). This is exactly the case that process-alarm practice (ISA-18.2 "delay-on-return") solves with an off-delay:
MinDurationalready covers the ON half; the OFF half is simply absent.This is a direct "exists for onset, missing for clear" asymmetry, not a new subsystem.
Proposed feature
A new opt-in property and a symmetric debounce stage:
OffDelay = 0— native parent-X units;0(default) = today's behavior.OffDelay; OFF-runs shorter thanOffDelayare bridged back to ON.Rough sketch
libs/SensorThreshold/MonitorTag.monly.OffDelay = 0in the publicpropertiesblock (next toMinDuration,MonitorTag.m:112), with aset.OffDelayvalidator mirroringset.MinDuration(MonitorTag.m:476).applyClearDebounce_— a near-mirror ofapplyDebounce_(MonitorTag.m:708) that fills OFF-runs shorter thanOffDelaywith ON, reusingfindRuns_, with a carry-in start argument for the streaming path (symmetric toongoingRunStart_).MinDurationon-delay in bothrecompute_(Stage 3,MonitorTag.m:405) and theappendDatastreaming path (MonitorTag.m:643). Add one carry-in field tocache_(mirrorslastHystState_/ongoingRunStart_) so streaming stays deterministic/replayable.s.offdelay = obj.OffDelay;totoStruct(mirrorss.minduration,MonitorTag.m:293) and'OffDelay', MonitorTag.fieldOr_(s, 'offdelay', 0)tofromStruct(mirrorsMonitorTag.m:1163).'OffDelay', <scalar >= 0>on the existing constructor;getXY()return contract unchanged (still a 0/1 vector on the parent grid).Value
High for the target persona (a MATLAB sensor-analysis engineer watching a dashboard, not the live sample stream). A monitor bound to a
StatusWidget/NumberWidgeton a noisy channel currently chatters;OffDelayis the standard off-delay knob that stops it. It completes the two-sided debounce that every real alarm system carries, using machinery that already exists on the ON side.Constraints check
applyDebounce_/findRuns_. No toolbox.0→ byte-identical output to today. Serialized tags that predate the field →fieldOr_(..., 0)→ keep working.getXY()/Tagcontract unchanged; existing scripts unaffected.MonitorTag/Taginterface.Effort estimate
S. One class. One property +
setvalidator, one privateapplyClearDebounce_(near-mirror of the existingapplyDebounce_), onecache_carry-in field, two serialization lines, one class-based test (tests/suite/TestMonitorTag.m).AI-proposed via /feature-scout — needs a human product decision before implementation.