# BMCLI Command Reference

BMCLI is a command-line interface for BUSMUST CAN FD / LIN analyzers. It communicates with hardware through the BMAPI library.

**Invocation**: `bmcli <category> <action> [options]`

**Global options**:

| Option | Description |
|--------|-------------|
| `--format=<FMT>` | Output format: `text` (default), `json`, `jsonl`, `csv` |
| `--to=<ADDR:PORT>` | Target daemon address. Default: `127.0.0.1:4502`. For remote: `192.168.1.100:4502` |
| `--verbose` | Print detailed diagnostic output |
| `--help`, `-h` | Show usage |
| `--version`, `-v` | Show version |

## Help and documentation contract

The installed executable is authoritative for current command discovery,
syntax, option names, required/default values, units, aliases, and short safety
notes:

```bash
bmcli help
bmcli help device
bmcli help channel autoset
bmcli channel autoset --help
bmcli help channel autoset --format=json
```

Use JSON Help when an AI or script needs the exact contract of the installed
version. This Reference adds detailed behavior, output schemas, protocol
semantics, workflows, and extended examples; it must agree with Help but is not
a second independently maintained option inventory. The Linux man page only
points to local Help and this Reference and contains no independent command
facts.

**Common conventions**:

| Concept | Format | Examples |
|---------|--------|---------|
| Channel | `--channel=<SPEC>` | See **Channel Specification** below |
| CAN ID | `--message=<ID>` or `--id=<ID>` | `0x123` (hex), `0x18FEDC01` (29-bit ext), `299` (decimal) |
| Flags | `--flags=<FLAGS>` | `fdf+brs+ide+rtr` (plus-separated) |
| Data | `--data=<HEX>` | `"AA BB CC DD"` (space-separated hex bytes) |
| Bitrate | `--bitrate=<N>` | `500`, `500K`, `500000`, and `500,000` all mean 500 kbps; `1M` and `1000000` mean 1000 kbps |
| Mode | `--mode=<MODE>` | `normal`, `listenonly`, `loopback`, `internal_loopback` |
| Term. resistor | `--tres=<VAL>` | `120`, `off` |

### Structured output and exit contract

Use `--format=json` for bounded commands and `--format=jsonl` for streams.

- A bounded JSON command writes exactly one JSON document to stdout.
- Successful documents contain `"status":"ok"`; failed documents contain
  `"status":"error"` and an actionable `message` or protocol-specific detail.
- JSONL writes one object per line. Progress and warning records use an `event`
  field while retaining `status`; CAN receive records are independently
  parseable objects.
- Non-fatal human notes and warnings may be written to stderr and do not corrupt
  JSON stdout.
- Process exit code `0` means the requested command completed successfully.
  Exit code `1` covers usage, configuration, environment, transport, hardware,
  timeout, and negative UDS response failures. Inspect JSON fields such as
  `nrc`, `reason`, and `message` to classify the failure instead of relying on
  additional exit-code values.
- A successful inspection can report a domain state such as `DEGRADED`; a
  validation with missing required values reports `status=error` and exits 1.

Existing fields keep their meaning within a major product version. New fields
may be added, so consumers must ignore unknown members. CAN IDs are hexadecimal
strings, booleans are JSON booleans, and units are carried in field names such
as `_kbps`, `_ms`, and `_seconds`.

### Channel Specification

The `--channel` parameter supports three forms:

| Form | Syntax | Example | Description |
|------|--------|---------|-------------|
| **SN/port** | `SN/PORT` | `10356/0` | Device serial number + port number |
| **UID/port** | `UID/PORT` | `012345678901/0` | Device UID (12 hex digits) + port number |
| **Legacy** | `N` | `0`, `0,1`, `0-3` | Enumerate index (backward compatible) |

**Auto-detection rules** (for the part before `/`):
- All digits, length 12 → UID
- All digits, length 1-11 (typically 4-6) → SN
- No `/` found → legacy numeric index

**Recommendation:** use SN/port in scripts, AI instructions, saved bench profiles,
and customer documentation. A legacy numeric index is suitable only for
short-lived interactive work after inspecting the current enumeration result.

**Why SN/port?** USB enumeration order is OS-dependent — the same device may appear at different indices across `BM_Enumerate()` calls. SN/port identification ensures deterministic device selection regardless of enumeration order.

```bash
# Open specific device by SN + port
bmcli daemon start --channel=10356/0,10356/1
bmcli channel open --channel=10356/0 --nbitrate=500k

# Multi-device: open ports on different devices
bmcli daemon start --channel=10356/0,10357/0

# Legacy (still supported, order-dependent)
bmcli daemon start --channel=0,1
```

---

## Execution Modes

| Mode | When | Channel open/close | Latency |
|------|------|--------------------|---------|
| **Standalone** | Daemon not running | Per-command (~2.75s overhead) | Slow |
| **Daemon** | Daemon running | Once at startup | ~50ms |

When a daemon is running, commands that support persistent execution are
forwarded to it. Some discovery, file-only, Help, update, and health commands
remain local; some stateful operations require the daemon. Consult command Help
for the installed-version execution requirement. The daemon avoids repeated
USB open/close and preserves channel/protocol state.

If no live daemon PID exists, BMCLI may execute the command in standalone mode.
If a daemon PID is live but TCP communication fails, BMCLI returns a non-zero
IPC error and does **not** fall back to standalone. This prevents an enumeration
index such as `0` from resolving to a different physical device while the daemon
holds the originally selected device exclusively. Connection attempts may be
retried only before any command bytes are sent; BMCLI never automatically
re-sends a command that may perform a hardware action.

**Explicit start**: BMCLI does not automatically create a daemon. Start it with
`bmcli daemon start` when a workflow needs persistent channel state, and stop
only a daemon owned by that workflow.

---

## doctor

Check the BMCLI runtime environment, including the BMCLI version, loaded BMAPI
DLL path and version, compile-time BMAPI header version, compiler, operating
system, and connected hardware.

```bash
bmcli doctor
bmcli doctor --channel=10356/0
```

With `--channel`, the command also opens the selected channel and dumps its live
mode, bitrate, terminal resistor, RX filters, CAN status, TX tasks, routes,
logging, and replay state. A DLL/header version mismatch is reported prominently
so that the deployed `libbmapi.dll` can be replaced before hardware tests.

---

## AI integration

BMCLI ships one portable `bmcli` skill. Claude Code and Codex use the same
`SKILL.md` and automotive workflow references; Codex additionally receives
`agents/openai.yaml` for its own interface metadata.

```bash
bmcli ai setup
bmcli ai setup --target=codex
bmcli ai setup --target=claude
bmcli ai status --format=json
```

`ai setup` is offline and idempotent. It copies the bundled skill to
`$AGENTS_HOME/skills/bmcli` (default `~/.agents/skills/bmcli`) and
`$CLAUDE_CONFIG_DIR/skills/bmcli` (default `~/.claude/skills/bmcli`). It does
not install BMCLI itself and does not modify project ECU/bench configuration.

