doc/help/Notifications.md
Dashboard-level notification system for publishing Info, Warning, and Critical events from frame parsers, dataset transforms, output widget scripts, C++ code, and the MCP API. Every event shows up in a scrollable log on the dashboard and can optionally surface as a native OS desktop notification.
Notifications are a Pro feature. The notify* functions are exposed to every scripting engine Serial Studio runs, but they raise a clear notify() requires a Pro license error when called without a valid Pro license. Level constants (Info, Warning, Critical) stay available at every tier so Pro-authored projects parse-load cleanly under GPL builds.
Notifications are a single shared bus. Any caller can post an event; the dashboard's Notification Log widget subscribes to the bus and renders events in time order, oldest at the top. Events carry four fields:
| Field | Type | Purpose |
|---|---|---|
level | int | 0 = Info, 1 = Warning, 2 = Critical. |
channel | string | Free-form label (for example "Power Events", "Engine"). Spaces allowed. |
title | string | Short event headline shown in bold in the log. |
subtitle | string | Optional detail line shown under the title with word wrap. |
Channels are not declared up front. They come into existence as soon as something posts to them. The Notification Log has a Filter by channel box that narrows the view to a single channel when needed.
flowchart LR
A["Script or C++
(parser, transform, widget)"] --> B["notifyInfo/Warning/Critical"]
B --> C["NotificationCenter
(ring buffer, dedup, tray)"]
C --> D["Notification Log widget"]
C --> E["OS tray toast (opt-in)"]
Legend: 1024-event ring buffer • 100 ms dedup window per
(level, channel, title, subtitle)tuple • Main-thread only • Cleared on disconnect
Every dataset transform (Lua or JavaScript) has the notify* functions injected into its engine at compile time. They behave identically to any other transform-scope function.
Frame parser scripts (parse(frame)) have the same five functions available. Use them to flag protocol errors, malformed frames, out-of-range fields, or heartbeat timeouts directly from the parser.
The transmit(value) function in output widgets can post notifications too. Useful for logging commands sent to the device, flagging clamped inputs, or warning when a setpoint exceeds a safe range.
Notifications can also be posted from the command-line MCP API (notifications.post with a level parameter, plus notifications.resolve, notifications.list, notifications.listChannels, etc.). That makes it easy to tie Serial Studio into larger test harnesses: your Python test driver posts an event, it shows up on the operator's dashboard immediately.
Five functions are available everywhere:
| Function | Level | Purpose |
|---|---|---|
notify(level, channel, title, subtitle) | caller chosen | Generic form. level is 0, 1, or 2, or use the Info/Warning/Critical constants. |
notifyInfo(channel, title, subtitle) | Info | Informational event. Does not increment the unread counter. |
notifyWarning(channel, title, subtitle) | Warning | Something worth attention but not critical. Increments the unread badge. |
notifyCritical(channel, title, subtitle) | Critical | Alarm-class event. Increments the unread badge. |
notifyClear(channel, title, subtitle) | Info | Emits a companion Resolved: <title> Info event. The original remains in history, so the resolution is visible in the timeline. |
Every notifyInfo / notifyWarning / notifyCritical / notifyClear accepts one, two, or three arguments. When you don't pass a channel, the event is posted to the default "Dashboard" channel:
| Arguments | Behaviour |
|---|---|
notifyInfo("Ready") | Channel = "Dashboard", title = "Ready", subtitle empty. |
notifyInfo("Ready", "System armed") | Channel = "Dashboard", title = "Ready", subtitle = "System armed". |
notifyInfo("Engine", "Ready", "") | Channel = "Engine", title = "Ready", subtitle = "". |
The generic notify() form always starts with the level, then follows the same 1/2/3-argument overload for the remaining fields.
Level constants are available as globals:
Info = 0
Warning = 1
Critical = 2
So notify(Critical, "Engine", "EGT exceeded", "1052 C") is equivalent to notifyCritical("Engine", "EGT exceeded", "1052 C").
The NotificationCenter drops events whose (level, channel, title, subtitle) tuple matches the previous event within 100 ms. That keeps transforms running at 10+ kHz from flooding the log with identical rows. A rising-value alarm whose subtitle includes the measured value (for example "1049 C", then "1050 C") is never collapsed, because the subtitle is part of the dedup key.
The examples below use dataset transforms, but the function signatures are identical in frame parsers and output widgets.
A magnetometer reports headings in radians. Log the first converted value so you can confirm the transform loaded:
Lua:
local announced = false
function transform(value)
if not announced then
notifyInfo("Transform loaded")
announced = true
end
return value * 180 / math.pi
end
JavaScript:
var announced = false;
function transform(value) {
if (!announced) {
notifyInfo("Transform loaded");
announced = true;
}
return value * 180 / Math.PI;
}
A battery monitor drops below 11 V. Warn on the "Power" channel with the measured value in the subtitle:
Lua:
function transform(value)
if value < 11.0 then
notifyWarning("Power", "Battery low",
string.format("%.2f V", value))
end
return value
end
JavaScript:
function transform(value) {
if (value < 11.0) {
notifyWarning("Power", "Battery low",
value.toFixed(2) + " V");
}
return value;
}
Don't re-trigger a critical alarm on every frame. Latch the state, post once on entry, then post a Resolved Info event when the condition clears:
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
JavaScript:
var latched = false;
function transform(value) {
if (value > 900 && !latched) {
notifyCritical("Engine", "EGT critical",
value.toFixed(1) + " C");
latched = true;
} else if (value < 870 && latched) {
notifyClear("Engine", "EGT critical",
"Back to " + value.toFixed(1) + " C");
latched = false;
}
return value;
}
Pick the level from a table or a computed expression:
Lua:
local function levelFor(rpm)
if rpm > 9500 then return Critical end
if rpm > 8500 then return Warning end
return Info
end
function transform(value)
notify(levelFor(value), "RPM", "Engine speed",
string.format("%.0f rpm", value))
return value
end
JavaScript:
function levelFor(rpm) {
if (rpm > 9500) return Critical;
if (rpm > 8500) return Warning;
return Info;
}
function transform(value) {
notify(levelFor(value), "RPM", "Engine speed",
value.toFixed(0) + " rpm");
return value;
}
A parser detects a bad checksum. Post a one-liner without specifying a channel; the event is posted to the default "Dashboard" channel:
Lua:
function parse(frame)
local parts = {}
for token in string.gmatch(frame, "([^,]+)") do
table.insert(parts, token)
end
local crc = tonumber(parts[#parts])
if crc ~= computeCrc(parts) then
notifyWarning("Frame dropped: bad CRC")
return {}
end
return parts
end
JavaScript:
function parse(frame) {
var parts = frame.split(",");
var crc = parseInt(parts[parts.length - 1], 10);
if (crc !== computeCrc(parts)) {
notifyWarning("Frame dropped: bad CRC");
return [];
}
return parts;
}
An output widget formats a setpoint command. Log the outgoing value so the operator can see what was sent:
function transmit(value) {
notifyInfo("Setpoint", "Sent",
"Target = " + value.toFixed(1));
return "SET " + value.toFixed(1) + "\r\n";
}
The Notification Log is a Pro-only dashboard widget that renders events from the shared bus. Enable it from the Start Menu (Notifications toggle) or from the taskbar's bell icon while the dashboard is visible.
| Behaviour | Description |
|---|---|
| Layout | One row per event, icon + title + channel pill + timestamp, optional subtitle on a wrapped second line. |
| Auto-scroll | Follows the tail automatically when new events arrive, unless the user has scrolled up to inspect history. |
| Border blink | The widget border pulses in the alarm color for 10 seconds after any Warning or Critical event. |
| Filter by channel | Case-sensitive substring match on the channel name. Live; rows hide and re-show as you type. |
| Clear all | Wipes the in-memory history. The ring buffer and the dedup window reset to empty. |
| Empty state | When the log is empty, the widget shows a large icon and a "No notifications yet" heading. |
The widget is global: only one exists per dashboard, and it isn't tied to any dataset group. Its position and size are saved with the project layout like any other dashboard widget.
The log is wiped automatically on every dashboard reset (disconnect, project reload, playback stop). This keeps the dashboard honest: events from the previous session don't bleed into the new one.
For Warning and Critical events, Serial Studio can also raise a native desktop notification via the system tray. This is opt-in and off by default. Enable it under Preferences → Notifications → System Notifications.
When enabled, Warning and Critical events fire a tray toast even when Serial Studio isn't the foreground window. The toast shows the channel and title on one line and the subtitle on a second line. Info events never produce OS notifications; they're logged in the widget only.
Requires a system tray to be available. On macOS the tray is always present; on Linux, the result depends on the desktop environment (GNOME, KDE, Wayland compositors all behave slightly differently).
Serial Studio already routes every qDebug(), qWarning(), qCritical(), and qFatal() call to the in-app Console widget. The NotificationCenter can additionally route Qt log messages:
| Qt level | Where it goes by default | Toggle |
|---|---|---|
qCriticalMsg / qFatalMsg | Console and NotificationCenter (System channel) | Always on. |
qWarningMsg | Console only | Opt-in under Preferences → Notifications → Route Warnings to Notifications. |
qInfoMsg / qDebugMsg | Console only | Not routed to notifications. |
Warnings are off by default because Qt and QML emit them frequently during normal operation (setGeometry hints, deprecation notices, platform quirks). Routing them unfiltered would drown out real alarms. Critical messages are rare and always meaningful, so they route unconditionally.
notify* calls under GPL raise a clear "notify() requires a Pro license. See https://serial-studio.com/pricing" error from the script engine.Info, Warning, Critical) are always defined so Pro-authored scripts parse-load cleanly on GPL builds. Only the call itself fails.(level, channel, title, subtitle) tuples posted within 100 ms. Include the live value in the subtitle if you want rising-value events to stay visible.notifyInfo does not increment the unread badge; only Warning and Critical do."Power" and "power" are different channels.QMetaObject::invokeMethod(&nc, Qt::QueuedConnection, ...) when posting from a worker.notify* in transforms are flagged as commercial by SerialStudio::commercialCfg, so the Pro-required overlay appears on the dashboard when loaded under GPL.notify* is most commonly used.parse(frame) function has the same notification API.transmit(value) can also post notifications.notifications.post, notifications.list, and related commands for external drivers.