# BMCLI Command Reference

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

BMCLI also provides an OS-native Modbus master/client service. Modbus RTU and
ASCII use Win32 COM or Linux tty devices; Modbus TCP uses ordinary sockets and
does not require BMAPI hardware.

**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"` plus `error.code`, `error.message`, `error.details`, and
  `error.retryable`. A top-level `message` remains available for CLI consumers.
- Bounded `message recv --format=json` returns one object with a `messages`
  array and summary fields. Use JSONL when each frame must be consumed as it
  arrives.
- 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
  `error.code`, `nrc`, and protocol-specific details 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 target/bench configuration.
`ai status` compares every installed bundled file byte-for-byte and reports
`installed`, `source_available`, and `up_to_date`. Run targeted `ai setup` when
an installed skill reports `up_to_date=false`.

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/target.json`: one portable target with named networks and addressed
  protocol nodes; it may be versioned.
- `.bmcli/bench.local.json`: machine-local logical-network to analyzer-channel
  bindings, wiring values, 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. One `.bmcli` directory is one target and
one bench; that bench may contain multiple logical networks and physical
channels. It separates portable target/network/node intent from machine-local
analyzer identity, wiring, and paths:

- `.bmcli/target.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 --target=vehicle-a --kind=vehicle \
  --network=vehicle-can --channel=10356/0 --nbitrate=500 --dbitrate=2M
