# Alarms and Notifications: Don't Miss a Threshold Crossing

> How Serial Studio's Info, Warning, and Critical notifications turn a dataset crossing a configured threshold into a logged alert, not just a line on a plot.
>
> Alarms · June 14, 2026 · by Alex Spataru · https://serial-studio.com/blog/alarms-and-notifications

A plot is a great way to watch one channel. Add a dozen more, and a human eye can't reliably hold that many moving lines at once, so a reading drifting past its safe limit can slide by unnoticed for minutes. Serial Studio's answer isn't a fancier chart. It's a notification: a script decides a value crossed a line and posts an event to a shared log, instead of asking you to catch a slope change on a plot. This post covers what the feature actually does: three severity levels, a shared event bus fed from five different places in the pipeline, and a scripted latch-and-resolve pattern for alarms that shouldn't nag on every frame.

## Why watching the plot stops working

A dashboard with two or three channels is easy to eyeball: you glance at the lines, and a spike or a flat stretch is obvious. That stops being true once a project grows past a handful of datasets, because attention doesn't scale with channel count the way a plot's Y axis does. A test bench with a dozen thermocouples, or a hydraulic rig reporting pressure, flow, vibration, and motor load together, asks an operator to notice one line among many the moment it happens, without ever looking away. Nobody does that reliably for an eight-hour shift.

Serial Studio's notification system exists for exactly this gap. Instead of relying on someone watching a chart, a frame parser, a dataset transform, an output widget script, the Control Loop, or C++ code decides that a value crossed a threshold and posts an event: an Info, Warning, or Critical severity, a channel label, a title, and an optional subtitle. That event lands in a scrollable log and, if you've turned it on, as a native desktop notification too, so the alarm is waiting for you even if you were looking at a different tab, or away from the screen entirely.

## A threshold is just a value your script compares

There's no separate "alarm" object to configure in Serial Studio. A threshold is an `if` statement inside a dataset transform, a frame parser, or an output widget's `transmit()` function, and the interesting part is the five functions available once that condition is true. `notifyInfo`, `notifyWarning`, and `notifyCritical` each take a channel, a title, and a subtitle, though you can skip the channel and the event falls back to a default `"Dashboard"` channel. The generic `notify(level, channel, title, subtitle)` form lets a script pick the level dynamically, for example stepping Info to Warning to Critical as an RPM channel climbs.

A battery monitor is the simplest real example, straight out of Serial Studio's own documentation: a dataset transform watches the voltage reading and warns once it drops below a safe minimum, sending the measured value along in the subtitle so the log entry means something on its own.

```lua
function transform(value)
  if value < 11.0 then
    notifyWarning("Power", "Battery low",
                  string.format("%.2f V", value))
  end
  return value
end
```

The threshold itself, `11.0`, is nothing special. It's a number you chose because you know the hardware, sitting in the same script that already holds the reading. That's the whole trick: the alarm logic lives exactly where the value already is, so there's no separate configuration surface to keep in sync with the transform.

## Five places a threshold check can live