The skill queries `bmcli help <category> <action> --format=json` at runtime.
Command syntax and option defaults come from the installed binary, while the
bundled references provide environment, development, verification/validation,
and debugging methodology.

The skill uses two project files when initializing a test environment:

- `.bmcli/ecu.json`: portable ECU/network/UDS intent; may be versioned.
- `.bmcli/bench.local.json`: analyzer SN/port and local paths; gitignore it.

Explicit user values override project configuration, which overrides local
bench values, which override authorized discovery. Missing DBC data degrades to
raw CAN IDs/payloads; missing safety-critical or state-changing values must not
be guessed.

---

## bench

`bench` is the first-party configuration layer shared by users, AI harnesses,
shell scripts, and Python automation. It separates portable ECU/network intent
from machine-local analyzer identity and paths:

- `.bmcli/ecu.json` may be versioned with the project.
- `.bmcli/bench.local.json` is local and is ignored by the generated
  `.bmcli/.gitignore`.

```bash
bmcli bench init --ecu=body-controller --variant=production-a \
  --channel=10356/1 --nbitrate=500 --dbitrate=2M --tres=120 \
  --reqid=0x791 --respid=0x799 --dbc=network/body.dbc \
  --validated-by=enumerate+autoset+uds-discover
bmcli bench show --format=json
bmcli bench validate --format=json
bmcli bench validate --hardware --format=json
```

`bench init` writes only explicit values and preserves omitted existing values.
It rejects order-dependent numeric channel indices. It does not enumerate,
AutoSet, transmit a functional UDS request, or stop/start the daemon; perform
those separately with the applicable authorization, then persist confirmed
results. `--force` is required to replace malformed existing JSON.

`bench validate` checks required values and referenced local files. With
`--hardware`, it also enumerates analyzers and requires the configured stable
channel to be present. Its state is `READY`, `DEGRADED` (for example, no DBC but
raw CAN remains possible), or `NEEDS_INPUT`; required-value, stale-path, JSON,
and hardware mismatch failures return nonzero.

---

## Installation and update

The first non-empty line of the bundled `changelist.txt` is the sole customer
product version source (for example, `1.0.0`). Archive names, website release
directories, `VERSION`, and manifest `version` use that three-part value.
Bamboo build number and Git commit remain separate traceability fields in
`BUILD_INFO.json`, the release manifest, and `bmcli version --format=json`;
they are not appended as a fourth or fifth public version segment.

Recommended website bootstrap commands:

```powershell
irm https://download.busmust.com/bmcli/install.ps1 | iex
```

```bash
curl -fsSL https://download.busmust.com/bmcli/install.sh | sh
```

The installer downloads the platform artifact from the release manifest,
verifies SHA-256, stages a versioned copy, updates the active runtime, adds the
binary directory to the user PATH, and runs `bmcli ai setup`. The Windows MinGW
package contains only `bmcli.exe` and `libbmapi.dll`; the GCC and C++ runtimes
are linked statically. It does not contain the MSVC `BMAPI.dll`.

```bash
bmcli version
bmcli update
bmcli update --version=1.0.0
bmcli update --dry-run --format=json
```

`bmcli update` invokes the bundled local installer, which uses the same verified
manifest path as first installation. A ZIP package remains available for
offline/manual deployment, but users should not manually manage PATH for the
normal online installation.

---

## device

### device enumerate

List all connected BUSMUST CAN FD / LIN analyzer channels.

```bash
bmcli device enumerate
bmcli device enumerate --format=json
bmcli device enumerate --format=csv
```

---

## channel

### channel info

Show channel information. Standalone mode reports static enumeration fields
(name, SN, firmware, VID/PID, capability). Daemon mode additionally reports the
open channel's runtime mode, bitrate, and terminal resistor.

```bash
bmcli channel info                     # all devices
bmcli channel info --channel=0         # by legacy index
bmcli channel info --channel=10356/0   # by SN/port
bmcli channel info --channel=0 --format=json
```

| Option | Description |
|--------|-------------|
| `--channel` | Channel spec (SN/port, UID/port, or legacy index). Omit for all. |

### channel open

Open and activate one or more CAN channels.

```bash
bmcli channel open --channel=10356/0 --nbitrate=500k --dbitrate=2000k
bmcli channel open --channel=10356/0,10356/1 --mode=loopback --tres=120

# Legacy index remains supported, but depends on the current enumeration order
bmcli channel open --channel=0 --nbitrate=500 --dbitrate=2000
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel spec(s): `0`, `10356/0`, `0,1`, `10356/0,10357/0` |
| `--mode` | `normal` | `normal`, `listenonly`, `loopback`, `internal_loopback` |
| `--nbitrate` | `500` | Nominal bitrate. Bare kbps, bps, K/kbps, and M/mbps forms are accepted. |
| `--dbitrate` | `2000` | Data bitrate. Bare kbps, bps, K/kbps, and M/mbps forms are accepted. |
| `--tres` | `120` | Terminal resistor: `120`, `off` |

### channel close

Close a previously opened channel.

```bash
bmcli channel close --channel=0
```

### channel status

Show CAN bus status (TEC, REC, busoff, TX/RX passive/warn).

```bash
bmcli channel status --channel=0
bmcli channel status --channel=0 --format=json
```

JSON output:
```json
{"status":"ok","channel":0,"busoff":0,"txbp":0,"rxbp":0,"txwarn":0,"rxwarn":0,"tec":0,"rec":0}
```

### channel config

Read or modify channel configuration. Dual-mode: `--key` without value = query, `--key=value` = set. No keys = show all.

```bash
# Query: show all config for channel 0
bmcli channel config --channel=0

# Query: specific parameters (no value after =)
bmcli channel config --channel=0 --nbitrate --tres

# Set: single parameter
bmcli channel config --channel=0 --nbitrate=500k --dbitrate=2000k
bmcli channel config --channel=0 --mode=listenonly
bmcli channel config --channel=0 --tres=off

# Set: multiple parameters at once
bmcli channel config --channel=0 --nbitrate=500k --mode=normal --tres=120

