doc/help/Project-Editor.md
The Project Editor lets you create and edit JSON project files that define how Serial Studio interprets incoming data and displays it on the dashboard. Open it from the toolbar's Project Editor button (or the wrench button in the device setup panel).
A project file describes three things: the structure of your data (groups and datasets), how to detect and parse frames from the wire, and what actions (commands) the user can send back to the device. Serial Studio reads the file at connect time and builds the dashboard from it.
The diagram below shows the tree structure of a Serial Studio project file and how each element maps to the dashboard.
flowchart TD
Root["Project Root"]
Root --> Groups
Root --> Actions
Root --> Sources["Sources · Pro"]
Groups --> G["Groups → Datasets"]
Actions --> A["Actions → Buttons"]
Sources --> S["Sources → Connections"]
Each value in the incoming data frame is assigned a 1-based frame index that you reference when configuring datasets. The mapping works like this:
flowchart LR
A["23.5, 1013, 45.2"] --> B["parse()"]
B --> C["[0]→IDX 1
[1]→IDX 2
[2]→IDX 3"]
The editor window has three areas: a toolbar across the top, a tree view on the left, and a property panel on the right.
File actions sit on the left; the rest of the toolbar is "add" buttons grouped by what they create.
.json or .ssproj file..proto) schema. Available in all builds; only the Pro widgets it can generate require a license. See Auto-Generating Projects.Every button, with its icon, is listed in the Toolbar & Button Reference.
Shows the project's hierarchical structure:
Project Root
Groups
Group: "Sensors"
Dataset: "Temperature" [IDX 1]
Dataset: "Humidity" [IDX 2]
Dataset: "Pressure" [IDX 3]
Group: "Status"
Dataset: "Battery" [IDX 4]
Actions
Action: "Reset Device"
Sources
Source: "Main Device"
The number in brackets is the dataset's frame index: its position in the parsed data array. Click any item to edit its properties in the right panel.
Shows a form for the selected tree item. Every change takes effect immediately in the project model. The form fields vary depending on whether you selected the project root, a group, a dataset, an action, or a source.
Select the project root in the tree and the right panel switches to the Project Overview (also called the Summary). It is a read-only diagram of the entire project configuration, laid out left-to-right in four columns:
Data tables, output widgets, and workspaces are drawn alongside as their own cards. Arrows show how parsed bytes flow into datasets and how transforms feed downstream consumers.
The diagram is interactive:
The overview is useful as a sanity check ("does my group widget have the three datasets it needs?") and as a navigation surface once a project gets too large to keep entirely in the tree.
When the project root is selected, the property panel shows the frame-parsing settings: how the byte stream is sliced into frames, how each frame is decoded, and which parser turns it into values. They apply globally in single-source projects, or per-source in multi-source projects.
These settings run before the parser and apply to every parser type (Built-In, Lua, and JavaScript alike):
| Setting | Description | Options |
|---|---|---|
| Frame detection method | How Serial Studio finds frame boundaries in the byte stream. | End Delimiter Only (frames end with a known sequence such as \n; the most common choice); Start and End Delimiter (bounded by a start and an end marker, e.g. /* and */); Start Delimiter Only (a header begins each frame, and the next header ends the previous one); No Delimiters (the whole captured chunk is one frame; use it for fixed-size or length-prefixed protocols). |
| Start delimiter / end delimiter | The actual delimiter strings. Which ones apply depends on the detection method. | Any string, e.g. \n, /*, */. |
| Hex delimiters | Tick when the delimiter strings are written in hex. | e.g. 0A for newline. |
| Data conversion (decoder) | How the bytes inside the delimiters are decoded before the parser sees them. | Plain Text (UTF-8) (default text mode); Hexadecimal (each byte pair read as a hex value); Base64 (Base64-decoded first); Binary Direct (raw bytes passed straight to the parser as a byte array/table). |
| Checksum algorithm | Optional integrity check appended to each frame; frames that fail are dropped. | XOR-8, MOD-256, CRC-8, CRC-16, CRC-16-MODBUS, CRC-16-CCITT, Fletcher-16, CRC-32, Adler-32. |
Picking the wrong decoder/detection pair silently mojibakes binary data or never produces a frame. The trap to remember: Plain Text routes through QString::fromUtf8, so any byte that is not valid UTF-8 (most binary payloads contain 0x00 or values above 0x7F) is replaced with U+FFFD and the original bytes are lost. For anything non-text, pick Binary Direct.
The parser turns a decoded frame into an array of values, one per dataset frame index. Pick the language from the Platform dropdown in the parser editor toolbar:
| Language | What you configure | Best for |
|---|---|---|
| Built-In | No code. Pick a template and fill in its parameter form. The default parser for a new project. | Common wire formats with no setup: delimited/CSV, fixed-width, key-value, NMEA 0183/2000, JSON, XML, YAML, MessagePack, Modbus, UBX, MAVLink, COBS/SLIP, and batched/time-series multi-frame data. |
| Lua | A parse(frame) function (recommended scripting language). | Custom logic the templates do not cover, with the lowest scripting overhead. |
| JavaScript | A parse(frame) function. | Custom logic when you prefer JavaScript or need JSON.parse-style ergonomics. |
For Built-In, the parameter form is per template. For example, Delimited text exposes a separator, an optional quote character, and trim/skip-empty toggles, while Modbus frames exposes a channel count, register offset, and a signed-registers toggle. The full template catalog and its parameters live in Frame Parser Scripting.
Writing a Lua or JavaScript parse() function is covered in Step 7 below. The Built-In templates need no script: the parameter form is the configuration, so you can skip straight to Step 3.
Groups organize related datasets and determine which group-level widget is used on the dashboard.
| Widget | Description | Dataset requirements |
|---|---|---|
| Data Grid | Tabular view of all values | Any number |
| Multiple Plot | Overlaid time-series curves | One or more |
| Accelerometer | 3D acceleration visualization | Exactly 3 (X, Y, Z) |
| Gyroscope | 3D orientation visualization | Exactly 3 (X, Y, Z) |
| GPS Map | Geographic tracking on a map | 2 or 3 (lat, lon, optional alt) |
| 3D Plot (Pro) | 3D scatter/trajectory | Exactly 3 (X, Y, Z) |
| Image View (Pro) | Binary image stream | None (image data in frame) |
| Painter Widget (Pro) | Custom JavaScript-rendered canvas | Any number |
| Web View | Embedded web page | Any number |
| None | No group widget. Datasets shown individually | Any number |
Datasets map to individual data fields in your device's output.
General
23.5,1013,45.2, then Temperature = 1, Pressure = 2, Humidity = 3.Plotting
FFT (frequency analysis)
Waterfall (Pro)
LED
Alarm bands
Widget range
Actions place buttons on the dashboard that send commands to the connected device.
RST).\n), Carriage Return (\r), CRLF (\r\n), or None. Disabled in binary mode.| Mode | Behavior |
|---|---|
| Off | Manual click only (default). |
| Auto Start | Timer starts automatically on connect. Command repeats at the configured interval. |
| Start on Trigger | Timer starts on the first click. Command repeats until stopped. |
| Toggle on Trigger | Each click toggles the repeating timer on or off. |
| Repeat N Times | Each click sends the command a fixed number of times (Repeat Count), spaced by the configured interval. |
The full action reference, including multi-source targeting and worked examples, is on the Actions page.
Sources define where data comes from. Single-device projects have one implicit source. Multi-device projects use explicit sources.
Each source has its own Frame Parser tab for a per-source parser script.
This step applies to the Lua and JavaScript parsers. If you picked a Built-In template in Step 2, the parameter form is your parser (there is no script to write), so skip ahead.
For data that isn't plain CSV and isn't covered by a Built-In template, write a parse() function to transform each frame into an array of values. Serial Studio supports Lua (default, recommended) and JavaScript. Pick the language from the Platform dropdown in the parser editor toolbar.
Lua (default):
function parse(frame)
-- 'frame' is a string (PlainText/Hex/Base64) or byte table (Binary Direct).
-- Return a table of values matching dataset frame indices.
local result = {}
for field in frame:gmatch("([^,]+)") do
result[#result + 1] = field
end
return result
end
JavaScript:
function parse(frame) {
// 'frame' is a string (PlainText/Hex/Base64) or byte array (Binary Direct).
// Return an array of values matching dataset frame indices.
return frame.split(",");
}
Rules:
parse and has to accept exactly one argument.parse() persist between calls. Useful for stateful protocols.console.log() (both languages) or print() (Lua shorthand) to print debug messages to the Serial Studio terminal. The full console table (log, debug, info, warn, error) is available in JavaScript and Lua alike; error also raises an application notification, and warn does too when Route Warnings to Notifications is enabled in Settings (off by default).Example: binary protocol (Lua).
function parse(frame)
-- frame is a byte table in Binary Direct mode (1-indexed)
local temp = (frame[1] << 8) | frame[2]
local humidity = (frame[3] << 8) | frame[4]
return {temp / 10.0, humidity / 10.0}
end
The Test With Sample Data button on the parser toolbar opens the Test Frame Parser dialog. It runs the same byte-to-channels pipeline the live dashboard uses, so what you see here is what the dashboard would see for the same input. (The separate Validate button in the code-editor toolbar only checks that the script compiles.)
The dialog has three sections:
01 A2 FF 3C) - the safe way to feed binary protocols. Plain text mode reads the field as UTF-8.frames extracted | bytes consumed | bytes buffered | dropped. Below it, a tree expands each extracted frame into its raw bytes (hex), its decoder output (what the parser receives), and the parsed rows with one node per channel.Reading the tree top-down tells you exactly which stage failed:
parse() that returned nil, undefined, or a non-array value.parse() is off; compare row indices to the Frame Index field on each dataset.nil / NaN) or a widget min/max is clipping it.The detection mode, delimiters, and decoder you configured in Step 2 run before the parser. They decide where each frame starts and stops, and what parse(frame) receives: a QString::fromUtf8 string for Plain Text, a hex or Base64 string, or the raw byte buffer (a 1-indexed table in Lua, a length-keyed object in JavaScript) for Binary Direct. See the table in Step 2 for the full option list and the UTF-8 trap.
Your device sends a sequence of values. The frame parser (or the default comma splitter) produces an array. Each dataset's Frame Index tells Serial Studio which array position to read:
Device sends: 23.5,1013,45.2
Parser returns: ["23.5", "1013", "45.2"]
^ ^ ^
Index 1 Index 2 Index 3
Each dataset can optionally define a transform(value) function that converts the raw parsed value into an engineering value before it reaches the dashboard. Use transforms for calibration, unit conversion, filtering, and signal conditioning.
To add a transform, select a dataset and click the Transform button in the toolbar. That opens a dedicated editor with syntax highlighting, built-in templates, and a live test area.
For the full documentation, see Dataset Value Transforms.
When a project has multiple sources, each source represents a separate physical device with its own connection, bus type, frame detection, and parser (Built-In, Lua, or JavaScript).
.ssproj file.Serial Studio watches the open project file. The check compares file content rather than timestamps, so the app's own saves never trigger it; only an external change does.
Reloading replaces the in-memory project. The Restore dialog still lists the snapshots taken before the reload, so an unwanted reload can be undone from Backups & Recovery.
Symptom. Widget shows "0", wrong data, or no data.
Fix. Check that each dataset's frame index matches the correct position in the parser's return array. Index 1 = first element, index 2 = second element, and so on.
Symptom. Console shows "undefined" or parsing errors.
Fix:
console.log() calls to inspect the raw frame and parsed output.Symptom. Frames aren't detected, or data is garbled.
Fix:
\r or \0.\n (most serial devices), \r\n (Windows-style), or custom markers like /* and */.Symptom. Widget appears but displays incorrectly.
Fix:
Symptom. Group widget doesn't appear on the dashboard.
Fix. Make sure the group has the required number of datasets for its widget type. See the table in Step 3.