bmcli bench init --network=sensor-private-can --node=sensor-controller \
  --channel=10356/1 --nbitrate=500 --dbitrate=2M --termination=120 \
  --request-id=0x791 --response-id=0x799 --database=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` updates the selected network, node, and local connection while
preserving all other entries. The first network and node become defaults. It
accepts repeatable, protocol-neutral `--database` references for DBC, LDF, A2L,
or future supported database types; `--database=none` clears the list. `--dbc`
is a mutually exclusive single-DBC convenience form. 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. Use
`--global` only to create or update the per-user fallback.

Without an explicit `--project`, BMCLI searches from the current directory to
the nearest parent `.bmcli`; when none exists it reads the per-user default.
Project and global profiles are not merged. An explicit physical channel always
works as before. A configured logical name is also accepted wherever the
existing `--channel` option is accepted:

```bash
bmcli message recv --channel=vehicle-can --duration=10 --format=jsonl
bmcli message recv --channel=10356/0 --duration=10 --format=jsonl
```

The client resolves logical names to local physical bindings before BMAPI access
or daemon slot selection. Resolution is exact: an unknown logical network fails
instead of falling back to slot 0 or an enumeration index. Node definitions are
returned by `bench show`; automatic selection of one diagnostic node among
several is not performed.

`bench validate` checks network/node/connection references, required timing,
database and local paths, and stable persisted channels. With `--hardware`, it
also enumerates every configured binding and verifies that channel capabilities
match the declared bus. Its state is `READY`, `DEGRADED` (for example, no DBC
but raw CAN remains possible), or `NEEDS_INPUT`; required-value, stale-path,
JSON, capability, and hardware mismatch failures return nonzero.

The complete schema, ownership, lookup, and acceptance contract is recorded in
[`bench_configuration_plan.md`](bench_configuration_plan.md).

---

## 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
bmcli device update 10356 --file=0083.3107.bin --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 and cannot be combined with `--check`.

An authorized engineering or production workflow may instead provide a complete
local HPM UF2 image with `--file` and the explicit `--force` gate. BMCLI
validates the TinyUF2 contract before touching hardware: every block must carry
the family-present flag, must not carry the not-main-flash flag, must use the
HPM family ID and a 256-byte payload, and must form one aligned, contiguous,
non-overlapping F4L2/0083 application image in
`[0x80020000, 0x80100000)` without address overflow or range escape. BMCLI also
requires exactly one embedded
generation-3 `BM_DeviceInfo` version. The caller remains responsible for
obtaining the exact file from an approved source and independently verifying
its published SHA-256. BMCLI does not infer the model or version from the
filename and does not accept a caller-supplied expected version.

For either source, BMCLI sends the generation-3 boot command, waits for the
`HPMBOOT` mass-storage volume (legacy `BMBOOT` is also accepted), copies the
complete firmware, and requires the same UID to re-enumerate with the expected
version (the published version for CID downloads or the embedded version for a
local image). A successful structured result identifies `source` as
`cid_server` or `local_file`.
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. CID
transport integrity relies on HTTPS, response sanity checks, and HPM bootloader
validation. Local-image workflows must verify the artifact hash before invoking
BMCLI. Post-update same-UID re-enumeration is mandatory before success is
reported in both cases.

---

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

In daemon mode, `channel open --format=json` returns one object with the
canonical channel identity, daemon slot, bus type, and effective timing/mode.
Opening the same physical endpoint again reports `state=already_open`.

### channel close

Close a previously opened channel.

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

`channel close --format=json` returns one bounded success object with
`state=closed` and the released daemon slot.

### channel claim / release

Acquire an optional local control lock before a multi-step configuration or
persistent workflow. CLI and REST share the same `LOCAL` principal. Ordinary
one-shot message send and passive receive/monitoring remain available to other
principals; protected operations such as channel configuration, close, TX Task,
route, LIN schedule/response, replay, ISO-TP, and UDS return busy.

```bash
bmcli channel claim --channel=10356/0,10357/0 --timeout=300000 --format=json
# protected workflow: configure, install tasks/routes, run UDS, replay, etc.
bmcli channel release --channel=10356/0,10357/0 --format=json
```

`--timeout` is milliseconds (`1000..86400000`, default `300000`). A repeated
claim by LOCAL renews the lock. A multi-channel claim is all-or-nothing. Release
has no force option and cannot override a remote owner. Use cleanup/finally logic
in generated shell or Python automation.

Claim is optional for backward compatibility. When no claim exists, local and
legacy remote configuration remains last-writer-wins; successful later writes
may replace earlier settings without notifying the earlier caller.

### 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 frame-slot duration from that
entry's start until the next entry starts; the hardware cycle is the sum of all
slot durations. The first entry has logical phase zero; BMCLI adds the shortest
slot duration as a common hardware phase bias to every entry to avoid the
legacy firmware's special handling of `delay=0`. This bias does not change any
slot duration or the table cycle. 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 with a normal positive response
bmcli uds request 3E 00

# Ask BMCLI to set suppressPosRspMsgIndicationBit (transmits 3E 80)
bmcli uds request 3E 00 --suppress-positive-response

# 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) |
| `--suppress-positive-response` | off | Set bit 7 on a supported subfunction and accept a successfully transmitted request with no positive response |
| `--dry-run` | — | Show request bytes without sending |

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

Value options accept both `--option=value` and `--option value`. Their value
tokens are never interpreted as positional request bytes. For example,
`--channel 10356/1` and `--channel=10356/1` produce the same payload.
Quote space-separated `--data` values, for example `--data "22 F1 90"`.

Suppression is service-aware. BMCLI accepts it only for UDS services whose
second request byte is a subfunction. It rejects the option for services such
as ReadDataByIdentifier (`0x22`) so bit 7 cannot silently corrupt the first DID
byte. A raw request that already contains the suppression bit, such as `3E 80`,
is recognized as suppressed even without the option.

### 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 / start / stop / status

Manage TesterPresent keep-alive. The default request is `3E 00`; BMCLI waits for
and validates `7E 00`. Add `--suppress-positive-response` to send `3E 80` and
avoid waiting for the deliberately suppressed positive response.

```bash
# Finite, validated requests
bmcli uds present send --count=3 --interval=2000

# Finite, suppressed requests
bmcli uds present send --count=10 --interval=2000 --suppress-positive-response

