# 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`, `listen-only`, `loopback`, `internal-loopback`, `external-loopback` |
| CAN termination | `--termination=<VAL>` | `120`, `off` |
| LIN pull-up | `--pullup=<VAL>` | `1k`, `off` |
| Channel power | `channel power <status|on|off|cycle>` | Current hardware controls the LIN 12 V output |

### 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` | `423BAE5191B9/0` | Device UID (12 alphanumeric characters) + port number |
| **Legacy** | `N` | `0`, `0,1`, `0-3` | Enumerate index (backward compatible) |

**Auto-detection rules** (for the part before `/`):
- Alphanumeric, length 12 → UID
- All digits, length 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 --termination=120 \
  --request-id=0x791 --response-id=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 --check
bmcli update
bmcli update --version=1.0.0
bmcli update --check --format=json
```

`bmcli update --check` reads the latest manifest, verifies its published
`changelist.txt` SHA-256, compares semantic versions, and displays only entries
newer than the installed version. It never downloads a binary or changes the
installation. `bmcli update` performs the same pre-check and invokes the bundled
installer only when latest is newer; equal versions report `up_to_date`, and a
local version newer than latest is not downgraded. `--version=X.Y.Z` explicitly
selects an upgrade or rollback target. A ZIP package remains available for
offline/manual deployment, but users should not manually manage PATH for normal
online installation.

---

## device

### device reset

Reboot one complete local analyzer and verify that the same physical UID returns:

```bash
bmcli device reset 10356
bmcli device reset 10356/0 --timeout=45000 --format=json
bmcli device reset --channel=423BAE5191B9/0 --format=json
```

The selector accepts a 4-6 digit serial, a 12-character UID, or either followed
by `/port`; `--device` and `--channel` forms are equivalent. It may be omitted
only when exactly one local physical analyzer is connected. Missing, malformed,
conflicting, and ambiguous selectors fail before any reset command is sent.

Stop the daemon first. `BM_ResetDevice()` invalidates every open channel/device
handle belonging to the selected analyzer and causes a USB disconnect comparable
to unplugging and reconnecting it. BMCLI opens the selected device in an inactive
mode, sends the reset, observes its disappearance, and then requires the same UID
to re-enumerate. The default timeout is 45000 milliseconds; the accepted range is
1000-60000 milliseconds. BMCLI returns as soon as the device is back, so this
upper bound does not slow a normal reset. USB auto-connect/capture rules in VMware
or other virtualization software can hold a reappearing analyzer between the host
and guest until their capture timeout expires, or attach it to the guest. Disable
that rule or disconnect the analyzer from the guest when deterministic reset time
is required.

Generation-3 firmware resets immediately from inside the USB control request, so
the low-level API may report `BM_ERROR_ILLOPERATION` because the transfer itself
was interrupted. BMCLI reports success only when physical disconnect and same-UID
re-enumeration are both observed; inspect the command exit code and top-level
`status`, not `api_status` alone. Other connected devices are never accepted as
evidence that the requested target returned.

### device update

Check or update the complete physical analyzer, not an individual channel:

```bash
bmcli device update 10356 --check
bmcli device update 10356/0 --check --format=json
bmcli device update 423BAE5191B9 --check
bmcli device update --device=10356
bmcli device update 10356 --force
```

The device selector accepts a 4-6 digit analyzer serial number, a 12-character
UID, or either identity followed by `/port`. A channel-qualified selector such
as `10356/0` locates its parent physical device; the operation still affects the
complete analyzer. `--device=<device>` and `--channel=<device>` are also accepted.
The selector may be omitted only when exactly one supported local generation-3
device is connected. An explicit selector that is absent or ambiguous fails and
never falls back to another connected device.

BMCLI obtains the firmware CID from the enumerated BMAPI product identifier and
downloads `https://download.busmust.com/files/Firmware_<CID>_Changelist.txt`.
The first non-empty line is the latest four-component firmware version. `--check`
compares it with `version[4]`, reports `update_available`, `up_to_date`, or
`local_newer`, and displays only newer changelist entries.