# Set: CAN ID filter
bmcli channel config --channel=0 --filter-code=0x100 --filter-mask=0x7FF
```

| Option | Description |
|--------|-------------|
| `--channel` | Channel port (required) |
| `--mode` | `normal`, `listenonly`, `loopback`, `internal_loopback` |
| `--nbitrate` | Nominal bitrate in kbps |
| `--dbitrate` | Data bitrate in kbps |
| `--tres` | Terminal resistor: `120`, `off` |
| `--filter-code` | CAN ID filter code (hex) |
| `--filter-mask` | CAN ID filter mask (hex) |

### channel autoset

Detect the nominal/data bitrate and terminal resistor. AutoSet runs outside the
daemon and requires exclusive hardware access. Stop a running BMCLI daemon
first. Passive mode is the default and only listens; active mode transmits probe
frames and must be explicitly selected on an isolated bench or authorized bus.

```bash
bmcli channel autoset --channel=10356/1 --timeout=10000
bmcli channel autoset --channel=10356/1 --timeout=10000 --format=json
bmcli channel autoset --channel=10356/1 --mode=active --timeout=10000 --format=json
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Stable `SN/port`, UID/port, or legacy index |
| `--mode` | `passive` | `passive`/`listen` observes existing traffic; `active` transmits probes and detects TX ACK evidence |
| `--timeout` | `10000` | Soft deadline, 1-60000 milliseconds. A candidate already showing plausible traffic is allowed to finish verification. |
| `--confirmations` | `3` | Required RX messages in passive mode or TX ACK confirmations in active mode, 3-255 |
| `--observation` | BMAPI default | Passive mode: observation time per candidate in milliseconds |
| `--probe-timeout` | BMAPI default | Active mode: write timeout per probe in milliseconds |
| `--probe-id` | `0x6A5` | Active mode: standard or extended CAN probe ID |
| `--probe-data` | eight zero bytes | Active mode: 1-64 probe payload bytes |

Passive success proves that the selected analyzer port observed valid traffic
at the reported timing. Active success proves that another CAN node acknowledged
repeated probes at the reported timing. Neither mode identifies a specific ECU.
A silent bus can make passive mode fail even when an ECU is connected; active
mode is faster in that case but can affect live network behavior. JSON output
identifies `mode` and `evidence_type`, with either observed-message or
acknowledged-probe counts.

### channel recover

Manually trigger fault recovery on a channel (BUSOFF, USB disconnect, etc.). The daemon also performs this automatically when `auto-recover` is on.

```bash
bmcli channel recover --channel=0
```

### channel auto-recover

Enable or disable automatic fault recovery in daemon mode. Default: on.

```bash
bmcli channel auto-recover on
bmcli channel auto-recover off
bmcli channel auto-recover status
```

### channel reset-defaults

Reset a channel's **runtime** state to factory-neutral. This is the reliable
way to clear device tables that a prior command may have populated:

- RX filters → accept-all (overwrite **every** device slot)
- TX tasks → none (overwrite every slot with `BM_TXTASK_INVALID`)
- message routes → none (overwrite every slot with `type=0`)
- online logging → off
- online replay → off
- CAN mode → NORMAL
- offline config storage → cleared (`BM_ClearConfig`)

```bash
bmcli channel reset-defaults --channel=0
# rxfilters:ok(cap=0) txtasks:ok(cap=64) routes:ok(cap=64) logging:off replay:off mode:normal offlinecfg:cleared
# OK: channel 0 reset to defaults
```

**Why capacity-based overwrite?** Passing `NULL`/`0` to `BM_SetMsgRoutes` /
`BM_SetTxTasks` / `BM_SetRxFilters` is a **no-op** — the BMAPI write loop runs
`for (i = 0; i < n; i++)`, so `n=0` writes nothing and leaves the device table
untouched. The only reliable clear is to overwrite **every** slot the device
actually exposes. BMCLI queries the device's real slot count via
`BM_Control(BM_GET_STAT, BM_STAT_MAX_*)` and writes that many invalid/accept-all
entries. The `(cap=N)` in the response is that queried count:

- `cap>0` — the device reported its real slot count; that many slots were
  overwritten (a true clear).
- `cap=0` — the stat item is unsupported on this firmware; BMCLI fell back to a
  hardware-specific default (see below).

**Fallback defaults when the stat query is unsupported** (`cap=0`). These are
the *real* slot counts of the hardware — writing more than this is an
out-of-bounds write that can corrupt the table, so the defaults MUST match the
hardware, not the (larger) BMAPI internal array sizes:

| Table | Fallback default | Notes |
|-------|------------------|-------|
| RX filters | **2** | Our hardware exposes 2 rxfilter slots. |
| TX tasks | **gen3 = 64, gen2 = 1** | Generation detected from the firmware version major byte (`version[0]==3` → gen3, `==2` → gen2). TX-task stat is normally supported on gen3. |
| Message routes | **4** | Our hardware exposes 4 route slots. |

> To inspect the live device runtime state (the same fields `reset-defaults`
> clears — RX filters, TX tasks, routes, logging/replay config, CAN status),
> use `bmcli doctor --channel=<spec>`, which now dumps all of them. Note:
> `BM_Get*` returns stale slot memory (the device does not zero unused slots),
> so the `active=N` counts are an *upper bound*, not ground truth — a slot that
> held an entry earlier in this power cycle may still read back as active after
> a logical clear. `reset-defaults` is the reliable clear.

> `channel reset-defaults` only resets the channel given by `--channel`. Routes
> and TX tasks are device-global tables, so on a multi-channel device you may
> want to run it on each open channel.

---

## message

### message send

Send CAN/CAN FD messages. Supports raw hex data, multi-frame batch, and DBC signal encoding.

```bash
# Single standard CAN frame
bmcli message send --channel=0 --message=0x123 --data="AA BB CC"

# CAN FD with bitrate switching
bmcli message send --channel=0 --message=0x100 --data="11 22" --flags=fdf+brs

# Extended ID (29-bit)
bmcli message send --channel=0 --message=0x18FEDC01 --flags=ide --data="01 02"

# Batch: 100 frames at 10ms interval (software-timed, blocks until done)
bmcli message send --channel=0 --message=0x100 --count=100 --cycle=10

# DBC signal encoding (uses globally loaded DBC, see `database load`)
bmcli message send --channel=0 --message=EngineStatus --signal=EngineSpeed:1500

# Multiple signals
bmcli message send --channel=0 --message=EngineStatus --signal=EngineSpeed:1500 --signal=CoolantTemp:85
```

**`--signal` format** — all of the following are accepted (separator interchangeable):

| Syntax | Example |
|--------|---------|
| `--signal=Name=Value` | `--signal=EngineSpeed=1500` |
| `--signal=Name:Value` | `--signal=EngineSpeed:1500` |
| `--signal Name=Value` | `--signal EngineSpeed=1500` |
| `--signal Name:Value` | `--signal EngineSpeed:1500` |

Repeat `--signal` for multiple signals. No spaces inside signal values.

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel |
| `--message` / `--id` | (required) | CAN ID or DBC message name |
| `--data` | all zeros | Payload hex bytes |
| `--dlc` | auto | DLC value override |
| `--flags` | DBC metadata or none | `ide`, `fdf`, `brs`, `rtr` (plus-separated). An explicit value overrides DBC frame attributes. |
| `--count` | `1` | Number of frames (software-timed loop) |
| `--cycle` | `0` | Interval between frames in ms |
| `--timeout` | `0` | Write timeout ms. 0 = async |
| `--signal` | — | Signal name/value pair. Repeatable. Requires loaded DBC |