# Daemon-owned keep-alive for a longer diagnostic workflow
bmcli uds present start --interval=2000 --suppress-positive-response
bmcli uds present status --format=json
bmcli uds present stop
```

`send` is finite: `--count` defaults to 1, `--interval` to 2000 ms, and
`--timeout` to 1000 ms. It may run standalone when all endpoint options are
provided, or use daemon session configuration.

`start` requires the daemon. It snapshots the channel, request ID, response ID,
interval, timeout, and suppression mode, then owns one background keep-alive
task until `stop`, daemon shutdown, or fatal daemon cleanup. `status` reports
whether the task is active plus sent, response, failure, and last-result
counters. The daemon serializes this task with foreground UDS transactions so
ISO-TP request/response pairs cannot overlap on the same BMAPI session. `stop`
is idempotent when no task is active.

| Option | Commands | Default | Description |
|--------|----------|---------|-------------|
| `--interval=<milliseconds>` | send, start | `2000` | Period between requests |
| `--count=<N>` | send | `1` | Finite request count |
| `--timeout=<milliseconds>` | send, start | `1000` | Send and positive-response timeout |
| `--suppress-positive-response` | send, start | off | Send `3E 80` and do not wait for `7E 00` |
| `--channel`, `--request-id`, `--response-id` | send, start | session | Override or snapshot the UDS endpoint |
| `--dry-run` | send, start | off | Resolve the operation without transmitting or starting a task |

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

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

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

# List only confirmed DTCs
bmcli uds dtc active --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

Persistent lifecycle for typed engineering database resources. `database` owns
registration, validation, discovery, and removal; protocol-specific operations
remain under `dbc`, `ldf`, and future database categories. Resources are stored
in the per-user BMCLI state directory and remain available to later processes.

### database load

Validate and register a DBC, LDF, A2L, or mapping file. The type is inferred
from the extension unless `--type` is supplied. The default resource name is
the filename stem. Loading the same canonical path again is idempotent.

```bash
bmcli database load --file=network.dbc --name=vehicle
bmcli database load --file=body.ldf --name=body-lin
```

### database unload

```bash
bmcli database unload vehicle
bmcli database unload --all
```

DBC, LDF, and A2L are peer typed resources. All three unload through this
generic lifecycle; there are no `dbc unload`, `ldf unload`, or `a2l unload`
aliases.

### database list / info / validate

```bash
bmcli database list --format=json
bmcli database list --type=dbc --format=json
bmcli database info vehicle --format=json
bmcli database validate --file=network.dbc --format=json
bmcli database validate vehicle --format=json
```

`validate --file` does not change registry state. `info` and validation of a
registered resource re-open the source file, so stale or damaged paths fail
instead of returning cached parser metadata.

## dbc

DBC-specific inspection and signal conversion. `dbc load` is the typed shortcut
for `database load` and registers the same persistent resource object. It is
not an alias for the entire `database` category.

### dbc load

```bash
bmcli dbc load --file=network.dbc --name=vehicle
```

### dbc decode

Decode raw CAN data into signal values. Select a registered resource by name,
or pass a DBC path directly. If exactly one DBC is registered it is selected
automatically; multiple registered DBCs require an explicit selector.

```bash
bmcli dbc decode --database=vehicle --message=EngineStatus --data="00 10 5C 00"
bmcli dbc decode --dbc=network.dbc --message=0x200 --data="00 10 5C 00"
```

| Option | Description |
|--------|-------------|
| `--database` | Registered DBC name or direct DBC path |
| `--dbc` | Direct DBC path for this invocation |
| `--message` | Message name or CAN ID (required) |
| `--data` | Raw hex data bytes (required) |

### dbc encode

Encode signal values into raw CAN bytes.

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

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

# Positional assignments are also accepted
bmcli dbc encode --database=vehicle --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.

### dbc message / signal

Inspect normalized DBC metadata without touching hardware.

```bash
bmcli dbc message list --database=vehicle --format=json
bmcli dbc message list Engine --database=vehicle
bmcli dbc message show EngineStatus --database=vehicle --format=json
bmcli dbc signal list --message=EngineStatus --database=vehicle --format=json
bmcli dbc signal show --message=EngineStatus EngineSpeed --database=vehicle --format=json
```

Message output includes extended-ID, CAN FD, and BRS attributes. Signal output
includes bit layout, endianness, signedness, scaling, range, unit, and
multiplexing metadata. `database decode`, `database encode`, and unscoped
`dbc list` are intentionally not commands: use the typed DBC operations above.

## ldf

Read-only LIN Description File model. `ldf load` and `database load` of the
same canonical `.ldf` path share one persistent typed resource. BMCLI never
creates, edits, or rewrites the source LDF.

### ldf load / info

```bash
bmcli ldf load --file=body.ldf --name=body-lin
bmcli ldf info --database=body-lin --format=json
```

`info` reports protocol/language version, bitrate, master node, and counts for
nodes, signals, frames, and schedules. `--database=<name|file>` selects a
registered resource or direct file; `--ldf=<file>` is the direct-file form. If
exactly one LDF is registered, it is selected automatically.

### ldf node / frame / signal

```bash
bmcli ldf node list --database=body-lin --format=json
bmcli ldf node show BodyMaster --database=body-lin --format=json
bmcli ldf frame list --type=unconditional --database=body-lin --format=json
bmcli ldf frame show MasterFrame --database=body-lin --format=json
bmcli ldf signal list --database=body-lin --format=json
bmcli ldf signal show Command --database=body-lin --format=json
```

The frame model distinguishes `unconditional`, `diagnostic`,
`event-triggered`, and `sporadic` frames. Frame details include identifier,
publisher, DLC, checksum model, master-read/master-write classification, signal
layout, and the initial payload derived from signal defaults.

### ldf schedule

```bash
bmcli ldf schedule list --database=body-lin --format=json
bmcli ldf schedule show NormalTable --database=body-lin --format=json
bmcli ldf schedule activate NormalTable \
  --database=body-lin --channel=10356/6 --format=json