Without `--check`, BMCLI downloads the requested HPM firmware completely over
HTTPS before touching hardware. By default, it proceeds if and only if the
published version is newer. `--force` explicitly permits a reinstall or
downgrade to the current official release published for the selected device's
CID; it does not accept an arbitrary version or local image and cannot be
combined with `--check`. After download, BMCLI sends the generation-3 boot
command, waits for the `HPMBOOT` mass-storage volume (legacy `BMBOOT` is also
accepted), copies the firmware, and
requires the same UID to re-enumerate with the expected version.
Existing `HPMBOOT`/`BMBOOT` volumes, remote devices, ambiguous devices, failed
downloads, and generation-2 STM32 devices are rejected. `--timeout` is in
milliseconds and defaults to 30000. Linux apply requires the boot volume to be
auto-mounted or BMCLI to run with mount permission; BMCLI never invokes `sudo`.

The boot control request can return `BM_ERROR_ILLOPERATION` while still
successfully disconnecting the application interface. BMCLI treats the observed
boot volume as authoritative, temporarily releases process-wide BMAPI state so
the mass-storage interface can bind, then reinitializes BMAPI for final UID and
version verification. A low-level error without a boot volume remains a failure.

The current firmware server does not publish a separate firmware hash. Transport
integrity relies on HTTPS, response sanity checks, and HPM bootloader validation;
post-update re-enumeration is mandatory before success is reported.

---

## channel

### channel enumerate

List all connected BUSMUST CAN FD / LIN analyzer channels. Each result is one
channel record; physical-device operations use the record's stable serial or
UID with `device reset` and `device update`.

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

### channel info

Show channel information. Standalone mode reports static enumeration fields
(name, SN, firmware, VID/PID, capability). Daemon mode additionally reports the
selected bus and runtime configuration. CAN channels report controller mode,
nominal/data bitrate, and termination. LIN channels report role, bitrate,
pull-up, supply direction, protocol version, and checksum model.

```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 CAN or LIN channel in the persistent daemon. The default
bus type is selected from the enumerated channel capability; `--bus` is not a
public argument. Repeat the command to open additional channels.

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

# LIN master and slave endpoints
bmcli channel open --channel=10356/4 --mode=master --bitrate=19200 --pullup=1k --power=on
bmcli channel open --channel=10357/4 --mode=slave --bitrate=19.2K --pullup=off --power=off

# 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) | One channel spec: `0`, `10356/0`, `UID/0`, or an exact enumerated name |
| `--mode` | CAN `normal`; LIN `listen-only` | CAN controller mode or LIN `master`, `slave`, `listen-only`, `internal-loopback` |
| `--nbitrate` | `500` | CAN nominal bitrate. Bare kbps, bps, K/kbps, and M/mbps forms are accepted. |
| `--dbitrate` | `2000` | CAN data bitrate. Bare kbps, bps, K/kbps, and M/mbps forms are accepted. |
| `--bitrate` | `19200` | LIN bitrate in bps, 1000..20000; `19200` and `19.2K` are equivalent. `auto` is slave-only and requires supporting firmware. |
| `--termination` | `120` | CAN termination: `120` or `off` |
| `--pullup` | master `1k`; other modes `off` | LIN pull-up: `1k` or `off` |
| `--power` | master `on`; other modes `off` | Enable or disable this channel's LIN 12 V output |
| `--lin-version` | `2.2` | LIN protocol version: `1.3`, `2.0`, `2.1`, `2.2` |
| `--checksum` | `auto` | LIN checksum: classic for IDs 0x3C/0x3D, enhanced otherwise |

Current HPM firmware cannot safely accept a zero bitrate for slave auto-detect,
so BMCLI rejects `--bitrate=auto` with a firmware-capability error instead of
silently substituting a fixed value.

### 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 power

Read or control channel power independently from LIN protocol configuration.
Current hardware maps this command to the LIN 12 V output. `off` disables this
device's output but does not prove an externally powered bus is unpowered.

```bash
bmcli channel power status --channel=10356/4 --format=json
bmcli channel power on --channel=10356/4
bmcli channel power cycle --channel=10356/4 --off-duration=500
```

### can config get/set

Read all CAN settings or idempotently apply supplied settings to an open daemon
channel. The protocol prefix keeps CAN termination and bitrate terms out of LIN.

```bash
bmcli can config get --channel=10356/0 --format=json
bmcli can config set --channel=10356/0 --mode=normal \
  --nbitrate=500K --dbitrate=2M --termination=120