**`--count`/`--cycle` vs `txtask`**: `message send --count --cycle` is a synchronous software-timed loop that blocks the command until all frames are sent. `txtask add` is an asynchronous hardware-timed task running on the device firmware — more precise, non-blocking, and persistent. Use `message send` for one-off batch tests; use `txtask` for long-running periodic messages.

### message recv

Receive CAN messages. Blocks until `--count` reached or `--duration` elapses (or Ctrl+C in standalone).

In **daemon mode**, omitting `--channel` receives from ALL open channels simultaneously. In **standalone mode**, `--channel` is required.

```bash
# Standalone: single channel, exit after 100 frames
bmcli message recv --channel=10356/1 --count=100

# Standalone: multi-channel, 5-second capture
bmcli message recv --channel=10356/0,10356/1 --duration=5

# Daemon: receive from all open channels for 10 seconds
bmcli message recv --duration=10

# With CAN ID filter
bmcli message recv --channel=10356/1 --id=0x100 --mask=0x7FF --duration=5
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | daemon: all open; standalone: required | Channel(s): `SN/port`, comma-separated list, or omit for all open (daemon only) |
| `--count` | `0` (infinite) | Max messages to receive |
| `--duration` | `0` (infinite, Ctrl+C to stop) | Wall-clock capture duration in seconds. Daemon mode defaults to 10s if neither count nor duration is given. |
| `--id` | none | CAN ID filter (software-filtered) |
| `--mask` | `0x7FF` | CAN ID filter mask |
| `--format` | `text` | `text`, `json` (array), `jsonl` (one JSON per line), `csv` |

**Note**: `message send` uses `--timeout` (ms, ACK wait); `message recv` uses `--duration` (seconds, capture window). Different parameters, different units, different semantics.

---

## txtask

Periodic transmit tasks running on device firmware (hardware-timed, precise). Max 64 tasks per channel.

### txtask add

```bash
# Fixed payload, 100ms cycle
bmcli txtask add --channel=0 --message=0x100 --cycle=100 --data="AA BB"

# Increment data: counter at startbit=0, 32-bit, step=1, range 0-255
bmcli txtask add --channel=0 --message=0x100 --cycle=100 --type=incdata \
    --inc-startbit=0 --inc-nbits=32 --inc-step=1 --inc-min=0 --inc-max=255

# Increment CAN ID: from 0x100 to 0x1FF, step=1
bmcli txtask add --channel=0 --message=0x100 --cycle=50 --type=incid \
    --inc-min=0x100 --inc-max=0x1FF --inc-step=1

# Random data at startbit=16
bmcli txtask add --channel=0 --message=0x100 --cycle=50 --type=random \
    --inc-startbit=16 --inc-nbits=16 --inc-min=0 --inc-max=65535

# With delay offset and finite rounds
bmcli txtask add --channel=0 --message=0x100 --cycle=50 --delay=10 --rounds=1000

```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel (SN/port, UID/port, legacy index, or name) |
| `--message` / `--id` | (required) | CAN ID |
| `--cycle` | (required) | Interval in ms |
| `--delay` | `0` | Delay offset within cycle in ms |
| `--data` | 8 zero bytes | Payload hex bytes |
| `--type` | `fixed` | `fixed`, `incdata`, `incid`, `random`/`randomdata`, `randomid` |
| `--flags` | none | Frame flags |
| `--rounds` | `infinite` | Number of cycles. `0xFFFF` or omit = infinite |
| `--inc-startbit` | `0` | Start bit for incdata/random |
| `--inc-nbits` | `32` | Bit width |
| `--inc-step` | `1` | Increment step |
| `--inc-min` | `0` | Minimum value |
| `--inc-max` | max | Maximum value |

### txtask remove

```bash
bmcli txtask remove --channel=0 --taskid=0
```

### txtask list

```bash
bmcli txtask list --channel=0
bmcli txtask list --channel=0 --format=json
```

---

## route

Message routing between channels.

### route add

Routes are a **hardware feature**: the route table lives in one device's firmware
and can only forward between that device's own ports. `--src` and `--dst` are
therefore resolved to **device-local port numbers**, and `--src`/`--dst` must be
on the **same device** — cross-device routing is rejected (see error below).

```bash
# Route all messages from 10356/CH1 to 10356/CH2 (SN/port, same device)
bmcli route add --src=10356/0 --dst=10356/1

# Route specific message ID
bmcli route add --src=10356/0 --dst=10356/1 --message=0x100

# Broadcast to multiple ports on the same device (--dst is a raw port bitmask,
# e.g. 0x0E = ports 1,2,3; NOT SN/port-resolvable)
bmcli route add --src=10356/0 --dst=0x0E --type=broadcast

# Route only CAN FD frames
bmcli route add --src=10356/0 --dst=10356/1 --flags=fdf
```

Cross-device error (hardware cannot do this):
```
ERROR: Hardware route cannot cross devices: src 10356 -> dst 10357. Routes are confined to one device's own ports.
```

| Option | Default | Description |
|--------|---------|-------------|
| `--src` | (required) | Source channel (SN/port, UID/port, legacy index, or name) |
| `--dst` | (required) | Destination: unicast → channel spec (must be same device as `--src`); broadcast → raw port bitmask (e.g. `0x0E`) |
| `--message` / `--id` | all | Match one exact CAN ID; omit to route all IDs |
| `--type` | `unicast` | `unicast` or `broadcast` |
| `--flags` | none | Match the exact supplied flags value |

### route remove

```bash
bmcli route remove --src=10356/0 --dst=10356/1 --message=0x100
```

| Option | Description |
|--------|-------------|
| `--src` | Source channel (required; SN/port or legacy index) |
| `--dst` | Match only routes to this destination (SN/port or raw target value) |
| `--message` | Match only routes with this CAN ID |

### route list

```bash
bmcli route list --channel=0
bmcli route list --channel=0 --format=json
```

---

## isotp

ISO 15765-2 (ISOTP) transport layer.

### isotp send

Send data via ISOTP (no response expected).

```bash
bmcli isotp send --channel=0 --reqid=0x7E0 --respid=0x7E8 --data="3E 80"
bmcli isotp send --channel=0 --reqid=0x7E0 --respid=0x7E8 --data="..." --flags=fdf --padding=0xCC
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel |
| `--reqid` | (required) | TX CAN ID |
| `--respid` | (required) | RX CAN ID |
| `--data` | (required) | Payload hex bytes |
| `--timeout` | `5000` | Timeout ms |
| `--flags` | none | `fdf` for CAN FD |
| `--padding` | no padding | Pad value, e.g. `0xCC` |

### isotp recv

Receive data via ISOTP (no request sent first).

```bash
bmcli isotp recv --channel=0 --reqid=0x7E0 --respid=0x7E8 --timeout=5000
```