bmcli ldf schedule deactivate --channel=10356/6 --format=json
```

`list` and `show` are offline, read-only operations. `activate` validates the
complete named table before changing hardware, then materializes it as the
daemon-owned `system` LIN schedule. The selected channel must already be open
as a LIN master. LDF delays are preserved in the model and rounded up to whole
milliseconds only when materialized because the BMAPI task interval is an
integer millisecond value.

Only unconditional and diagnostic frame entries can be materialized in this
phase. Event-triggered frames, sporadic frames, and configuration schedule
commands such as `AssignNAD` remain queryable but activation fails explicitly;
BMCLI does not guess their runtime policy. A failed entry application returns
nonzero and may leave a partial `system` table, which the caller can inspect or
clear. No hidden retry or source-file rollback is performed.

---

## logging

Online (host-side) CAN/CAN FD and LIN traffic recording writes ASC or BLF. For
on-device (offline) ASC or BLF 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/CAN FD or LIN traffic to a host ASC/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,10356/1 --output=log.blf --format=blf
bmcli logging start --hardware --channel=10356/0,10356/1 --output=log.asc       # on-device (== offline logging start)
bmcli logging start --hardware --channel=10356/0 --output=log.blf --format=blf
```

| 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` in host and on-device modes. |
| `--duration` | infinite | Duration in seconds |
| `--hardware` | off | Record ON-DEVICE to generation-2.5 storage. Generation-3 devices support online recording only. Equivalent to `offline logging start`. |

### logging stop

```bash
bmcli logging stop        # stop host-side recording
bmcli logging stop --hardware --channel=<generation-2.5-SN/port>   # stop on-device recording
```

### 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; devices without offline storage report
`supported=false` and omit unsupported storage fields.

In daemon mode, `logging start`, `logging stop`, and `logging status` honor the
global `--format=json` output contract. Status returns one object containing a
`host` object and a `devices` array; it never prefixes or appends human-readable
survey lines to JSON stdout.

Host status includes `errors` and `last_error`. Open, write, flush, and close
failures move the background worker to `stopped` and remain visible until the
next successful start; an accepted start is not evidence that later file I/O
succeeded. This is especially important for full disks and removable paths.

---

## replay

Online (host-side) replay of an ASC or uncompressed BLF log file. CAN/CAN FD
works in standalone and daemon modes; LIN ASC/BLF replay uses a daemon-opened LIN channel so its role,
electrical setup and schedule ownership remain explicit. 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`.

Host replay likewise reports terminal `errors`/`last_error` for invalid input,
read, timing, write, or close failures instead of remaining deceptively active.

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

In daemon mode, `replay start`, `replay stop`, and `replay status` honor the
global `--format=json` output contract. Status returns one object containing a
`host` object and a `devices` array; it never prefixes or appends human-readable
survey lines to JSON stdout.

---

## offline

On-device (offline) recording/replay is available only on generation-2.5
hardware. Generation-3 analyzers support online host recording/replay, not this
offline storage workflow. Device-config save/load/clear has its own capability
contract and must not be inferred from recording support.

`offline logging` shares its underlying implementation with the `--hardware`
form of `logging`. On-device replay uses the explicit `offline replay`
lifecycle; there is no duplicate `replay --hardware` entry.

