Skip to content

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

  1. Raw bytes from the serial port are hex-encoded and fed into a ring buffer + byte histogram.
  2. The driver auto-detects framing (lines-lf, lines-crlf, stx-etx, fixed:N, ascii-printable, binary).
  3. 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.
  4. Local CRC/XOR heuristics validate every new frame against the hypothesis and report a match ratio. When it stabilises, the AI calls confirmHypothesis and exportConnector to generate a ready-to-use connector .js file.

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.

ActionArgsDescription
listPortsEnumerate 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.
closePortClose 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

PropertyTypeAccessDescription
connectedboolRWhether a serial port is open
portPathstringRActive serial port path (empty when closed)
baudRateintRActive baud rate (0 when closed)
availablePortsarrayRSerial ports discovered on the host
linkSettingsobjectRActive link-layer options (dataBits, parity, stopBits, delimiter, timeoutMs)
bytesReceivedintRTotal bytes received on the current port (resets on openPort)
bytesSentintRTotal bytes sent on the current port (resets on openPort)
framingstringRDetected framing mode
alphabetstringRascii-printable / binary / mixed
printableRatiodoubleRFraction of printable bytes (0..1)
candidateDelimitersobjectRTop recurring byte values that may be delimiters
recentFramesobjectRUp to 30 most-recent frames, deduplicated with counts
captureLabelsobjectRNames of stored captures
currentQuestionstringROpen question for the operator (empty when none)
hypothesisStatusstringRnone / testing / confirmed
hypothesisMatchRatiodoubleRFraction of frames that match the active hypothesis

Investigation Actions

ActionArgsDescription
capture{ label, durationMs? }Start a named capture window (default 5000 ms)
stopCaptureStop the active capture immediately
listCapturesList 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
confirmHypothesisMark 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].
  • durationMs1500.
  • reopenAt"original" (reopen at whatever baud was active before scanning).

reopenAt accepts:

  • "original" — the baud that was active when scanBaud was 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 _framingMode for 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, or null if 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:

  • portPath and baudRate from the currently open port — exportConnector throws if no port is open, so the emitted config is never under-specified.
  • framing from the confirmed hypothesis, or the driver's best auto-detection if no hypothesis was registered.
  • notes from the hypothesis (or "(no notes)").

The starter code is deliberately minimal:

  • driver defaults to "SerialMonitor"; pass driverName to target another serial-speaking driver.
  • Only a single sendLine method 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.

StreamDescription
trafficUnified RX/TX log, auto hex-or-ASCII per chunk, prefixed with (received) or (sent). Best default for a single terminal.
rxHexReceived bytes as spaced uppercase hex per chunk (e.g. 4B 65 69 74 68 6C 65 79)
rxTextReceived bytes as printable ASCII, with C-style escapes for control bytes (\r, \n, \t, \xNN)
txHexSent bytes as spaced uppercase hex
txTextSent bytes as printable ASCII (same escape scheme as rxText)
framesParsed frames as they are emitted by the framer
eventsLifecycle 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:

  1. Link layer setup — if connected is false, call listPorts and openPort (ask the operator via askUser when ambiguous). Switch ports/baud any time via setPort, setBaudRate, or another openPort.
  2. Identify — ask for brand/model/connector/manual via askUser. If a known protocol is named, skip ahead.
  3. Baud probescanBaud({ candidates: [...], reopenAt: "recommended" }) if the baud rate is uncertain.
  4. Passive observecapture({ label: "idle", durationMs: 8000 }) and read framing / alphabet / candidateDelimiters.
  5. Stimulus pairs — use askUser to have the operator trigger events, then capture and compareCaptures to isolate relevant frames.
  6. Probe — try stimulus({ send: "*IDN?\n", waitMs: 300 }) or stimulus({ sendHex: "01 03 00 00 00 0A C5 CD", waitMs: 300 }) for Modbus.
  7. Hypothesisehypothesize({ framing }); the driver validates every new frame and exposes hypothesisMatchRatio.
  8. Confirm & export — when ratio ≥ 0.95 over ≥ 50 frames, call confirmHypothesis and exportConnector({ name }). The returned { suggestedPath, code } can be written to workspace/connectors/<name>.js via your file tools. The generated config bakes in the portPath and baudRate that 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

javascript
// 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:

javascript
export default {
  driver: "SerialProbe",
  config: { port: "COM3", baudRate: 9600 },
};

Safety Notes

  • SerialProbe writes to the device whenever send, sendHex, or stimulus is called. Ask the operator before probing unknown hardware — some protocols interpret arbitrary bytes as firmware-erase commands.
  • openPort / setPort / setBaudRate / scanBaud all close the active transport first. If another connector is sharing the port, it will lose its connection.
  • The generated connector from exportConnector uses SerialMonitor as its driver by default; override with driverName if you want something else.

Muxit — Hardware Orchestration Platform