### isotp ecu-sim

ISOTP ECU simulator: listens for ISOTP requests and responds with canned responses loaded from a file.

```bash
bmcli isotp ecu-sim --channel=4 --reqid=0x7E0 --respid=0x7E8 --responses=rules.txt --count=10
```

The responses file is a text file with one rule per line: `<request_hex_pattern> <response_hex>`. Lines starting with `#` are comments. Use `*` for wildcard (match any request). Prints `READY` to stdout when initialized, `ECU-SIM: <hex> -> <hex>` to stderr for each exchange.

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel to listen on |
| `--reqid` | (required) | CAN ID to listen for (requests) |
| `--respid` | (required) | CAN ID to send (responses) |
| `--responses` | (required) | Response rules file path |
| `--count` | `0` (infinite) | Stop after N exchanges |
| `--flags` | none | `fdf` for CAN FD |

**Note**: For full UDS protocol simulation with state management (sessions, security, DTC database, download), use `test/uds_ecu_sim.py` instead — it implements a complete UDS ECU via Python/ctypes.

---

## uds

UDS (ISO 14229) diagnostic services over ISOTP. Maintains session state set by `uds configure`.

### uds discover

Discover standard 11-bit physical UDS response IDs by transmitting one
single-frame request to functional ID `0x7DF`. This command runs standalone,
requires exclusive hardware access, and refuses to transmit unless the caller
passes `--allow-functional` after user authorization.

```bash
bmcli uds discover --channel=10356/1 --allow-functional --dry-run --format=json
bmcli uds discover --channel=10356/1 --allow-functional --format=json
bmcli uds discover --channel=10356/1 --allow-functional --probes=3 --timeout=500 --format=json
bmcli uds discover --channel=10356/1 --allow-functional --data="22 F1 90" --format=json
```

The default request is TesterPresent (`3E 00`). `--data` accepts a documented
1-7 byte UDS request. The default `--probes=3` sends three independent requests;
an ID is qualified only if it returns a matching UDS response in every probe
window. For TesterPresent, `02 7E 00` is matched using the ISO-TP length, so any
following CAN padding bytes are ignored. `--timeout` is the response window for
each probe. Matching responses are collected from any 11-bit CAN ID; the
implementation does not assume the OBD-only `0x7E8`-`0x7EF` range.
The output includes conventionally inferred physical request IDs (`response ID
- 8`), so pairs such as `0x791/0x799` are represented correctly. Treat inferred
IDs as candidates until an ECU identity read confirms the target. If several
IDs answer every probe, BMCLI returns `status=needs_selection` and
`selection_required=true`. The caller must show those IDs to the user and must
not choose or persist one automatically.

### uds configure

Set UDS session parameters. Session persists for subsequent UDS commands.

```bash
bmcli uds configure --channel=0 --reqid=0x7E0 --respid=0x7E8
bmcli uds configure --channel=0 --reqid=0x7E0 --respid=0x7E8 --flags=fdf --padding=0xCC
```

| Option | Required | Description |
|--------|----------|-------------|
| `--channel` | yes | Channel (e.g. `0`, `10356/1`) |
| `--reqid` | yes | TX CAN ID (varies per ECU, e.g. `0x7E0`, `0x791`) |
| `--respid` | yes | RX CAN ID (varies per ECU, e.g. `0x7E8`, `0x799`) |
| `--flags` | no | `fdf` for CAN FD |
| `--padding` | no | ISOTP pad value |

There are no defaults for `--channel`/`--reqid`/`--respid` — they vary per ECU and silent defaults would only mask missing parameters with a misleading "Bus timeout". Always specify them explicitly.

### uds request

Send a UDS request and receive the response. Supports raw hex bytes and named services from loaded UDS XML definitions.

**Exit codes (script fail-fast)**:
- `0` — positive response (SID | 0x40).
- `2` — negative response (`7F <SID> <NRC>`); also prints `Negative response: ...` (text) or `"status":"error","nrc":"0xNN"` (JSON). This non-zero exit lets batch/sh scripts abort on any NRC mid-sequence (`set -e` in sh, `if errorlevel 1` in bat).
- `1` — no response (timeout / bus error).

**NRC 0x78 (responsePending) handling**: if the ECU replies `7F <SID> 78`, bmcli does NOT treat it as a final failure — it restarts the P2* wait and reads again for the deferred final response (a new ISOTP message), per ISO 14229-1 §7.5.3. Caps at 30 iterations / 30 s. This matters for real ECUs that stall on session switches (`10 02`), resets (`11 01`), or long services (`31 01`/`34`/`37`).

Requires either prior `uds configure` or `--channel`/`--reqid`/`--respid` on this command line. Missing parameters produce a clear error (no silent defaults).

