SerialProbe
Turn an unknown serial device into a working Muxit connector with AI assistance.
SerialProbe is a Tier 1 JavaScript driver bundled with Muxit. It attaches to a serial port in raw-byte mode, runs local heuristics on the traffic, and exposes a compact action set that the AI follows step-by-step via its built-in ai.instructions. The AI never sees the raw byte stream — only summaries returned by local analysis — so a full reverse-engineering session stays cheap.
Typical targets: a thermometer with an undocumented ASCII line protocol, a JBC-style soldering station with a binary frame format, any USB-serial instrument without an obvious manual.
How It Works
- Raw bytes from the serial port are hex-encoded and fed into a ring buffer + byte histogram.
- The driver auto-detects framing (
lines-lf,lines-crlf,stx-etx,fixed:N,ascii-printable,binary). - The AI drives the session: picks a port, asks the user to trigger events, labels captures, diffs them, probes with
stimulus(), and registers framing hypotheses. - Local CRC/XOR heuristics validate every new frame against the hypothesis and report a match ratio. When it stabilises, the AI calls
confirmHypothesisandexportConnectorto generate a ready-to-use connector.jsfile.
Runtime Port Configuration
The port, baud rate, and other link-layer settings are configured at runtime via actions — the connector config does not need to pre-create a serial transport. This lets the AI (or you from a dashboard) freely test different configurations: switch ports, try different baud rates, change parity, without editing files.
| Action | Args | Description |
|---|---|---|
listPorts | — | Enumerate serial ports visible to the host |
openPort | { portPath, baudRate?, dataBits?, parity?, stopBits?, delimiter?, timeoutMs? } | Close any active port and open a new one. Resets counters, histogram and recentFrames. |
closePort | — | Close the active serial port |
setBaudRate | { baudRate } | Re-open the current port at a new baud rate |
setPort | { portPath, baudRate? } | Switch to a different serial port (optionally at a new baud) |
scanBaud | { portPath?, candidates?, durationMs?, reopenAt? } | Close the port, probe candidate baud rates, reopen at "original" (default), "recommended", or a numeric baud |
Configs start disconnected — call openPort to connect. To pin a starting port, set config.port (and optionally config.baudRate) — see the example at the end of this page.
Properties
| Property | Type | Access | Description |
|---|---|---|---|
connected | bool | R | Whether a serial port is open |
portPath | string | R | Active serial port path (empty when closed) |
baudRate | int | R | Active baud rate (0 when closed) |
availablePorts | array | R | Serial ports discovered on the host |
linkSettings | object | R | Active link-layer options (dataBits, parity, stopBits, delimiter, timeoutMs) |
bytesReceived | int | R | Total bytes received on the current port (resets on openPort) |
bytesSent | int | R | Total bytes sent on the current port (resets on openPort) |
framing | string | R | Detected framing mode |
alphabet | string | R | ascii-printable / binary / mixed |
printableRatio | double | R | Fraction of printable bytes (0..1) |
candidateDelimiters | object | R | Top recurring byte values that may be delimiters |
recentFrames | object | R | Up to 30 most-recent frames, deduplicated with counts |
captureLabels | object | R | Names of stored captures |
currentQuestion | string | R | Open question for the operator (empty when none) |
hypothesisStatus | string | R | none / testing / confirmed |
hypothesisMatchRatio | double | R | Fraction of frames that match the active hypothesis |
Investigation Actions
| Action | Args | Description |
|---|---|---|
capture | { label, durationMs? } | Start a named capture window (default 5000 ms) |
stopCapture | — | Stop the active capture immediately |
listCaptures | — | List captures with byte/frame counts |
clearCapture | { label } | Delete a stored capture |
compareCaptures | { labelA, labelB } | Diff two captures — returns frames unique to each |
analyzeFrames | { label } | Local heuristics: length histogram + CRC-16/Modbus + XOR-trailing checks |
stimulus | { send?, sendHex?, waitMs, label? } | Send bytes and capture the response window |
askUser | { question, choices? } | Register a question for the operator |
answerUser | { answer } | Provide an answer to the open question |
hypothesize | { framing, notes? } | Register a parser hypothesis to validate against incoming frames |
confirmHypothesis | — | Mark current hypothesis as confirmed |
exportConnector | { name, driverName? } | Generate a connector .js source from the confirmed hypothesis. Requires an open port. |
send | { text } | Send raw text |
sendHex | { hex } | Send a hex string (spaces allowed: AA 55 01) |
Supported framing modes: lines-lf, lines-crlf, stx-etx, fixed:N (e.g. fixed:16), modbus-rtu, ascii-printable, binary.
The tables above are summary rows. Each action and property also ships extended docs via the descriptor details layer — visible in the Drivers → SerialProbe page (click Show details on any row), in the script editor hover popup, and in the on-demand get_connector_schema / get_driver_schema response. The five subtle actions — scanBaud, hypothesize, analyzeFrames, stimulus, exportConnector — are reproduced below.
Action Reference
The detail copy below is the source of truth shared by the driver's details descriptor field, the driver doc viewer, the Monaco IntelliSense hover, and the AI's full-schema response.
scanBaud
scanBaud({ portPath?, candidates?, durationMs?, reopenAt? })Detaches the current transport, opens portPath at each baud in candidates for durationMs (default 1500 ms), then reopens at the chosen baud. Results are ranked by printableRatio descending, with total bytes as the tiebreaker.
Defaults:
portPath— the currently open port. Throws if no port is open and none is supplied.candidates—[9600, 19200, 38400, 57600, 115200].durationMs—1500.reopenAt—"original"(reopen at whatever baud was active before scanning).
reopenAt accepts:
"original"— the baud that was active whenscanBaudwas called."recommended"— the top-ranked candidate (highest printable ratio).- any number — an explicit baud to reopen at.
If every candidate errors, recommended falls back to originalBaud. If the final reopen fails, the port is left closed and a warning is logged — check connected before your next call.
hypothesize
hypothesize({ framing, notes? })Registers the hypothesis, resets match counters, and sets hypothesisStatus to "testing". Every subsequent frame updates hypothesisMatchRatio. Calling hypothesize again replaces the current hypothesis (counters reset).
framing has two categories:
- Extractable modes — change how the framer slices raw bytes into frames:
lines-lf,lines-crlf,stx-etx,fixed:N(e.g.fixed:8),ascii-printable,none. Setting one of these switches_framingModefor live extraction. - Annotations — metadata only, do not change extraction:
modbus-rtu(also triggers CRC-16 verification when computing the match ratio), and free-form strings for your own notes.
Use notes for anything the annotation alone doesn't capture (slave ID, expected payload, manual reference). Call confirmHypothesis once hypothesisMatchRatio is high enough over a meaningful number of frames.
analyzeFrames
analyzeFrames({ label })Returns a summary object with frameCount, uniqueFrames, lengthHistogram, checksumCandidates, and up to 5 sampleFrames.
Checksum heuristics (non-configurable):
crc16Modbus— assumes the last 2 bytes are CRC-16/Modbus in little-endian, over the preceding bytes.xorTrailing— assumes the last byte is the XOR of the preceding bytes.
A ratio near 1.0 means the assumption holds on almost every frame — strong signal that framing and checksum are right. Ratios near 0 rule it out. Frames shorter than 4 bytes are skipped (the checksum fields would be returned as null if no frames qualified).
There is no way to configure an alternate CRC polynomial or position from this action — if your protocol uses a different checksum, treat both candidates as null and validate manually.
stimulus
stimulus({ send?, sendHex?, waitMs, label? })Opens a capture window for waitMs, sends the stimulus, then waits slightly longer (waitMs + 60) before returning the captured response.
Pass exactly one of send or sendHex:
send: "*IDN?\n"— text with JS escape sequences; written as-is.sendHex: "01 03 00 00 00 0A"— hex string, whitespace-tolerant; each pair becomes one byte.
label defaults to stim-<timestamp> when omitted; the capture is stored under that name and is visible via listCaptures.
Return shape (truncated for token discipline):
frames— first 8 parsed frames only.raw— concatenated hex of every received chunk, clipped to 256 characters (128 bytes).latencyMs— time from capture start to first frame, ornullif no frames arrived.
Call clearCapture(label) if you need to reuse a label or reclaim memory.
exportConnector
exportConnector({ name, driverName? })Returns a string of JavaScript suitable for saving to workspace/connectors/<name>.js. This action does not write the file — it only returns the source. Use the file tools or the UI to save it.
The generated config bakes in current state:
portPathandbaudRatefrom the currently open port —exportConnectorthrows if no port is open, so the emitted config is never under-specified.framingfrom the confirmed hypothesis, or the driver's best auto-detection if no hypothesis was registered.notesfrom the hypothesis (or"(no notes)").
The starter code is deliberately minimal:
driverdefaults to"SerialMonitor"; passdriverNameto target another serial-speaking driver.- Only a single
sendLinemethod is emitted. Expect to extend the connector by hand with device-specific properties and actions before using it in production.
Streams
Patch any of these into a Terminal dashboard widget to watch traffic live. Each chunk is emitted as one line.
| Stream | Description |
|---|---|
traffic | Unified RX/TX log, auto hex-or-ASCII per chunk, prefixed with ← (received) or → (sent). Best default for a single terminal. |
rxHex | Received bytes as spaced uppercase hex per chunk (e.g. 4B 65 69 74 68 6C 65 79) |
rxText | Received bytes as printable ASCII, with C-style escapes for control bytes (\r, \n, \t, \xNN) |
txHex | Sent bytes as spaced uppercase hex |
txText | Sent bytes as printable ASCII (same escape scheme as rxText) |
frames | Parsed frames as they are emitted by the framer |
events | Lifecycle events: portOpened:<path>@<baud>, portClosed:<path>, portChanged:<path>@<baud>, captureStarted:<label>, captureStopped:<label>, questionOpened, questionAnswered |
AI Workflow (built-in)
The driver's meta.ai.instructions walks the AI through:
- Link layer setup — if
connectedis false, calllistPortsandopenPort(ask the operator viaaskUserwhen ambiguous). Switch ports/baud any time viasetPort,setBaudRate, or anotheropenPort. - Identify — ask for brand/model/connector/manual via
askUser. If a known protocol is named, skip ahead. - Baud probe —
scanBaud({ candidates: [...], reopenAt: "recommended" })if the baud rate is uncertain. - Passive observe —
capture({ label: "idle", durationMs: 8000 })and readframing/alphabet/candidateDelimiters. - Stimulus pairs — use
askUserto have the operator trigger events, thencaptureandcompareCapturesto isolate relevant frames. - Probe — try
stimulus({ send: "*IDN?\n", waitMs: 300 })orstimulus({ sendHex: "01 03 00 00 00 0A C5 CD", waitMs: 300 })for Modbus. - Hypothesise —
hypothesize({ framing }); the driver validates every new frame and exposeshypothesisMatchRatio. - Confirm & export — when ratio ≥ 0.95 over ≥ 50 frames, call
confirmHypothesisandexportConnector({ name }). The returned{ suggestedPath, code }can be written toworkspace/connectors/<name>.jsvia your file tools. The generated config bakes in theportPathandbaudRatethat were active when the hypothesis was confirmed.
The AI is instructed never to dump raw traffic into chat — compareCaptures and analyzeFrames return already-summarised JSON, and recentFrames is capped at 30 entries with duplicate counts. This keeps a full reverse-engineering session well under a few tens of thousands of input tokens.
Example Connector
// workspace/connectors/serial-probe.js
export default {
driver: "SerialProbe",
poll: [
"connected",
"portPath",
"baudRate",
"bytesReceived",
"framing",
"currentQuestion",
"hypothesisStatus",
"hypothesisMatchRatio",
],
};Open the connector, then in the AI chat say: "Help me figure out this device's protocol." The AI picks up the ai.instructions, calls listPorts / openPort, and drives the session.
If you prefer to pin a starting port, add port and baudRate to config:
export default {
driver: "SerialProbe",
config: { port: "COM3", baudRate: 9600 },
};Safety Notes
- SerialProbe writes to the device whenever
send,sendHex, orstimulusis called. Ask the operator before probing unknown hardware — some protocols interpret arbitrary bytes as firmware-erase commands. openPort/setPort/setBaudRate/scanBaudall close the active transport first. If another connector is sharing the port, it will lose its connection.- The generated connector from
exportConnectorusesSerialMonitoras its driver by default; override withdriverNameif you want something else.