Plugin Development
Table of Contents
- Overview
- How Plugins Work
- Getting Started
- Plugin Structure
- info.json Reference
- Connecting to Serial Studio
- Platform-Specific Builds
- State Persistence
- Lifecycle Events
- Extension Manager API
- Testing Your Plugin
- Packaging and Distribution
Overview
Plugins are external programs that connect to Serial Studio's API to receive live data, compute statistics, display custom visualizations, or automate workflows. They run as separate processes alongside Serial Studio and communicate over the network.
Plugins can be written in any language (Python, C++, Go, Rust, Node.js, and so on) and are distributed through the Extension Manager.
What plugins can do
- Custom visualizations. 3D renders, maps, specialized charts.
- Data processing. Filtering, FFT analysis, anomaly detection.
- Export to external systems. Push data to databases, cloud services, or other tools.
- Automated test sequences. Connect, configure, validate, and report.
- Hardware control. Send commands back to the device based on incoming data.
How Plugins Work
flowchart LR
A[Serial Studio] -->|gRPC port 8888| B[Plugin process]
A -->|TCP/JSON port 7777| B
- The user installs the plugin via the Extension Manager.
- The user clicks Run on the plugin card.
- Serial Studio ensures the API server is running, then launches the plugin process.
- The plugin connects to Serial Studio and starts sending commands or streaming data.
- The user clicks Stop (or Serial Studio exits) to terminate the plugin.
Getting Started
Minimal Python Plugin (TCP/JSON)
Create a folder with two files:
info.json:
{
"id": "my-first-plugin",
"type": "plugin",
"title": "My First Plugin",
"description": "Prints connection status every second.",
"author": "Your Name",
"version": "1.0.0",
"entry": "plugin.py",
"runtime": "python3",
"terminal": true,
"files": ["info.json", "plugin.py"]
}
import socket
import json
import time
def send_command(sock, command, params=None):
msg = {"type": "command", "id": "1", "command": command}
if params:
msg["params"] = params
sock.sendall((json.dumps(msg) + "\n").encode())
return json.loads(sock.recv(4096).decode())
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 7777))
while True:
result = send_command(sock, "io.getStatus")
print(f"Connected: {result.get('result', {}).get('isConnected')}")
time.sleep(1)
if __name__ == "__main__":
main()
While a device is streaming, the server also pushes frame batches (
{"frames": [...]}), raw data, and lifecycle events on the same socket. A production plugin should read line by line and treat only lines with"type": "response"as command responses, matching them to requests byid. See Server-Push Messages.
Minimal Python Plugin (gRPC)
For real-time frame streaming, use gRPC:
info.json:
{
"id": "my-grpc-plugin",
"type": "plugin",
"title": "My gRPC Plugin",
"description": "Streams frames via gRPC.",
"author": "Your Name",
"version": "1.0.0",
"entry": "plugin.py",
"runtime": "python3",
"grpc": true,
"files": [
"info.json",
"plugin.py",
"serialstudio_pb2.py",
"serialstudio_pb2_grpc.py"
]
}
import grpc
import serialstudio_pb2 as pb
import serialstudio_pb2_grpc as rpc
def main():
channel = grpc.insecure_channel("localhost:8888")
stub = rpc.SerialStudioAPIStub(channel)
print("Streaming frames...")
for batch in stub.StreamFrames(pb.StreamRequest()):
for frame in batch.frames:
print(frame.frame)
if __name__ == "__main__":
main()
Generate serialstudio_pb2.py and serialstudio_pb2_grpc.py from the .proto file (see the gRPC Server guide).
Plugin Structure
A plugin lives in its own folder inside a repository:
plugin/my-plugin/
info.json # Required: metadata and entry point
plugin.py # Entry point script
requirements.txt # Optional: Python dependencies
run.sh # Optional: launcher for macOS/Linux
run.cmd # Optional: launcher for Windows
serialstudio_pb2.py # Optional: gRPC stubs
serialstudio_pb2_grpc.py # Optional: gRPC stubs
Launcher Scripts
Launcher scripts (run.sh / run.cmd) are useful for:
- Auto-installing dependencies (e.g.,
grpciovia pip in a local venv). - Setting environment variables.
- Running native binaries without specifying a runtime.
Example run.sh:
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# Create venv and install deps if needed
if [ ! -d "venv" ]; then
echo "Setting up virtual environment..."
python3 -m venv venv
echo "Installing required packages (this may take a moment)..."
./venv/bin/pip install -r requirements.txt
echo "Setup complete."
fi
./venv/bin/python plugin.py
Example run.cmd:
@echo off
setlocal
set "SCRIPT_DIR=%~dp0"
cd /d "%SCRIPT_DIR%"
if not exist "venv" (
echo Setting up virtual environment...
python -m venv venv
echo Installing required packages (this may take a moment)...
venv\Scripts\pip install -r requirements.txt
echo Setup complete.
)
venv\Scripts\python plugin.py
When using launcher scripts, set "runtime": "" in info.json (the script itself is the executable).
info.json Reference
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier (lowercase, hyphens). Must be unique across all extensions. |
type |
Yes | Must be "plugin". |
title |
Yes | Display name in the Extension Manager. |
description |
Yes | Short description shown on the card. |
author |
Yes | Author name or organization. |
version |
Yes | Semantic version string (e.g., "1.0.0"). |
license |
No | License identifier (e.g., "MIT", "GPL-3.0"). |
category |
No | Category for filtering in the Extension Manager. |
screenshot |
No | Relative path to a preview image. |
files |
Yes | Array of relative file paths to download/install. Must include info.json itself. |
entry |
Yes | Script or binary entry point (e.g., "plugin.py", "run.sh"). |
runtime |
Yes | Interpreter command (e.g., "python3"). Empty string "" for native binaries or launcher scripts. |
terminal |
No | true to launch in a system terminal window. Default: false. |
grpc |
No | true if the plugin uses gRPC (port 8888). Serial Studio ensures the gRPC server is running before launch. Default: false. |
platforms |
No | Per-platform overrides (see Platform-Specific Builds). |
Full Example
{
"id": "signal-analyzer",
"type": "plugin",
"title": "Signal Analyzer",
"description": "Real-time FFT and spectral analysis of incoming data.",
"author": "Example Corp",
"version": "2.1.0",
"license": "MIT",
"category": "Analysis",
"screenshot": "screenshot.png",
"entry": "plugin.py",
"runtime": "python3",
"terminal": false,
"grpc": true,
"files": [
"info.json",
"plugin.py",
"analyzer.py",
"serialstudio_pb2.py",
"serialstudio_pb2_grpc.py",
"requirements.txt",
"screenshot.png"
],
"platforms": {
"darwin/*": { "entry": "run.sh", "runtime": "", "files": ["run.sh"] },
"linux/*": { "entry": "run.sh", "runtime": "", "files": ["run.sh"] },
"windows/*": { "entry": "run.cmd", "runtime": "", "files": ["run.cmd"] }
}
}
Connecting to Serial Studio
Option 1: gRPC (Recommended for Real-Time Data)
Use gRPC when your plugin needs to stream frames at high rates. Set "grpc": true in info.json.
import grpc
import serialstudio_pb2 as pb
import serialstudio_pb2_grpc as rpc
channel = grpc.insecure_channel("localhost:8888")
stub = rpc.SerialStudioAPIStub(channel)
# Execute any API command
resp = stub.ExecuteCommand(pb.CommandRequest(
id="1", command="io.getStatus"))
# Stream frames (each item is a FrameBatch)
for batch in stub.StreamFrames(pb.StreamRequest()):
for frame in batch.frames:
process(frame.frame)
See the gRPC Server documentation for the full service definition and stub generation instructions.
Option 2: TCP/JSON (Simpler Setup)
Use TCP/JSON when gRPC tooling is not available or for simple command-and-response patterns.
import socket
import json
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 7777))
msg = json.dumps({
"type": "command",
"id": "1",
"command": "io.getStatus"
}) + "\n"
sock.sendall(msg.encode())
response = json.loads(sock.recv(4096).decode())
See the API Reference for the complete command list and protocol specification.
Platform-Specific Builds
Plugins can provide different entry points for each operating system and architecture. Use the platforms field in info.json:
"platforms": {
"darwin/*": { "entry": "run.sh", "runtime": "", "files": ["run.sh"] },
"linux/x86_64": { "entry": "run.sh", "runtime": "", "files": ["run.sh", "bin/analyzer-linux-x64"] },
"linux/arm64": { "entry": "run.sh", "runtime": "", "files": ["run.sh", "bin/analyzer-linux-arm64"] },
"windows/*": { "entry": "run.cmd", "runtime": "", "files": ["run.cmd", "bin/analyzer.exe"] }
}
Platform keys use the format os/arch or os/* (for universal builds):
| Key | Matches |
|---|---|
darwin/* |
macOS (any architecture) |
darwin/arm64 |
macOS Apple Silicon only |
linux/x86_64 |
Linux x86_64 |
linux/arm64 |
Linux ARM64 (Raspberry Pi, etc.) |
windows/* |
Windows (any architecture) |
windows/x86_64 |
Windows x86_64 only |
* |
Any platform (generic fallback) |
Resolution order: exact os/arch key first, then os/*, then *.
- Platform-specific
filesare merged with the basefilesarray during installation. - If a plugin has no matching platform entry, the Install button is disabled and an Unavailable badge is shown.
- If no
platformsfield is present, the plugin is assumed to work on all platforms.
State Persistence
Plugin state (window positions, settings, configurations) is saved in the project file alongside widget layout data. Different projects can have different plugin configurations.
When State is Saved
- When the device disconnects.
- When the plugin is stopped.
- When Serial Studio exits.
When State is Restored
- When the plugin starts (from saved project data).
- When a new device connects (the project may have changed).
Saving State via API
Plugins save and restore their state using extension API commands:
# Save state
send_command(sock, "extensions.saveState", {
"pluginId": "my-plugin",
"state": {"windowX": 100, "windowY": 200, "zoom": 1.5}
})
# Load state
result = send_command(sock, "extensions.loadState", {
"pluginId": "my-plugin"
})
state = result.get("result", {}).get("state", {})
Auto-Relaunch
Plugins that were running when Serial Studio closed are remembered and automatically relaunched the next time a device connects (when the dashboard becomes available).
Lifecycle Events
The API server broadcasts events to all connected TCP/JSON clients as standalone JSON lines. Plugins should listen for these to coordinate with Serial Studio. gRPC clients do not receive lifecycle events; they can poll io.getStatus or watch stream activity instead.
| Event | When | Typical Plugin Action |
|---|---|---|
{"event": "connected"} |
Device connected | Start processing, restore state |
{"event": "disconnected"} |
Device disconnected | Save state, pause processing |
# TCP/JSON: listen for events on the socket
import json
while True:
data = sock.recv(4096).decode()
for line in data.strip().split("\n"):
msg = json.loads(line)
if msg.get("event") == "connected":
on_device_connected()
elif msg.get("event") == "disconnected":
on_device_disconnected()
Extension Manager API
Plugins can also interact with the Extension Manager programmatically:
| Command | Description |
|---|---|
extensions.list |
List all available extensions. |
extensions.getInfo |
Get details for a specific extension (extensionId). |
extensions.install |
Install an extension by index (addonIndex). |
extensions.uninstall |
Uninstall an extension by index (addonIndex). |
extensions.refresh |
Refresh catalogs from all repositories. |
extensions.saveState |
Save plugin state to the project (pluginId, state). |
extensions.loadState |
Load plugin state from the project (pluginId). |
extensions.listRepositories |
List repository URLs (Pro only). |
extensions.addRepository |
Add a repository URL (Pro only). |
extensions.removeRepository |
Remove a repository by index (Pro only). |
Testing Your Plugin
Manual Testing
- Create a folder with your
info.jsonand plugin files. - Point the Extension Manager to a local repository (Pro) or install manually by copying files to
~/Documents/Serial Studio/Extensions/plugin/your-plugin/. - Click Run in the Extension Manager detail view.
- Check the log panel for output and errors.
Testing the API Connection
Before building a full plugin, test your API connection independently:
# Test TCP/JSON
echo '{"type":"command","id":"1","command":"io.getStatus"}' | nc localhost 7777
# Test gRPC
grpcurl -plaintext localhost:8888 serialstudio.SerialStudioAPI/ListCommands
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Connection refused | API server not enabled | Enable in Preferences → General → Advanced |
grpcio import error |
Package not installed | pip install grpcio grpcio-tools |
| Plugin exits immediately | Unhandled exception | Set "terminal": true to see errors |
| No frames streaming | Device not connected | Connect a device first |
Packaging and Distribution
Repository Structure
my-extensions-repo/
manifest.json
plugin/my-plugin/
info.json
plugin.py
run.sh
run.cmd
requirements.txt
screenshot.png
manifest.json
{
"version": 1,
"repository": "My Extensions",
"extensions": [
"plugin/my-plugin/info.json"
]
}
Hosting Options
- GitHub: Push to a repository and share the raw manifest URL:
https://raw.githubusercontent.com/your-org/extensions/main/manifest.json - Local folder: Use Browse in Repository Settings to point to a local folder during development.
- Any web server: Host files on HTTP(S). Relative paths in
filesresolve against theinfo.jsonURL.
Installation Path
Installed plugins are stored at:
~/Documents/Serial Studio/Extensions/plugin/your-plugin/
Comments