```bash
# --- Raw hex ---

# TesterPresent (suppress response)
bmcli uds request 3E 80

# ReadDataByIdentifier (VIN)
bmcli uds request 22 F1 90

# DiagnosticSessionControl -> extended session
bmcli uds request 10 03

# ECUReset -> hard reset
bmcli uds request 11 03

# CommunicationControl -> enable RX/TX
bmcli uds request 28 00 01

# RoutineControl -> start routine by identifier
bmcli uds request 31 01 FF 00

# RequestDownload
bmcli uds request 34 00 44 00 00 00 00 00 00 10 00

# TransferData (block sequence 0x01)
bmcli uds request 36 01 AA BB CC DD EE FF

# --- Named services (requires loaded uds.xml) ---

bmcli uds request hardReset
bmcli uds request DiagnosticSessionControl extended
bmcli uds request ReadDataByIdentifier VIN
bmcli uds request ReadDataByIdentifier 0xF190
bmcli uds request CommunicationControl enableRxAndTx
bmcli uds request RoutineControl startRoutine 0xFF00

# --- Options ---

# Dry-run: show built request bytes without sending
bmcli uds request --dry-run 22 F1 90

# Override session parameters for this request
bmcli uds request --timeout=3000 --channel=1 3E 00
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | session | Override channel |
| `--reqid` | session | Override TX ID |
| `--respid` | session | Override RX ID |
| `--timeout` | `5000` | Timeout ms |
| `--data` | — | Raw hex data (alternative to positional args) |
| `--dry-run` | — | Show request bytes without sending |

Positional args: hex bytes (`3E 80`) or service name + parameters.

### uds load

Load UDS service definitions from an XML file.

```bash
bmcli uds load --file=res/uds.xml
```

### uds save

Save current UDS definitions to XML.

```bash
bmcli uds save --file=output.xml
```

### uds list

List loaded UDS services and their sub-functions.

```bash
bmcli uds list
bmcli uds list DiagnosticSessionControl
```

### uds unlock

Perform SecurityAccess (service 0x27) — the request-seed / send-key sequence.

`--algo` is **always a path to an external security DLL/.so** (must export
`GenerateKeyEx` or `GenerateKeyExOpt`). UDS SecurityAccess has no standard
algorithm, so bmcli ships no builtins — the OEM algorithm is always provided
as a DLL. `--level` accepts hex (`0x11`) or decimal (`17`); it is the
SecurityAccess sub-function.

Both 0x27 responses are read with NRC 0x78 (responsePending) retry, so an ECU
that defers its reply is handled correctly.

```bash
bmcli uds unlock --level=0x11 --algo=res/sa.dll        # Windows DLL
bmcli uds unlock --level=0x11 --algo=./libsecurity.so  # Linux .so
```

| Option | Description |
|--------|-------------|
| `--level` | Security access level (UDS sub-function), hex or decimal (required, e.g. `0x11`) |
| `--algo` | Path to security algorithm DLL/.so (required) |
| `--timeout` | Timeout ms (default 5000) |

### uds download

Transfer a firmware image to ECU memory via services 0x34 (RequestDownload),
0x36 (TransferData), 0x37 (RequestTransferExit). Multi-format, multi-segment:

- Parses Intel HEX (`.hex`, type 04 ext-linear addresses), Motorola S19
  (`.s19`/`.srec`, S1/S2/S3), or raw binary (`.bin`). Use `--format=auto`
  to detect by extension/content.
- A multi-segment file is downloaded as separate 34/36/37 cycles per segment.
- The `0x74` response's `maxNumberOfBlockLength` is **parsed and respected**
  (override with `--maxblock`). Each `0x36` response is validated (SID + seq);
  `0x37` is validated too. A wrong-sequence or NRC aborts with a structured,
  AI-readable error (stage / segment / percent / expect / actual).
- NRC 0x78 (responsePending) on any response is handled transparently.

```bash
bmcli uds download --file=firmware.bin --addr=0x08000000
bmcli uds download --file=app.hex --format=auto --alf=0x44 --maxblock=0x0FFF
```

| Option | Description |
|--------|-------------|
| `--file` | Firmware file: `.bin` / `.hex` / `.s19` (required) |
| `--format` | `auto` (default) / `bin` / `hex` / `s19` |
| `--addr` | Start address (bin only; hex/s19 use per-segment addresses) |
| `--size` | Truncate to N bytes (bin only) |
| `--alf` | addressAndLengthFormatIdentifier for `0x34` (default `0x44`; high nibble = addr bytes, low = size bytes) |
| `--maxblock` | Override the `0x74`-negotiated maxNumberOfBlockLength |

### checksum

Compute a checksum / CRC of a file. This is a **general-purpose tool** (not part
of UDS) — useful for assembling RoutineControl `31 01 02 02 <crc>` check requests.
Lives under its own top-level category, parallel to `convert`/`database`.

`--algo` follows the same unified resolution rule as `uds unlock`: builtin name
first, else external DLL/.so exporting `ComputeChecksum`.

```bash
bmcli checksum --file=app.hex --algo=crc32-ieee          # builtin
bmcli checksum --file=app.hex --algo=crc32-ieee --segment=0   # one segment only
bmcli checksum --file=app.hex --algo=./compute_checksum.dll   # external DLL
bmcli checksum --self-test                                # validate CRC check values
```

| Option | Description |
|--------|-------------|
| `--file` | Input file (required, or use `--self-test`) |
| `--format` | `auto` (default) / `bin` / `hex` / `s19` |
| `--algo` | Builtin name (`crc32-ieee`, `crc16-ccitt`, `crc16-xmodem`, `crc16-modbus`) or external DLL/.so (default `crc32-ieee`) |
| `--segment` | `all` (default) or segment index N — compute over one segment |
| `--poly` / `--init` / `--xorout` | Override CRC parameters (advanced) |

Built-in `crc32-ieee` matches Python `zlib.crc32` and the IEEE 802.3 CRC-32
(poly 0x04C11DB7, init 0xFFFFFFFF, reflected, xorout 0xFFFFFFFF; check value
`0xCBF43926` for ASCII "123456789"). Output JSON includes both the combined
checksum and per-segment CRCs.

The external DLL signature (`example/checksum_dll/compute_checksum_impl.c`):
```c
int ComputeChecksum(const uint8_t *data, uint32_t dataLen,
                    uint8_t *outChecksum, uint32_t maxOutSize, uint32_t *outSize,
                    uint32_t polynomial, uint32_t initValue, uint32_t xorOutValue,
                    void *userArg);
```

### uds present

Send periodic TesterPresent (3E 80 with suppressPosRspMsgIndicationBit) to keep the diagnostic session alive.

```bash
# Start continuous TesterPresent in background (daemon mode)
bmcli uds present start --interval=2000

# Stop background TesterPresent
bmcli uds present stop

# Send a fixed number of frames (both modes)
bmcli uds present --count=10 --interval=2000
```

| Sub-command | Description |
|-------------|-------------|
| `start` | Start continuous background TesterPresent. `--interval=N` (default 2000ms) |
| `stop` | Stop background TesterPresent |
| (no sub-command) | Send `--count` frames (default 10) at `--interval` ms, then stop |

### uds dtc

Read, query, and clear Diagnostic Trouble Codes (UDS services 0x19 / 0x14).
DTC codes are 3 bytes (24-bit) per ISO 14229-1.

```bash
# List all active DTCs
bmcli uds dtc
bmcli uds dtc active

# Query all DTCs the ECU defines, regardless of status
bmcli uds dtc supported

# Same as above, using hex subfunction code directly
bmcli uds dtc 0x0A

# Get number of DTCs matching a mask (count only, no detail)
bmcli uds dtc count

# List only confirmed DTCs
bmcli uds dtc --mask=0x09

# Query snapshot record IDs
bmcli uds dtc snapshot-id

# Clear all DTCs
bmcli uds dtc clear
```

| Subcommand | Hex Alias | UDS Service | Description |
|------------|-----------|-------------|-------------|
| (default) / `active` | `0x02` | `0x19 0x02` | List active DTCs with 3-byte codes + status |
| `count` | `0x01` | `0x19 0x01` | Count of DTCs matching mask |
| `supported` | `0x0A` | `0x19 0x0A` | All DTCs the ECU supports |
| `snapshot-id` | `0x03` | `0x19 0x03` | Snapshot record IDs per DTC |
| `snapshot` | `0x04` | `0x19 0x04` | Snapshot data for a specific DTC |
| `extended` | `0x06` | `0x19 0x06` | Extended data for a specific DTC |
| `clear` | — | `0x14 FF FF FF` | Clear all DTCs |

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | session | Override channel |
| `--mask` | `0xFF` | Status mask filter (for sub 0x01, 0x02, 0x0A) |
| `--timeout` | `5000` | Timeout ms |

> **Deprecated**: `--sub=XX` is still accepted for backward compatibility but prefer named subcommands or hex aliases.

---

## database (alias: dbc)

DBC file operations. Load DBC files globally, then use in `message send`, `database decode`, etc.

### database load

Load a DBC file. Up to 8 files simultaneously. Once loaded, available to all subsequent commands.

```bash
bmcli database load --file=test.dbc
```

### database unload

```bash
bmcli database unload --file=test.dbc
bmcli database unload --all
```

### database decode

Decode raw CAN data into signal values. Uses globally loaded DBC by default. `--dbc` is a helper that auto-loads a file for this invocation only.

```bash
# Using globally loaded DBC
bmcli database decode --message=EngineStatus --data="00 10 5C 00"