```

### lin config get/set

Read all LIN role, timing, electrical, and protocol settings or apply supplied
settings to an open daemon channel. A role change clears the active hardware TX
task table before switching, preventing a master schedule or slave response from
surviving into the opposite role.

```bash
bmcli lin config get --channel=10356/4 --format=json
bmcli lin config set --channel=10356/4 --mode=master --bitrate=19200 \
  --pullup=1k --power=on --lin-version=2.2 --checksum=enhanced
```

`config` is a noun and is always followed by `get` or `set`. Bare
`channel config` and `configure` forms are not supported.

### 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 or LIN messages. `--type` selects CAN `data`/`remote` or LIN
`master-write`/`master-read`/`sync-break`. CAN supports raw data, repeated
sends, and DBC signal encoding. LIN supports IDs 0x00..0x3F and payloads up to
8 bytes.

```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

# LIN master publishes a data frame
bmcli message send --channel=10356/4 --id=0x20 --type=master-write --data="01 02 03 04" --bitrate=19200

# LIN master read: payload+checksum response is required
bmcli message send --channel=10356/4 --id=0x20 --type=master-read --dlc=8 --timeout=500 --format=json

# LIN sync break: a slave payload response is an error
bmcli message send --channel=10356/4 --id=0x21 --type=sync-break --timeout=500 --format=json
```

**`--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) | CAN or LIN channel; bus is inferred from capability |
| `--message` / `--id` | (required) | CAN ID/DBC message name, or LIN frame ID 0x00..0x3F |
| `--data` | all zeros | Payload hex bytes; LIN supports up to 8 and forbids data with header-only operations |
| `--type` | CAN `data`; LIN `master-write` | CAN: `data`, `remote`. LIN: `master-write`, `master-read`, `sync-break` |
| `--dlc` | auto; LIN master-read `8` | CAN DLC or expected LIN response length 0..8; sync-break uses 0 |
| `--flags` | DBC metadata or none | CAN only: `ide`, `fdf`, `brs`, `rtr` (plus-separated) |
| `--checksum` | `auto` | LIN only: `auto`, `classic`, or `enhanced` |
| `--count` | `1` | Number of frames (software-timed loop) |
| `--cycle` | `0` | Interval between frames in ms |
| `--timeout` | standalone `0`; daemon `100` | CAN write/ACK timeout, or LIN operation completion deadline, in ms |
| `--signal` | — | CAN only: repeatable DBC signal assignment |
| `--bitrate` | LIN `19200` | Standalone LIN bitrate; daemon channels keep their open-time configuration |

**`--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.

For LIN header operations, `sent` counts submitted headers and `completed`
counts outcomes that matched the selected intent. Each JSON `results` item has
an `outcome`. Master-read succeeds only with the expected DLC, payload/checksum,
and no LIN errors. Sync-break succeeds only for a header-only `sync_break=true`
event; a payload is reported as `unexpected-response` and returns nonzero.

### message recv

Receive CAN, CAN FD, or LIN frames. Blocks until `--count` is reached or
`--duration` elapses (or Ctrl+C in standalone). Standalone capture accepts
multiple channels only when they have the same bus type.

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

# Passive LIN capture
bmcli message recv --channel=10357/4 --mode=listen-only --bitrate=19200 --duration=5 --format=jsonl
```

| 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 or LIN ID filter (software-filtered) |
| `--mask` | `0x7FF` | ID filter mask |
| `--format` | `text` | `text`, `json` (array), `jsonl` (one JSON per line), `csv` |
| `--bitrate` | LIN `19200` | Standalone bus timing; CAN also accepts a nominal/data pair |
| `--mode` | LIN `listen-only` | Standalone LIN role |

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

---

## lin

### lin response

