Skip to content
View as Markdown

Saleae Logic ​

Driver for Saleae Logic analysers — Logic 4, Logic 8, Logic Pro 8 and Logic Pro 16. A single connector arms captures, attaches protocol analyzers, and writes the decoded results to disk.

Muxit does not talk to the USB device directly. Saleae's supported automation surface is a gRPC API served by the Logic 2 desktop application, and this driver is a client of it via the official logic2-automation package. Logic 2 therefore has to be running on the same machine as MuxitServer.

Tier-2 free driver — distributed as a .muxdriver Python package.

Prerequisites ​

  1. Install Saleae Logic 2 on the machine running MuxitServer.
  2. In Logic 2, turn on Preferences → Automation → Enable automation server. The default port is 10430.
  3. Leave Logic 2 running — or set config.launch = true and the driver starts it for you.

The logic2-automation Python package installs itself into the driver's per-driver venv on first activation; you do not need to pip install anything yourself.

Connector config ​

javascript
export default {
  driver: "Saleae Logic",
  config: {
    // Connection
    address: "127.0.0.1",   // Optional. Logic 2 host. Default 127.0.0.1.
    port: 10430,            // Optional. Automation server port.
    connect_timeout_seconds: 20,
    launch: false,          // Optional. Start Logic 2 instead of connecting to it.
    // application_path: "C:/Program Files/Logic/Logic.exe",  // with launch: true

    // Device
    device_id: "",          // Optional. Blank = let Logic 2 pick the device.

    // Where relative export paths land
    output_directory: "captures",

    // Staged capture settings (all writable at runtime as properties too)
    digital_channels: [0, 1],
    analog_channels: [],
    digital_sample_rate: 12000000,
    duration_seconds: 1.0,
  },
  methods: {
    capture: { fn: (s = 1.0) => driver.capture({ duration_seconds: s }) },
  },
};

Every key in the staged capture configuration may be seeded from config and changed later as a property, so a connector can ship a ready-to-run setup without a script.

Capture lifecycle ​

start_capture() arms the device and returns immediately. A background thread inside the driver does the blocking wait, so a ten-minute capture is just a connector sitting in capture_state == "running" rather than an RPC that times out. Watch capture_state, or subscribe to the capture stream.

idle ──start_capture()──▶ running ──▶ complete ──close_capture()──▶ idle
                             │
                             └──▶ error        (read capture_error)

load_capture() ──▶ loaded ──close_capture()──▶ idle

Three capture modes, selected with the capture_mode property or the mode argument to start_capture():

ModeEnds whenNotes
timedduration_seconds elapsesThe default.
manualyou call stop_capture()Runs indefinitely until stopped.
digital_triggerthe trigger fires and after_trigger_seconds elapsesUses trigger_channel, trigger_type and the pulse-width bounds.

Saleae's API forbids mixing stop() and wait() on one capture, so stop_capture() only works on a manual capture — timed and triggered captures end on their own, and calling stop_capture() on them raises.

Analyzers and exports only work once capture_state is complete or loaded; calling them on a running capture raises with that reason. Always close_capture() when you're done — an open capture stays as a tab in Logic 2 and keeps its samples in memory.

Quick examples ​

Record two seconds and decode UART ​

javascript
const logic = connector("logic");

await logic.capture({ duration_seconds: 2.0 });   // start + wait
await logic.add_analyzer({
  name: "Async Serial",
  label: "uart",
  settings: { "Input Channel": 0, "Bit Rate (Bits/s)": 115200 },
});
const { path } = await logic.export_data_table({ path: "uart.csv" });
log.info(`decoded frames written to ${path}`);
await logic.close_capture();

Analyzer names and settings keys must match the Logic 2 UI exactly — the application rejects anything else.

A long capture, without blocking ​

capture() blocks, and the connector RPC timeout is 30 seconds. For anything longer, start it and poll:

javascript
const logic = connector("logic");

logic.duration_seconds = 300;                     // five minutes
await logic.start_capture();

while (logic.capture_state === "running") {
  await delay(1000);
}
if (logic.capture_state === "error") throw new Error(logic.capture_error);

await logic.export_raw_csv({ directory: "long-run" });
await logic.close_capture();

Trigger on a falling edge ​

javascript
const logic = connector("logic");

logic.digital_channels = [0, 1, 2];
logic.capture_mode = "digital_trigger";
logic.trigger_channel = 2;
logic.trigger_type = "falling";
logic.after_trigger_seconds = 0.5;

await logic.start_capture();
// ... make the event happen ...
await logic.save_capture({ path: "glitch.sal" });

The trigger channel has to be one of the recorded digital_channels; the driver rejects the capture up front if it isn't.

Stop a manual capture from a dashboard button ​

javascript
await connector("logic").start_capture({ mode: "manual" });
// later, from another script or a button:
await connector("logic").stop_capture();

Properties ​

Connection and identity (read-only) ​

PropertyTypeDescription
connectedboolTrue while the gRPC connection to Logic 2 is up
app_versionstringLogic 2 application version
api_versionstringAutomation API version reported by Logic 2
device_typestringModel of the selected device, e.g. LOGIC_PRO_16
device_countintNumber of Saleae devices Logic 2 currently sees