# Auto-load DBC for this invocation
bmcli database decode --dbc=test.dbc --message=EngineStatus --data="00 10 5C 00"

# By CAN ID
bmcli database decode --message=0x200 --data="00 10 5C 00"
```

| Option | Description |
|--------|-------------|
| `--dbc` | DBC file path (optional; uses loaded DBC if omitted) |
| `--message` | Message name or CAN ID (required) |
| `--data` | Raw hex data bytes (required) |

### database encode

Encode signal values into raw CAN bytes.

```bash
# Preferred: --signal=Name=Value (consistent with 'message send')
bmcli database encode --message=EngineStatus --signal=EngineSpeed=1500 --signal=EngineTemp=85

# Auto-load DBC
bmcli database encode --dbc=test.dbc --message=EngineStatus --signal=EngineSpeed=1500 --signal=EngineTemp=85

# Legacy positional form (still accepted for backward compatibility)
bmcli database encode --message=EngineStatus EngineSpeed=1500 EngineTemp=85
```

Signal values may be given as `--signal=Name=Value` (preferred) or as positional
`Name=Value` pairs. `--signal=` is the canonical form and matches `message send`.
JSON output includes `extended`, `fd`, and `brs`. BMCLI preserves DBC
`VFrameFormat` and `CANFD_BRS`; `message send` uses those values as the default
IDE/FDF/BRS flags unless `--flags` is explicitly supplied.

### database list

List messages from loaded DBC files.

```bash
bmcli database list
bmcli database list Engine    # filter by name substring
```

Text, JSON, and CSV list output includes the parsed extended-ID, CAN FD, and BRS
attributes so automation can validate the DBC before transmitting.

---

## logging

Online (host-side) CAN traffic recording to ASC. For on-device (offline) recording to the device's flash, use `logging start --hardware` or the equivalent `offline record start` (see [offline](#offline-alias-storage)).

Multi-channel: `--channel=A,B,C` records all listed channels interleaved into ONE output file. Each frame is tagged with a LOGICAL channel number (1-based position in this recording, NOT the device's physical port). The file header carries a `// ch<N> = SN<serial>/port<physical>` mapping comment so the logical numbers can be decoded.

### logging start

Record CAN traffic to ASC (or BLF) file. In daemon mode, runs as a background thread (singleton — a 2nd `logging start` while one is active returns `ERROR: logging already active`).

```bash
bmcli logging start --channel=10356/1 --output=log.asc
bmcli logging start --channel=10356/0,10356/1 --output=log.asc --duration=60   # multi-channel -> one file
bmcli logging start --channel=10356/0 --output=log.blf --format=blf
bmcli logging start --hardware --channel=10356/0,10356/1 --output=log.asc       # on-device (== offline record start)
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel(s): comma-separated `SN/port` (e.g. `10356/1`), `UID/port`, legacy index, or name. |
| `--output` | (required) | Output file path |
| `--format` | `asc` | `asc` or `blf` |
| `--duration` | infinite | Duration in seconds |
| `--hardware` | off | Record ON-DEVICE to the device's flash storage (third-gen hw). Equivalent to `offline record start`. |

### logging stop

```bash
bmcli logging stop        # stop host-side recording
bmcli logging stop --hardware   # stop on-device recording (== offline record stop)
```

### logging status

Survey of all recording-capable entities. Does NOT require `--channel`.

```
$ bmcli logging status
[host]      active=yes  channels=10356/0,10356/1  file=all.asc  dur=12s  msgs=1234
[dev 10356] offline=on  mode=always_on channels=0x0003 path=log.asc storage=12/64 MB free
[dev 10357] offline=off
```

- `[host]` — host-side (online) recording, tracked by the daemon (standalone mode: a foreground recording IS this process, so nothing to survey).
- `[dev SN]` — each USB device's on-device recording state via `BM_GetLogging`. Devices are independent (per-device on/off). Devices without offline storage show `n/a`.

Optional `--channel=<spec>` filters to the matching entity. `--format=json` produces structured output (null fields where a device lacks offline support).

---

## replay

Online (host-side) replay of an ASC log file to the CAN bus. For on-device replay from the device's flash, use `offline replay start` (see [offline](#offline-alias-storage)).

### replay start

```bash
bmcli replay start --channel=0 --file=logfile.asc
bmcli replay start --channel=10356/0 --file=logfile.asc --loop --speed=2
# Replay a file recorded on a different channel as-is:
bmcli replay start --channel=10356/0 --file=recorded_on_ch2.asc --ignore-channel
# Fixed 20ms inter-frame gap instead of original timestamps:
bmcli replay start --channel=0 --file=logfile.asc --msg-delay=20
# As fast as possible (back-to-back):
bmcli replay start --channel=0 --file=logfile.asc --ignore-timestamp
# Loop with 500ms gap between iterations:
bmcli replay start --channel=0 --file=logfile.asc --loop --cycle-delay=500
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel list (comma-separated `SN/port`, UID/port, or legacy index). Opened in listed order; the i-th opened channel is routing position i (1-based). Single channel = position 1 only. |
| `--file` | (required) | ASC log file path |
| `--speed` | `1.0` | Speed multiplier (2 = twice as fast). Only meaningful in retain-timestamp mode. |
| `--loop` | off | Loop cyclically |
| `--ignore-channel` | off | Send every frame to the first opened channel (single-channel dump), bypassing positional routing. |
| `--map-channel=<asc_ch>:<spec>` | (none) | Override: route ASC channel `<asc_ch>` to physical channel `<spec>`. Repeatable. See routing rules below. |
| `--map-strict` | off | Only ASC channels with an explicit `--map-channel` entry are sent; all others are skipped (and counted as errors). Disables positional default. |
| `--msg-delay=<ms>` | (off) | Fixed inter-frame delay, overrides original timestamps. `0` = back-to-back. First frame is always sent immediately. |
| `--ignore-timestamp` | off | Semantic alias for `--msg-delay=0`: send frames back-to-back with no inter-frame delay. |
| `--cycle-delay=<ms>` | `0` | Gap between loop iterations (only with `--loop`). |

