app/rcc/ai/docs/frame_parser_js.md
A frame parser converts raw bytes (or pre-split text) emitted by a device into the array of dataset values the dashboard consumes.
You define a single function taking exactly one parameter:
function parse(frame) {
// returns Array<number | string>
}
frame's shape depends on the source's selected decoder: PlainText gives
a UTF-8 String, Hexadecimal/Base64 give the encoded text as a String,
and Binary gives an Array of byte values (0–255).parse(frame, separator). That two-parameter signature is
a Serial Studio v2 relic, removed years ago; the validator rejects it when
the script is loaded for authoring or dry-run, and even where a legacy
script slips through, separator is undefined. There is no separator
argument — split the frame yourself with the delimiter your protocol uses
(e.g. frame.split(',')).Array whose values map positionally to the datasets in the
project (in group/dataset declaration order).[] or null skips the frame silently. Extra values are
ignored; datasets whose index exceeds the returned length keep their
previous value.The QJSEngine running parsers is isolated from the host system. Available:
console, gc(), plain JS built-ins, and the injected host API —
deviceWrite, actionFire, the seven dashboard helpers,
tableGet/tableSet/datasetGetRaw/datasetGetFinal, notify*,
apiCall, and the generated SerialStudio SDK (io.*, project.*, ...):
XMLHttpRequest, fetch, setTimeout, setInterval.import/require. Single-file scripts only.Each source runs its parser in its own engine; top-level var declarations
are real globals within that per-source engine, so they don't bleed into
other sources' parsers.
This function runs on every frame, sometimes at 10+ kHz. Cheap operations:
String.prototype.splitparseInt, parseFloat, NumberString.prototype.match with a precompiled RegExpAvoid:
JSON.parse on every frame unless the protocol is JSON-linefunction parse(frame) {
return frame.split(',');
}
function parse(frame) {
const parts = frame.split('\t');
const out = new Array(parts.length);
for (let i = 0; i < parts.length; ++i)
out[i] = parseFloat(parts[i]);
return out;
}
const RE = /^\$([A-Z]+),(-?\d+\.\d+),(-?\d+\.\d+),(-?\d+)\*([0-9A-F]{2})/;
function parse(frame) {
const m = frame.match(RE);
if (!m)
return [];
return [parseFloat(m[2]), parseFloat(m[3]), parseInt(m[4], 10)];
}
function parse(frame) {
try {
const obj = JSON.parse(frame);
return [obj.x, obj.y, obj.z];
} catch (_e) {
return [];
}
}
With the Binary decoder, frame is an Array of byte values (0–255), not a
string — string methods throw (frame.split is not a function). Index it
directly; wrap it in a DataView for multi-byte or float fields:
function parse(frame) {
if (frame.length < 6)
return [];
const view = new DataView(Uint8Array.from(frame).buffer);
return [view.getFloat32(0, true), view.getInt16(4, true)];
}
If your device emits multiple sensor rows per frame, return a 2-D array; the FrameBuilder will flatten to multiple frames.
function parse(frame) {
const rows = frame.split(';');
return rows.map(r => r.split(',').map(parseFloat));
}
deviceWrite()deviceWrite(data, sourceId?) -> { ok: true } | { ok: false, error: "..." }
data is a string (UTF-8 encoded on the wire) or an array of byte values
(0–255).sourceId is optional; default is the source this parser belongs to.[deviceWrite] source=N bytes=M written=K.Use for closed-loop control from inside parse(): ack a frame, raise an
alarm, request a status push. Not for user-triggered actions — those
belong on an Output Widget.
function parse(frame) {
const values = frame.split(",");
if (parseFloat(values[0]) > 80) {
const r = deviceWrite("ALARM=1\n");
if (!r.ok) console.warn("alarm write failed:", r.error);
}
return values;
}
actionFire()actionFire(actionId) -> { ok: true } | { ok: false, error: "..." }
Reuses an existing project Action (with its prebuilt payload + timer
mode). actionId is the action's stable id, NOT its index in the list.
Same shape as deviceWrite; never throws.
Seven UI helpers, all returning { ok: true } or { ok: false, error: "..." }. None of them log. Call once on a state transition, NOT on every frame.
clearPlots() // wipe line/multiplot/FFT/GPS/3D/waterfall buffers
setPlotPoints(n) // horizontal sample window (n >= 1)
setTerminalVisible(visible) // bool
setNotificationLogVisible(visible) // bool
setClockVisible(visible) // bool
setStopwatchVisible(visible) // bool
setActiveWorkspace(idOrName) // workspaceId (int >= 1000) OR title (case-insensitive)
Typical use: reset a GPS trace the moment a valid fix arrives so the line from origin to first real sample disappears.
var hadFix = false;
function parse(frame) {
const [lat, lon, q] = frame.split(",").map(parseFloat);
if (!hadFix && q > 0) {
clearPlots();
hadFix = true;
}
return [lat, lon, q];
}
These affect the active dashboard window only. They do NOT modify the project file or the user's persisted preferences.
Throwing an exception logs a watchdog warning and skips the frame. Returning
non-numeric strings for a numeric dataset coerces via Number(); NaN
becomes 0.
Each source has its own parser script; you cannot reach across sources from
a parser. To compute cross-source values, use a per-dataset transform with
the tableGet/datasetGetFinal API instead.