| 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` | — | Start on-device replay (`BM_SetReplay`) |
| `offline replay stop`  | — | 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 --channel=<generation-2.5-SN/port>
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.

BMCLI owns the GWR-compatible UDP listeners and wire parser directly. It does
not reverse `BM_OpenRemote` into a server API. Control records are translated
to `BM_Control` on the daemon's existing physical handles; generic-I/O records
are translated between UDP and `BM_Write`/RxBroker. This keeps the ordinary
BMAPI remote-client path independent from gateway-server routing.

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.

Gateway startup does not claim a channel. While no explicit claim exists, local
and legacy remote protected controls retain their historical last-writer-wins
behavior. RxBroker fans each received frame independently to the gateway, local
`message recv`, host logging, signal decoding, and Web streams. Ordinary local
and remote message send is also shared.

For deterministic control, local automation uses `channel claim`; a new remote
BMAPI client uses `BM_QueryRemoteSession` and `BM_ClaimRemoteSession`. The remote
session lazily owns a channel on its first protected write. A conflicting
configuration, close, TX Task, route, LIN schedule/response, replay, ISO-TP, or
UDS operation returns busy, while send and passive monitoring remain available.
Remote `BM_Close` completes the deployed ordered neutralization sequence and
returns that one channel without releasing the other channels in the session.

### gateway status

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

Returns the state, configured target and advertised addresses, exported channel
count, cumulative `local_to_remote`, `remote_to_local`, `dropped`, and `errors`
counters, plus `session_claimed`, owner metadata, claimed channel count, lease
expiry, and configuration generation.

### gateway stop

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

Stops forwarding, closes UDP listeners `3502`/`2502`, and releases the
gateway's RxBroker cursors. 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_QueryRemoteSession(gateway_ipv4, ...)          # recommended capability check
BM_ClaimRemoteSession(&session, gateway_ipv4, ...) # new clients; starts heartbeat
BM_OpenEx(... remote channel info ...)
BM_GetNotification() / BM_WaitForNotifications()
BM_Read() and BM_Write()
BM_Close()
BM_ReleaseRemoteSession(session)
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.

Old BMAPI clients that never claim remain compatible in unlocked mode. A new
client connecting to an old GWR receives unsupported/timeout from the session
query and must explicitly decide whether unlocked fallback is acceptable. A
failed claim never silently falls back. The public declarations, structures,
enums, error mappings, wire opcode, and compatibility rules are normative in
`bmcli_multi_endpoint_control_design.md`, section **Public BMAPI additions**.

---

## web

Host the packaged engineering console and BUSMUST Automation API v1 inside the
BMCLI daemon. The service exposes typed bench and device snapshots, channel
lifecycle and statistics, CAN/CAN FD/LIN message send, cursor-based raw history,
SSE, binary WebSocket frames, DBC signal decode plans, logging/replay/gateway
lifecycle, bounded firmware-check jobs, and runtime command discovery. It does
not execute arbitrary command lines. `/api/v1/operations` reports every current
CLI operation, explicit permission/execution policy, parameter metadata, and
whether its typed HTTP handler has been migrated.

### web start

```bash
# Default: loopback only, packaged console, no token required
bmcli web start --listen=127.0.0.1:8080 --format=json

# Explicit project Web root
bmcli web start --listen=127.0.0.1:8080 --web-root=<directory> --format=json