**Channel routing** (per frame, highest-precedence first). The hardware `port` number is **not** consulted — ASC channel numbers are purely positional:
1. explicit `--map-channel=N:spec` match → mapped channel
2. `--map-strict` → skip + count as error
3. `--ignore-channel` → first opened channel (`--channel` position 1)
4. positional: ASC channel N → the N-th opened channel (if N is within the opened list)
5. otherwise → skip

So with `--channel=A,B,C` opened, ASC ch1→A, ch2→B, ch3→C by default. ASC ch4 (no position 4) is skipped unless `--ignore-channel` or mapped. With a single `--channel=A`, only ASC ch1 is sent by default — use `--ignore-channel` to dump all frames onto A, or `--map-channel=N:A` to route a specific channel there.

```bash
# Multi-channel recording -> multi-channel hardware (purely positional)
bmcli replay start --file=multi.asc --channel=10356/0,10356/1,10357/0
#   ASC ch1->10356/0, ch2->10356/1, ch3->10357/0

# Override specific channels (map on top of positional)
bmcli replay start --file=multi.asc --channel=10356/0 \
    --map-channel=1:10356/2 --map-channel=3:10357/0 --map-strict
#   ch1->10356/2, ch3->10357/0; everything else skipped (strict)

# Single channel, dump entire file onto it
bmcli replay start --channel=10356/0 --file=recorded_on_ch5.asc --ignore-channel
```

`--map-channel` resolves its spec independently of the `--channel` list (it opens the target channel). Do not list the same physical port in both `--channel` and `--map-channel` (the device-port open will fail — ports are exclusive).

**Timing modes** (evaluated in this order):
1. `--ignore-timestamp` → back-to-back, no delay
2. `--msg-delay=<ms>` → fixed delay between every frame
3. default → retain original inter-frame timestamps (scaled by `1/speed`)

In all modes the **first frame of each loop iteration is sent immediately** (its timestamp is treated as the base, not waited on) — matching BUSMASTER's behavior.

**ASC format support**: Both classic CAN and CAN FD messages. Classic format supports old (`Ext/Std` + `R/T`) and new Vector (`Rx/Tx`) styles.

### replay stop

```bash
bmcli replay stop
```

In standalone mode, use Ctrl+C.

### replay status

Survey of all replay-capable entities (mirror of `logging status`). Does NOT require `--channel`.

```
$ bmcli replay status
[host]      active=yes  channels=10356/0  file=x.asc  sent=1234
[dev 10356] replay=off
[dev 10357] replay=on   mode=always_on channels=0x0001 path=stored.asc cyclic=yes
```

- `[host]` — host-side (online) replay, tracked by the daemon.
- `[dev SN]` — each USB device's on-device replay state via `BM_GetReplay`.

Optional `--channel=<spec>` filters to the matching entity.

---

## offline (alias: storage)

On-device (offline) operations: recording/replaying to/from the device's own flash storage, plus device-config save/load/clear. Third-generation hardware only.

`offline record` and `offline replay` share their underlying implementation with the `--hardware` forms of `logging`/`replay` — two entry points for the same on-device behavior:

| Action | Equivalent to | Description |
|--------|---------------|-------------|
| `offline record start` | `logging start --hardware` | Start on-device recording (`BM_SetLogging`) |
| `offline record stop`  | `logging stop --hardware`  | Stop on-device recording |
| `offline record status`| (per-device part of `logging status`) | Query on-device recording state (`BM_GetLogging`) |
| `offline replay start` | `replay start --hardware` (planned) | Start on-device replay (`BM_SetReplay`) |
| `offline replay stop`  | `replay stop --hardware` (planned)  | Stop on-device replay |
| `offline save` | — | Save config to device flash (`BM_SaveConfig`) |
| `offline load` | — | Load config from device flash (`BM_LoadConfig`) |
| `offline clear`| — | Clear stored config (`BM_ClearConfig`) |

```bash
bmcli offline record start --channel=10356/0,10356/1 --output=log.asc
bmcli offline record stop
bmcli offline replay start --channel=10356/0 --file=stored.asc --loop
bmcli offline save --channel=10356/0 --items=rxfilter,txtask,route
```

**`--items` values** (save/load/clear): `mode`, `bitrate`, `tres`, `rxfilter`, `txtask`, `route`, `logging`, `replay`, `all` (default).

---

## convert

BLF-to-ASC format conversion. Currently returns "not supported" (requires BLF library).

```bash
bmcli convert <input> <output>
```

Positional arguments: input file path and output file path.

---

## daemon

Background process keeping channels open for low-latency command execution. Communicates via TCP socket on `127.0.0.1:4502` (cross-platform, same on Windows and Linux).

On Linux, daemon state follows the XDG base-directory convention:

- PID: `$XDG_RUNTIME_DIR/bmcli/daemon_4502.pid`; fallback `/tmp/bmcli-<uid>/daemon_4502.pid`.
- Log: `$XDG_STATE_HOME/bmcli/daemon_4502.log`; fallback `~/.local/state/bmcli/daemon_4502.log`.
- IPC remains the loopback TCP endpoint above; there is no filesystem socket.

**Recommendation**: Always specify `--nbitrate`, `--dbitrate`, `--mode`, and `--tres` explicitly at startup. Using incorrect default bitrate will cause bus errors on the physical CAN network.

### daemon start

```bash
# Recommended: stable SN/port and explicit bus parameters
bmcli daemon start --channel=10356/0 --nbitrate=500k --dbitrate=2M --tres=120

# Multi-channel on same device
bmcli daemon start --channel=10356/0,10356/1 --nbitrate=250k --dbitrate=1M --mode=normal --tres=120

# Multi-device
bmcli daemon start --channel=10356/0,10357/0 --nbitrate=500k --tres=120

# Legacy index remains supported for temporary interactive use
bmcli daemon start --channel=0 --nbitrate=500 --dbitrate=2000 --tres=120
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | — | Channels to open at startup |
| `--mode` | `normal` | CAN mode |
| `--nbitrate` | `500` | Nominal bitrate kbps |
| `--dbitrate` | `2000` | Data bitrate kbps |
| `--tres` | `120` | Terminal resistor: `120`, `off` |

Blocks until daemon socket is ready (up to 10s).

### daemon stop

```bash
bmcli daemon stop
```

### daemon restart

Close and reopen all channels. Re-applies saved txtasks and routes.

```bash
bmcli daemon restart
```

### daemon status

```bash
bmcli daemon status
```

Shows: open channel count, PID, uptime.

### Daemon features

- **SN-based channel identification**: Channels are tracked by SN+port internally, so recovery after USB reconnection always finds the correct device regardless of enumeration order
- **BUSOFF recovery**: Auto-detects bus-off, switches to loopback, sends 256 dummy frames, restores normal mode
- **USB reconnect**: On USB disconnect (ILLOPERATION), waits 1s then re-enumerates and reopens using stored SN+port
- **Config persistence**: Txtasks and routes are re-applied after channel recovery
- **Background threads**: Logging, replay, and uds present run in separate threads

