# auto_can_test **Repository Path**: tom-embedded/auto_can_test ## Basic Information - **Project Name**: auto_can_test - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-10-26 - **Last Updated**: 2026-10-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Auto CAN Test `auto_can_test` is a Windows-first, data-driven CAN automation test MVP for ECU testing. It provides multi-device/multi-channel CAN configuration, logical bus to DBC binding, virtual/ZLG/PCAN adapters, extensible physical ECU controls, YAML/Excel/Python tests, ASC/JSONL/HTML/JUnit artifacts, ISO-TP diagnostics, semantic UDS/OBD request steps, and J1939 single-frame plus BAM/RTS-CTS transport. ## Install and run ```powershell python -m pip install -e . python -m auto_can_test run examples/cases/demo_signal.yaml --out runs/demo python -m auto_can_test excel-template --out examples/template.xlsx python -m auto_can_test script examples/scripts/demo_script.py --dbc examples/demo.dbc --bus powertrain --out runs/script-demo ``` ## Bench configuration Hardware and DBC bindings are kept in a separate bench file such as `examples/configs/demo_bench.yaml`: ```yaml kind: bench name: virtual_demo_bench default_bus: powertrain devices: virtual_can: type: virtual channels: can0: index: 0 can: fd: false nominal_bitrate: 500000 receive_own_messages: true buses: powertrain: device: virtual_can channel: can0 dbc: ../demo.dbc ``` J1939 node identity and defaults belong to the logical bus, not to every test step: ```yaml buses: charge: device: zlg_box_1 channel: can0 dbc: ../dbc/gbt27930.dbc j1939: local_address: 0xF4 remote_address: 0x56 default_priority: 6 default_transport: auto packet_interval_ms: 10 ``` Sending uses `local_address -> remote_address`; receiving expects the reverse direction. Step-level address fields override these defaults. PDU2 messages ignore the remote address and use broadcast semantics. `type` supports `virtual`, `zlg`, `pcan`, and `user`. A user adapter supplies `module` and `class`. ZLG devices can be located by `device_type` plus `device_index`, or by a configured device name; PCAN devices are located by their PCAN channel names. ## Physical ECU control Physical controls are configured separately from CAN devices. Logical point names such as `kl30` and `ignition` keep test cases independent from relay numbers and serial protocols. ```yaml controls: relay_box: type: user module: user_controls.usb_relay class: UsbRelayAdapter locator: port: COM7 baudrate: 9600 points: kl30: address: 1 direction: output initial_value: false safe_value: false ignition: address: 2 direction: output initial_value: false safe_value: false ``` A user control adapter receives the complete controller config and implements `open()`, `close()`, `set_value(point, value)`, `get_value(point)`, and `capabilities()`. COM-port framing and relay vendor commands remain in that adapter; the core does not depend on a specific serial library. YAML steps support direct writes, timed pulses, and polled assertions: ```yaml - control_set: target: relay_box.kl30 value: true - control_pulse: target: relay_box.ignition active_value: true duration_ms: 500 - control_expect: target: relay_box.ignition_feedback equals: true timeout_ms: 1000 stable_ms: 50 ``` Python scripts use `ctx.control.set()`, `ctx.control.get()`, `ctx.control.pulse()`, and `ctx.control.expect()`. Controllers open once per suite, apply `initial_value` on startup, and attempt every configured `safe_value` before closing even when a test fails. The built-in `type: virtual` controller is used by the demo and tests. ## ISO-TP, UDS, and OBD Diagnostic addressing belongs to the bench. Test steps refer to a connection name, so CAN IDs and ISO-TP timing are not repeated in every case: ```yaml diagnostics: uds_engine: bus: powertrain addressing_mode: normal_11bits tx_id: 0x7E0 rx_id: 0x7E8 request_timeout_ms: 1000 p2_timeout_ms: 1000 p2_star_timeout_ms: 5000 uds_standard_version: 2020 use_server_timing: true tester_present: enabled: true period_ms: 2000 suppress_positive_response: true nrc_window_ms: 100 late_response_guard_ms: 50 timeout_ms: 500 during_requests: pause isotp: stmin: 0 blocksize: 8 wftmax: 0 tx_data_length: 8 tx_padding: 0xAA obd_engine: bus: powertrain addressing_mode: normal_11bits tx_id: 0x7DF rx_id: 0x7E8 ``` The transport uses [can-isotp](https://can-isotp.readthedocs.io/en/latest/isotp/implementation.html) for ISO-TP segmentation, flow control, timing, and reassembly, with [udsoncan's connection API](https://udsoncan.readthedocs.io/en/latest/udsoncan/connection.html) as the request/response queue. Diagnostic connections require can-isotp blocking send so multi-frame transmission and flow-control complete before P2 starts. A request uses P2 for its first response and P2* after each matching `0x78`, while `request_timeout_ms` remains an independent total deadline. A successful `DiagnosticSessionControl` can update P2/P2* for that diagnostic connection. Explicit `tester_present` configuration starts keepalive after `before_each`, pauses it while the same connection has an active transaction, and stops it before `after_each` recovery. ```yaml - uds_request: diagnostic: uds_engine service: diagnostic_session_control session: 0x03 total_timeout_ms: 1000 - uds_request: diagnostic: uds_engine service: read_data_by_identifier did: vehicle_speed between: [0, 250] - obd_request: diagnostic: obd_engine mode: 0x01 pid: 0x0C between: [1700, 1800] ``` UDS steps use semantic service names and structured parameters. They support positive/NRC expectations, decoded value and field assertions, raw response fallback, variable saving, suppress-positive-response for modeled subfunction services, and one final transaction report. Suppressed requests use an explicit NRC observation window plus a late-response guard; no response means only that no NRC was observed and never confirms ECU state. Numeric SID plus `data`/response-prefix fields are intentionally rejected; schema-backed `custom_service` definitions are the controlled OEM extension. Common OBD Mode 01 PIDs are decoded for numeric assertions; unknown PIDs are still logged as raw response bytes. Python scripts use `ctx.uds.request()` and `ctx.obd.request()`. In Excel, use `message=uds_engine` and `signals=service=diagnostic_session_control;session=0x03` for UDS. For OBD, use `message=obd_engine`, `signals=mode=0x01;pid=0x0C`, and an expectation such as `value between 1700 1800`. ## UDS capability boundary The built-in semantic services are `0x10`, `0x11`, `0x14`, the documented `0x19` operations, `0x22`, `0x27`, `0x28`, `0x2E`, `0x2F`, `0x31`, `0x34`, `0x36`, `0x37`, `0x3E`, and `0x85`. A single request always uses the configured physical response path. Although ISO-TP address objects may contain `functional_id`, this release does not expose functional request execution or collect multiple ECU responses. Explicit per-connection TesterPresent lifecycle, firmware adapters/read-only metadata, runtime SecurityAccess algorithms, bounded firmware segment download, and unified `run` execution with a dedicated programming page are supported. The framework does not infer proprietary container formats, addresses, targets, algorithms, or project workflows; ODX, functional multi-response execution, resume flashing, and production ECU validation are not implemented. The `readable` and `writable` DID flags are editor hints and metadata, not runtime guards. This is deliberate: a case may still issue a read or write marked unsupported and assert the ECU's expected NRC. See [the configuration guide](docs/CONFIGURATION_GUIDE.md) for supported subfunctions, timing semantics, and definition workflows. ## Suites and cases A suite references one bench and contains one or more cases: ```yaml kind: suite name: smoke config: ../configs/demo_bench.yaml defaults: bus: powertrain timeout_ms: 1000 cases: - id: TC001 name: Engine signal test data: speed: 1500 steps: - send: message: 0x100 signals: RequestedSpeed: ${speed} - expect: signal: 257.EngineSpeed between: [1490, 1510] ``` A standalone file uses `kind: case` and its own `config`. Suites may import standalone cases with `cases: [{ref: ../cases/tc001.yaml}]`. Reusable cases are called from a step: ```yaml - call: ref: ../common/power_on.yaml with: target_voltage: 650 ``` Use `file.yaml#case_id` to select one case from a multi-case file. References are resolved relative to the caller, reject cycles, and have a maximum nesting depth of 16. `before_each` and `after_each` accept the same step syntax. Variables support safe calculations with `${{ ... }}`. Use `calculate` when a result is reused by later steps: ```yaml data: base_speed: 1200 ratio: 1.25 steps: - calculate: target_speed: ${{ round(base_speed * ratio) }} - send: message: EngineCommand signals: RequestedSpeed: ${target_speed} ``` A suite may load one firmware snapshot through an explicitly selected user adapter. The framework does not infer or parse proprietary container formats: ```yaml firmware: path: ../firmware/firmware.pkg adapter: path: ../firmware_adapters/custom_container.py function: load_firmware options: expected_variant: ecu_a cases: - id: write_crc steps: - uds_request: diagnostic: uds_engine service: write_data_by_identifier did: firmware_crc value: ${{ firmware.segments.application.metadata.crc32 }} ``` The adapter signature is `load_firmware(*, path, options) -> FirmwareImage`. Loading, integrity checks, immutable snapshot creation, path validation, and statically resolvable firmware references complete before any bus is opened. Expressions can read package metadata, segment address/length/metadata, and ordered group descriptions; segment data bytes are intentionally unavailable. See [`examples/firmware_adapters/synthetic_json.py`](examples/firmware_adapters/synthetic_json.py) for a public synthetic format example; project-specific parsers remain outside the public repository. SecurityAccess is available both as semantic `uds_request` exchanges and as one bounded composite step: ```yaml - uds_security_access: diagnostic: uds_engine level: 1 seed_length: 4 key_length: 4 total_timeout_ms: 1000 algorithm_timeout_ms: 300 algorithm: path: ../scripts/security.py function: calculate_key options: {} secret_env: project_secret: ECU_SECURITY_SECRET ``` The callback is `calculate_key(*, seed: bytes, level: int, context: dict) -> bytes`. It runs in a terminated-on-timeout subprocess; secrets are read from named environment variables in that subprocess and exposed through `context["secrets"]`. The framework never precomputes a key from firmware metadata, never changes session automatically, and never retries an invalid key. SecurityAccess and download CAN frames remain in ASC/BLF for debugging; only algorithm configuration data such as secret values is sanitized from step events. Frozen firmware data can be sent with the `uds_download` composite step: ```yaml - uds_download: diagnostic: uds_engine_fd group: programming # mutually exclusive with segment address_length: 4 size_length: 4 data_format_identifier: 0 max_block_length: 1024 # optional test-side cap for the complete 0x36 request request_timeout_ms: 1000 segment_timeout_ms: 30000 total_timeout_ms: 60000 ``` Each selected segment independently runs RequestDownload → TransferData blocks → RequestTransferExit in adapter/group order. If `max_block_length` is omitted, the ECU-returned complete-message length is used directly; when configured, it is only a test-side cap, so the smaller value is used. The two-byte UDS overhead is then deducted. Progress advances only after a valid echoed block response. There is no automatic retry, resume, session change, unlock, erase, or verification. Transfer transactions retain bounded digest evidence in JSON reports, while all diagnostic transfer and flow-control frames remain available in ASC/BLF for debugging. See `examples/cases/demo_uds_download.yaml` for the public synthetic example. ## Firmware programming with run Programming uses the same case/suite format and `run` command as other tests. Declare `firmware` globally and compose the existing UDS steps; no programming operations are added implicitly. Validate without opening the bus: ```powershell autocan run examples/cases/demo_complete_programming.yaml --validate-only --json ``` Each execution loads and validates its own immutable snapshot once; a separate validation command does not reserve a snapshot for a later invocation: ```powershell autocan run examples/cases/demo_complete_programming.yaml ` --firmware path/to/firmware.pkg ` --out runs/project-flash ``` Programming runs automatically use append-only step logs. Download progress is printed at fixed 10% checkpoints based only on ECU-confirmed bytes, without KiB, speed, ETA, or cursor movement, so local and Jenkins logs have the same structure. Interactive terminals color the status labels by default; redirected output and `NO_COLOR` stay plain. Use `--color always` or `--color never` to override detection, `--log-detail step` to enable step logs for any case, `--log-detail case` to keep case-only output, or `--json` for machine-readable output without progress text. For repeated stability testing, `autocan run CASE --repeat N` runs each case in that single case/suite file `N` consecutive times. Every execution receives a distinct report/capture ID such as `TC001__repeat_01` and a visible name suffix such as `[repeat 1/10]`; `before_each` and `after_each` run for every execution, and `fail_fast` still stops at the first failed execution. `N` must be a positive integer. Repetition does not apply to `--caselist`; combining `--repeat` with `--caselist` is rejected as invalid input. Use `--host-profile FILE` with `autocan run` or `autocan validate` to select host-local physical CAN/relay bindings without editing shared cases or bench files. A single profile can cover several named benches in a caselist. See [the configuration guide](docs/CONFIGURATION_GUIDE.md#主机-profile只切换物理连接) and [Windows](examples/profiles/zlg_windows.yaml) / [Linux](examples/profiles/zlg_linux.yaml) examples. This CLI option does not yet affect the graphical editor's execution button. Start the editor and open `/flash` for the ordinary case preview/run page. It opens the same case/suite as the graphical editor and requires saved content plus an offline preview token before start, so execution consumes the validated in-memory snapshot instead of silently rereading mutable inputs. It shows every case, normal steps, calls/cleanup context, each current download, overall download count, elapsed time, result, and HTML report link. `uds_poll` is an optional normal step for bounded RoutineControl-result or ReadDID polling; it is never inserted implicitly. CLI exit codes are `0` for valid preview/success, `1` for execution failure, `2` for case/firmware validation failure, `3` for a host-local resource conflict, and `130` for cancellation. Ordinary cases use standard step assertions and reports; the presence of firmware alone does not claim a complete programming workflow. Legacy `kind: flash_profile` files remain accepted only for migration compatibility. The separate `flash` command has been removed. Use `--firmware FILE` to override a single file’s optional global firmware path, or `--validate-only` for offline validation; `--caselist` supports validation but not a shared firmware override. JUnit, event, and capture artifacts continue to use the same report pipeline. See `examples/cases/demo_complete_programming.yaml` for a public synthetic ordinary-case example. `expect_all` waits until the latest received values from multiple messages satisfy every condition: ```yaml - expect_all: timeout_ms: 500 messages: - message: EngineStatus signals: EngineSpeed: {between: [1490, 1510]} - bus: charge message: ChargeStatus signals: Ready: 1 ``` `send` and `periodic` can bind DBC signals to a rolling counter and a user checksum function. The checksum signal is zeroed for provisional encoding; without a module its final value defaults to zero. ```yaml - periodic: message: ProtectedCommand signals: RequestedSpeed: 1500 period_ms: 10 bindings: rolling_counter: signal: RollingCounter checksum: signal: Checksum module: project_e2e.checksum ``` Periodic jobs can use a stable ID: ```yaml - periodic: id: heartbeat message: Heartbeat period_ms: 100 - stop_periodic: id: heartbeat ``` For an exact finite transmission count, use `count` instead of `duration_ms`; the two fields are mutually exclusive: ```yaml - periodic: message: Heartbeat period_ms: 10 count: 1000 ``` `examples/cases/performance_10ms.yaml` is the 1000-frame Windows timing acceptance case. Its report contains all 1000 jitter samples summarized as p95/max and marks whether p95 meets the 3 ms engineering target. ## DBC references Message names, decimal IDs, and hexadecimal IDs are interchangeable for sending and signal assertions: ```yaml message: EngineCommand message: 256 message: 0x100 signal: EngineStatus.EngineSpeed signal: 257.EngineSpeed signal: 0x101.EngineSpeed ``` IDs are complete 11-bit or 29-bit arbitration IDs. J1939 PGNs are configured separately through J1939 actions. ## J1939 Send raw single-frame or multi-packet messages with `j1939_send`; `transport: auto` selects BAM for global destinations and RTS/CTS for directed destinations. A DBC message name is the preferred PGN reference for fixed charging messages. ```yaml - j1939_send: bus: charge message: BHM signals: MaxAllowVoltage: 750 - j1939_expect: bus: charge message: BRM timeout_ms: 1000 - j1939_request: bus: charge message: BCP ``` `message` is resolved in the DBC bound to the selected bus. Raw communication can still use `pgn`. If both are present, their PGNs must match. For sending, `signals` and raw `data` are mutually exclusive. The transport manager validates TP size and packet count, tracks sessions by bus/SA/DA/PGN, reassembles out-of-order TP.DT packets, expires stale sessions, and handles CTS windows, EOM acknowledgement, and abort. GB/T 27930 tests should be expressed as J1939 message interactions in data-driven cases rather than a built-in simplified state machine. `expect_dm1` and `expect_dm2` decode DTCs after single-frame or TP reassembly and optionally filter by `spn` and `fmi`. Python scripts use `ctx.j1939.expect_dm1()` and `ctx.j1939.expect_dm2()`. ## Logging and reports Each run writes a standards-compliant Vector ASC file, one ASC per logical bus, `events.jsonl`, `summary.json`, `report.html`, and `junit.xml`. Transmit attempts are logged as `Tx` before the adapter call, while received frames and optional local echo are logged as `Rx`. The merged ASC assigns stable numeric channels in first-seen bus order; `summary.json` records the `asc_channels` mapping back to logical bus names. The files are readable by `python-can.ASCReader`. CAN receive threads only enqueue immutable frames. A dedicated writer thread performs ASC, per-bus ASC, optional BLF, and batched JSONL I/O, so slow disks do not normally delay the receive path. The queue applies backpressure only after 100,000 pending records and is fully drained before reports are generated. ## Hardware preflight Before running a case, open the configured CAN devices in passive preflight mode: ```powershell autocan probe examples/configs/zlg_bench.yaml --listen-ms 2000 autocan probe examples/configs/pcan_bench.yaml --listen-ms 2000 ``` `probe` validates DBC paths and adapter capabilities, opens every enabled channel, listens without transmitting, reads channel status where the adapter supports it, and then closes the devices. It never opens configured physical control devices. A zero `received_frames` count is not itself a failure because a quiet bus is valid; verify traffic with the ECU powered when traffic is expected. For the first connection, use `mode: listen_only` where the hardware supports it, verify channel mapping and bitrates, then switch only the channels that must transmit to `normal`. Confirm the total bus termination with a meter while power is off; software termination settings cannot detect an incorrect external resistor network. ## ZLG adapter The bundled ZLG SDK is the 20260414 x64 runtime under `auto_can_test/vendor/zlg/` and requires 64-bit Python. The adapter validates the PE architecture before loading the DLL so an incompatible runtime fails with a clear error. A complete configuration template is available at `examples/configs/zlg_bench.yaml`. Software termination control maps to the SDK's `initenal_resistance` setting; CAN FD maps nominal/data bitrates to `canfd_abit_baud_rate` and `canfd_dbit_baud_rate`. ZLG preflight health data comes from `ZCAN_ReadChannelErrInfo`. The vendor marks `ZCAN_ReadChannelStatus` as unsupported and potentially deprecated, so the adapter does not call it; `controller_status_available` is therefore `false` and only the documented error-information bytes are reported. ZLG CAN FD hardware is initialized with `ZCAN_TYPE_CANFD` even when a logical channel carries Classic CAN frames, matching the vendor's USBCANFD example. In that case the controller data bitrate defaults to the nominal bitrate and may be overridden with `channels..options.controller_data_bitrate`. Hardware timestamps default to microseconds; use device or channel option `hardware_timestamp_unit: ns|us|ms` if the selected ZLG product differs. The built-in ZLG adapter accepts indexed or named device location. `open.exclusive: true` may be used to assert the SDK's exclusive-open behavior. Shared opening, `reset_on_open: true`, timeout options, and unknown open options are rejected because the vendor API used here cannot guarantee those semantics. ## PCAN adapter The built-in `type: pcan` adapter supports Classic CAN and CAN FD through python-can's PCAN-Basic interface on Windows and Linux. Drivers and runtimes are not bundled. Windows requires the PEAK driver and matching-bitness `PCANBasic.dll`; Linux requires PEAK's **chardev** driver and `libpcanbasic.so` (the kernel SocketCAN driver alone is insufficient). See the [python-can PCAN documentation](https://python-can.readthedocs.io/en/v4.6.1/interfaces/pcan.html) and [PEAK Linux requirements](https://www.peak-system.com/fileadmin/media/linux/can-pcan-basic.php). ```yaml devices: pcan_usb: type: pcan locator: method: channel_name channel_prefix: PCAN_USBBUS options: send_timeout_ms: 100 channels: can0: index: PCAN_USBBUS1 mode: normal can: fd: false nominal_bitrate: 500000 can1: index: PCAN_USBBUS2 mode: listen_only can: fd: false nominal_bitrate: 250000 ``` PCAN channel names are the physical locators. Supported families are `PCAN_ISABUSx`, `PCAN_DNGBUSx`, `PCAN_PCIBUSx`, `PCAN_USBBUSx`, `PCAN_PCCBUSx`, and `PCAN_LANBUSx`, where numbering starts at 1. An integer `index` can also be combined with `locator.channel_prefix`. For CAN FD, use the same channel settings as ZLG: `can.fd: true`, `nominal_bitrate`, `data_bitrate`, and `brs`. Optional sample points and SJWs are supported. FD timing uses an 80 MHz clock with exact bitrates and the closest achievable sample points (default 80%); unrepresentable timing fails before opening any channel. Classic CAN retains its fixed bitrate presets and supports optional custom timing at 8 MHz. The adapter supports normal/listen-only modes, standard/extended IDs, up to 64-byte FD frames, Classic CAN remote frames, receive filters, timestamps, status, reset, LED flashing, and receive-own-message mode when supported by the installed PCAN-Basic driver. FD channels can also transmit Classic CAN frames; the frame format comes from the DBC. BRS follows the channel setting and is never set on Classic CAN frames. Software termination control and loopback mode are unsupported. See `examples/configs/pcan_bench.yaml` for the existing dual-channel Classic CAN bench, `examples/configs/pcan_fd_bench.yaml` for a single-channel FD bench shared by Windows/Linux, and [PCAN FD hardware testing](docs/PCAN_FD_TESTING.md) for installation and send/receive checks.