# Non-loopback plain HTTP requires authentication and explicit risk acknowledgement
bmcli web start --listen=0.0.0.0:8080 --token=<secret> --allow-insecure-http --format=json
```

`web start` starts the daemon when needed. Open hardware channels with `daemon
start --channel=...` or `channel open` before using hardware endpoints. The
default history capacity is 8192 events and can be changed with
`--history-capacity=<count>`.

| Endpoint | Purpose |
|---|---|
| `GET /api/v1/info` | Product, build, API, and service identity |
| `GET /api/v1/health` | Daemon, Web, and channel-broker health |
| `GET /api/v1/capabilities` | Transports, resources, limits, and security defaults |
| `GET /api/v1/openapi.json` | Packaged and regression-gated OpenAPI 3.1 contract |
| `GET /api/v1/operations[/{operation_id}]` | Runtime CommandRegistry and HTTP migration policy |
| `GET /api/v1/bench` | Selected project/global bench snapshot |
| `GET /api/v1/devices` | Local physical analyzers, deduplicated across channels |
| `POST /api/v1/devices/{selector}/firmware-check` | Queue a non-disruptive firmware release check |
| `GET /api/v1/jobs[/{job_id}]` | List or inspect bounded asynchronous jobs |
| `GET/POST /api/v1/channels` | List or open daemon channels |
| `GET/PATCH/DELETE /api/v1/channels/{selector}` | Inspect, configure, or close a daemon channel |
| `GET /api/v1/channels/{selector}/stats` | Hardware counters, rates, bus state, and broker retention |
| `GET/POST /api/v1/databases` | List or load typed database resources inside the project working root |
| `GET/DELETE /api/v1/databases/{name}` | Inspect or unload one database resource |
| `GET/POST /api/v1/tx-tasks` | List or add a channel TX task/LIN slave response |
| `DELETE /api/v1/tx-tasks/{task_id}` | Remove one channel TX task |
| `GET/POST /api/v1/routes` | List or add a hardware message route |
| `DELETE /api/v1/routes/{route_id}` | Remove one hardware message route |
| `POST /api/v1/messages` | Typed CAN/CAN FD/LIN send or LIN transaction |
| `GET /api/v1/messages/history` | Cursor-based raw history with explicit expiry |
| `GET /api/v1/messages/stream` | SSE raw-message stream with gap events |
| `GET /api/v1/messages/ws` | `bmcli.frames.v1` BMF1 binary stream |
| `GET /api/v1/signals` | Active DBC decode plans |
| `POST /api/v1/signals/plans` | Create a bounded DBC signal plan |
| `DELETE /api/v1/signals/plans/{plan_id}` | Delete a signal plan |
| `GET /api/v1/signals/latest` | Latest decoded signal sample |
| `GET /api/v1/signals/history` | Cursor-based decoded signal history |
| `GET /api/v1/signals/stream` | SSE decoded-signal stream |
| `GET/POST/DELETE /api/v1/logging` | Inspect, start, or stop project-confined ASC logging |
| `GET/POST/DELETE /api/v1/replay` | Inspect, start, or stop project-confined ASC/BLF replay |
| `GET/POST/DELETE /api/v1/gateway` | Inspect, start, or stop the remote-BMAPI gateway |

The exact request fields, units, limits, errors, query parameters, and WebSocket
layout are authoritative in `/api/v1/openapi.json`. Read it at runtime instead
of copying this table into an application. Percent-encode `/` when a stable
`SN/port` selector is placed in a path segment, for example `10356%2F0`; query
parameters and JSON fields keep the ordinary `10356/0` form. `POST
/api/v1/execute` is not a
shell: v1 accepts only the migrated one-token `version` operation. Use dedicated
resource endpoints for hardware work.

One channel PATCH may contain only one hardware setter group. CAN nominal/data
timing is one group and LIN version/checksum is one group; mode, termination,
pull-up, bitrate, and power are separate groups. Valid same-value updates are
idempotent. This prevents a later BMAPI setter failure from leaving an
undisclosed partially applied multi-field request.

All ordinary daemon receive consumers share one RxBroker owner per channel.
`message recv`, software logging, gateway forwarding, Web history, SSE,
WebSocket, and DBC signal decoding therefore use independent cursors instead of
competing calls to `BM_Read`. History expiry returns `409 cursor_expired` with
`error.details.oldest_available` and `newest_available`; SSE and WebSocket streams
emit an explicit gap event with the same recovery bounds. A resumable client
continues from `oldest_available - 1`, de-duplicates by sequence, and must not
silently replace an expired cursor with `now`.

### Security and limits

- Default bind is `127.0.0.1`; CORS is disabled.
- Any non-loopback bind requires `--token`; API callers use `Authorization:
  Bearer <token>`. WebSocket clients may use `bmcli.token.<token>` as a second
  subprotocol.
- Non-loopback bind also requires the presence-only `--allow-insecure-http`
  acknowledgement. This option does not weaken loopback defaults or add
  encryption; it prevents an accidental remote bind from looking secure merely
  because a token was supplied.
- The current embedded server uses plain HTTP. A bearer token authenticates a
  caller but does not encrypt headers, payloads, bus traffic, or the token
  itself. Keep loopback as the normal deployment. Use a non-loopback listener
  only on a trusted isolated engineering network with host firewall rules, or
  place it behind an approved HTTPS reverse proxy; do not expose it directly to
  an untrusted LAN or the Internet.
- Linux packages include an audited Nginx profile at
  `share/bmcli/packaging/https/`. It terminates TLS, preserves SSE/WebSocket
  streaming, and proxies only to the BMCLI loopback listener. Certificate
  validation must pass normally; do not use browser security bypasses.
- Static files are read-only and reject path traversal.
- Request body limit is 64 KiB; response limit is 2 MiB; simultaneous client,
  SSE, and WebSocket limits are reported by `capabilities`.
- Treat Web message send and signal-plan creation as active bench operations.
  Apply the same authorization and cleanup rules as direct BMCLI commands.

### web status / stop

```bash
bmcli web status --format=json
bmcli web stop --format=json
```

`web stop` releases the listener and stream clients but leaves a pre-existing
daemon and its open channels running.

## A2L and XCP on CAN/CAN FD

BMCLI treats A2L as a read-only symbol/address contract, not an editor. The
first parser scope is intentionally consumer-driven: module and byte order,
scalar `MEASUREMENT` and `CHARACTERISTIC`, address/extension, record layout,
bit mask and matrix size, plus IDENTICAL, LINEAR and RAT_FUNC conversion.
Unknown balanced blocks are skipped; an unsupported layout or conversion is
reported rather than guessed, especially before a write.

For multi-ECU files, symbols, `COMPU_METHOD`, `RECORD_LAYOUT` and inherited
`BYTE_ORDER` remain scoped to their containing `MODULE`. Duplicate symbol names
must be qualified with `--module=<name>`; BMCLI never chooses one ECU
implicitly. A missing `ECU_ADDRESS`, `READ_ONLY` value, non-scalar
characteristic, unsafe mask, or unsupported conversion remains visible for
inspection but cannot be used for an XCP access.

Discover exact commands and options from:

```bash
bmcli help a2l --format=json
bmcli help xcp --format=json
```

The available XCP families cover endpoint configuration, connect/status/
disconnect, external security unlock, raw memory read/write/checksum,
A2L-backed measurement and characteristic access, and bounded DAQ. XCP on CAN
uses direct CTO/DTO frames and does not use ISO-TP. UDS uses BMCLI's software
ISO-TP transport. Both protocols can run on one analyzer channel when their
CAN IDs are distinct; raw receive, logging and Web continue through independent
RxBroker cursors.

`xcp memory write` supports `--dry-run` to show the exact address, extension
and bytes without transmitting, and `--verify` for exact readback after a
write. CONNECT address granularity and live DAQ entry resolution are honored;
byte lengths that cannot be represented by the negotiated granularity are
rejected rather than rounded. UDS and XCP response routes are validated in
both configuration orders.

The first DAQ implementation supports one list/ODT with at most 32 scalar
symbols and a 256-sample host queue. `xcp daq status` reports queue and route
drop counters. A full queue drops the oldest retained sample explicitly; a
caller must not interpret that as a lossless acquisition.
`xcp daq receive` returns one bounded JSON document with a `samples` array.
Use `--format=json`; JSONL and CSV are rejected for this command. For a workflow
that owns its XCP state, clean up in this order: DAQ stop, DAQ delete,
disconnect, then `database unload <resource>` for only the A2L it loaded.

OEM seed/key logic is always external. BMCLI loads a same-bitness shared
library that exports:

```c
int BMCLI_XcpComputeKey(
    uint8_t resource,
    const uint8_t *seed,
    uint32_t seed_length,
    uint8_t *key,
    uint32_t *key_length);