device_id (string, R/W) selects the device captures run on. Blank lets Logic 2 choose. Writing an unattached serial raises and lists what is attached.

Capture lifecycle (read-only) ​

PropertyTypeDescription
capture_statestringidle, running, stopping, complete, loaded or error
capture_errorstringFailure reason when capture_state is error, blank otherwise
capture_openboolTrue while a capture tab is open and can be exported

Staged capture configuration ​

All read/write. They are read when start_capture() runs; changing them afterwards does not affect a capture already in flight.

PropertyTypeUnitDescription
digital_channelsint[]—Digital channel indices to record
analog_channelsint[]—Analog channel indices to record
digital_sample_rateintHzDigital sample rate; 0 lets Logic 2 pick
analog_sample_rateintHzAnalog sample rate; 0 lets Logic 2 pick
digital_threshold_voltsdoubleVLogic threshold (Logic Pro only); 0 uses the device default
buffer_size_megabytesintMBCapture buffer size; 0 uses the Logic 2 setting
capture_modestring—timed, manual or digital_trigger
duration_secondsdoublesRecording length for timed
trim_data_secondsdoublesKeep only the last N seconds; 0 keeps everything
trigger_channelint—Digital channel the trigger watches
trigger_typestring—rising, falling, pulse_high or pulse_low
after_trigger_secondsdoublesRecording length after the trigger fires; 0 uses the Logic 2 default
min_pulse_width_secondsdoublesLower bound for pulse triggers; 0 disables it
max_pulse_width_secondsdoublesUpper bound for pulse triggers; 0 disables it

0 is the "leave it to Logic 2" sentinel throughout: those fields are omitted from the request rather than sent as a zero, and read back as 0.

At least one of digital_channels / analog_channels must be non-empty — starting a capture with neither raises.

Results ​

PropertyTypeAccessDescription
analyzersstring[]RLabels of the analyzers currently on the capture
output_directorystringR/WDirectory relative export paths resolve against
last_export_pathstringRPath written by the most recent export
last_capture_pathstringRPath of the most recently saved or loaded .sal

Actions ​

Discovery ​

ActionArgumentsReturns
list_devicesinclude_simulation_devices[{ device_id, device_type, is_simulation }]
app_info—{ app_version, api_version, app_pid }
reconnect—{ connected, app_version, api_version }

Capture ​

ActionArgumentsReturns
start_capturemode, duration_seconds, device_id{ state, mode, device_id, duration_seconds }
captureduration_seconds{ state, duration_seconds, timed_out }
wait_capturetimeout_seconds (default 25){ state, timed_out, error }
stop_capture—{ state }
close_capture—{ state, closed }
save_capturepath{ path }
load_capturepath{ path, state }

wait_capture timing out is not an error: the capture keeps running and timed_out: true says so. Relative paths resolve against output_directory, and missing parent directories are created.

Analyzers ​

ActionArgumentsReturns
add_analyzername, label, settings{ label, name }
add_high_level_analyzerextension_directory, name, input_label, label, settings{ label, name, input_label }
remove_analyzerlabel{ label, removed }

label defaults to the analyzer name and is how every later call refers to it. Setting values must be strings, numbers or booleans. Analyzers already present in a capture loaded from a .sal file are not visible to the automation API — that is a Logic 2 limitation, not a driver one.

Exports ​

ActionArgumentsReturns
export_raw_csvdirectory, analog_channels, digital_channels, analog_downsample_ratio, iso8601_timestamp{ directory }
export_raw_binarydirectory, analog_channels, digital_channels, analog_downsample_ratio{ directory }
export_data_tablepath, labels, radix, columns, iso8601_timestamp{ path, analyzers }
export_analyzerpath, label, radix{ path, label }

Omitting analog_channels / digital_channels exports whatever was captured; passing an empty array exports nothing. radix is one of hexadecimal (default), decimal, binary or ascii. export_data_table defaults to every analyzer added.

Glitch filters ​

set_glitch_filters({ filters }) takes a list of { channel_index, pulse_width_seconds } and applies it to the next capture. Pass an empty list to clear them. They can also be seeded from config.glitch_filters.

Streams ​

capture — one event per lifecycle transition:

json
{ "event": "started",  "state": "running",  "mode": "timed", "duration_seconds": 2.0 }
{ "event": "complete", "state": "complete", "mode": "timed" }
{ "event": "error",    "state": "error",    "mode": "timed", "error": "..." }
{ "event": "loaded",   "state": "loaded",   "mode": "",      "path": "..." }
{ "event": "closed",   "state": "idle",     "mode": "" }

Use it in a script with on("logic:capture", frame => ...) when you'd rather react than poll.

Limitations ​

  • Logic 2 must run on the same machine as MuxitServer. The automation API is loopback-only in practice, and launch starts a local process.
  • One capture at a time per connector. Add a second connector (pointing at a second device_id) to drive two analysers in parallel.
  • The connector RPC timeout is 30 seconds, so capture() and wait_capture() cap their wait at 25. Longer captures work fine — start them non-blocking and poll capture_state.
  • Sample data is exported to files rather than returned inline: a multi-second capture is far too large to move through a connector call.
  • Analyzers inside a loaded .sal file can't be read back through the API.

Muxit — Hardware Orchestration Platform