Manage the header-triggered response table of an open LIN slave channel. Entries
are keyed by LIN ID; repeated `set` replaces one ID without disturbing other
responses. BMCLI validates the device TX-task capacity and submits one checked
`BM_SetTxTasks` snapshot. It does not retry, read back, or roll back a failed
write.

```bash
bmcli lin response set --channel=10357/4 --id=0x20 --data="01 02 03 04"
bmcli lin response get --channel=10357/4 --id=0x20 --format=json
bmcli lin response list --channel=10357/4 --format=json
bmcli lin response remove --channel=10357/4 --id=0x20
bmcli lin response clear --channel=10357/4
```

### lin schedule

Manage the BMCLI-owned master schedule named `system`. Entries are ordered and
addressed by index. Each positive `--delay` is the time before its slot; the
hardware cycle is the sum of all slot delays. The first entry auto-activates the
table, edits are applied immediately while active, and `clear` stops all slots.
An inactive table remains inactive while edited.

```bash
bmcli lin schedule add --channel=10356/4 --id=0x20 \
  --type=master-read --dlc=4 --delay=20
bmcli lin schedule show system --channel=10356/4 --format=json
bmcli lin schedule deactivate --channel=10356/4
bmcli lin schedule activate --channel=10356/4
bmcli lin schedule remove --channel=10356/4 --entry=0
bmcli lin schedule clear --channel=10356/4
```

BMCLI does not edit LDF files. Future LDF loading will expose read-only named
schedules through `list/show/activate/deactivate`; AI or users generate and edit
the LDF text itself.

---

## txtask

Device-firmware transmit tasks. CAN tasks are hardware-timed periodic or pattern
generators. A LIN task on a slave channel is a fixed, header-triggered response,
not a periodic transmission. Persistent LIN response tasks require daemon mode.

### 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

# LIN slave response: first open the channel as slave in the daemon
bmcli txtask add --channel=10357/4 --id=0x20 --data="01 02 03 04" --cycle=0 --checksum=enhanced
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | (required) | Channel (SN/port, UID/port, legacy index, or name) |
| `--message` / `--id` | (required) | CAN ID or LIN response ID 0x00..0x3F |
| `--cycle` | CAN required; LIN `0` | CAN interval in ms; LIN responses are header-triggered |
| `--delay` | `0` | Delay offset within cycle in ms |
| `--data` | 8 zero bytes | Payload hex bytes |
| `--type` | `fixed` | CAN patterns; LIN requires `fixed` |
| `--flags` | none | CAN frame flags |
| `--checksum` | `auto` | LIN response checksum model |
| `--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 |

Open every master and slave endpoint before installing a finite-round LIN
response task. Reconfiguring another channel on the same analyzer can consume
or reset a task that was armed too early.

### 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. Daemon workflows maintain
endpoint state through `uds config get|set`.

### 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 config get/set

Read or set daemon UDS endpoint parameters. The configuration persists for
subsequent UDS commands while that daemon is running.

```bash
bmcli uds config set --channel=10356/0 --request-id=0x7E0 --response-id=0x7E8
bmcli uds config set --channel=10356/0 --request-id=0x7E0 --response-id=0x7E8 --flags=fdf --padding=0xCC
bmcli uds config get --format=json
```

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

There are no defaults for `--channel`, `--request-id`, or `--response-id`.
They vary per ECU, and silent defaults would only mask missing parameters with
a misleading bus timeout. Always specify them explicitly when setting the
configuration.

### 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, or a deliberately suppressed response after the
  request was transmitted successfully.
- `1`: negative response, timeout, transport failure, configuration error, or
  other command failure. Structured JSON fields such as `nrc`, `reason`, and
  `message` distinguish these cases.

**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 config set` or `--channel`, `--request-id`, and
`--response-id` on this command line. Missing parameters produce a clear error
with 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=10356/0 \
  --request-id=0x7E0 --response-id=0x7E8 3E 00
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | session | Override channel |
| `--request-id` | session | Override TX ID |
| `--response-id` | 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

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

```bash
bmcli uds present send --count=10 --interval=2000
```