```

Return zero on success. On entry `*key_length` is the key-buffer capacity
(currently at most 62 bytes); on return it is the produced length. Resource
bits are CAL/PAG `0x01`, DAQ `0x04`, STIM `0x08`, and PGM `0x10`. BMCLI never
derives, records, or guesses an OEM algorithm.

The Web service exposes bounded XCP operations at `GET/POST /api/v1/xcp`.
The POST body uses the same tokenized `argv` model documented by the live
OpenAPI contract. A2L upload, durable time-series storage, users, reports,
calibration approval and other general application concerns belong to the
surrounding Web backend, not the BMCLI bus-domain service.

## Modbus RTU, ASCII, and TCP

BMCLI 1.2.1 is a Modbus master/client, not a slave/server. It supports FC01,
FC02, FC03, FC04, FC05, FC06, FC15, FC16, FC20, FC21, and FC24. Addresses are
always zero-based protocol addresses; a vendor display reference such as 40001
must be converted according to that vendor's documentation.
All `modbus` commands produce structured data and require `--format=json` or
`--format=jsonl`; use JSON for bounded commands and REST responses.

Use runtime Help as the exact option contract:

```text
bmcli help modbus --format=json
bmcli help modbus read --format=json
bmcli help modbus resource read --format=json
bmcli help modbus poll start --format=json
```

Explicit raw access accepts either a logical `--channel` imported into the
bench or a complete endpoint. A TCP example is:

```text
bmcli modbus read --transport=tcp --host=127.0.0.1 --unit-id=1 --table=holding-registers --address=0 --count=2 --format=json
```

Serial endpoints require `--transport=rtu|ascii`, `--port`, `--baud`,
`--data-bits`, `--parity`, `--stop-bits`, and `--unit-id`. RTU requires eight
data bits. BMCLI validates CRC/LRC, Unit ID, function code, transaction ID,
response length, exception responses, timeout, and protocol quantity limits.
Read operations may be retried by a supervising application; writes are not
automatically retried because a timeout leaves completion ambiguous.
`modbus status` only opens the selected transport. Its
`transport_open=true, slave_checked=false` result does not prove that a Unit ID
responds; issue a bounded read of a known address for that check.

### EdgeX profiles and logical devices

BMCLI reads the Linux Foundation EdgeX Modbus Device Profile subset needed for
`deviceResources` and `deviceCommands`, including value type, read/write access,
scale, offset, unit, range, mappings, byte/word order, and Modbus attributes.
Unknown YAML/JSON fields are skipped. This is a tolerant read-only importer,
not an EdgeX editor.
The official EdgeX table spelling is `DISCRETES_INPUT`; the older
`DISCRETE_INPUTS` spelling is accepted only as a compatibility alias.
EdgeX Device Definitions may bind `modbus-tcp`, `modbus-rtu`, or
`modbus-ascii`. Command writes accept raw mapping keys or unambiguous published
mapping labels.

```text
bmcli database load --file=boiler-profile.yaml --type=modbus-edgex --name=boiler --format=json
bmcli bench import --file=devices.yaml --source-format=edgex-modbus --device=Boiler-RTU --format=json
bmcli bench import --file=devices.yaml --source-format=edgex-modbus --device=Boiler-RTU --apply --format=json
bmcli modbus resource read --database=boiler --resource=Temperature --channel=boiler-rtu --format=json
```

`bench import` is dry-run by default and never guesses a COM/tty path, host,
Unit ID, device selection, or collision replacement. Use `--replace` only with
an explicitly reviewed `--apply` operation. Profiles share the generic typed
database lifecycle and unload with `database unload`.

Writing a resource with EdgeX access `W` sends exactly one write and does not
attempt a readback that the profile declares unavailable. Access `RW` retains
write verification. If a multi-resource command fails after earlier writes,
the structured error identifies `failed_resource`, `failed_operation`,
`completed_operations`, and whether a write may already have completed; callers
must not blindly retry the whole command.

### Polling, REST, and Dashboard

A daemon-owned poll job reads one EdgeX resource or command immediately and
then at a requested interval. Each job retains at most 256 complete dynamically
sized samples and 1 MiB in memory, and reports sequence, quality,
freshness/stale state, errors, gaps, and missed deadlines. The daemon owns one
persistent session per physical endpoint; poll jobs, one-shot CLI operations,
and Web calls serialize transactions through it. A different configuration for
an already-owned physical endpoint fails explicitly. The session reconnects
after an I/O/protocol failure. `--on-change` suppresses unchanged good values
but retains errors.

```text
bmcli modbus poll start --job=boiler-temp --interval=500 --database=boiler --resource=Temperature --channel=boiler-rtu --format=json
bmcli modbus value latest --job=boiler-temp --format=json
bmcli modbus value history --job=boiler-temp --count=100 --format=json
bmcli modbus value history --job=boiler-temp --after=120 --count=100 --format=json
bmcli modbus poll stop --job=boiler-temp --format=json
```

The typed Web API exposes raw operations, EdgeX resources/commands, poll
lifecycle, latest/history, and SSE updates under `/api/v1/modbus`. The bundled
engineering page provides endpoint inspection, polling, value, quality and
history visualization. History returns `oldest_sequence`, `latest_sequence`,
`next_after`, and gaps. SSE uses the sample sequence as its event ID, drains
every retained sample in order, resumes from `Last-Event-ID`, and emits an
explicit `gap` event if that cursor has expired. Runtime OpenAPI is
authoritative. Poll state and history are intentionally process-local and are
deleted when stopped or when the daemon exits; durable storage belongs to a
surrounding application service.
