doc/help/API-Reference.md
The command-by-command reference below is regenerated by hand and drifts behind the C++ registry over time. The protocol shape, the connection details, and the prose sections (overview, security, usage, recommended patterns, troubleshooting) are kept current. The per-command sections under Complete Command Reference are best read as a structured tour, not a contract.
The authoritative live surface is whatever the running server registers at startup (
app/src/API/Handlers/*.cpp). To enumerate it:
- Legacy JSON: send
api.getCommands(no params). It returns{name, description}for every registered command.- MCP: send
tools/listover JSON-RPC 2.0 (see MCP Client) for the same surface plus full per-command input schemas.- REPL:
python test_api.py listfrom examples/API Test/.- From the AI Assistant:
meta.listCategoriesfollowed bymeta.listCommandsandmeta.describeCommand.
The Serial Studio API Server is a TCP server that listens on port 7777 (default) and accepts JSON-formatted commands to control Serial Studio programmatically. It provides programmatic control over Serial Studio through a TCP socket connection.
Identifier conventions. Most API commands address objects through some combination of
sourceId,groupId,datasetId,index, anduniqueId. They look interchangeable but are not. See the Dataset Identity Model for the rules of thumb (mutate by(groupId, datasetId), read byuniqueId, position byindex).
The API Server is available in both Serial Studio GPL and Serial Studio Pro builds:
Legend:
The commands in this document are also reachable from inside Serial Studio's scripting surfaces (Lua and JavaScript) via a generic apiCall() gateway. No TCP socket required: the call is dispatched in-process on the dashboard thread. A project's own scripts are first-party code, so the gateway is ungated for them — the full catalog below is callable with no allow-list, rate limit, or payload cap. The user-consent gate applies only to remote clients over TCP. See Frame Parser Scripting for examples.
local r = apiCall("project.dataset.list")
if r.ok then
print("Got " .. #r.result.datasets .. " datasets")
end
const r = apiCall("dashboard.getStatus");
if (!r.ok) console.warn(r.error);
Return shape: { ok, result?, error?, errorCode?, errorData? }. See Frame Parser Scripting -> apiCall for the full signature, examples, and the list of focused helpers (clearPlots, deviceWrite, actionFire, setActiveWorkspace, ...) that exist as thin shortcuts over apiCall.
apiCall runs synchronously and never throws. Treat it as a one-shot, event-driven trigger, not as something to invoke on every frame.
Launch Serial Studio
Enable the API Server:
Test the connection using curl (or any TCP client):
echo '{"type":"command","id":"1","command":"io.getStatus"}' | nc localhost 7777
Download the Python test client (optional):
examples/API Test/ in the Serial Studio repositorypython test_api.py send io.getStatusThe API Server starts immediately and will remember your preference across restarts.
Test the API is running:
# Linux/macOS
nc -zv 127.0.0.1 7777
# Or test with the Python client
cd examples/API\ Test
python test_api.py send io.getStatus
Expected output:
{
"isConnected": false,
"paused": false,
"busType": 0,
"configurationOk": false
}
The API Server always listens on port 7777; the port is not configurable. By default it is:
When external connections are enabled, the server binds to all interfaces and non-loopback clients must authenticate with an access token. See Authentication for External Connections.
By default, the API Server only accepts connections from 127.0.0.1 (localhost). In this default configuration it is:
This is the recommended configuration for most users. Remote access is opt-in: enabling Allow External API Connections binds the server to all interfaces and requires non-loopback clients to authenticate (see Authentication for External Connections).
By default the server binds to localhost only and requires no authentication, so any local process can connect. This is safe on single-user machines; on shared or multi-user systems, remember that any local process can control Serial Studio.
When external connections are enabled, the server also accepts connections from other machines. To keep that exposure safe, every non-loopback client must authenticate with an access token before any command or raw data is honored. Loopback (127.0.0.1 / ::1) clients are always exempt, so existing local tooling keeps working unchanged.
Implications:
The access token gate applies only to non-loopback clients and only when external connections are enabled. Local (loopback) clients never need a token, so scripts running on the same machine are unaffected.
Where to find the token
The token is generated automatically the first time external connections are enabled and persists across restarts.
How a client authenticates
A non-loopback client must send an auth handshake as its first line, before any other command:
{"type":"auth","token":"<your-token>"}
On success the server replies, and the connection stays authenticated for its lifetime:
{"type":"response","success":true,"result":{"authenticated":true}}
Any command sent before authenticating is rejected with an EXECUTION_ERROR ("Authentication required"), and the connection is closed after a few failed attempts. Loopback clients skip this step entirely.
Example (Python)
import json
import socket
TOKEN = "paste-your-token-here"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("192.168.1.50", 7777)) # the machine running Serial Studio
# Authenticate first (required only for non-loopback connections).
sock.sendall((json.dumps({"type": "auth", "token": TOKEN}) + "\n").encode())
print(sock.recv(4096).decode()) # {"type":"response","success":true,...}
# Now send commands as usual.
status = {"type": "command", "id": "1", "command": "io.getStatus"}
sock.sendall((json.dumps(status) + "\n").encode())
print(sock.recv(65536).decode())
sock.close()
Token storage. The token is kept in cleartext in Serial Studio's application settings, alongside other preferences. Treat it like a password: anyone holding it with network access to port 7777 can control Serial Studio. Regenerate it if you suspect it leaked, and keep external connections disabled unless you need them.
For production or multi-user systems:
Ensure your firewall does not expose port 7777 externally:
Linux:
# Check if port is listening externally
netstat -tuln | grep 7777
# Should show: 127.0.0.1:7777 (not 0.0.0.0:7777)
Windows:
netstat -an | findstr 7777
macOS:
netstat -an | grep 7777
# Good: Validate inputs before sending to API
python validate_config.py && python configure_device.py
# Bad: Blindly forwarding untrusted data to API
curl http://external-source/config.json | python send_to_api.py
# Good: Use local scripts with known behavior
./scripts/connect_uart.py
# Bad: Running unknown scripts with API access
wget http://example.com/script.py && python script.py
Best Practices:
Rate limiting and basic connection logging are already present. The server enforces a per-window message cap (clients that exceed it are disconnected), and connect, disconnect, and authentication events are logged.
Planned security features (not yet implemented):
The API is designed for:
# Automated hardware-in-the-loop testing using test_api.py
import subprocess
import time
# Configure device
subprocess.run(["python", "test_api.py", "send", "io.setBusType", "-p", "busType=0"])
subprocess.run(["python", "test_api.py", "send", "io.uart.setBaudRate", "-p", "baudRate=115200"])
subprocess.run(["python", "test_api.py", "send", "io.uart.setPortIndex", "-p", "portIndex=0"])
subprocess.run(["python", "test_api.py", "send", "io.connect"])
# Your test code here - e.g., send test commands, read responses, etc.
# Export data for later analysis
subprocess.run(["python", "test_api.py", "send", "csvExport.setEnabled", "-p", "enabled=true"])
time.sleep(10) # Let it record data
subprocess.run(["python", "test_api.py", "send", "csvExport.close"])
# Your analysis code here - e.g., parse CSV, validate data ranges, etc.
# Production line testing using the API client
import json
import socket
def send_command(command, params=None):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 7777))
msg = {"type": "command", "id": "qa", "command": command}
if params:
msg["params"] = params
sock.sendall((json.dumps(msg) + "\n").encode())
response = json.loads(sock.recv(65536).decode())
sock.close()
return response
# Example: Test 100 units in production
for unit_id in range(100):
send_command("io.connect")
# Your testing logic here - e.g., send test commands,
# read sensor values, validate against specifications
send_command("io.disconnect")
# Your logging/reporting logic here
#!/bin/bash
# Example: Multi-device coordination using shell scripts
# Setup device connection
python test_api.py send io.setBusType -p busType=0
python test_api.py send io.uart.setBaudRate -p baudRate=115200
python test_api.py send io.connect
python test_api.py send csvExport.setEnabled -p enabled=true
# Monitor connection for extended experiment (e.g., 24 hours)
for i in {1..1440}; do # 1440 = 24 hours * 60 minutes
STATUS=$(python test_api.py send io.getStatus --json)
IS_CONNECTED=$(echo $STATUS | jq -r '.result.isConnected')
if [ "$IS_CONNECTED" != "true" ]; then
echo "$(date): Connection lost, reconnecting..." >> experiment.log
python test_api.py send io.connect
fi
sleep 60 # Check every minute
done
python test_api.py send csvExport.close
echo "$(date): Experiment complete" >> experiment.log
# Example: Raspberry Pi sensor gateway
import subprocess
import json
import time
while True:
result = subprocess.run(
["python", "test_api.py", "send", "io.getStatus", "--json"],
capture_output=True, text=True
)
status = json.loads(result.stdout)
# Your cloud upload logic here - e.g., HTTP POST, MQTT publish, etc.
# Example data to send:
# {
# "connected": status["result"]["isConnected"],
# "bus_type": status["result"]["busType"],
# "timestamp": time.time()
# }
time.sleep(300) # Check every 5 minutes
# Example: GitLab CI / GitHub Actions workflow
jobs:
hardware_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Start Serial Studio
run: |
# Start Serial Studio in background (assuming it's installed)
serial-studio &
sleep 5
- name: Configure Device via API
run: |
cd examples/API\ Test
python test_api.py send io.setBusType -p busType=0
python test_api.py send io.uart.setBaudRate -p baudRate=115200
python test_api.py send io.connect
- name: Run Your Hardware Tests
run: |
# Your test commands here
# e.g., send test data, verify responses, etc.
python test_api.py send io.getStatus
#!/bin/bash
# Example: Classroom demonstration automation
# Step 1: Connect to Arduino
python test_api.py send io.setBusType -p busType=0
python test_api.py send io.uart.setBaudRate -p baudRate=9600
python test_api.py send io.uart.setPortIndex -p portIndex=0
python test_api.py send io.connect
sleep 2
# Step 2: Enable CSV logging
python test_api.py send csvExport.setEnabled -p enabled=true
echo "Recording data for 30 seconds..."
sleep 30
# Step 3: Stop recording
python test_api.py send csvExport.close
echo "Data saved to CSV file"
# Analyze the data with your own tools (e.g., Python, Excel, MATLAB, etc.)
127.0.0.1 (localhost only)7777 (default)\n)sequenceDiagram
participant Client
participant Server as Serial Studio API Server
Client->>Server: Connect to 127.0.0.1:7777
Client->>Server: Send JSON command + \n
Note right of Server: Process
Server->>Client: JSON response + \n
Client->>Server: (keep connection open for more)
Client->>Server: Close connection
127.0.0.1:7777\n)The API server accepts multiple concurrent client connections. Each client operates independently:
# Multiple clients can connect simultaneously
client1 = connect_to_api() # Control connection
client2 = connect_to_api() # Monitoring connection
client3 = connect_to_api() # Data export control
# Command responses go only to the socket that sent the command.
# Push messages (frames, raw data, lifecycle events) go to every client.
status = client1.send("io.getStatus")
# State changes made by one client are visible to all clients
Connection lifecycle:
127.0.0.1:7777gRPC: The entire API is also available via gRPC on port 8888, with high-performance binary streaming. See the gRPC Server documentation.
All messages are JSON objects terminated by a newline (\n). The server processes one command per connection or multiple commands in sequence.
The API supports two message types:
{
"type": "command",
"id": "unique-request-id",
"command": "io.getStatus",
"params": {
"key": "value"
}
}
Fields:
type (string, required): Always "command" for single commandsid (string, optional): Unique identifier for this request (echoed in response)command (string, required): Command name (e.g., "io.connect")params (object, optional): Parameters for the command{
"type": "batch",
"id": "batch-request-id",
"commands": [
{
"command": "io.setBusType",
"id": "cmd-1",
"params": {"busType": 0}
},
{
"command": "io.uart.setBaudRate",
"id": "cmd-2",
"params": {"baudRate": 115200}
}
]
}
Fields:
type (string, required): Always "batch" for batch requestsid (string, optional): Unique identifier for the entire batchcommands (array, required): Array of command objects{
"type": "response",
"id": "unique-request-id",
"success": true,
"result": {
"isConnected": false,
"paused": false
}
}
Fields:
type (string): Always "response"id (string): Matches the request IDsuccess (boolean): true if command succeededresult (object): Command-specific result data{
"type": "response",
"id": "unique-request-id",
"success": false,
"error": {
"code": "INVALID_PARAM",
"message": "Invalid port: 70000. Valid range: 1-65535"
}
}
Error Codes:
| Code | Description | Example |
|---|---|---|
INVALID_JSON | Malformed JSON message | Missing closing brace, invalid syntax |
INVALID_MESSAGE_TYPE | Unknown or missing message type | "type": "unknown" or missing type field |
UNKNOWN_COMMAND | Command not recognized | "command": "invalid.command" |
INVALID_PARAM | Parameter value out of range or invalid | Port 70000, negative baud rate |
MISSING_PARAM | Required parameter not provided | Missing baudRate for setBaudRate |
EXECUTION_ERROR | Command failed during execution | Disconnect when not connected |
OPERATION_FAILED | Operation could not be completed | File I/O error, hardware error |
{
"type": "response",
"id": "batch-request-id",
"success": false,
"results": [
{
"id": "cmd-1",
"success": true,
"result": {"busType": 0}
},
{
"id": "cmd-2",
"success": false,
"error": {"code": "INVALID_PARAM", "message": "..."}
}
]
}
Notes:
success is false if any command failsresults arrayBesides command responses, the server writes unsolicited JSON lines to every connected client:
| Message | When it is sent |
|---|---|
{"frames": [{"data": {...}}, ...]} | Batch of parsed dashboard frames, pushed as data arrives |
{"data": "<base64>"} | Raw bytes received from the device, base64-encoded |
{"event": "connected"} / {"event": "disconnected"} | Device connection lifecycle changes |
A client that issues commands on a long-lived socket while a device is streaming must demultiplex incoming lines: a line whose type field is "response" answers a command (match it to the request by id); lines with a frames, data, or event key are push messages. A naive recv() after send() can pick up a frame batch instead of the response.
The API provides 300+ commands across multiple modules; enumerate the live surface with api.getCommands:
The per-module counts below are approximate and drift behind the C++
registry; treat api.getCommands as the source of truth.
When a command is invoked through the AI Assistant rather than a raw TCP/JSON client, it is additionally gated by a five-tier safety system (Safe, Confirm, Always confirm, Device-gated, Blocked); see The safety tiers.
GPL Build:
Pro Build Additional:
api.getCommandsReturns every command the live server exposes, with descriptions.
Parameters: None
Returns:
{
"commands": [
{"name": "io.connect", "description": "Open the configured connection"},
{"name": "io.uart.setBaudRate", "description": "Set the UART baud rate (params: baudRate)"}
]
}
Example:
python test_api.py send api.getCommands
Over MCP, the equivalent is the standard
tools/listJSON-RPC method, which returns the same surface plus per-command input schemas.
Connection and bus management:
io.getStatusGet current connection status and configuration.
Parameters: None
Returns:
{
"isConnected": false,
"paused": false,
"busType": 0,
"busTypeLabel": "UART (serial port)",
"busTypeSlug": "uart",
"configurationOk": true,
"readOnly": false,
"readWrite": true,
"busTypeName": "UART/COM",
"_summary": "Not connected. UART (serial port) is configured and ready to open."
}
io.listBusesGet list of supported bus types.
Parameters: None
Returns:
{
"buses": [
{"index": 0, "name": "UART/COM"},
{"index": 1, "name": "Network Socket"},
{"index": 2, "name": "Bluetooth LE"}
]
}
io.setBusTypeSet the active bus/driver type.
Parameters:
busType (int): 0=UART, 1=Network, 2=BLE, 3=Audio, 4=Modbus, 5=CAN, 6=USB, 7=HID, 8=Process, 9=MQTT (3-9 require Pro)Example:
python test_api.py send io.setBusType -p busType=0
io.setPausedPause or resume data processing.
Parameters:
paused (bool): true to pause, false to resumeExample:
python test_api.py send io.setPaused -p paused=true
io.connectOpen connection to the configured device.
Parameters: None
Returns:
{
"connected": true
}
Errors:
EXECUTION_ERROR: Already connected or configuration invalidExample:
python test_api.py send io.connect
io.disconnectClose current device connection.
Parameters: None
Returns:
{
"connected": false
}
Errors:
EXECUTION_ERROR: Not connectedExample:
python test_api.py send io.disconnect
io.writeDataSend data to the connected device.
Parameters:
data (string): Base64-encoded data to sendReturns:
{
"bytesWritten": 12
}
Example:
# Send "Hello World" (SGVsbG8gV29ybGQ= in Base64)
echo -n "Hello World" | base64 # SGVsbG8gV29ybGQ=
python test_api.py send io.writeData -p data=SGVsbG8gV29ybGQ=
Errors:
EXECUTION_ERROR: Not connected, or device write denied by the userMISSING_PARAM: Missing data parameterINVALID_PARAM: Invalid base64 encodingDevice-write commands (
io.writeData,io.ble.writeCharacteristic,console.send) sent by a remote API/MCP client trigger a one-time consent prompt; the user's answer is remembered. Headless runs cannot show the prompt, so set the environment variableSERIAL_STUDIO_API_AUTO_CONSENT=1to allow API device writes in that mode (used by CI).
Frame-detection mode and start/finish delimiter sequences are no longer live runtime commands. They are per-source project settings configured in the Project Editor (or via
project.source.update) and persisted in the.ssprojfile.
io.getLatestFrameGet the latest raw frame received from the device, including channel tokens not yet mapped to any dataset.
Parameters:
sourceId (int, optional): Source filter; omit for the newest frame across all sourcesencoding (string, optional, default text): Payload encoding - text, base64, or bothReturns:
{
"hasData": true,
"sequence": 42,
"timestampMs": 123456,
"ageMs": 8,
"sourceId": 0,
"text": "25.3,60\n",
"valueCount": 2,
"values": ["25.3", "60"]
}
Notes:
timestampMs is a monotonic clock in milliseconds, not Unix epoch time; use it only for deltas between frameshasData: false means no data or no active consumerSerial port configuration:
io.uart.getConfigGet current UART configuration.
Parameters: None
Returns:
{
"portIndex": 1,
"portName": "/dev/ttyUSB0",
"baudRate": 115200,
"parityIndex": 0,
"parityName": "None",
"dataBitsIndex": 3,
"dataBitsValue": 8,
"stopBitsIndex": 0,
"stopBitsValue": 1,
"flowControlIndex": 0,
"flowControlName": "None",
"dtrEnabled": true,
"autoReconnect": false,
"isOpen": false,
"configurationOk": true
}
io.uart.listPortsGet list of available serial ports.
Parameters: None
Returns:
{
"portList": [
{"index": 0, "name": "COM1"},
{"index": 1, "name": "COM3"}
],
"currentPortIndex": 0,
"ports": [
{"index": 0, "name": "None"},
{"index": 1, "name": "/dev/ttyUSB0"}
]
}
io.uart.listBaudRatesGet list of common baud rates.
Parameters: None
Returns:
{
"baudRateList": ["110", "150", "300", "1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800", "576000", "921600"],
"currentBaudRate": 9600,
"baudRates": [
{"index": 0, "value": 1200},
{"index": 1, "value": 2400}
]
}
io.uart.setDeviceRegister a custom serial port device name.
Parameters:
device (string): Device name (e.g., "COM3", "/dev/ttyUSB0")Example:
python test_api.py send io.uart.setDevice -p device=COM3
io.uart.setPortIndexSelect serial port by index from port list.
Parameters:
portIndex (int): Index from getPortListExample:
python test_api.py send io.uart.setPortIndex -p portIndex=0
io.uart.setBaudRateSet baud rate.
Parameters:
baudRate (int): Baud rate (e.g., 9600, 115200)Example:
python test_api.py send io.uart.setBaudRate -p baudRate=115200
io.uart.setParitySet parity mode.
Parameters:
parityIndex (int): 0=None, 1=Even, 2=Odd, 3=Space, 4=MarkExample:
python test_api.py send io.uart.setParity -p parityIndex=0
io.uart.setDataBitsSet data bits.
Parameters:
dataBitsIndex (int): 0=5 bits, 1=6 bits, 2=7 bits, 3=8 bitsExample:
python test_api.py send io.uart.setDataBits -p dataBitsIndex=3
io.uart.setStopBitsSet stop bits.
Parameters:
stopBitsIndex (int): 0=1 bit, 1=1.5 bits, 2=2 bitsExample:
python test_api.py send io.uart.setStopBits -p stopBitsIndex=0
io.uart.setFlowControlSet flow control mode.
Parameters:
flowControlIndex (int): 0=None, 1=Hardware, 2=SoftwareExample:
python test_api.py send io.uart.setFlowControl -p flowControlIndex=0
io.uart.setDtrEnabledEnable or disable DTR signal.
Parameters:
dtrEnabled (bool): true to enable, false to disableExample:
python test_api.py send io.uart.setDtrEnabled -p dtrEnabled=false
io.uart.setAutoReconnectSet auto-reconnect behavior.
Parameters:
autoReconnect (bool): true to enable, false to disableExample:
python test_api.py send io.uart.setAutoReconnect -p autoReconnect=true
TCP/UDP configuration:
io.network.getConfigGet current network configuration.
Parameters: None
Returns:
{
"socketTypeIndex": 0,
"socketTypeName": "TCP",
"remoteAddress": "192.168.1.1",
"tcpPort": 8080,
"udpLocalPort": 0,
"udpRemotePort": 8081,
"udpMulticast": false,
"isOpen": false
}
io.network.listSocketTypesGet list of available socket types.
Parameters: None
Returns:
{
"socketTypes": [
{"index": 0, "name": "TCP"},
{"index": 1, "name": "UDP"}
]
}
io.network.setRemoteAddressSet remote host address.
Parameters:
address (string): IP address or hostnameExample:
python test_api.py send io.network.setRemoteAddress -p address=192.168.1.100
io.network.setTcpPortSet TCP port number.
Parameters:
port (int): Port number (1-65535)Example:
python test_api.py send io.network.setTcpPort -p port=8080
io.network.setUdpLocalPortSet UDP local port.
Parameters:
port (int): Port number (0-65535, 0=any available port)Example:
python test_api.py send io.network.setUdpLocalPort -p port=9000
io.network.setUdpRemotePortSet UDP remote port.
Parameters:
port (int): Port number (1-65535)Example:
python test_api.py send io.network.setUdpRemotePort -p port=9001
io.network.setSocketTypeSet socket type.
Parameters:
socketTypeIndex (int): 0=TCP (client), 1=UDP. Serial Studio does not act as a TCP server.Example:
python test_api.py send io.network.setSocketType -p socketTypeIndex=0
io.network.setUdpMulticastEnable or disable UDP multicast.
Parameters:
enabled (bool): true to enable, false to disableExample:
python test_api.py send io.network.setUdpMulticast -p enabled=false
io.network.lookupPerform DNS lookup for a hostname.
Parameters:
host (string): Hostname to resolveReturns:
{
"host": "example.com",
"addresses": ["93.184.216.34"]
}
Example:
python test_api.py send io.network.lookup -p host=google.com
BLE device management:
io.ble.getStatusGet Bluetooth adapter status.
Parameters: None
Returns:
{
"operatingSystemSupported": true,
"adapterAvailable": true,
"isOpen": false,
"deviceCount": 0
}
io.ble.getConfigGet current BLE configuration.
Parameters: None
Returns:
{
"deviceIndex": -1,
"characteristicIndex": 0,
"isOpen": false,
"configurationOk": false,
"deviceName": "Arduino BLE",
"characteristicName": "Temperature"
}
io.ble.listDevicesGet list of discovered BLE devices.
Parameters: None
Returns:
{
"deviceList": [
{"index": 0, "name": "My BLE Device"},
{"index": 1, "name": "Heart Rate Monitor"}
],
"currentDeviceIndex": -1,
"devices": [
{"index": 0, "name": "Arduino BLE", "address": "00:11:22:33:44:55"}
]
}
io.ble.listServicesGet list of services for selected device.
Parameters: None
Returns:
{
"serviceList": [
{"index": 0, "name": "Generic Access"},
{"index": 1, "name": "Heart Rate"}
],
"services": [
{"index": 0, "name": "Environmental Sensing", "uuid": "181A"}
]
}
io.ble.listCharacteristicsGet list of characteristics for selected service.
Parameters: None
Returns:
{
"characteristicList": [
{"index": 0, "name": "Heart Rate Measurement"}
],
"currentCharacteristicIndex": -1,
"characteristics": [
{"index": 0, "name": "Temperature", "uuid": "2A6E"}
]
}
io.ble.startDiscoveryStart scanning for BLE devices.
Parameters: None
Example:
python test_api.py send io.ble.startDiscovery
io.ble.selectDeviceSelect a discovered BLE device.
Parameters:
deviceIndex (int): Device index from listDevicesExample:
python test_api.py send io.ble.selectDevice -p deviceIndex=0
io.ble.selectServiceSelect a BLE service.
Parameters:
serviceIndex (int): Service index from listServicesExample:
python test_api.py send io.ble.selectService -p serviceIndex=0
io.ble.setCharacteristicIndexSelect a BLE characteristic.
Parameters:
characteristicIndex (int): Characteristic index from listCharacteristicsExample:
python test_api.py send io.ble.setCharacteristicIndex -p characteristicIndex=0
io.ble.selectServiceByUuidSelect a BLE service by UUID instead of index. The device must already be connected.
Parameters:
serviceUuid (string): Service UUID (16-bit short form like fff0 or full 128-bit UUID)Example:
python test_api.py send io.ble.selectServiceByUuid -p serviceUuid=fff0
io.ble.setNotifyCharacteristicSubscribe to the notify characteristic with the given UUID for incoming data. A service must be selected first.
Parameters:
characteristicUuid (string): Notify characteristic UUID (16-bit short form like fff2 or full 128-bit UUID)Example:
python test_api.py send io.ble.setNotifyCharacteristic -p characteristicUuid=fff2
io.ble.writeCharacteristicWrite raw bytes to a BLE characteristic resolved by UUID, independent of the selected notify characteristic. Pairs with io.ble.setNotifyCharacteristic for split read/write devices. The device must be connected and the owning service selected.
Parameters:
characteristicUuid (string): Target characteristic UUID (16-bit short form like fff1 or full 128-bit UUID)data (string): Base64-encoded bytes to writeErrors:
EXECUTION_ERROR: Not connected, no service selected, or device write denied by the userThis is one of the device-write commands covered by the consent prompt described under
io.writeDataabove; the same one-time prompt andSERIAL_STUDIO_API_AUTO_CONSENT=1override apply here.
CSV file export control:
csvExport.getStatusGet CSV export status.
Parameters: None
Returns:
{
"enabled": false,
"isOpen": false
}
csvExport.setEnabledEnable or disable CSV export.
Parameters:
enabled (bool): true to enable, false to disableExample:
python test_api.py send csvExport.setEnabled -p enabled=true
csvExport.closeClose current CSV file.
Parameters: None
Example:
python test_api.py send csvExport.close
CSV file playback control:
csvPlayer.openOpen a CSV file for playback.
Parameters:
filePath (string): Path to CSV fileExample:
python test_api.py send csvPlayer.open -p filePath=/path/to/data.csv
csvPlayer.closeClose current CSV file.
Parameters: None
csvPlayer.setPausedPause or resume playback.
Parameters:
paused (bool): true to pause, false to resumeExample:
python test_api.py send csvPlayer.setPaused -p paused=true
csvPlayer.stepStep the player by a number of frames.
Parameters:
delta (int, optional, default 1): frames to advance; negative steps backwardExample:
python test_api.py send csvPlayer.step -p delta=-1
csvPlayer.setProgressSeek to position in file.
Parameters:
progress (double): Position from 0.0 to 1.0Example:
python test_api.py send csvPlayer.setProgress -p progress=0.5
csvPlayer.getStatusGet player status.
Parameters: None
Returns:
{
"isOpen": true,
"isPlaying": false,
"frameCount": 1000,
"framePosition": 500,
"progress": 0.5,
"timestamp": "00:05:23.456",
"filename": "data.csv"
}
This section documents a representative subset; the live Console module registers 17 commands. Run
api.getCommandsfor the full list.
Console/terminal control:
console.setEchoEnable or disable echo mode.
Parameters:
enabled (bool): true to enable, false to disableconsole.setShowTimestampShow or hide timestamps.
Parameters:
enabled (bool): true to show, false to hideconsole.setDisplayModeSet display mode.
Parameters:
modeIndex (int): 0=PlainText, 1=Hexadecimalconsole.setDataModeSet data mode.
Parameters:
modeIndex (int): 0=UTF8, 1=Hexadecimalconsole.setLineEndingSet line ending mode.
Parameters:
endingIndex (int): 0=None, 1=LF, 2=CR, 3=CRLFconsole.setFontFamilySet console font.
Parameters:
fontFamily (string): Font nameconsole.setFontSizeSet console font size.
Parameters:
fontSize (int): Font size in pointsconsole.setChecksumMethodSet checksum method.
Parameters:
methodIndex (int): Checksum method indexconsole.clearClear console output.
Parameters: None
console.sendSend data to device.
Parameters:
data (string): Data to sendconsole.getConfigGet console configuration.
Parameters: None
Returns:
{
"echo": false,
"showTimestamp": true,
"displayMode": 0,
"dataMode": 0,
"lineEnding": 1,
"fontFamily": "Courier New",
"fontSize": 10,
"checksumMethod": 0
}
This section documents a representative subset; the live Dashboard Configuration module registers 13 commands. Run
api.getCommandsfor the full list.
Dashboard settings and visualization control:
dashboard.getStatusGet all dashboard configuration settings.
Parameters: None
Returns:
{
"operationMode": 0,
"operationModeName": "ProjectFile",
"fps": 60,
"timeRange": 10.0,
"widgetCount": 3,
"datasetCount": 12,
"running": false
}
Example:
python test_api.py send dashboard.getStatus
dashboard.setOperationModeSet the dashboard operation mode.
Parameters:
mode (int): 0=ProjectFile, 1=ConsoleOnly, 2=QuickPlotReturns:
{
"mode": 0,
"modeName": "ProjectFile"
}
Example:
python test_api.py send dashboard.setOperationMode -p mode=1
Operation Modes:
0 - ProjectFile: Use a JSON project file to define dashboard layout1 - ConsoleOnly: No parsing. Raw bytes flow only to the terminal, no dashboard.2 - QuickPlot: Automatic plotting of incoming numeric datadashboard.getOperationModeGet the current dashboard operation mode.
Parameters: None
Returns:
{
"mode": 0,
"modeName": "ProjectFile"
}
Example:
python test_api.py send dashboard.getOperationMode
dashboard.setFpsSet the visualization refresh rate.
Parameters:
fps (int): Refresh rate in frames per second (1-240 Hz)Returns:
{
"fps": 60
}
Example:
python test_api.py send dashboard.setFps -p fps=60
Notes:
dashboard.getFpsGet the current visualization refresh rate.
Parameters: None
Returns:
{
"fps": 60
}
Example:
python test_api.py send dashboard.getFps
dashboard.setTimeRangeSet the visible plot time window, in seconds.
Parameters:
seconds (double): Visible plot time window in seconds (0.001-300)Returns:
{
"seconds": 10.0
}
Example:
python test_api.py send dashboard.setTimeRange -p seconds=10
Notes:
project.dashboard.setTimeRange (per-project; survives project reload)dashboard.getTimeRangeGet the current visible plot time window, in seconds.
Parameters: None
Returns:
{
"seconds": 10.0
}
Example:
python test_api.py send dashboard.getTimeRange
This section documents 61 of the 64 commands the live Project Management module registers.
project.new,project.open, andproject.saveare covered above in prose rather than as separate entries. Runapi.getCommandsfor the full list.
Project file and configuration management:
Project file I/O is exposed as runtime API commands:
project.new,project.open, andproject.save(with an optionalfilePathfor a headless save-as), plusproject.loadJsonto replace the in-memory model. The GUI is one of several entry points. Programmatic project authoring can also useproject.exportJson(read) plus theproject.group.*,project.dataset.*, andproject.action.*mutators documented below (write) on the in-memory model.project.workspace.*is a separate registered command family (seeWorkspacesHandler.cpp); see Workspace Commands below.
project.getStatusGet project info.
Returns:
{
"title": "My Project",
"filePath": "/path/to/project.json",
"modified": false,
"groupCount": 3,
"datasetCount": 12
}
project.snapshotComposite read of the active project: title, sources, groups + datasets,
workspaces summary, and data-tables summary, in one round trip. Prefer this
over chaining project.group.list / project.dataset.list / etc. Every
level (top, source, group, dataset) carries an _explanations object with
prose translations of enum/bitflag fields (operationMode, busType,
frameDetection, frameParserLanguage, enabledOptions, transformLanguage)
and a one-line summary of the project's operating shape.
Parameters:
verbose (bool, optional): Include frame parser source and source-level
frame settings (frameStart, frameEnd, checksumAlgorithm,
frameDetection, frameParserCode) for each source.sections (array of string, optional): Which top-level blocks to include:
any of "sources", "groups", "workspaces", "dataTables" (default:
all). groupCount/datasetCount always reflect the whole project even
when a section is omitted.offset (int, optional): Skip this many groups before returning results
(default 0); pass the previous reply's nextOffset to page through a large
project's groups array.limit (int, optional): Max groups to return (default 0 = all).Returns:
{
"snapshot": {
"title": "My Project",
"pointCount": 512,
"filePath": "/path/to/project.json",
"modified": false,
"sources": [
{
"sourceId": 0,
"title": "Serial Source",
"busType": 0,
"frameParserLanguage": 0,
"frameParserSize": 256,
"_explanations": {}
}
],
"groups": [
{
"groupId": 0,
"title": "Sensors",
"widget": "",
"datasetCount": 1,
"datasets": [ { "...": "same shape as project.dataset.list entries" } ],
"_explanations": {
"widget": "No group widget (datasets render independently)"
}
}
],
"groupCount": 1,
"datasetCount": 1,
"workspaces": [
{ "workspaceId": 0, "title": "Default", "widgetCount": 4 }
],
"dataTables": [
{
"title": "Calibration",
"path": "Calibration",
"registerCount": 6,
"constantCount": 2,
"computedCount": 1
}
],
"operationMode": 1,
"_explanations": {
"operationMode": "Project File (full dashboard with parser)",
"summary": "Project File mode: 1 source(s), 1 group(s), 1 dataset(s), 1 workspace(s), 1 data table(s). Frame parser code is authoritative."
}
},
"hint": "Pass verbose=true to include frame parser source and source-level frame settings. For per-table register details, call project.dataTable.get with the table name.",
"projectEpoch": 9
}
When groups is windowed by offset/limit and more groups remain,
snapshot.nextOffset carries the offset for the next page.
project.loadJsonReplace the current project with a JSON object, in memory only (no file association). The JSON must match the .ssproj schema (top-level title, frameStart, frameEnd, frameDetection, decoder, frameParser, groups, actions, ...). Prefer project.template.apply for canned starters.
Parameters:
config (object): Full project JSON documentdryRun (bool, optional): If true, return a wouldDiscard/wouldApply summary instead of loadingReturns:
{
"loaded": true,
"title": "My Project",
"groupCount": 3,
"datasetCount": 12
}
With dryRun: true, no project is loaded and the response summarizes both sides of the replacement instead:
{
"dryRun": true,
"wouldDiscard": {
"title": "Old Project",
"groupCount": 3,
"datasetCount": 12,
"sourceCount": 1,
"filePath": "/path/to/old.ssproj",
"groupTitles": ["Sensors"]
},
"wouldApply": {
"title": "New Project",
"groupCount": 1,
"datasetCount": 4,
"groupTitles": ["Telemetry"],
"sourceCount": 1
},
"warning": "DRY RUN: no project was loaded. wouldDiscard shows what would be lost; wouldApply shows what would replace it. Confirm before re-issuing without dryRun:true."
}
Errors:
MISSING_PARAM: Missing config parameterINVALID_PARAM: config is emptyEXECUTION_ERROR: Project failed to load from JSON (validation error)project.exportJsonExport the current in-memory project as JSON, in the same schema used by project files on disk.
Parameters: None
Returns:
{
"config": {
"title": "My Project",
"schemaVersion": "1.0.0",
"groups": [],
"actions": [],
"sources": []
}
}
project.setTitleRename the project (in-app title only). Does not move or rename the .ssproj file on disk; auto-save keeps writing to the existing file path. To save to a different file, use project.save with a filePath.
Parameters:
title (string): Project titleReturns:
{
"title": "My Project"
}
Errors:
MISSING_PARAM: Missing title parameterINVALID_PARAM: title is emptyproject.activateLoad the current in-memory project into FrameBuilder, making it the live frame-parsing configuration. Fails if the project has no groups, or no datasets and no image/painter group.
Parameters: None
Returns:
{
"loaded": true,
"source": "API",
"title": "My Project",
"groupCount": 3,
"datasetCount": 12
}
project.validateWalk the loaded project and report inconsistencies: missing source references, frame parser
compile errors, empty groups, duplicate dataset indexes, and similar issues. Call before
project.save when building a project programmatically.
Parameters: None
Returns:
{
"ok": true,
"issues": [
{
"level": "warning",
"location": "group[0]",
"message": "Group has no datasets; nothing to display"
}
],
"groupCount": 3,
"sourceCount": 1,
"actionCount": 0,
"issueCount": 1
}
level is one of info, warning, or error. ok is false when any error-level issue
is present.
project.template.listList the built-in starter project templates available to project.template.apply.
Parameters: None
Returns:
{
"templates": [
{
"id": "blank",
"title": "Blank project",
"description": "Empty project with one default UART source. Useful as a starting point when the user wants to build from scratch.",
"file": "blank.json"
},
{
"id": "imu_uart",
"title": "IMU over UART",
"description": "Single UART source with a 9-DOF IMU group: accelerometer (X/Y/Z), gyroscope (X/Y/Z), magnetometer (X/Y/Z). Comma-separated frame parser. Plots + accelerometer 3-axis widget.",
"file": "imu_uart.json"
}
]
}
Six templates are registered: blank, imu_uart, gps_uart_nmea, scope_multichannel_uart,
telemetry_udp, and mqtt_subscriber.
project.template.applyReplace the current project with a built-in starter template. Discards any unsaved state in the current project; auto-save writes the new template to disk shortly after applying.
Parameters:
templateId (string): Template id from project.template.list (blank, imu_uart,
gps_uart_nmea, scope_multichannel_uart, telemetry_udp, mqtt_subscriber)dryRun (bool, optional): If true, summarize the template without applying itReturns:
{
"templateId": "imu_uart",
"title": "IMU over UART",
"groupCount": 1,
"datasetCount": 9
}
With dryRun: true, no project is replaced; the response summarizes both sides of the swap
instead:
{
"dryRun": true,
"templateId": "imu_uart",
"wouldDiscard": {
"title": "My Project",
"groupCount": 3,
"datasetCount": 12,
"sourceCount": 1,
"filePath": "/path/to/project.json",
"groupTitles": ["Sensors", "GPS", "Status"]
},
"wouldApply": {
"title": "IMU over UART",
"groupCount": 1,
"datasetCount": 9,
"groupTitles": ["IMU"],
"sourceCount": 1
},
"warning": "DRY RUN: template not applied. wouldDiscard shows what would be lost; wouldApply shows what would replace it. Confirm before re-issuing without dryRun:true."
}
Note: The mqtt_subscriber template configures an MQTT subscriber-mode project. Applying
it works in any build, but the resulting project only receives data once an MQTT connection is
configured, which is a Pro feature.
project.group.addAdd new group.
Parameters:
title (string): Group titlewidgetType (int): Widget type (0-8): 0=DataGrid, 1=Accelerometer,
2=Gyroscope, 3=GPS, 4=MultiPlot, 5=NoGroupWidget, 6=Plot3D, 7=ImageView,
8=Painterproject.group.deleteDelete a group by id.
Parameters:
groupId (int, required): Group id to deletedryRun (bool, optional): If true, return the affected entities without
committing. Auto-runs without an approval card.Returns:
{
"deleted": {
"groupId": 0,
"title": "Sensors",
"widget": "MultiPlot",
"datasetCount": 1,
"datasets": [
{"datasetId": 0, "uniqueId": 1024, "title": "Temperature"}
]
},
"renumbered": [
{"oldGroupId": 1, "newGroupId": 0, "title": "GPS", "datasetCount": 3}
],
"backupPath": "/path/to/backup.ssbak",
"warnings": [
"groupId values shifted after deletion; uniqueIds of every dataset in renumbered groups are now stale -- re-read project state before further mutations.",
"Pre-mutation snapshot saved at backupPath; pass it to assistant.restore to undo."
],
"projectEpoch": 9
}
With dryRun: true, the response sets "dryRun": true, omits backupPath,
and nothing is written.
project.group.duplicateDuplicate a group by id.
Parameters:
groupId (int, required): Group id to duplicateReturns:
{
"groupId": 0,
"duplicated": true
}
project.group.updatePatch any subset of group fields by id.
Parameters:
groupId (int, required): Target group idtitle (string, optional)widget (string, optional): Group widget identifier (e.g. "MultiPlot")columns (int, optional)sourceId (int, optional)painterCode (string, optional): Same as project.painter.setCode, addressable
through the generic patch endpointUnknown fields are accepted but ignored, and surfaced in result.warnings[].fields
with code unknown_field.
Returns:
{
"groupId": 0,
"updated": true
}
project.group.moveReorder a group within the project. This renumbers groupId for the moved
group and every group it crosses. uniqueId is a persistent id assigned at
creation and is unaffected by the renumbering, so workspace refs and any
script that pinned a uniqueId keep working unchanged. Pass dryRun: true
to preview the renumbering without committing.
Parameters:
groupId (int): Group id to move.newPosition (int): New 0-based position; clamped to the valid range.dryRun (bool, optional): If true, return the affected entities without
committing. Auto-runs without an approval card.expectedProjectEpoch (int, optional): Same stale-project check as
project.dataset.move.Returns:
{
"oldPosition": 0,
"newPosition": 1,
"moved": true,
"renumbered": [
{
"oldGroupId": 1,
"newGroupId": 0,
"title": "GPS",
"datasetCount": 3
}
],
"warning": "Group reorder renumbers groupId. Dataset uniqueIds and Group.uniqueId stay stable, so workspace refs and xAxisId references survive untouched.",
"projectEpoch": 9
}
With dryRun: true, the response additionally sets "dryRun": true and
"moved": false, and nothing is written.
Errors:
MISSING_PARAM: Missing groupId or newPosition parameterINVALID_PARAM: groupId is out of rangeproject.group.listList all groups.
Returns:
{
"groups": [
{
"groupId": 0,
"title": "Sensors",
"widget": "MultiPlot",
"datasetCount": 5
}
],
"groupCount": 1
}
project.dataset.addAdd a dataset to a group.
Parameters:
groupId (int, required): Group to attach the dataset tooptions (int, required): Dataset options (bit flags 0-127): 1=Plot, 2=FFT, 4=Bar,
8=Gauge, 16=Compass, 32=LED, 64=WaterfallReturns:
{
"groupId": 0,
"options": 9
}
project.dataset.addManyBulk-create multiple datasets on a group in one call (1-1024).
Parameters:
groupId (int): Group to attach the datasets tocount (int): How many datasets to create (1-1024)options (int): Visualization bit flags applied to every created dataset (same bits as project.dataset.add): 1=Plot, 2=FFT, 4=Bar, 8=Gauge, 16=Compass, 32=LED, 64=WaterfalltitlePattern (string, optional): Title template, e.g. "LED {n}". {n} is replaced with startNumber + i, {i} with the zero-based index. Omit to keep the auto-generated title.startNumber (int, optional): First {n} value. Default 1.startIndex (int, optional): First parser-slot index to assign. Default -1 (auto-assign the next free slot); 0 leaves the index unset; 1+ assigns consecutive slots starting there.Returns:
{
"groupId": 0,
"count": 3,
"created": [
{"groupId": 0, "datasetId": 4, "title": "LED 1", "index": 5, "uniqueId": "..."},
{"groupId": 0, "datasetId": 5, "title": "LED 2", "index": 6, "uniqueId": "..."},
{"groupId": 0, "datasetId": 6, "title": "LED 3", "index": 7, "uniqueId": "..."}
]
}
project.dataset.deleteDelete a dataset by id.
Parameters:
groupId (int, required): Owning group iddatasetId (int, required): Dataset id within the groupdryRun (bool, optional): If true, return the affected entities without
committing. Auto-runs without an approval card.Returns:
{
"deleted": {
"groupId": 0,
"groupTitle": "Sensors",
"datasetId": 0,
"uniqueId": 1024,
"title": "Temperature",
"units": "°C"
},
"renumbered": [
{"groupId": 0, "oldDatasetId": 1, "newDatasetId": 0, "uniqueId": 1025, "title": "Humidity"}
],
"backupPath": "/path/to/backup.ssbak",
"warnings": [
"datasetId values in groupId=0 were renumbered; cached uniqueIds for the affected datasets are now stale -- re-read project state before further mutations.",
"Pre-mutation snapshot saved at backupPath; pass it to assistant.restore to undo."
],
"projectEpoch": 9
}
With dryRun: true, the response sets "dryRun": true, omits backupPath,
and nothing is written.
project.dataset.duplicateDuplicate a dataset by id.
Parameters:
groupId (int, required): Owning group iddatasetId (int, required): Dataset id within the groupReturns:
{
"groupId": 0,
"datasetId": 0,
"duplicated": true
}
project.dataset.updatePatch any subset of dataset fields by group id and dataset id.
Parameters:
groupId (int, required): Target group iddatasetId (int, required): Target dataset idtitle, units, widget (string, optional): widget is the widget identifier
string (e.g. "bar", "gauge")graph, fft, led, waterfall (bool, optional): Toggle the same flags as
project.dataset.setOption; use here when patching multiple fields at onceindex (int, optional): 1-based parser-output slot (0 = unassigned); must be >= 0sourceId (int, optional)xAxisId (int, optional), waterfallYAxis (int, optional)fftSamples (int, optional), fftSamplingRate (int, optional)fftWindow (int, optional): FFT window function, 0-14 (5 = Blackman-Harris,
the default); see the dataset fields table in Widget ReferencefftMin, fftMax, pltMin, pltMax, wgtMin, wgtMax, ledHigh (number, optional)alarmBands (array, optional): Array of {min, max, severity, color, label, blink}
objects, severity 0-3 for Info/Ok/Warning/Critical. Replaces the dataset's entire
band list.alarmLow, alarmHigh, alarmEnabled (optional): Legacy 2-band simple mode, used
only when alarmBands is not presentdisplayTickCount (int, optional, clamped to >= 0)displayFormat (string, optional)decimalPoints (int, optional, clamped to -1..15)log (bool, optional)overviewDisplay (bool, optional), hideOnDashboard (bool, optional)transformCode (string, optional): Per-dataset value transform sourcetransformLanguage (int, optional): -1=inherit from source, 0=JavaScript, 1=Luavirtual (bool, optional): Set true when the value comes from transformCode
rather than a parser-output slot; without it a dataset with no slot reads
channels[index-1] and stays emptyAny applied field forces a tree rebuild, so the epoch-gated dashboard apply and editor
reload fire. Unknown fields are accepted but ignored, and surfaced in
result.warnings[].fields with code unknown_field.
Returns:
{
"groupId": 0,
"datasetId": 0,
"updated": true,
"projectEpoch": 12
}
May also include warnings (unknown fields, stale-epoch), warning (transform
language/code mismatch), and hint (when transformCode is set but virtual is
false and index <= 0).
project.dataset.moveReorder a dataset within its group. This renumbers datasetId for the moved
dataset and every dataset it crosses. uniqueId is a persistent id assigned
at creation and is unaffected by the renumbering, so workspace refs and any
script that pinned a uniqueId keep working unchanged. Pass dryRun: true
to preview the renumbering without committing.
Parameters:
uniqueId (int): uniqueId of the dataset to move.newPosition (int): New 0-based position within the group; clamped to the
valid range.dryRun (bool, optional): If true, return the affected entities without
committing. Auto-runs without an approval card.expectedProjectEpoch (int, optional): The projectEpoch this call assumes
as current (from a prior project.snapshot/list read). When it no longer
matches, the response carries a stale_project warning instead of failing
the call.Returns:
{
"uniqueId": 1024,
"groupId": 0,
"oldPosition": 0,
"newPosition": 2,
"moved": true,
"renumbered": [
{
"groupId": 0,
"oldDatasetId": 0,
"newDatasetId": 2,
"uniqueId": 1024,
"title": "Temperature"
}
],
"warning": "Dataset reorder renumbers datasetId within the group. uniqueId stays stable across reorders, so workspace refs and xAxisId references survive untouched.",
"projectEpoch": 8
}
With dryRun: true, the response additionally sets "dryRun": true and
"moved": false, and nothing is written.
Errors:
MISSING_PARAM: Missing uniqueId or newPosition parameterINVALID_PARAM: No dataset has this uniqueIdproject.dataset.setOptionToggle a single dataset option flag.
Deprecated. Prefer
project.dataset.setOptions, which takes the full bitfield in one call and removes the singular/plural ambiguity. Kept for backward compatibility with existing scripts.
Parameters:
groupId (int, required): Owning group iddatasetId (int, required): Dataset id within the groupoption (string or int, required): Preferred form is a slug ("plot",
"fft", "bar", "gauge", "compass", "led", "waterfall"). An
integer DatasetOption bitflag is also accepted (1, 2, 4, 8, 16, 32, 64).enabled (bool, required): Enable or disableReturns:
{
"groupId": 0,
"datasetId": 2,
"option": 8,
"optionSlug": "gauge",
"enabled": true
}
project.dataset.setOptionsApply several DatasetOption flags at once (plural form of project.dataset.setOption).
Parameters:
groupId (int): Owning group iddatasetId (int): Dataset id within the groupoptions (array or int): Preferred form is an array of slugs, e.g. ["plot", "fft", "waterfall"]. An integer bitfield is also accepted: 1=Plot, 2=FFT, 4=Bar, 8=Gauge, 16=Compass, 32=LED, 64=Waterfall (Pro).Any flag not present in options is disabled on the dataset. Bar/Gauge/Compass are mutually exclusive; if more than one is set, the highest bit wins. Updates the group's compatibleWidgetTypes immediately. Note: these are DatasetOption bitflags, not DashboardWidget enum values; the numbers do not line up with project.workspace.addWidget's widgetType.
Returns:
{
"groupId": 0,
"datasetId": 2,
"options": 9,
"optionsSlugs": ["plot", "gauge"]
}
project.dataset.setVirtualToggle the virtual flag on a dataset.
Parameters:
groupId (int): Owning group iddatasetId (int): Dataset id within the groupvirtual (bool): Mark the dataset as virtual (computed by a transform, no slot in the parser output array)Returns:
{
"groupId": 0,
"datasetId": 2,
"virtual": true,
"updated": true
}
project.dataset.setTransformCodeSet a dataset's per-value transform code.
Parameters:
groupId (int): Owning group iddatasetId (int): Dataset id within the groupcode (string): Transform source (Lua or JS, must match language). Pass an empty string to clear it.language (int, optional): 0=JavaScript, 1=Lua. If omitted, the dataset inherits the source's frameParserLanguage (Built-In sources inherit Lua).If this dataset is compute-only (no slot in the parser output array), also set virtual: true via project.dataset.setVirtual or project.dataset.update -- otherwise the dataset reads empty channel data. A transform may read RAW values from any dataset but only FINAL values of datasets earlier in project.dataset.getExecutionOrder.
Returns:
{
"groupId": 0,
"datasetId": 2,
"codeLength": 42,
"language": 1,
"updated": true
}
When language was omitted, the response also includes languageInherited: true and an inheritNotice string. When the code's syntax looks like it does not match language, the response includes a warning string. When code is non-empty but the dataset is not virtual and has no parser slot (index <= 0), the response includes a hint string.
project.dataset.transform.dryRunCompile and run a value-transform script against sample values, without touching the live project or any dataset.
Parameters:
code (string): Transform source. Must define transform(value).language (int): 0=JavaScript, 1=Luavalues (array of number|string): Sample values to pass through transform(). Each entry may be a number or a string.Returns:
{
"ok": true,
"outputs": [50.6, "on", null],
"hint": "outputs[i] is the result of transform(values[i]). null means transform returned a non-finite value -- the live runtime falls back to the raw value in that case."
}
Returns an error response (not ok:false) if the script fails to compile or does not define transform(value).
project.dataset.getAlarmBandsGet a dataset's coloured alarm bands.
Parameters:
groupId (int): Owning group iddatasetId (int): Dataset id within the groupReturns:
{
"groupId": 0,
"datasetId": 2,
"alarmBands": [
{"min": 80, "max": 100, "severity": 3, "color": "#ff0000", "label": "Overheat", "blink": true}
],
"count": 1,
"rangeMin": 0,
"rangeMax": 100
}
rangeMin/rangeMax are the dataset's wgtMin/wgtMax, returned so callers can validate band ranges before writing them back. An empty project (no bands configured) returns an empty alarmBands array. Applies to bar/gauge/meter widgets and LED-panel datasets; calling it on other widget types succeeds with an empty array.
project.dataset.setAlarmBandsAtomically replace a dataset's alarm-band array.
Parameters:
groupId (int): Owning group iddatasetId (int): Dataset id within the groupalarmBands (array): Full replacement list. Each entry:
min (number): Lower bound (inclusive)max (number): Upper bound (exclusive at top of range); entries with max <= min are droppedseverity (int): 0=Info, 1=OK, 2=Warning, 3=Criticalcolor (string, optional): "#rrggbb" override; empty/omitted uses the severity's theme colourlabel (string, optional): Band name, shown in band-edge notificationsblink (bool, optional): LED panels flash the LED while the band is activeBands may have gaps and may overlap; rendering paints them in array order behind the value indicator. Severity of Warning or higher triggers a notification when the value enters the band (3-second per-dataset cooldown suppresses repeat spam). Pass an empty array to clear all alarms.
Returns:
{
"groupId": 0,
"datasetId": 2,
"count": 1,
"updated": true
}
If any entries were dropped for max <= min, the response also includes droppedInvalid with the number of dropped entries.
project.dataset.getByPathResolve a dataset by a "Group/Dataset" or "Source/Group/Dataset" title path
(segments are titles, separated by /). Preferred over getByTitle for
human-readable addressing, since a path survives renumbering of positional
ids (groupId/datasetId).
Parameters:
path (string): 'Group/Dataset' or 'Source/Group/Dataset'.Returns: Same object shape as an entry of project.dataset.list, plus
groupId, groupTitle, sourceId, uniqueId, enabledFeatures,
enabledOptions/enabledOptionsSlugs,
enabledWidgetTypes/enabledWidgetTypesSlugs, hasTransform, isVirtual,
and an _explanations object.
{
"groupId": 0,
"groupTitle": "Sensors",
"sourceId": 0,
"datasetId": 1,
"uniqueId": 1024,
"index": 1,
"title": "Temperature",
"value": "25.3",
"units": "°C",
"widget": "bar",
"enabledFeatures": "plot, bar",
"enabledOptions": 3,
"enabledOptionsSlugs": ["plot", "bar"],
"hasTransform": false,
"isVirtual": false
}
Errors:
MISSING_PARAM: Missing path parameterINVALID_PARAM: path is not 2 or 3 /-separated segments, the source
segment (3-part path) doesn't match a source title, or no dataset matchesproject.dataset.getByTitleResolve a dataset by exact title match. Pass sourceId and/or groupId to
disambiguate when the same title is reused across groups or sources.
Parameters:
title (string): Dataset title (exact match).sourceId (int, optional): Restrict the search to this source.groupId (int, optional): Restrict the search to this group.Returns: Same object shape as project.dataset.getByPath above.
Errors:
MISSING_PARAM: Missing title parameterINVALID_PARAM: title is empty, no dataset matches, or the title is
ambiguous (multiple matches); an ambiguous match returns errorData with
a matches array (the full dataset objects) and a hint to narrow the
search with sourceId/groupId or switch to getByPathproject.dataset.getByUniqueIdResolve a dataset by its persisted uniqueId.
Parameters:
uniqueId (int): Opaque persisted handle allocated at dataset creation;
stable across reorders. Read it from project.dataset.list or
project.snapshot responses; never compute it.Returns: Same object shape as project.dataset.getByPath above.
Errors:
MISSING_PARAM: Missing uniqueId parameterINVALID_PARAM: No dataset has this uniqueIdproject.dataset.getExecutionOrderReturn the order datasets execute in during transform processing (the same
order FrameBuilder traverses groups/datasets). A transform can read the raw
value of any dataset, but only reads the final (post-transform) value of
datasets that appear earlier in this list.
Parameters: None
Returns:
{
"order": [
{
"uniqueId": 1024,
"title": "Temperature",
"sourceId": 0,
"groupId": 0,
"datasetId": 1,
"hasTransform": false,
"isVirtual": false,
"transformLanguage": 0
}
],
"count": 1,
"_explanations": {
"summary": "Datasets execute in (group-array, dataset-array) order. A transform may read raw values of ALL datasets via datasetGetRaw(uid), but only final values of datasets EARLIER in this list via datasetGetFinal(uid)."
},
"projectEpoch": 7
}
project.dataset.listList all datasets.
Returns:
{
"datasets": [
{
"groupId": 0,
"groupTitle": "Sensors",
"index": 0,
"title": "Temperature",
"units": "°C",
"widget": "bar",
"value": "25.3"
}
],
"datasetCount": 5
}
project.action.addAdd new action.
Parameters: None
project.action.deleteDelete an action by id.
Parameters:
actionId (int, required): Action id to deleteReturns:
{
"actionId": 0,
"deleted": true
}
project.action.duplicateDuplicate an action by id.
Parameters:
actionId (int, required): Action id to duplicateReturns:
{
"actionId": 0,
"duplicated": true
}
project.action.updatePatch action fields by id.
Parameters:
actionId (int, required): Target action idtitle (string, optional)icon (string, optional)txData (string, optional): Data to transmiteolSequence (string, optional): End-of-line sequence appended to txDatatimerMode (int, optional): Timer mode enumtimerIntervalMs (int, optional)repeatCount (int, optional)sourceId (int, optional)txEncoding (int, optional)binaryData (bool, optional)autoExecuteOnConnect (bool, optional)Unknown fields are accepted but ignored, and surfaced in result.warnings[].fields
with code unknown_field.
Returns:
{
"actionId": 0,
"updated": true
}
project.action.listList all actions.
Returns:
{
"actions": [],
"actionCount": 0
}
project.outputWidget.addAdd an output widget to a group. Output widgets attach to a group's output panel; they are a Pro dashboard feature, but this command runs in GPL builds too (the widget is stored in the project file) and only fails to render as an interactive control there.
Parameters:
groupId (int): Group id to add the widget totype (int): OutputWidgetType enum: 0=Button, 1=Slider, 2=Toggle,
3=TextField, 4=KnobReturns:
{
"groupId": 0,
"type": 0,
"added": true
}
project.outputWidget.deleteDelete an output widget by id.
Parameters:
groupId (int): Owning group idwidgetId (int): Widget id within the groupReturns:
{
"groupId": 0,
"widgetId": 0,
"deleted": true
}
project.outputWidget.duplicateDuplicate an output widget by id.
Parameters:
groupId (int): Owning group idwidgetId (int): Widget id within the groupReturns:
{
"groupId": 0,
"widgetId": 0,
"duplicated": true
}
project.outputWidget.getRead the configuration of an output widget by id. Use this before rewriting
transmitFunction so you preserve the widget's current ranges and labels.
Parameters:
groupId (int): Owning group idwidgetId (int): Widget id within the groupReturns:
{
"groupId": 0,
"widgetId": 0,
"type": 2,
"title": "Relay",
"icon": "toggle-power",
"monoIcon": false,
"minValue": 0,
"maxValue": 100,
"stepSize": 1,
"initialValue": 0,
"sourceId": 0,
"txEncoding": 0,
"transmitFunction": "function transmit(value) { return value ? [0x01] : [0x00]; }"
}
project.outputWidget.updatePatch any subset of output-widget fields by id.
Parameters:
groupId (int): Target group idwidgetId (int): Target widget index within the grouptitle (string, optional): Widget titleicon (string, optional): Icon identifiertransmitFunction (string, optional): JavaScript source defining
transmit(value), converting UI state into device bytes. Validate first
with project.outputWidget.dryRun.sourceId (int, optional): Target source/device idtxEncoding (int, optional): Text encoding for string payloads
(SerialStudio::TextEncoding enum, 0=UTF-8)monoIcon (bool, optional): Use monochrome icon stylingminValue (double, optional): Minimum allowed valuemaxValue (double, optional): Maximum allowed valuestepSize (double, optional): Value increment stepinitialValue (double, optional): Initial widget valueReturns:
{
"groupId": 0,
"widgetId": 0,
"updated": true
}
Unrecognized parameter names are ignored and reported back as a warnings
array (code: "unknown_field") rather than failing the command.
project.outputWidget.dryRunCompile an output-widget transmit function without touching the live
project. Verifies the script compiles and defines transmit(value).
transmitFunction is JavaScript only and runs with the same table API (plus,
in Pro builds, the injected Modbus/CAN helper globals) as the live widget.
Validate here before project.outputWidget.update.
Parameters:
code (string): Transmit source. Must define transmit(value)inputValue (string, optional): Sample value to run transmit() againsthex (bool, optional): Treat inputValue as space-separated hex bytes.
Default false.Returns (compile error):
{
"ok": false,
"compileError": "SyntaxError: Expected token `}`",
"line": 3
}
Returns (compiled, no sample run):
{
"ok": true,
"hasTransmit": true,
"hint": "Compiled and transmit(value) is defined. Pass inputValue (and hex:true for hex byte input) to also execute it and see the produced bytes."
}
Returns (with inputValue):
{
"ok": true,
"hasTransmit": true,
"sampleRun": {
"ok": true,
"byteCount": 4,
"outputHex": "01 02 03 04"
}
}
A failing sample run reports sampleRun.ok: false with runtimeError and
line instead of outputHex/byteCount.
project.painter.getCodeGet the Painter widget JavaScript for a group.
Parameters:
groupId (int, required): Target group idReturns:
{
"groupId": 0,
"code": "function paint(ctx, w, h) { ... }"
}
project.painter.setCodeSet the Painter widget code for a group. JavaScript only — Painter scripts run in
QJSEngine, not Lua. The entry point is paint(ctx, w, h), with globals ctx (2D
canvas context, QPainter-like), w, h (canvas dimensions),
datasetGetFinal(uid)/datasetGetRaw(uid), and an optional zero-arg onFrame()
callback. Validate with project.painter.dryRun before setting. The Painter widget
itself requires a Pro license, but this command is not license-gated.
Parameters:
groupId (int, required): Target group id (from project.group.list)code (string, required): Painter widget JS source. Replaces any existing code
for the group.Returns:
{
"groupId": 0,
"codeLength": 256
}
project.painter.dryRunCompile a painter program without touching the live project or rendering to a canvas.
Parameters:
code (string): Painter source. Must define paint(ctx, w, h).Returns:
{
"ok": true,
"hasPaint": true,
"hasOnFrame": false,
"hint": "Compile + paint() lookup succeeded. Note: dry-run does NOT actually render -- runtime errors inside paint() (out-of-bounds reads, missing moveTo before arc, etc.) only surface when the live widget mounts."
}
On failure, ok is false and the response carries compileError (and line when the engine reports one) instead of hasPaint/hasOnFrame/hint -- either because the script failed to compile, or because it compiled but did not define paint(ctx, w, h).
project.frameParser.setCodeSet frame parser code.
Parameters:
code (string): Frame parser source codelanguage (int, optional): 0=JavaScript, 1=Lua, 2=Built-In. Pass it to lock in the runtime engine; a mismatch silently fails to compile.project.frameParser.getCodeGet frame parser code.
Returns:
{
"sourceId": 0,
"language": 0,
"code": "function parse(frame) { ... }",
"codeLength": 256
}
Built-In (language: 2) sources also return template and params, and code carries the JSON descriptor.
project.frameParser.getConfigGet the project-wide frame parser configuration (delimiters, checksum, operation mode, frame detection).
Parameters: None
Returns:
{
"startSequence": "/*",
"endSequence": "*/",
"startSequences": ["/*"],
"endSequences": ["*/"],
"checksumAlgorithm": "CRC-16",
"operationMode": 0,
"frameDetection": 0
}
startSequence/endSequence are the primary (first) delimiters; startSequences/endSequences list every configured delimiter. operationMode is 0=ProjectFile, 1=DeviceSendsJSON (Console Only), 2=QuickPlot. frameDetection is 0-3 (End Delimiter Only, Start and End Delimiter, No Delimiters, Start Delimiter Only).
project.frameParser.updateConfigure frame parser delimiters, checksum, frame detection, and operation mode.
Parameters:
sourceId (int, optional): Source index (default 0)startSequence (string, optional): Frame start delimiterendSequence (string, optional): Frame end delimiterchecksumAlgorithm (string, optional): Checksum algorithm nameframeDetection (int, optional): Frame detection mode (0-3)operationMode (int, optional): Operation mode (0-2)Returns:
{
"updated": true,
"sourceId": 0
}
operationMode is always applied project-wide (via AppState), regardless of sourceId. For sourceId: 0, startSequence/endSequence/checksumAlgorithm/frameDetection are the global frame parser settings and take effect immediately, resetting the frame reader. For a non-zero sourceId, those same fields configure a per-source override stored on that source instead of the global settings. Out-of-range frameDetection/operationMode values are ignored rather than rejected.
project.frameParser.getLanguageGet the script language used by the frame parser for a given source.
Parameters:
sourceId (int, optional): Source identifier (default 0)Returns:
{
"sourceId": 0,
"language": 0
}
language is 0=JavaScript, 1=Lua, 2=Built-In. Fails with an InvalidParam error if sourceId does not match an existing source.
project.frameParser.setLanguageSwitch a source between JavaScript, Lua, and Built-In frame parsers.
Parameters:
language (int): Script language: 0=JavaScript, 1=Lua, 2=Built-InsourceId (int, optional): Source identifier (default 0)Returns:
{
"sourceId": 0,
"language": 1
}
Switching to JavaScript or Lua wipes the source's existing frame parser code: the default template for the new language replaces it. To preserve the source, read it first with project.frameParser.getCode, switch, then write the translated code back with project.frameParser.setCode. Switching to Built-In (2) keeps any previously-saved template/params for that source (or seeds the default delimited template if none exists) and leaves the source's JS/Lua code intact for later round-trips. Fails with an InvalidParam error for an unknown sourceId or an out-of-range language.
project.frameParser.listTemplatesList the available Built-In (C++) frame parser templates.
Parameters: None
Returns:
{
"templates": [
{
"id": "delimited",
"name": "Delimited",
"description": "Splits a frame on a separator character."
}
],
"count": 1
}
Built-In templates parse without user code. Inspect a template's parameters with project.frameParser.getTemplateSchema before applying it with project.frameParser.setTemplate.
project.frameParser.getTemplateGet the Built-In frame parser configuration for a source.
Parameters:
sourceId (int, optional): Source index (default 0)Returns:
{
"sourceId": 0,
"language": 2,
"template": "delimited",
"params": {}
}
template is empty and params is empty when the source has never used the Built-In language. Fails with an InvalidParam error if sourceId does not match an existing source.
project.frameParser.getTemplateSchemaGet the parameter schema of a Built-In frame parser template.
Parameters:
template (string): Template id from project.frameParser.listTemplatesReturns:
{
"id": "delimited",
"name": "Delimited",
"description": "Splits a frame on a separator character.",
"params": [
{
"key": "separator",
"type": "char",
"label": "Separator",
"description": "Character used to split the frame",
"default": ","
}
]
}
Param type is one of string, char, int, float, bool, enum (enum options come from options[].value); numeric params may also carry min/max. Fails with an InvalidParam error for an unknown template id.
project.frameParser.setTemplateSelect a Built-In frame parser template for a source and switch the source to the Built-In language.
Parameters:
template (string): Template id from project.frameParser.listTemplatessourceId (int, optional): Source index (default 0)params (object, optional): Template parameters (see project.frameParser.getTemplateSchema); omitted keys use the schema defaultsReturns:
{
"sourceId": 0,
"language": 2,
"template": "delimited",
"params": { "separator": "," }
}
Params are validated against the template's schema before the change is applied. Fails with an InvalidParam error for an unknown template id, invalid params, or an unknown sourceId. Preview the parsed output before or after applying with project.frameParser.dryRun (language 2, the JSON descriptor as code).
project.frameParser.dryCompileCompile-only check for frame parser code. Catches syntax errors and the "wrong-language" mismatch without executing the parser.
Parameters:
code (string): Frame parser source.language (int): 0=JavaScript, 1=Lua, 2=Built-In.Returns:
{
"ok": false,
"language": "javascript",
"error": "Compile failed or parse(frame) is not defined.",
"warning": "Code looks like Lua but language is set to JavaScript."
}
ok:true omits error and warning. warning is only produced for JavaScript/Lua when the code's shape suggests the other language; for Built-In (language: 2) error instead names the expected {"template": id, "params": {...}} descriptor shape.
project.frameParser.dryRunCompile and execute frame parser code against raw stream bytes, driving the full pipeline (extraction, decoder, parse()) without touching the live project.
Parameters:
code (string): Frame parser source.language (int): 0=JavaScript, 1=Lua.inputBytes (string, optional): Raw stream bytes as UTF-8 text. Lossy for binary payloads.inputBytesHex (string, optional): Raw stream bytes as a space-tolerant hex string. Binary-safe. One of inputBytes / inputBytesHex is required and must decode to a non-empty byte array.decoderMethod (int, optional): 0=PlainText (default), 1=Hexadecimal, 2=Base64, 3=Binary. Binary is the only mode safe for non-text protocols (COBS, Modbus, custom binary).frameDetection (int, optional): 0=EndDelimiterOnly (default), 1=StartAndEndDelimiter, 2=NoDelimiters, 3=StartDelimiterOnly.frameStart (string, optional): Start delimiter. Hex when hexadecimalDelimiters is true.frameEnd (string, optional): End delimiter. Hex when hexadecimalDelimiters is true.hexadecimalDelimiters (bool, optional): Parse frameStart/frameEnd as hex bytes. Default: false.checksumAlgorithm (string, optional): Checksum name to validate trailing bytes. Default: none.operationMode (int, optional): 0=ProjectFile (default; runs decoder + parser), 2=QuickPlot (line extractor, comma-split, parser bypassed). 1=ConsoleOnly is invalid for this command.Returns:
{
"ok": true,
"frames": [
{
"rawHex": "32 35 2e 33 2c 36 30 0a",
"rawByteCount": 8,
"decoderOutput": "25.3,60",
"decoderIsBinary": false,
"rows": [["25.3", "60"]],
"rowCount": 1
}
],
"frameCount": 1,
"extractedCount": 1,
"consumedBytes": 8,
"remainingBytes": 0,
"droppedFrames": 0,
"totalRows": 1,
"hint": "Bytes flow through extraction (delimiters / detection) -> decoder method -> parser, the same path the live FrameBuilder uses. Pick the Binary decoder for non-text streams (COBS, Modbus, custom binary) -- PlainText / Hex / Base64 route through QString::fromUtf8 and mojibake non-ASCII bytes."
}
Returns an error response if extraction, decoding, or parser compile/execution fails.
project.batchRun several project mutations atomically with respect to autosave: all ops execute sequentially under one suspended-autosave window, and a single save is flushed at the end. Use this instead of more than ~5 sequential mutations (renames, retypes, reindexes) — N round-trips cost N times the latency and N times the autosave/tree-rebuild churn.
Parameters:
ops (array, required): Up to 1024 {command, params} objects, executed in order.
command is a registered command name (e.g. project.dataset.update); nesting
project.batch is rejected. params is the arguments object exactly as passed at
the top level for that command.stopOnError (bool, optional, default false): Abort the batch on the first op
failure. Default is best-effort — every op is attempted regardless of earlier
failures.dryRun (bool, optional, default false): Preview every op without committing.
Rejected when any op's command is outside the dry-run-aware set (project.dataset.delete,
project.group.delete, project.dataset.move, project.group.move,
project.workspace.delete, project.workspace.clearAll, project.new, project.open,
project.loadJson, project.template.apply, project.batch,
assistant.project.bulkApply) — patch-style commands such as project.dataset.update,
project.group.update, project.action.update, and project.painter.setCode are not
in that set, so a batch mixing them with dryRun:true is rejected outright.Not transactional: already-applied ops are not rolled back if a later op fails — this is a save-suspend wrapper, not a database transaction.
Returns:
{
"results": [
{"index": 0, "command": "project.dataset.update", "success": true, "result": {}}
],
"total": 1,
"succeeded": 1,
"failed": 0,
"aborted": false,
"autoSaveMode": "flushed"
}
Each result entry carries index, command, success, and either result (the
wrapped command's success payload) or error (code, message, optional data).
When dryRun is true, the top-level response also includes "dryRun": true,
autoSaveMode is "none", and a warning notes that no ops were committed.
Example:
{
"ops": [
{"command": "project.dataset.update", "params": {"groupId": 0, "datasetId": 0, "title": "LED 1", "index": 1}},
{"command": "project.dataset.update", "params": {"groupId": 0, "datasetId": 1, "title": "LED 2", "index": 2}}
]
}
project.dryRun.endToEndRun one or more sample frames through the project's frame parser, then apply every dataset's transform in execution order, without touching live state.
Parameters:
sampleFrame (string, optional): Single frame body (no delimiters). One of sampleFrame / sampleFrames is required.sampleFrames (array of string, optional): Multiple frame bodies, run sequentially through one parser engine instance.sourceId (int, optional): Source index to use for the parser and dataset transforms. Default: 0.code (string, optional): Frame parser source override. Default: the live project's parser for sourceId.language (int, optional): Parser language override, 0=JavaScript, 1=Lua. Default: the live source's language.verbose (bool, optional): Include each row's raw cell value alongside its transformed value. Default: false.Returns:
{
"ok": true,
"sourceId": 0,
"frameCount": 1,
"frames": [
{
"rowCount": 1,
"rows": [
{
"rawCells": ["25.3", "60"],
"datasets": [
{
"uniqueId": 1001,
"title": "Temperature",
"groupId": 0,
"datasetId": 0,
"index": 1,
"isVirtual": false,
"final": 25.3,
"transformApplied": false
}
]
}
]
}
]
}
rawCells[i] maps to dataset.index = i + 1. The table API (tableGet/tableSet/datasetGetRaw/datasetGetFinal) is not injected here: transforms that read other datasets see 0/null; test those individually with project.dataset.transform.dryRun. If one or more dataset transforms fail to compile, the response still succeeds: their final value is null, their uniqueId is listed in transformCompileFailures, and a top-level warning is added. A frame-parser compile failure, a missing sampleFrame/sampleFrames, or an out-of-range sourceId returns an error response instead.
project.workspace.*is a separate registered command family (WorkspacesHandler.cpp) from Project Management above. It manages dashboard workspaces (taskbar tabs) and the widget tiles pinned to them. Every mutating command requiresProjectFileoperation mode;delete,rename,update,addWidget, andremoveWidgetalso requirecustomizeWorkspacesto be enabled first viaproject.workspace.setCustomizeMode. Workspace ids:1000-1001are the auto-generated Overview/AllData workspaces,1002-4999are auto-generated per-group/per-folder workspaces,5000+ are user-created.Group.uniqueId(not the positional array index, notGroup.groupId) is the stable identifier used to attach widgets to a group; it never changes across group reorders.
project.workspace.listList all workspaces with widget counts.
Parameters: None
Returns:
{
"workspaces": [
{"id": 1000, "title": "Overview", "icon": "qrc:/icons/panes/overview.svg", "description": "", "widgetCount": 4}
],
"count": 1,
"customizeEnabled": false
}
project.workspace.getReturn widget refs for a single workspace.
Parameters:
id (int, required): Workspace idReturns:
{
"id": 1000,
"title": "Overview",
"icon": "qrc:/icons/panes/overview.svg",
"description": "",
"widgets": [
{
"widgetType": 9,
"widgetTypeSlug": "plot",
"groupId": 1024,
"relativeIndex": 0,
"widgetId": "ws1000:plot:g1024:0"
}
]
}
Returns INVALID_PARAM if id does not match a live workspace.
project.workspace.addCreate a new workspace. Requires ProjectFile mode.
Parameters:
title (string, optional, default "Workspace"): Workspace titleReturns:
{
"id": 5000,
"title": "Workspace",
"added": true
}
project.workspace.deleteDelete a workspace by id. Requires ProjectFile mode and customizeWorkspaces
enabled (see project.workspace.setCustomizeMode).
Parameters:
id (int, required): Workspace iddryRun (bool, optional): If true, return the workspace + widgetCount that
would be removed without committing. Auto-runs without an approval card.Returns:
{
"id": 5000,
"deleted": true,
"workspace": {"id": 5000, "title": "My Layout", "widgetCount": 3}
}
With dryRun: true, the response adds "dryRun": true and a warning, sets
"deleted": false, and nothing is written.
project.workspace.clearAllWipe every workspace in one call. Forces customizeWorkspaces on and leaves
an empty list. Use instead of looping project.workspace.delete: one
approval card, one autosave flush. Follow with project.workspace.autoGenerate
to restore the default Overview/AllData/per-group layout, or
project.workspace.add + addWidget to build a custom layout. Requires
ProjectFile mode; does not require customizeWorkspaces to already be on.
Parameters:
dryRun (bool, optional): If true, return the list of workspaces that
would be cleared. Auto-runs without an approval card.Returns:
{
"cleared": 3,
"remaining": 0,
"deleted": [
{"id": 1000, "title": "Overview", "widgetCount": 4}
],
"hint": "All workspaces removed; customize mode is on. Call project.workspace.autoGenerate to recreate the default Overview/AllData/per-group layout, or build a custom layout with project.workspace.add + project.workspace.addWidget."
}
With dryRun: true, the response instead carries "dryRun": true, "cleared": 0,
"remaining" (unchanged workspace count), and "wouldDelete" in place of "deleted".
project.workspace.renameRename a workspace. Requires ProjectFile mode and customizeWorkspaces enabled.
Parameters:
id (int, required): Workspace idtitle (string, required): New workspace title (cannot be empty/whitespace)Returns:
{
"id": 5000,
"title": "New Title",
"renamed": true
}
project.workspace.updatePatch workspace title, icon, and/or description. Requires ProjectFile
mode and customizeWorkspaces enabled. At least one optional field is required.
Parameters:
id (int, required): Workspace idtitle (string, optional): New title (cannot be empty/whitespace)icon (string, optional): New icon path, e.g. "qrc:/icons/panes/overview.svg"description (string, optional): Free-text intent / audience noteReturns:
{
"id": 5000,
"title": "New Title",
"updated": true
}
Only the fields that were set are echoed back alongside id and updated.
project.workspace.reorderReorder user-defined workspaces (id >= 5000) in the taskbar. System
workspaces (auto-generated, ids 1000-4999) keep their original slots.
Requires ProjectFile mode.
Parameters:
workspaceIds (array of int, required): Every current user-workspace id
(>= 5000), in the desired order. Must exactly match the set of current
user workspaces; partial reorders are rejected to avoid silent corruption.Returns:
{
"ok": true,
"newOrder": [5001, 5000, 5002]
}
Returns INVALID_PARAM if any id is below 5000, or if the id set does not
exactly match the current user workspaces.
project.workspace.autoGenerateMaterialise the synthetic Overview/AllData/per-group workspaces into the
customized set. No-op if already customized. Requires ProjectFile mode.
Parameters: None
Returns:
{
"firstWorkspaceId": 1000,
"generated": true
}
generated is false and firstWorkspaceId is -1 when the project was
already customized (no-op).
project.workspace.getCustomizeModeReturn the customizeWorkspaces flag.
Parameters: None
Returns:
{
"enabled": false
}
project.workspace.setCustomizeModeFlip the customizeWorkspaces flag. Requires ProjectFile mode. Most other
project.workspace.* mutators require this flag to be true first.
Parameters:
enabled (bool, required): Enable (true) or disable (false)Returns:
{
"enabled": true,
"updated": true
}
project.workspace.addWidgetPin a visualization tile onto a workspace: the tile renders the dashboard
widget of the given widgetType fed by the given groupId. Requires
ProjectFile mode and customizeWorkspaces enabled. Call project.group.list
(for groupId + compatibleWidgetTypes) and project.workspace.list (for
workspaceId) before calling this command.
Parameters:
workspaceId (int, required): Workspace id from project.workspace.listgroupId (int, required): Group.uniqueId from project.group.list
(stable across reorders), not the positional array index and not Group.groupIdwidgetType (string or int, required): Widget kind. Preferred form is a
string slug: "plot", "fft", "bar", "gauge", "compass", "led",
"datagrid", "multiplot", "accelerometer", "gyroscope", "gps",
"plot3d", "imageview" (Pro), "output-panel" (Pro),
"notification-log" (Pro), "waterfall" (Pro), "painter" (Pro). An
integer DashboardWidget enum is also accepted for back-compat.datasetId (int, optional): For per-dataset widgets
(plot/fft/bar/gauge/meter/compass/waterfall), pin the tile to this dataset
within groupId. When omitted, the first dataset in the group with the
matching option enabled is used. Ignored for group-level widgets.relativeIndex (int, optional, discouraged): Dashboard-level global index
across all widgets of this type in the project. Omit it and the command
computes the correct index from groupId (and datasetId for per-dataset
widgets).Returns:
{
"workspaceId": 5000,
"widgetType": 9,
"widgetTypeSlug": "plot",
"groupId": 1024,
"relativeIndex": 0,
"relativeIndexAutoAssigned": true,
"widgetId": "ws5000:plot:g1024:0",
"added": true
}
datasetId is echoed back when it was supplied. widgetId is the opaque
identifier to pass to project.workspace.removeWidget. Returns
INVALID_PARAM if the workspace or group does not exist, widgetType is
unknown or a sentinel value (0=Terminal, 17=NoWidget), widgetType is
not in the group's compatibleWidgetTypes, or datasetId is not in
groupId.
project.workspace.removeWidgetDetach a widget ref from a workspace. Requires ProjectFile mode and
customizeWorkspaces enabled.
Parameters:
widgetId (string, optional): Opaque identifier in the form
"ws<workspaceId>:<slug>:g<groupId>:<relativeIndex>" (returned by
project.workspace.addWidget and project.workspace.get). Provide either
widgetId or all four tuple fields below.workspaceId (int, optional): Workspace id (tuple form)widgetType (string or int, optional): DashboardWidget slug or enum (tuple form)groupId (int, optional): Group.uniqueId of the source group (tuple form)relativeIndex (int, optional): Relative widget index (tuple form)Returns:
{
"workspaceId": 5000,
"widgetType": 9,
"widgetTypeSlug": "plot",
"groupId": 1024,
"relativeIndex": 0,
"widgetId": "ws5000:plot:g1024:0",
"removed": true
}
Returns INVALID_PARAM for a malformed widgetId, or MISSING_PARAM if
neither widgetId nor the full tuple is provided.
project.workspace.validateWalk workspace widget refs and report orphaned references (the
group/dataset/widgetType combo no longer exists) plus groups not pinned to
any workspace. Read-only: does not modify project state or toggle
customizeWorkspaces, and does not require ProjectFile mode. Pair with
project.workspace.cleanup to repair.
Parameters:
workspaceId (int, optional): Limit validation to one workspace; omit to
validate all workspacesReturns:
{
"ok": true,
"issues": [],
"workspaceCount": 3,
"orphanedCount": 0,
"unreferencedGroups": [],
"issueCount": 0
}
Each entry in issues carries level ("error" for orphaned refs,
"warning" for unreferenced groups), location, and a message; orphan
entries also carry workspaceId, widgetType, widgetTypeSlug, groupId,
relativeIndex, and widgetId. unreferencedGroups (checked only when
workspaceId is omitted) is skipped when validation is scoped to one workspace.
project.workspace.cleanupDrop workspace widget refs that no longer resolve to a live
group/dataset/widgetType. Scope is orphans only: it does not touch refs where
the group still exists but compatibleWidgetTypes has changed. Requires
ProjectFile mode unless dryRun is true.
Parameters:
workspaceId (int, optional): Limit cleanup to one workspacedryRun (bool, optional, default false): Report what would be removed
without mutatingReturns:
{
"ok": true,
"dryRun": false,
"removed": 2,
"removedRefs": [
{
"workspaceId": 5000,
"widgetType": 9,
"widgetTypeSlug": "plot",
"groupId": 1024,
"relativeIndex": 0,
"widgetId": "ws5000:plot:g1024:0"
}
]
}
When dryRun is true, removed is the count of removedRefs and nothing
is written.
Note: These commands require a Serial Studio Pro license.
io.modbus.getConfigGet current Modbus configuration.
Parameters: None
io.modbus.listProtocolsGet list of supported Modbus protocols.
Returns:
{
"protocolList": [
{"index": 0, "name": "Modbus RTU"},
{"index": 1, "name": "Modbus TCP"}
]
}
io.modbus.setProtocolIndexSet Modbus protocol.
Parameters:
protocolIndex (int): 0=RTU, 1=TCPio.modbus.setSlaveAddressSet Modbus slave address.
Parameters:
address (int): Slave address (1-247)io.modbus.setPollIntervalSet polling interval.
Parameters:
intervalMs (int): Interval in milliseconds (minimum 10)io.modbus.setHostSet Modbus TCP host address.
Parameters:
host (string): IP address or hostnameio.modbus.setPortSet Modbus TCP port.
Parameters:
port (int): Port number (1-65535)io.modbus.setSerialPortIndexSet RTU serial port.
Parameters:
portIndex (int): Serial port indexio.modbus.setBaudRateSet RTU baud rate.
Parameters:
baudRate (int): Baud rateio.modbus.setParityIndexSet RTU parity.
Parameters:
parityIndex (int): Parity indexio.modbus.setDataBitsIndexSet RTU data bits.
Parameters:
dataBitsIndex (int): Data bits indexio.modbus.setStopBitsIndexSet RTU stop bits.
Parameters:
stopBitsIndex (int): Stop bits indexio.modbus.addRegisterGroupAdd a register group to poll.
Parameters:
type (int): Register type (0=Holding, 1=Input, 2=Coils, 3=Discrete)startAddress (int): Starting register address (0-65535)count (int): Number of registers to read (1-125)Example:
python test_api.py send io.modbus.addRegisterGroup -p type=2 startAddress=0 count=10
io.modbus.removeRegisterGroupRemove a register group.
Parameters:
groupIndex (int): Group index to removeio.modbus.clearRegisterGroupsClear all register groups.
Parameters: None
io.modbus.listSerialPortsio.modbus.listParitiesio.modbus.listDataBitsio.modbus.listStopBitsio.modbus.listBaudRatesio.modbus.listRegisterTypesio.modbus.listRegisterGroupsNote: These commands require a Serial Studio Pro license.
io.canbus.getConfigGet current CAN bus configuration.
Parameters: None
io.canbus.listPluginsGet list of available CAN plugins.
Returns:
{
"pluginList": [
{"index": 0, "name": "socketcan", "displayName": "SocketCAN"},
{"index": 1, "name": "peakcan", "displayName": "PEAK PCAN"}
]
}
io.canbus.listInterfacesGet list of available CAN interfaces.
Returns:
{
"interfaceList": [
{"index": 0, "name": "can0"},
{"index": 1, "name": "can1"}
]
}
io.canbus.listBitratesGet list of supported bitrates.
Returns:
{
"bitrateList": ["10000", "20000", "50000", "100000", "125000", "250000", "500000", "800000", "1000000"]
}
io.canbus.getInterfaceErrorGet interface error message if any.
Parameters: None
io.canbus.setPluginIndexSelect CAN plugin.
Parameters:
pluginIndex (int): Plugin index from listPluginsio.canbus.setInterfaceIndexSelect CAN interface.
Parameters:
interfaceIndex (int): Interface index from listInterfacesio.canbus.setBitrateSet CAN bitrate.
Parameters:
bitrate (int): Bitrate in bits/second (e.g., 250000)io.canbus.setCanFdEnable or disable CAN FD.
Parameters:
enabled (bool): true to enable CAN FD, false for standard CANNote: These commands require a Serial Studio Pro license.
MQTT is split into two independent surfaces, each configured with a single
patch-style setConfig command instead of one command per field. The
publisher forwards dashboard data to a broker and runs alongside any data
source. The subscriber is the MQTT data-source driver: configure it here,
then open it with io.setBusType + io.connect like any other driver.
Shared rules for both setConfig commands:
password requires username in the same call. The pair is stored
in the encrypted credential vault, never in the project file, and is never
returned by getConfig (check hasCredentials instead). Pass empty strings
for both to clear the stored pair.mqttVersion, sslProtocol, peerVerifyMode) take
integer indices; getConfig returns the canonical lookup tables
(mqttVersions, sslProtocols, peerVerifyModes).project.mqtt.publisher.getConfigRead the publisher configuration (broker, mode, TLS). Includes the enum lookup
tables and hasCredentials.
Parameters: None
project.mqtt.publisher.setConfigPatch one or more publisher fields. Setting enabled: true starts publishing.
Parameters (all optional):
enabled (bool): Start or stop publishinghostname (string), port (int): Broker endpointclientId (string), customClientId (bool): Client identity; with
customClientId: false the ID regenerates automaticallyusername, password (string): Credential pair (vault rules above)topicBase (string): Publish topic rootmode (int): 0=RawRxData, 1=ScriptDriven, 2=DashboardCsv, 3=DashboardJsonpublishFrequency (int): Publish rate in Hz (1-30)publishNotifications (bool), notificationTopic (string): Forward app
notifications to MQTTcleanSession (bool), keepAlive (int): Session settingsmqttVersion, sslEnabled, sslProtocol, peerVerifyMode,
peerVerifyDepth: Protocol and TLS settingsproject.mqtt.publisher.getStatusSnapshot of publisher live state (connected, messagesSent, broker
endpoint). Does not trigger a connection attempt.
Parameters: None
project.mqtt.subscriber.getConfigRead the subscriber driver configuration, as used the next time io.connect
runs with the MQTT bus type. Password is never returned.
Parameters: None
project.mqtt.subscriber.setConfigPatch one or more subscriber driver fields. Changing fields while connected schedules a reconnect.
Parameters (all optional):
hostname (string), port (int): Broker endpointclientId (string): Client identityusername, password (string): Credential pair (vault rules above)topicFilter (string): Subscribe filter; + and # wildcards supportedcleanSession (bool), keepAlive (int), autoKeepAlive (bool): Session
settingsmqttVersion, sslEnabled, sslProtocol, peerVerifyMode,
peerVerifyDepth: Protocol and TLS settingsproject.mqtt.subscriber.getStatusSnapshot of subscriber driver live state (isOpen, hostname, port).
Parameters: None
Note: These commands require a Serial Studio Pro license.
mdf4Export.getStatusGet MDF4 export status.
Parameters: None
Returns:
{
"enabled": false,
"isOpen": false
}
mdf4Export.setEnabledEnable or disable MDF4 export.
Parameters:
enabled (bool): true to enable, false to disablemdf4Export.closeClose current MDF4 file.
Parameters: None
MDF4 file playback control (same interface as CSV Player):
mdf4Player.openOpen MDF4/MF4 file for playback.
Parameters:
filePath (string): Path to MDF4 fileExample:
python test_api.py send mdf4Player.open -p filePath=/path/to/data.mf4
mdf4Player.closeClose current MDF4 file.
Parameters: None
mdf4Player.setPausedPause or resume playback.
Parameters:
paused (bool): true to pause, false to resumeExample:
python test_api.py send mdf4Player.setPaused -p paused=true
mdf4Player.stepStep the player by a number of frames.
Parameters:
delta (int, optional, default 1): frames to advance; negative steps backwardExample:
python test_api.py send mdf4Player.step -p delta=-1
mdf4Player.setProgressSeek to position in file.
Parameters:
progress (double): Position from 0.0 to 1.0Example:
python test_api.py send mdf4Player.setProgress -p progress=0.25
mdf4Player.getStatusGet player status.
Returns:
{
"isOpen": true,
"isPlaying": true,
"frameCount": 5000,
"framePosition": 1250,
"progress": 0.25,
"timestamp": "00:01:15.678",
"filename": "data.mf4"
}
Note: These commands require a Serial Studio Pro license.
io.audio.setInputDeviceSelect audio input device.
Parameters:
deviceIndex (int): Device indexio.audio.setOutputDeviceSelect audio output device.
Parameters:
deviceIndex (int): Device indexio.audio.setSampleRateSet sample rate.
Parameters:
rateIndex (int): Sample rate indexio.audio.setInputSampleFormatSet input sample format.
Parameters:
formatIndex (int): Format indexio.audio.setInputChannelConfigSet input channel configuration.
Parameters:
channelIndex (int): Channel config indexio.audio.setOutputSampleFormatSet output sample format.
Parameters:
formatIndex (int): Format indexio.audio.setOutputChannelConfigSet output channel configuration.
Parameters:
channelIndex (int): Channel config indexio.audio.listInputDevicesGet list of input devices.
Returns:
{
"devices": ["Microphone", "Line In"],
"selectedIndex": 0
}
io.audio.listOutputDevicesGet list of output devices.
Returns:
{
"devices": ["Speakers", "Headphones"],
"selectedIndex": 0
}
io.audio.listSampleRatesGet list of sample rates.
Returns:
{
"sampleRates": ["8000", "44100", "48000"],
"selectedIndex": 2
}
io.audio.listInputFormatsGet list of input sample formats.
Returns:
{
"formats": ["Unsigned 8-bit", "Signed 16-bit", "Signed 24-bit", "Signed 32-bit", "Float 32-bit"],
"selectedIndex": 0
}
io.audio.listOutputFormatsGet list of output sample formats.
Returns:
{
"formats": ["Unsigned 8-bit", "Signed 16-bit", "Signed 24-bit", "Signed 32-bit", "Float 32-bit"],
"selectedIndex": 0
}
io.audio.getConfigGet complete audio configuration.
Returns:
{
"selectedInputDevice": 0,
"selectedOutputDevice": 0,
"selectedSampleRate": 2,
"selectedInputSampleFormat": 0,
"selectedInputChannelConfig": 0,
"selectedOutputSampleFormat": 0,
"selectedOutputChannelConfig": 0
}
Note: These commands require a Serial Studio Pro license. USB is a
libusb-based driver (BusType::RawUsb); select it via io.setBusType with
busType=6 before configuring it.
io.usb.setDeviceIndexSelect USB device by index.
Parameters:
deviceIndex (int): USB device index (0 = placeholder)io.usb.setTransferModeSet the transfer mode.
Parameters:
mode (int): 0=Bulk, 1=Advanced, 2=Isochronousio.usb.setInEndpointIndexSelect the IN endpoint after connection.
Parameters:
endpointIndex (int): IN endpoint indexio.usb.setOutEndpointIndexSelect the OUT endpoint after connection.
Parameters:
endpointIndex (int): OUT endpoint indexio.usb.setIsoPacketSizeSet the ISO transfer packet size, in bytes.
Parameters:
size (int): ISO transfer packet size in bytes (1-49152)io.usb.listDevicesGet list of available USB devices.
Parameters: None
Returns:
{
"devices": ["USB Device 0"],
"selectedIndex": 0
}
io.usb.getConfigGet complete USB driver configuration.
Parameters: None
Returns:
{
"deviceIndex": 0,
"transferMode": 0,
"inEndpointIndex": 0,
"outEndpointIndex": 0,
"isoPacketSize": 512,
"inEndpoints": [],
"outEndpoints": []
}
Note: These commands require a Serial Studio Pro license. HID is a
hidapi-based driver (BusType::HidDevice); select it via io.setBusType
with busType=7 before configuring it.
io.hid.setDeviceIndexSelect HID device by index.
Parameters:
deviceIndex (int): HID device index (0 = placeholder)io.hid.listDevicesGet list of available HID devices.
Parameters: None
Returns:
{
"devices": ["HID Device 0"],
"selectedIndex": 0
}
io.hid.getConfigGet complete HID driver configuration.
Parameters: None
Returns:
{
"deviceIndex": 0,
"usagePage": 0,
"usage": 0
}
Note: These commands require a Serial Studio Pro license. Process I/O
(BusType::Process) either launches an external executable or attaches to a
named pipe/FIFO; select it via io.setBusType with busType=8 before
configuring it. Executable and path parameters are checked against the
same allow-list as other filesystem-facing commands.
io.process.setModeSet the driver mode.
Parameters:
mode (int): 0=Launch, 1=NamedPipeio.process.setExecutableSet the executable path for Launch mode.
Parameters:
executable (string): Absolute path to the executableio.process.setArgumentsSet the command-line arguments for Launch mode.
Parameters:
arguments (string): Shell-style argument stringio.process.setWorkingDirSet the working directory for Launch mode.
Parameters:
workingDir (string): Absolute path to the working directoryio.process.setPipePathSet the named pipe / FIFO path for NamedPipe mode.
Parameters:
pipePath (string): Named pipe or FIFO pathio.process.listRunningRefresh and return the list of running processes.
Parameters: None
Returns:
{
"processes": [],
"count": 0
}
io.process.getConfigGet complete Process driver configuration.
Parameters: None
Returns:
{
"mode": 0,
"executable": "",
"arguments": "",
"workingDir": "",
"pipePath": ""
}
Request:
{
"type": "command",
"id": "status-1",
"command": "io.getStatus"
}
Response:
{
"type": "response",
"id": "status-1",
"success": true,
"result": {
"isConnected": false,
"paused": false,
"busType": 0,
"configurationOk": false
}
}
Request (Batch):
{
"type": "batch",
"id": "uart-setup",
"commands": [
{"command": "io.setBusType", "id": "1", "params": {"busType": 0}},
{"command": "io.uart.setBaudRate", "id": "2", "params": {"baudRate": 115200}},
{"command": "io.uart.setParity", "id": "3", "params": {"parityIndex": 0}},
{"command": "io.uart.setDataBits", "id": "4", "params": {"dataBitsIndex": 3}},
{"command": "io.uart.setStopBits", "id": "5", "params": {"stopBitsIndex": 0}},
{"command": "io.uart.setPortIndex", "id": "6", "params": {"portIndex": 0}},
{"command": "io.connect", "id": "7"}
]
}
First, Base64-encode your data:
echo -n "Hello World" | base64
# Output: SGVsbG8gV29ybGQ=
Request:
{
"type": "command",
"id": "send-1",
"command": "io.writeData",
"params": {
"data": "SGVsbG8gV29ybGQ="
}
}
{
"type": "batch",
"id": "network-setup",
"commands": [
{"command": "io.setBusType", "id": "1", "params": {"busType": 1}},
{"command": "io.network.setSocketType", "id": "2", "params": {"socketTypeIndex": 0}},
{"command": "io.network.setRemoteAddress", "id": "3", "params": {"address": "192.168.1.100"}},
{"command": "io.network.setTcpPort", "id": "4", "params": {"port": 8080}},
{"command": "io.connect", "id": "5"}
]
}
{
"type": "command",
"id": "csv-1",
"command": "csvExport.setEnabled",
"params": {
"enabled": true
}
}
Request (Batch):
{
"type": "batch",
"id": "dashboard-setup",
"commands": [
{"command": "dashboard.setOperationMode", "id": "1", "params": {"mode": 1}},
{"command": "dashboard.setFps", "id": "2", "params": {"fps": 60}},
{"command": "dashboard.setTimeRange", "id": "3", "params": {"seconds": 10}},
{"command": "dashboard.getStatus", "id": "4"}
]
}
Shell Example:
python test_api.py send dashboard.setOperationMode -p mode=1
python test_api.py send dashboard.setFps -p fps=60
python test_api.py send dashboard.setTimeRange -p seconds=10
python test_api.py send dashboard.getStatus
{
"type": "command",
"id": "list-1",
"command": "api.getCommands"
}
Returns:
{
"type": "response",
"id": "list-1",
"success": true,
"result": {
"commands": [
{"name": "io.connect", "description": "Open the configured connection"},
{"name": "io.uart.setBaudRate", "description": "Set UART baud rate"}
]
}
}
Over MCP, the equivalent request is {"jsonrpc": "2.0", "id": 1, "method": "tools/list"} for the same surface plus input schemas.
Configure UART Connection:
python test_api.py send io.setBusType -p busType=0
python test_api.py send io.uart.setBaudRate -p baudRate=115200
python test_api.py send io.uart.setPortIndex -p portIndex=0
python test_api.py send io.connect
Configure Network (TCP) Connection:
python test_api.py send io.setBusType -p busType=1
python test_api.py send io.network.setSocketType -p socketTypeIndex=0
python test_api.py send io.network.setRemoteAddress -p address=192.168.1.100
python test_api.py send io.network.setTcpPort -p port=8080
python test_api.py send io.connect
Enable Data Export:
python test_api.py send csvExport.setEnabled -p enabled=true
Configure Dashboard Settings:
python test_api.py send dashboard.setOperationMode -p mode=1
python test_api.py send dashboard.setFps -p fps=60
python test_api.py send dashboard.setTimeRange -p seconds=10
python test_api.py send dashboard.getStatus
The examples/API Test/test_api.py script provides a full-featured client.
Location: examples/API Test/test_api.py
Features:
Usage:
# Single command
python test_api.py send io.getStatus
# With parameters
python test_api.py send io.uart.setBaudRate -p baudRate=115200
# Interactive mode
python test_api.py interactive
# Run tests
python test_api.py test
# List all commands
python test_api.py list
Installation:
cd examples/API\ Test/
python test_api.py --help
#!/usr/bin/env python3
import socket
import json
import time
class SerialStudioAPI:
def __init__(self, host="127.0.0.1", port=7777):
self.host = host
self.port = port
def send_command(self, command, params=None):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
msg = {
"type": "command",
"id": f"cmd-{time.time()}",
"command": command
}
if params:
msg["params"] = params
sock.sendall((json.dumps(msg) + "\n").encode())
response = json.loads(sock.recv(65536).decode())
sock.close()
return response
def connect_uart(self, baud=115200, port_index=0):
self.send_command("io.setBusType", {"busType": 0})
self.send_command("io.uart.setBaudRate", {"baudRate": baud})
self.send_command("io.uart.setPortIndex", {"portIndex": port_index})
return self.send_command("io.connect")
# Usage
api = SerialStudioAPI()
status = api.send_command("io.getStatus")
print(f"Connected: {status['result']['isConnected']}")
const net = require('net');
class SerialStudioAPI {
constructor(host = '127.0.0.1', port = 7777) {
this.host = host;
this.port = port;
}
sendCommand(command, params = {}) {
return new Promise((resolve, reject) => {
const client = net.createConnection({ host: this.host, port: this.port });
const msg = {
type: 'command',
id: `cmd-${Date.now()}`,
command: command,
params: params
};
client.on('connect', () => {
client.write(JSON.stringify(msg) + '\n');
});
client.on('data', (data) => {
const response = JSON.parse(data.toString());
client.end();
resolve(response);
});
client.on('error', (err) => {
reject(err);
});
});
}
}
// Usage
const api = new SerialStudioAPI();
api.sendCommand('io.getStatus')
.then(response => console.log(response))
.catch(err => console.error(err));
#!/bin/bash
API_HOST="127.0.0.1"
API_PORT="7777"
send_command() {
local command=$1
local params=${2:-"{}"}
local msg=$(cat <<EOF
{
"type": "command",
"id": "bash-cmd",
"command": "$command",
"params": $params
}
EOF
)
echo "$msg" | nc $API_HOST $API_PORT
}
# Usage
send_command "io.getStatus"
send_command "io.uart.setBaudRate" '{"baudRate": 115200}'
# Using netcat
echo '{"type":"command","id":"1","command":"io.getStatus"}' | nc localhost 7777
# Using curl (if supported)
curl -X POST http://localhost:7777 \
-H "Content-Type: application/json" \
-d '{"type":"command","id":"1","command":"io.getStatus"}'
import socket
import json
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 7777))
request = {
"type": "command",
"id": "test-1",
"command": "io.getStatus"
}
sock.sendall((json.dumps(request) + "\n").encode())
response = json.loads(sock.recv(65536).decode())
print(response)
sock.close()
using System;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
var client = new TcpClient("127.0.0.1", 7777);
var stream = client.GetStream();
var request = new {
type = "command",
id = "test-1",
command = "io.getStatus"
};
var json = JsonSerializer.Serialize(request) + "\n";
var data = Encoding.UTF8.GetBytes(json);
stream.Write(data, 0, data.Length);
var buffer = new byte[65536];
var bytes = stream.Read(buffer, 0, buffer.Length);
var response = Encoding.UTF8.GetString(buffer, 0, bytes);
Console.WriteLine(response);
client.Close();
✅ DO:
❌ DON'T:
Good:
status = api.send_command("io.getStatus")
if not status["result"]["isConnected"]:
api.send_command("io.connect")
# Bad - assuming connection state
api.send_command("io.writeData", {"data": "..."})
Use batch commands when executing multiple related operations:
Good (Batch):
{
"type": "batch",
"commands": [
{"command": "io.setBusType", "id": "1", "params": {"busType": 0}},
{"command": "io.uart.setBaudRate", "id": "2", "params": {"baudRate": 115200}},
{"command": "io.connect", "id": "3"}
]
}
Less Efficient (Sequential):
# Three separate TCP connections
send_command("io.setBusType", {"busType": 0})
send_command("io.uart.setBaudRate", {"baudRate": 115200})
send_command("io.connect")
Always check for errors in responses:
# Good
response = api.send_command("io.connect")
if not response.get("success"):
error = response.get("error", {})
code = error.get("code")
message = error.get("message")
if code == "EXECUTION_ERROR":
# Handle connection failure
print(f"Connection failed: {message}")
else:
# Handle other errors
print(f"Error {code}: {message}")
else:
print("Connected successfully")
# Bad - ignoring errors
api.send_command("io.connect")
# Continue without checking...
Validate parameters before sending:
# Good
def set_baud_rate(rate):
valid_rates = [1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
if rate not in valid_rates:
raise ValueError(f"Invalid baud rate: {rate}")
return api.send_command("io.uart.setBaudRate", {"baudRate": rate})
# Bad - sending unchecked values
set_baud_rate(user_input) # Could be anything!
Track connection state in your client:
class SerialStudioClient:
def __init__(self):
self.connected = False
self.bus_type = None
def connect(self):
response = self.send_command("io.connect")
if response.get("success"):
self.connected = True
return response
def disconnect(self):
response = self.send_command("io.disconnect")
if response.get("success"):
self.connected = False
return response
# Good
try:
api.send_command("io.connect")
api.send_command("csvExport.setEnabled", {"enabled": True})
# Do work...
finally:
api.send_command("csvExport.close")
api.send_command("io.disconnect")
# Bad - leaving resources open
api.send_command("io.connect")
# Exit without cleanup
# Good - keep connection open for monitoring
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 7777))
while monitoring:
msg = {"type": "command", "command": "io.getStatus"}
sock.sendall((json.dumps(msg) + "\n").encode())
response = sock.recv(65536)
# Process response
time.sleep(1)
sock.close()
# Bad - reconnecting every time
while monitoring:
response = api.send_command("io.getStatus") # New connection each time!
time.sleep(1)
Problem: Connection refused or timeout
Solutions:
telnet localhost 7777 or nc localhost 7777Problem: UNKNOWN_COMMAND or INVALID_PARAM
Solutions:
python test_api.py listpython test_api.py list against the live serverlistBaudRates, getPortList, etc. to see valid valuesProblem: All commands in batch fail
Solutions:
id fieldcommands is an arrayProblem: Connection closes unexpectedly
Solutions:
Problem: Response ID differs from request ID
Solutions:
Problem: UNKNOWN_COMMAND for Modbus/CAN/MQTT commands
Solutions:
Problem: EXECUTION_ERROR when command fails
Solutions:
Connection Pooling:
class ConnectionPool:
def __init__(self, size=5):
self.connections = [create_connection() for _ in range(size)]
self.available = self.connections.copy()
def get(self):
return self.available.pop() if self.available else None
def release(self, conn):
self.available.append(conn)
Pipelining Commands:
# Send multiple commands without waiting for responses
for cmd in commands:
send_async(cmd)
# Then collect all responses
responses = [receive() for _ in commands]
LabVIEW Integration: Use LabVIEW's TCP/IP VIs to communicate with the API Server.
MATLAB Integration:
% Connect to Serial Studio
t = tcpclient('127.0.0.1', 7777);
% Send command
request = struct('type', 'command', 'id', 'matlab-1', 'command', 'io.getStatus');
json = jsonencode(request);
write(t, [json, newline]);
% Read response
response = read(t);
data = jsondecode(char(response));
% Close
clear t;
Docker/Container Usage:
# Expose host's Serial Studio to container
docker run -it --network host my-automation-script
# Inside container, connect to 127.0.0.1:7777
Build language-specific wrappers for easier use:
class SerialStudio:
def __init__(self, host='127.0.0.1', port=7777):
self.api = SerialStudioAPI(host, port)
def uart(self):
return UARTDriver(self.api)
def network(self):
return NetworkDriver(self.api)
class UARTDriver:
def __init__(self, api):
self.api = api
def set_baud_rate(self, rate):
return self.api.send("io.uart.setBaudRate", {"baudRate": rate})
def get_ports(self):
response = self.api.send("io.uart.listPorts")
return response.get("result", {}).get("portList", [])
# Usage
ss = SerialStudio()
ss.uart().set_baud_rate(115200)
ports = ss.uart().get_ports()
Serial Studio includes a built-in MCP (Model Context Protocol) server that exposes the API Server functionality to AI models like Claude and ChatGPT. AI assistants can control Serial Studio directly (connecting to devices, reading sensor data, sending commands, and managing exports) through natural language instructions.
The MCP handler wraps the Serial Studio TCP API (port 7777) in an MCP-compliant interface. Any MCP-capable AI client can discover and call all 300+ API commands as tools.
See the MCP Client example in the examples directory for a complete working client implementation.
examples/API Test/ directory (client, REPL, and smoke tests in test_api.py)examples/API Test/test_api.py testexamples/MCP Client/ directorydashboard.getStatus - Get all dashboard settingsdashboard.setOperationMode / dashboard.getOperationMode - Control operation modedashboard.setFps / dashboard.getFps - Control visualization refresh ratedashboard.setPoints / dashboard.getPoints - Control plot data pointsThe Serial Studio API Server is dual-licensed:
See the main LICENSE file for details.
Found a bug or have a suggestion?
For security issues, please contact privately rather than creating a public issue.
Total Commands: 300+ — the registry grows with every release; enumerate the live surface with api.getCommands.
Made with ❤️ by the Serial Studio team
For questions and support, visit the Serial Studio GitHub repository.