Which of those five places you pick changes what the threshold is actually checking. A frame parser sees the raw field straight off the wire, before any calibration or unit conversion runs, so it's the right spot to flag a malformed frame, a bad checksum, or a value that never should have arrived (the documentation's own checksum example does exactly this, warning `"Frame dropped: bad CRC"` before the bad frame ever reaches a dataset). A [dataset transform](https://serial-studio.com/help/dataset-transforms) sees the value after your own math has already run, which is why the battery example above compares against 11.0 volts rather than a raw ADC code: the threshold means something physical because the transform converted it first.

Output widgets get the same five functions inside `transmit(value)`, so a slider or a text field can flag a clamped input or log the command it just sent, the pattern [covered in more depth for SCPI and G-code devices](https://serial-studio.com/blog/control-devices-scpi-gcode). The [Control Loop](https://serial-studio.com/help/control-script)'s `setup()` and `loop()` functions carry the same API, which is where a staleness watchdog lives: checking how long it's been since the last frame arrived is a job no parser or transform can do on its own, because both only run when a new frame shows up in the first place.

## Do you have to catch it the moment it happens?

No, and that's really the point of a logged event instead of a chart you have to be staring at. A `notifyCritical` call doesn't flash and disappear: it adds a row to the Notification Log that stays there, its title rendered in the alarm color, until someone clears the log or the dashboard resets. Look away for ten minutes and the row is exactly where you left it, timestamp attached.

The one thing worth watching is repetition. If a transform runs on every frame and the condition stays true, calling `notifyCritical` on every frame would flood the log with an identical row hundreds of times a second. Serial Studio's NotificationCenter deduplicates automatically: it drops any event whose level, channel, title, and subtitle exactly match the previous one within a 100 ms window. That's enough to survive a transform running at kilohertz rates without swallowing a genuinely rising alarm, because a subtitle carrying the live value ("1049 C", then "1050 C") is never identical twice, so it never gets collapsed.

Deduplication stops flooding, but it doesn't stop an alarm from re-firing every frame the condition is still true, which is its own kind of noise. Serial Studio's documentation shows the fix as a scripting pattern rather than a checkbox: a latched boolean that posts once on entry and calls `notifyClear` once the value has fallen back past a second, lower bound.

```lua
local latched = false
function transform(value)
  if value > 900 and not latched then
    notifyCritical("Engine", "EGT critical",
                   string.format("%.1f C", value))
    latched = true
  elseif value < 870 and latched then
    notifyClear("Engine", "EGT critical",
                string.format("Back to %.1f C", value))
    latched = false
  end
  return value
end
```

The gap between 900 and 870 is the whole trick. Without it, a reading hovering right at 900 would trip the alarm and clear it dozens of times a second as noise carries it back and forth across the line. `notifyClear` doesn't erase the original alarm either. It posts a companion "Resolved: EGT critical" Info event, so the log keeps a plain-language record of when the exhaust got hot and when it settled back down, in order, without anyone needing to have watched it happen live.

## What you actually see when it fires

The Notification Log is a Pro-only dashboard widget: one row per event with an icon, a bold title, a channel pill, and a timestamp, with the subtitle wrapped onto a second line underneath. It auto-scrolls to follow new events unless you've scrolled up to read history, and a count badge next to its filter box tracks unread Warning and Critical events (Info never counts, capped at "99+"). Typing into the filter box runs a live, case-sensitive substring match against the channel name, which matters once a project has several scripts posting to the same log: type `Engine` and everything from `Power` or `Dashboard` disappears from view.

Turn on **Preferences → Notifications → System Notifications** and Warning and Critical events also raise a native desktop toast through the OS tray, even when Serial Studio isn't the foreground window. It's off by default. Info events never produce a toast, on the idea that a desktop notification should mean "look now," not "here's a status update." The catch is platform-dependent: macOS always has a tray to post to, while on Linux it depends on the desktop environment.

One thing the log deliberately doesn't do is survive a reconnect. History is wiped on every dashboard reset, meaning a disconnect, a project reload, or stopping playback, so a new session always starts clean instead of carrying yesterday's alarms into today's run.

## Where this actually gets used

The pattern shows up anywhere a project already has more channels than someone can watch continuously. An industrial test stand polling pressure, flow, vibration, and motor load over Modbus is the clearest case: a pressure channel approaching a relief-valve setting, or a vibration reading crossing an ISO 10816 limit, is exactly the kind of threshold a dataset transform can flag the moment it happens, instead of relying on someone noticing a gauge needle drift. A bench setup running an exhaust-temperature or battery-voltage channel through an overnight test faces the same problem at a smaller scale: nobody wants to babysit a plot for eight hours to catch one crossing.

The Control Loop's staleness watchdog covers a different failure mode: silence. A sensor that stops transmitting doesn't show up as a spike on any plot, it just stops updating, which is easy to miss on a screen full of numbers that all still look plausible. Checking a frame's age and firing `notifyCritical` once it passes a couple of seconds turns "the last update was a while ago" into a timestamped row in the log, rather than a stale dashboard nobody questioned, a pattern [covered in more detail elsewhere](https://serial-studio.com/blog/telemetry-lessons-from-production-firmware).

The same API is reachable from outside Serial Studio, too. The [MCP API](https://serial-studio.com/help/api-reference) exposes `notifications.post`, so an external test harness, a Python script driving a bench sequence, can post its own events onto the same bus your project's scripts use. They show up on the operator's dashboard immediately, in the same log, with the same severity colors.

## Try it without hardware

Serial Studio's [Modbus PLC Simulator example](https://serial-studio.com/examples#Modbus%20PLC%20Simulator) is a good sandbox for this, even though the project as shipped doesn't call `notify()` anywhere. It's a physics-based hydraulic test stand that already documents a real threshold: its pressure register carries the comment "alarm at 2800 PSI," and the simulator's own console log labels the flag `A:Alarm(>2800PSI)` next to a relief-valve setting at 3000 PSI. A vibration channel scored against ISO 10816 limits, unacceptable above roughly 11 mm/s in the simulator's own failure sequence, sits right next to it.

Open the project, add the battery-low pattern above to the Pressure dataset's transform with `2800` in place of `11.0`, and connect. The simulator cycles through a `PRESSURE_TEST` phase every 60 seconds and, rarely, a randomized failure sequence that pushes pressure toward the relief valve, so the alarm you just wrote has a real chance to fire without you touching a single wire.

## Frequently asked questions

### Do notifications work without a Pro license?

Partly. The `Info`, `Warning`, and `Critical` level constants are always defined, so a Pro-authored script still loads under a GPL build without erroring out. Calling `notify()` or any of its variants from a frame parser, dataset transform, or output widget raises a clear `notify() requires a Pro license` error at runtime; a Control Loop call returns an error result instead.

### Does an alarm interrupt me if Serial Studio isn't the active window?

Only if you've turned it on. **Preferences → Notifications → System Notifications** is off by default, and Warning and Critical events fire a native OS tray toast once it's enabled, even in the background. Info-level events never produce a desktop toast, only a row in the in-app log.

### What stops a fast-changing reading from flooding the log?

A 100 ms deduplication window drops any event whose level, channel, title, and subtitle exactly match the previous one, which keeps a transform running at kilohertz rates from spamming identical rows. Put the live value inside the subtitle, and a rising alarm stays visible on every frame instead of getting collapsed away.

### Do old alarms carry over into a new session?

No. The Notification Log's history clears on every dashboard reset, meaning a disconnect, a project reload, or stopping a playback session, by design. Each new connection starts with an empty log instead of yesterday's alarms mixed in with today's.