`send` is finite: `--count` defaults to 1 and `--interval` defaults to 2000 ms.
Background `start`, `stop`, and `status` lifecycle commands are not currently
implemented and therefore are not exposed by runtime Help.

### 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

# Read one snapshot or extended-data record for DTC 0x001234
bmcli uds dtc snapshot --dtc=0x001234 --record=0x01
bmcli uds dtc extended --dtc=0x001234 --record=0x01

# Use record 0xFF when the ECU supports returning all records
bmcli uds dtc snapshot --dtc=0x001234 --record=0xFF

# Clear all DTC groups (the default group is 0xFFFFFF)
bmcli uds dtc clear
bmcli uds dtc clear --group=0xFFFFFF
```

| 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` | `19 04 <DTC3> <record>` | Snapshot data for a specific DTC and record selector |
| `extended` | `0x06` | `19 06 <DTC3> <record>` | Extended data for a specific DTC and record selector |
| `clear` | — | `14 <group3>` | Clear a DTC group; `FFFFFF` selects all groups |

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | session | Override channel |
| `--mask` | `0xFF` | Status mask filter for `active` and `count` |
| `--dtc` | required | 24-bit DTC for `snapshot` and `extended`; hexadecimal notation is recommended |
| `--record` | required | Record byte for `snapshot` and `extended`; `0xFF` requests all records |
| `--group` | `0xFFFFFF` | 24-bit group for `clear` |
| `--timeout` | `5000` | Timeout ms |
| `--request-id` / `--response-id` | session | Override the configured diagnostic IDs |

`active`, `count`, `supported`, `snapshot-id`, `snapshot`, `extended`, and
`clear` each have an action-specific machine-readable Help contract. For
example, run `bmcli uds dtc snapshot --help --format=json` before constructing
the request. Positional hex aliases remain available for the implemented 0x19
subfunctions; the former `--sub` option is not accepted.

The positive response to `count` is interpreted as
`59 01 <status-availability> <DTC-format> <count-high> <count-low>`. Snapshot
and extended-data requests are exactly six bytes, including service and
subfunction; no extra status-mask byte is inserted.

---

## 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)
logging to the device's flash, use `logging start --hardware` or the equivalent
`offline logging start` (see [offline](#offline)).

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 logging 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 logging start`. |

### logging stop

```bash
bmcli logging stop        # stop host-side recording
bmcli logging stop --hardware   # stop on-device recording (== offline logging 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)).

### 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

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 logging` and `offline replay` share their underlying implementation
with the `--hardware` forms of `logging`/`replay`.

| Action | Equivalent to | Description |
|--------|---------------|-------------|
| `offline logging start` | `logging start --hardware` | Start on-device logging (`BM_SetLogging`) |
| `offline logging stop`  | `logging stop --hardware`  | Stop on-device logging |
| `offline logging status`| (per-device part of `logging status`) | Query on-device logging 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 replay status` | (per-device part of `replay status`) | Query on-device replay state (`BM_GetReplay`) |
| `offline config save` | — | Save config to device flash (`BM_SaveConfig`) |
| `offline config load` | — | Load config from device flash (`BM_LoadConfig`) |
| `offline config clear`| — | Clear stored config (`BM_ClearConfig`) |

```bash
bmcli offline logging start --channel=10356/0,10356/1 --output=log.asc
bmcli offline logging stop
bmcli offline replay start --channel=10356/0 --file=stored.asc --loop
bmcli offline replay status --channel=10356/0 --format=json
bmcli offline config 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 the real timing, role/mode, and termination
or pull-up at startup. The bus type is inferred from channel capability.
Incorrect defaults can disturb a physical CAN or LIN network.

### daemon start

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

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

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

# LIN master startup
bmcli daemon start --channel=10356/4 --mode=master --bitrate=19200 --pullup=1k --power=on

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

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | — | Channels to open at startup |
| `--mode` | CAN `normal`; LIN `listen-only` | CAN mode or LIN role |
| `--nbitrate` | `500` | CAN nominal bitrate kbps |
| `--dbitrate` | `2000` | CAN data bitrate kbps |
| `--bitrate` | `19200` | LIN bitrate bps |
| `--termination` | `120` | CAN termination: `120` or `off` |
| `--pullup` | mode default | LIN pull-up: `1k` or `off` |
| `--power` | mode default | LIN 12 V output: `on` or `off` |
| `--lin-version` | `2.2` | LIN protocol version |
| `--checksum` | `auto` | LIN checksum model |

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

### daemon stop

```bash
bmcli daemon stop
```

### daemon restart

Close and capability-reopen all CAN and LIN channels. Re-applies saved TX
tasks; CAN routes are also restored.

```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**: CAN channels are checked without consuming traffic and recovered through `BM_RecoverBusOff`; if recovery fails, the daemon capability-reopens the channel and restores its persistent configuration. LIN channels are excluded from CAN BUSOFF handling.
- **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**: Host logging and replay run in separate threads

---

## gateway

Expose physical CAN, CAN FD, and LIN channels through the deployed GWR remote
BMAPI UDP protocol. An upper computer uses the normal public BMAPI calls
`BM_EnumerateRemote`, `BM_OpenEx`, `BM_Read`, and `BM_Write`; it does not need a
BMCLI-specific client or protocol adapter.

The initial implementation has one configured consumer IPv4 address and uses
UDP control port `3502` plus generic-I/O port `2502`. The protocol has no
authentication, authorization, encryption, or replay protection. Run it only
on an isolated or otherwise trusted engineering network with host firewall
rules restricted to the intended client.

### gateway start

```bash
# Export two cross-connected CAN FD channels to a Linux upper computer
bmcli gateway start \
  --channel=10356/0,10357/0 \
  --target=192.168.65.129 \
  --advertise=192.168.65.1 \
  --nbitrate=500 --dbitrate=2000 --termination=120

# Export a LIN master/slave pair
bmcli gateway start \
  --channel=10356/6,10357/4 \
  --target=192.168.65.129 \
  --bitrate=19200 --mode=slave --power=off
```

| Option | Default | Description |
|--------|---------|-------------|
| `--channel` | existing daemon channels | Physical channels to export; required when the command creates a daemon |
| `--target` | required | IPv4 address of the intended remote BMAPI consumer |
| `--advertise` | route-selected local IPv4 | Gateway address shown in remote channel names |
| `--name` | `BMCLI Gateway` | Remote server display name |
| daemon bus options | capability defaults | Applied only when `gateway start` creates the daemon; query runtime Help for the exact current list |

When no daemon is running, `gateway start` creates one, opens the requested
channels, and owns that daemon. A later `gateway stop` then also stops it. If a
daemon already exists, every channel already open in it is exported and an
optional `--channel` list must match that complete set. The pre-existing daemon
remains running after gateway stop.

Only physical channels may be exported. Remote and virtual channels are
rejected to avoid forwarding loops. Export order is deterministic: first-opened
physical device order, then ascending physical port within each device. Remote
clients use the returned remote channel information rather than assuming a
particular numeric index.

While the gateway is active, it exclusively consumes the exported channel RX
queues and rejects other local BMCLI bus operations. Stop the gateway before
using those daemon channels locally. Automatic daemon handle recovery is also
suspended because reopening a handle would invalidate the remote channel map.

### gateway status

```bash
bmcli gateway status --format=json
```

Returns the state, configured target and advertised addresses, exported channel
count, and cumulative `local_to_remote`, `remote_to_local`, `dropped`, and
`errors` counters.

### gateway stop

```bash
bmcli gateway stop --format=json
```

Stops forwarding and closes the server-side remote BMAPI handles. It also stops
the daemon only when that daemon was auto-created by the matching gateway start.

### Remote BMAPI client sequence

```text
BM_Init()
BM_EnumerateRemote(gateway_ipv4, channel_info_array, ...)
BM_OpenEx(... remote channel info ...)
BM_GetNotification() / BM_WaitForNotifications()
BM_Read() and BM_Write()
BM_Close()
BM_UnInit()
```

Opening a remote channel applies the client's requested CAN or LIN mode,
bitrate, termination/pull-up, and power settings to the corresponding physical
channel. The gateway translates remote logical channel numbers to physical
ports in both control requests and data headers.

