mirror of
https://github.com/espressif/esp-idf.git
synced 2025-10-03 10:30:58 +02:00
Merge branch 'features/twai_utils_example' into 'master'
feat(twai/example): Adds a TWAI utility example like can-utils. Closes IDF-6093 See merge request espressif/esp-idf!39387
This commit is contained in:
@@ -371,7 +371,7 @@ Application Examples
|
|||||||
|
|
||||||
.. list::
|
.. list::
|
||||||
|
|
||||||
- Temporary no.
|
- :example:`peripherals/twai/twai_utils` demonstrates how to use the TWAI (Two-Wire Automotive Interface) APIs to create a command-line interface for TWAI bus communication, supporting frame transmission/reception, filtering, monitoring, and both classic and FD formats for testing and debugging TWAI networks.
|
||||||
|
|
||||||
API Reference
|
API Reference
|
||||||
-------------
|
-------------
|
||||||
|
@@ -371,7 +371,7 @@ TWAI控制器能够检测由于总线干扰产生的/损坏的不符合帧格式
|
|||||||
|
|
||||||
.. list::
|
.. list::
|
||||||
|
|
||||||
- 暂无
|
- :example:`peripherals/twai/twai_utils` 演示了如何使用 TWAI(Two-Wire Automotive Interface,双线汽车接口)API 创建一个命令行工具,用于 TWAI 总线通信,支持帧的发送/接收、过滤、监控,以及经典和 FD 格式,以便测试和调试 TWAI 网络。
|
||||||
|
|
||||||
API 参考
|
API 参考
|
||||||
--------
|
--------
|
||||||
|
@@ -530,6 +530,12 @@ examples/peripherals/twai/twai_self_test:
|
|||||||
temporary: true
|
temporary: true
|
||||||
reason: lack of runners
|
reason: lack of runners
|
||||||
|
|
||||||
|
examples/peripherals/twai/twai_utils:
|
||||||
|
disable:
|
||||||
|
- if: SOC_TWAI_SUPPORTED != 1
|
||||||
|
depends_components:
|
||||||
|
- esp_driver_twai
|
||||||
|
|
||||||
examples/peripherals/uart/uart_dma_ota:
|
examples/peripherals/uart/uart_dma_ota:
|
||||||
disable:
|
disable:
|
||||||
- if: SOC_UHCI_SUPPORTED != 1
|
- if: SOC_UHCI_SUPPORTED != 1
|
||||||
|
8
examples/peripherals/twai/twai_utils/CMakeLists.txt
Normal file
8
examples/peripherals/twai/twai_utils/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||||
|
# in this exact order for cmake to work correctly
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||||
|
idf_build_set_property(MINIMAL_BUILD ON)
|
||||||
|
project(twai_utils)
|
572
examples/peripherals/twai/twai_utils/README.md
Normal file
572
examples/peripherals/twai/twai_utils/README.md
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
| Supported Targets | ESP32 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-H21 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
|
||||||
|
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- |
|
||||||
|
|
||||||
|
# TWAI Console Example
|
||||||
|
|
||||||
|
This example demonstrates using the TWAI (Two-Wire Automotive Interface) driver through an interactive console interface. It provides comprehensive TWAI functionality including frame transmission/reception, message filtering, and bus monitoring. The example can be used for both standalone testing via loopback mode and real TWAI network communication.
|
||||||
|
|
||||||
|
**Supported Commands:**
|
||||||
|
|
||||||
|
| Command | Description | Linux can-utils Equivalent |
|
||||||
|
|---------|-------------|----------------------------|
|
||||||
|
| `twai_init <controller> -t <tx> -r <rx> [opts]` | Initialize TWAI controller with GPIO and mode options | `ip link set can0 up type can bitrate 500000` |
|
||||||
|
| `twai_deinit <controller>` | Deinitialize TWAI controller | `ip link set can0 down` |
|
||||||
|
| `twai_send <controller> <frame>` | Send TWAI frame (standard/extended/RTR/FD) | `cansend can0 123#DEADBEEF` |
|
||||||
|
| `twai_dump <controller>[,filter] [-t mode]` / `twai_dump <controller> --stop` | Monitor TWAI traffic with hardware filtering and timestamps | `candump can0` |
|
||||||
|
| `twai_info <controller>` | Display controller configuration, status | `ip -details link show can0` |
|
||||||
|
| `twai_recover <controller> [-t <ms>]` | Recover controller from Bus-Off state | N/A |
|
||||||
|
|
||||||
|
- Note: `twai_dump` runs continuously in the background. Use `twai_dump <controller> --stop ` to stop monitoring.
|
||||||
|
|
||||||
|
## How to Use This Example
|
||||||
|
|
||||||
|
### Hardware Required
|
||||||
|
|
||||||
|
- Any ESP development board with TWAI support.
|
||||||
|
- A TWAI transceiver (e.g., SN65HVD230, TJA1050).
|
||||||
|
- Jumper wires.
|
||||||
|
|
||||||
|
### Hardware Setup
|
||||||
|
|
||||||
|
Connect the ESP board to a transceiver:
|
||||||
|
```
|
||||||
|
ESP32 Pin Transceiver TWAI Bus
|
||||||
|
--------- ----------- --------
|
||||||
|
GPIO4 (TX) --> CTX
|
||||||
|
GPIO5 (RX) <-- CRX
|
||||||
|
3.3V --> VCC
|
||||||
|
GND --> GND
|
||||||
|
TWAI_H <--> TWAI_H
|
||||||
|
TWAI_L <--> TWAI_L
|
||||||
|
```
|
||||||
|
*Note: The specific GPIO pins for TX and RX must be provided in the `twai_init` command.*
|
||||||
|
|
||||||
|
### Quick Start - No Transceiver Mode
|
||||||
|
|
||||||
|
For immediate testing without any external hardware, you can use the **No Transceiver Mode** by connecting a single GPIO pin to itself.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect GPIO4 to itself (or leave it unconnected for self-test)
|
||||||
|
# Initialize with the same TX/RX GPIO, and enable loopback and self-test modes.
|
||||||
|
twai> twai_init twai0 -t 4 -r 4 --loopback --self-test
|
||||||
|
|
||||||
|
# Send a test frame
|
||||||
|
twai> twai_send twai0 123#DEADBEEF
|
||||||
|
|
||||||
|
# Check controller status
|
||||||
|
twai> twai_info twai0
|
||||||
|
```
|
||||||
|
|
||||||
|
This mode is ideal for learning the commands, testing application logic, and debugging frame formats without a physical bus.
|
||||||
|
|
||||||
|
### Configure the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idf.py menuconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigate to **Example Configuration** -> **TWAI Configuration** and configure:
|
||||||
|
|
||||||
|
- **Default Arbitration Bitrate**: Default arbitration bitrate in bits per second (bps).
|
||||||
|
- **Default FD Data Bitrate**: Default data bitrate for TWAI-FD in bits per second (bps).
|
||||||
|
- **Enable TWAI-FD Support**: Enable TWAI-FD (Flexible Data-rate) support (default: disabled)
|
||||||
|
- **TX Queue Length**: Length of the transmission queue for TWAI messages (default: 10)
|
||||||
|
|
||||||
|
**Note:** For every controller, you must specify the TX and RX pins explicitly with the `-t` and `-r` options when issuing `twai_init`. Failing to do so will make initialization return an error.
|
||||||
|
|
||||||
|
### Build and Flash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idf.py -p PORT flash monitor
|
||||||
|
```
|
||||||
|
(To exit the serial monitor, type `Ctrl-]`.)
|
||||||
|
|
||||||
|
## Command Reference
|
||||||
|
|
||||||
|
### `twai_init`
|
||||||
|
Initializes and starts the TWAI driver. **TX and RX pins are required.**
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
`twai_init <controller> -t <tx_gpio> -r <rx_gpio> [options]`
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<controller>`: Controller ID (`twai0`, `twai1`).
|
||||||
|
- `-t, --tx`: TX GPIO pin number (required).
|
||||||
|
- `-r, --rx`: RX GPIO pin number (required).
|
||||||
|
- `-b, --bitrate`: Arbitration bitrate in bps (default: CONFIG_EXAMPLE_DEFAULT_BITRATE).
|
||||||
|
- `-B, --fd-bitrate`: Data bitrate for TWAI-FD (FD-capable chips only, default: CONFIG_EXAMPLE_DEFAULT_FD_BITRATE).
|
||||||
|
- `--loopback`: Enable loopback mode.
|
||||||
|
- `--self-test`: Enable self-test mode (internal loopback).
|
||||||
|
- `--listen`: Enable listen-only mode.
|
||||||
|
- `-c, --clk-out`: Clock output GPIO pin (optional).
|
||||||
|
- `-o, --bus-off`: Bus-off indicator GPIO pin (optional).
|
||||||
|
|
||||||
|
### `twai_deinit`
|
||||||
|
Stops and de-initializes the TWAI driver.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
`twai_deinit <controller>`
|
||||||
|
|
||||||
|
### `twai_send`
|
||||||
|
Sends a standard, extended, RTR, or TWAI-FD frame.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
`twai_send <controller> <frame_str>`
|
||||||
|
|
||||||
|
**Frame Formats:**
|
||||||
|
- **Standard:** `123#DEADBEEF` (11-bit ID)
|
||||||
|
- **Extended:** `12345678#CAFEBABE` (29-bit ID)
|
||||||
|
- **RTR:** `456#R` or `456#R8` (Remote Transmission Request)
|
||||||
|
- **TWAI-FD:** `123##{flags}{data}` (FD-capable chips only)
|
||||||
|
- **flags**: single hex nibble `0..F`
|
||||||
|
- bit0 (`0x1`) = BRS (Bit Rate Switch, accelerate data phase)
|
||||||
|
- bit1 (`0x2`) = ESI (Error State Indicator)
|
||||||
|
- other bits reserved (set to 0)
|
||||||
|
- **data**: up to 64 bytes (0..64) of hex pairs, optional `.` separators allowed (e.g. `11.22.33`)
|
||||||
|
- example: `123##1DEADBEEF` (BRS enabled, data = DE AD BE EF)
|
||||||
|
|
||||||
|
### `twai_dump`
|
||||||
|
Monitors TWAI bus messages with filtering and candump-style output. This command runs in the background.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
- `twai_dump [-t <mode>] <controller>[,filter...]`
|
||||||
|
- `twai_dump <controller> --stop`
|
||||||
|
|
||||||
|
- **Options:**
|
||||||
|
- `-t <mode>`: Timestamp mode. Output format is `(seconds.microseconds)` with 6-digit microsecond precision, e.g. `(1640995200.890123)`.
|
||||||
|
- `a`: Absolute time (esp_timer microseconds since boot)
|
||||||
|
- `d`: Delta time between frames (time since previous frame)
|
||||||
|
- `z`: Zero-relative time from start (time since dump started)
|
||||||
|
- `n`: No timestamp (default)
|
||||||
|
- `--stop`: Stop monitoring the specified controller.
|
||||||
|
|
||||||
|
**Filter Formats:**
|
||||||
|
- `id:mask`: Mask filter (e.g., `123:7FF`).
|
||||||
|
- `low-high`: Range filter (e.g., `100-200`, FD-capable chips only).
|
||||||
|
- Multiple filters can be combined with commas (e.g., `twai0,123:7FF,100-200`).
|
||||||
|
|
||||||
|
### `twai_info`
|
||||||
|
Displays the TWAI controller's configuration and real-time status.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
`twai_info <controller>`
|
||||||
|
|
||||||
|
**Output Includes:**
|
||||||
|
- Status (Stopped, Running, Bus-Off)
|
||||||
|
- Node State (Error Active, Error Passive, etc.)
|
||||||
|
- TX/RX Error Counters
|
||||||
|
- Bitrate (Arbitration and Data)
|
||||||
|
- Configured GPIOs and operational modes.
|
||||||
|
|
||||||
|
### `twai_recover`
|
||||||
|
Initiates recovery for a controller that is in the Bus-Off state.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
`twai_recover <controller> [-t <ms>]`
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-t <ms>`: Recovery timeout.
|
||||||
|
- `-1`: Block indefinitely until recovery completes (default).
|
||||||
|
- `0`: Asynchronous recovery (returns immediately).
|
||||||
|
- `>0`: Timeout in milliseconds.
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Recovery only works when the controller is in Bus-Off state
|
||||||
|
- Use `twai_info <controller>` to check current node state
|
||||||
|
- Recovery may fail if bus conditions are still problematic
|
||||||
|
- In async mode (timeout=0), use `twai_info` to monitor recovery progress
|
||||||
|
|
||||||
|
**Typical Command Sequence:**
|
||||||
|
1. `twai_init` - Initialize controller
|
||||||
|
2. `twai_info` - Check status
|
||||||
|
3. `twai_dump` - Start monitoring (optional)
|
||||||
|
4. `twai_send` - Send frames
|
||||||
|
5. `twai_recover` - Recover from errors (if needed)
|
||||||
|
6. `twai_deinit` - Cleanup
|
||||||
|
|
||||||
|
Basic usage example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize controller 0 (bitrate 500 kbps, specify TX/RX pins)
|
||||||
|
twai> twai_init twai0 -b 500000 -t 4 -r 5
|
||||||
|
|
||||||
|
# Display controller information
|
||||||
|
twai> twai_info twai0
|
||||||
|
TWAI0 Status: Running
|
||||||
|
Node State: Error Active
|
||||||
|
Error Counters: TX=0, RX=0
|
||||||
|
Bitrate: 500000 bps
|
||||||
|
GPIOs: TX=GPIO4, RX=GPIO5
|
||||||
|
|
||||||
|
# Send standard frame on controller 0
|
||||||
|
twai> twai_send twai0 123#DEADBEEF
|
||||||
|
|
||||||
|
# Start monitoring controller 0 (accept all frames)
|
||||||
|
twai> twai_dump twai0
|
||||||
|
|
||||||
|
# Example received frame display (with default no timestamps)
|
||||||
|
twai0 123 [4] DE AD BE EF
|
||||||
|
```
|
||||||
|
|
||||||
|
### FD-Capable Chips Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize controller 0 with TWAI-FD enabled
|
||||||
|
twai> twai_init twai0 -b 1000000 -t 4 -r 5 -B 2000000
|
||||||
|
|
||||||
|
twai> twai_info twai0
|
||||||
|
TWAI0 Status: Running
|
||||||
|
Node State: Error Active
|
||||||
|
Error Counters: TX=0, RX=0
|
||||||
|
Bitrate: 1000000 bps (FD: 2000000 bps)
|
||||||
|
GPIOs: TX=GPIO4, RX=GPIO5
|
||||||
|
|
||||||
|
# Send FD frame with BRS on controller 0
|
||||||
|
twai> twai_send twai0 456##1DEADBEEFCAFEBABE1122334455667788
|
||||||
|
```
|
||||||
|
|
||||||
|
### PC Environment Setup (For Full Testing)
|
||||||
|
|
||||||
|
To test bidirectional communication between ESP32 and PC, set up a SocketCAN environment on Ubuntu:
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
- Ubuntu 18.04 or later
|
||||||
|
- USB-to-CAN adapter (PEAK PCAN-USB recommended)
|
||||||
|
- sudo access for network interface configuration
|
||||||
|
|
||||||
|
#### Quick Setup
|
||||||
|
|
||||||
|
1. **Install CAN utilities:**
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y can-utils
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
candump --help
|
||||||
|
cansend --help
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure CAN interface:**
|
||||||
|
```bash
|
||||||
|
# Classic CAN setup (500 kbps)
|
||||||
|
sudo ip link set can0 up type can bitrate 500000
|
||||||
|
|
||||||
|
# CAN-FD setup (1M arbitration, 4M data) - requires FD-capable adapter
|
||||||
|
sudo ip link set can0 up type can bitrate 1000000 dbitrate 4000000 fd on
|
||||||
|
|
||||||
|
# Verify interface
|
||||||
|
ip -details link show can0
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Test PC setup:**
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Monitor
|
||||||
|
candump can0
|
||||||
|
|
||||||
|
# Terminal 2: Send test frame
|
||||||
|
cansend can0 123#DEADBEEF
|
||||||
|
|
||||||
|
# Send FD frame (if FD adapter available)
|
||||||
|
cansend can0 456##1DEADBEEFCAFEBABE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bidirectional Testing
|
||||||
|
|
||||||
|
Once both PC and ESP32 are set up:
|
||||||
|
|
||||||
|
**ESP32 to PC:**
|
||||||
|
```bash
|
||||||
|
# PC: Start monitoring
|
||||||
|
candump can0
|
||||||
|
|
||||||
|
# ESP32: Initialize controller 0 and send frame
|
||||||
|
twai> twai_init twai0 -t 4 -r 5
|
||||||
|
twai> twai_send twai0 123#DEADBEEF
|
||||||
|
|
||||||
|
# PC shows: can0 123 [4] DE AD BE EF
|
||||||
|
# ESP32 shows: twai0 123 [4] DE AD BE EF
|
||||||
|
```
|
||||||
|
|
||||||
|
**PC to ESP32:**
|
||||||
|
```bash
|
||||||
|
# ESP32: Start monitoring controller 0
|
||||||
|
twai> twai_dump twai0
|
||||||
|
|
||||||
|
# PC: Send frame
|
||||||
|
cansend can0 456#CAFEBABE
|
||||||
|
|
||||||
|
# ESP32 shows: twai0 456 [4] CA FE BA BE
|
||||||
|
|
||||||
|
# Stop monitoring
|
||||||
|
twai> twai_dump twai0 --stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Frame Formats
|
||||||
|
|
||||||
|
- **Standard frames:** `123#DEADBEEF` (11-bit ID)
|
||||||
|
- **Extended frames:** `12345678#CAFEBABE` (29-bit ID)
|
||||||
|
- **RTR frames:** `456#R8` (Remote Transmission Request)
|
||||||
|
- **TWAI-FD frames:** `123##1DEADBEEF` (FD with flags, FD-capable chips only)
|
||||||
|
- **Data separators:** `123#DE.AD.BE.EF` (dots ignored)
|
||||||
|
|
||||||
|
|
||||||
|
### TWAI-FD Frame Format and Examples (FD-capable chips only)
|
||||||
|
|
||||||
|
TWAI-FD frames use two `#` characters: `ID##{flags}{data}`.
|
||||||
|
|
||||||
|
- Flags are a single hex nibble (`0..F`). Bit meanings:
|
||||||
|
- `0x1` BRS: Enable Bit Rate Switch for the data phase
|
||||||
|
- `0x2` ESI: Error State Indicator
|
||||||
|
- Other bits are reserved (set to 0)
|
||||||
|
- Data payload supports up to 64 bytes. The driver maps the payload length to the proper DLC per CAN FD rules automatically.
|
||||||
|
- Payload hex pairs may include `.` separators for readability (ignored by the parser).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# FD frame without BRS (flags = 0) on controller 0
|
||||||
|
twai> twai_send twai0 123##0DEADBEEFCAFEBABE1122334455667788
|
||||||
|
|
||||||
|
# FD frame with BRS (flags = 1, higher data speed)
|
||||||
|
twai> twai_send twai0 456##1DEADBEEFCAFEBABE1122334455667788
|
||||||
|
|
||||||
|
# FD frame with ESI (flags = 2)
|
||||||
|
twai> twai_send twai0 789##2DEADBEEF
|
||||||
|
|
||||||
|
# FD frame with BRS + ESI (flags = 3)
|
||||||
|
twai> twai_send twai0 ABC##3DEADBEEF
|
||||||
|
|
||||||
|
# Large FD frame (up to 64 bytes)
|
||||||
|
twai> twai_send twai0 DEF##1000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtering and Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monitor controller 0 (accept all frames)
|
||||||
|
twai> twai_dump twai0
|
||||||
|
twai0 123 [4] DE AD BE EF
|
||||||
|
twai0 456 [2] CA FE
|
||||||
|
twai0 789 [8] 11 22 33 44 55 66 77 88
|
||||||
|
|
||||||
|
# Monitor with absolute timestamps
|
||||||
|
twai> twai_dump -t a twai0
|
||||||
|
(1640995200.890123) twai0 123 [4] DE AD BE EF
|
||||||
|
(1640995200.895555) twai0 456 [2] CA FE
|
||||||
|
(1640995200.901000) twai0 789 [8] 11 22 33 44 55 66 77 88
|
||||||
|
|
||||||
|
# Monitor with delta timestamps (time between frames)
|
||||||
|
twai> twai_dump -t d twai0
|
||||||
|
(0.000000) twai0 123 [4] DE AD BE EF
|
||||||
|
(0.005432) twai0 456 [2] CA FE
|
||||||
|
(0.005445) twai0 789 [8] 11 22 33 44 55 66 77 88
|
||||||
|
|
||||||
|
# Monitor with zero-relative timestamps (from start of monitoring)
|
||||||
|
twai> twai_dump -t z twai0
|
||||||
|
(0.000000) twai0 123 [4] DE AD BE EF
|
||||||
|
(0.005432) twai0 456 [2] CA FE
|
||||||
|
(0.010877) twai0 789 [8] 11 22 33 44 55 66 77 88
|
||||||
|
|
||||||
|
# Monitor without timestamps (default)
|
||||||
|
twai> twai_dump -t n twai0
|
||||||
|
twai0 123 [4] DE AD BE EF
|
||||||
|
twai0 456 [2] CA FE
|
||||||
|
twai0 789 [8] 11 22 33 44 55 66 77 88
|
||||||
|
|
||||||
|
# Monitor controller 0 with exact ID filter (only receive ID=0x123)
|
||||||
|
twai> twai_dump twai0,123:7FF
|
||||||
|
Mask Filter 0: ID=0x00000123, mask=0x000007FF, STD
|
||||||
|
twai0 123 [4] DE AD BE EF
|
||||||
|
|
||||||
|
# Monitor controller 0 with ID range 0x100-0x10F (mask filter approach)
|
||||||
|
twai> twai_dump twai0,100:7F0
|
||||||
|
Mask Filter 0: ID=0x00000100, mask=0x000007F0, STD
|
||||||
|
|
||||||
|
# Monitor controller 0 with range filter (0xa to 0x15) - FD-capable chips only
|
||||||
|
twai> twai_dump twai0,a-15
|
||||||
|
Range Filter 0: 0x0000000a - 0x00000015, STD
|
||||||
|
|
||||||
|
# Monitor controller 0 with range filter (0x000 to 0x666)
|
||||||
|
twai> twai_dump twai0,000-666
|
||||||
|
Range Filter 0: 0x00000000 - 0x00000666, STD
|
||||||
|
|
||||||
|
# Monitor controller 0 with mixed filters (mask + range)
|
||||||
|
twai> twai_dump twai0,123:7FF,a-15
|
||||||
|
Mask Filter 0: ID=0x00000123, mask=0x000007FF, STD
|
||||||
|
Range Filter 0: 0x0000000a - 0x00000015, STD
|
||||||
|
|
||||||
|
# Monitor controller 0 with dual filters
|
||||||
|
twai> twai_dump twai0,020:7F0,013:7F8
|
||||||
|
Mask Filter 0: ID=0x00000020, mask=0x000007F0, STD
|
||||||
|
Mask Filter 1: ID=0x00000013, mask=0x000007F8, STD
|
||||||
|
|
||||||
|
# Monitor controller 0 with multiple range filters
|
||||||
|
twai> twai_dump twai0,10-20,100-200
|
||||||
|
Range Filter 0: 0x00000010 - 0x00000020, STD
|
||||||
|
Range Filter 1: 0x00000100 - 0x00000200, STD
|
||||||
|
|
||||||
|
# Monitor all frames on controller 0 (no filter)
|
||||||
|
twai> twai_dump twai0
|
||||||
|
|
||||||
|
# Stop monitoring controller 0
|
||||||
|
twai> twai_dump twai0 --stop
|
||||||
|
```
|
||||||
|
|
||||||
|
**Filter Types (FD-capable chips only):**
|
||||||
|
- **Mask filters:** `id:mask` format - Uses bitwise matching with configurable mask
|
||||||
|
- **Range filters:** `low-high` format - Hardware range filtering for ID ranges
|
||||||
|
- **Mixed filtering:** Combine both types in one command for maximum flexibility
|
||||||
|
|
||||||
|
### Testing Modes
|
||||||
|
|
||||||
|
**No Transceiver Mode (Testing without external hardware):**
|
||||||
|
```bash
|
||||||
|
# Use same GPIO for TX and RX with loopback and self-test
|
||||||
|
twai> twai_init twai0 -t 4 -r 4 --loopback --self-test
|
||||||
|
twai> twai_dump twai0
|
||||||
|
twai> twai_send twai0 123#DEADBEEF
|
||||||
|
# Frame appears immediately in dump output:
|
||||||
|
twai0 123 [4] DE AD BE EF
|
||||||
|
twai> twai_dump twai0 --stop
|
||||||
|
```
|
||||||
|
**Note:** This mode is perfect for testing TWAI functionality without external transceivers or wiring. The same GPIO is used for both TX and RX, and the combination of `--loopback` and `--self-test` flags ensures frames are properly transmitted and received internally.
|
||||||
|
|
||||||
|
**Loopback mode (with external transceiver):**
|
||||||
|
```bash
|
||||||
|
twai> twai_init twai0 -t 4 -r 5 --loopback
|
||||||
|
twai> twai_dump twai0
|
||||||
|
twai> twai_send twai0 123#54455354
|
||||||
|
# Frame appears immediately in dump output:
|
||||||
|
twai0 123 [4] 54 45 53 54
|
||||||
|
|
||||||
|
# Stop monitoring when done
|
||||||
|
twai> twai_dump twai0 --stop
|
||||||
|
|
||||||
|
# FD loopback test (FD-capable chips only)
|
||||||
|
twai> twai_init twai0 -t 4 -r 5 --loopback -B 2000000
|
||||||
|
twai> twai_dump twai0
|
||||||
|
twai> twai_send twai0 456##1DEADBEEFCAFEBABE1122334455667788
|
||||||
|
twai0 456 [16] DE AD BE EF CA FE BA BE 11 22 33 44 55 66 77 88 # FD frame (BRS)
|
||||||
|
|
||||||
|
# Stop monitoring
|
||||||
|
twai> twai_dump twai0 --stop
|
||||||
|
```
|
||||||
|
|
||||||
|
**Listen-only mode:**
|
||||||
|
```bash
|
||||||
|
twai> twai_init twai0 -t 4 -r 5 --listen
|
||||||
|
twai> twai_dump twai0
|
||||||
|
# Can receive but cannot send frames
|
||||||
|
# Stop with: twai_dump twai0 --stop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Recovery and Diagnostics
|
||||||
|
|
||||||
|
**Bus-Off Recovery:**
|
||||||
|
The TWAI controller can enter a Bus-Off state due to excessive error conditions. Use the `twai_recover` command to initiate recovery:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic recovery (default: block until complete)
|
||||||
|
twai> twai_recover twai0
|
||||||
|
I (1234) cmd_twai_core: Starting recovery from Bus-Off state...
|
||||||
|
I (1345) cmd_twai_core: Waiting for recovery to complete...
|
||||||
|
I (1456) cmd_twai_core: Recovery completed successfully in 100 ms
|
||||||
|
|
||||||
|
# Recovery with custom timeout
|
||||||
|
twai> twai_recover twai0 -t 5000
|
||||||
|
I (1234) cmd_twai_core: Starting recovery from Bus-Off state...
|
||||||
|
I (1345) cmd_twai_core: Waiting for recovery to complete...
|
||||||
|
I (1456) cmd_twai_core: Recovery completed successfully in 150 ms
|
||||||
|
|
||||||
|
# Asynchronous recovery (return immediately)
|
||||||
|
twai> twai_recover twai0 -t 0
|
||||||
|
I (1234) cmd_twai_core: Starting recovery from Bus-Off state...
|
||||||
|
I (1245) cmd_twai_core: Recovery initiated (async mode)
|
||||||
|
|
||||||
|
# If node is not in Bus-Off state
|
||||||
|
twai> twai_recover twai0
|
||||||
|
I (1234) cmd_twai_core: Recovery not needed - node is Error Active
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enhanced Status Information:**
|
||||||
|
The `twai_info` command now displays real-time dynamic status information:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
twai> twai_info twai0
|
||||||
|
TWAI0 Status: Running
|
||||||
|
Node State: Error Active
|
||||||
|
Error Counters: TX=0, RX=0
|
||||||
|
Bitrate: 500000 bps
|
||||||
|
GPIOs: TX=GPIO4, RX=GPIO5
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status and Node State Interpretations:**
|
||||||
|
- **Status: Running**: Driver initialized and operational (not Bus-Off)
|
||||||
|
- **Status: Bus-Off**: Controller offline due to excessive errors, requires recovery
|
||||||
|
- **Status: Stopped**: Driver not initialized
|
||||||
|
- **Node State: Error Active**: Normal operation, can transmit and receive freely
|
||||||
|
- **Node State: Error Warning**: Warning level reached (error counters ≥ 96)
|
||||||
|
- **Node State: Error Passive**: Passive mode (error counters ≥ 128, limited transmission)
|
||||||
|
- **Node State: Bus Off**: Controller offline (TX error counter ≥ 256, requires recovery)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Restoring Default Configuration:**
|
||||||
|
Use `twai_deinit <controller>` followed by `twai_init <controller>` to reset the driver to default settings.
|
||||||
|
|
||||||
|
**Bus-Off Recovery Issues:**
|
||||||
|
- **"Node is not in Bus-Off state"**: Recovery only works when the controller is in Bus-Off state. Use `twai_info` to check current node state.
|
||||||
|
- **Recovery timeout**: If recovery takes longer than expected, try increasing the timeout with `-t` option (e.g., `-t 15000` for 15 seconds).
|
||||||
|
- **Recovery fails**: Check physical bus conditions, ensure proper termination and that other nodes are present to acknowledge recovery frames.
|
||||||
|
|
||||||
|
**TWAI-FD specific issues:**
|
||||||
|
- **"TWAI-FD frames not supported"**: Your chip doesn't support FD mode. Use chips like ESP32-C5
|
||||||
|
- **FD bitrate validation**: Ensure FD data bitrate is higher than arbitration bitrate
|
||||||
|
- **PC FD compatibility**: Ensure your PC CAN adapter supports FD mode
|
||||||
|
|
||||||
|
**PC CAN issues:**
|
||||||
|
```bash
|
||||||
|
# Reset PC interface
|
||||||
|
sudo ip link set can0 down
|
||||||
|
sudo ip link set can0 up type can bitrate 500000
|
||||||
|
|
||||||
|
# For FD mode
|
||||||
|
sudo ip link set can0 up type can bitrate 1000000 dbitrate 4000000 fd on
|
||||||
|
```
|
||||||
|
|
||||||
|
**No communication:**
|
||||||
|
- Verify bitrates match on both sides
|
||||||
|
- For FD: Ensure both sides support FD and have compatible transceivers
|
||||||
|
- Check physical connections and transceiver power
|
||||||
|
- GPIO pins must be specified in the twai_init command (common: TX=GPIO4, RX=GPIO5)
|
||||||
|
- For no-transceiver testing, use the same GPIO for both TX and RX with `--loopback --self-test` flags
|
||||||
|
- Ensure proper CAN bus termination (120Ω resistors)
|
||||||
|
- Use loopback mode to test ESP32 functionality independently
|
||||||
|
|
||||||
|
**Quick diagnostic with no-transceiver mode:**
|
||||||
|
```bash
|
||||||
|
# Test if basic TWAI functionality works
|
||||||
|
twai> twai_init twai0 -t 4 -r 4 --loopback --self-test
|
||||||
|
twai> twai_send twai0 123#DEADBEEF
|
||||||
|
# If this fails, check ESP-IDF installation and chip support
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Error Messages:**
|
||||||
|
```bash
|
||||||
|
# Controller ID missing
|
||||||
|
twai> twai_send 123#DEADBEEF
|
||||||
|
E (1234) cmd_twai_send: Controller ID is required
|
||||||
|
|
||||||
|
# Interface not initialized
|
||||||
|
twai> twai_send twai0 123#DEADBEEF
|
||||||
|
E (1234) cmd_twai_send: TWAI0 not initialized
|
||||||
|
|
||||||
|
# Invalid frame format
|
||||||
|
twai> twai_send twai0 123DEADBEEF
|
||||||
|
E (1456) cmd_twai_send: Frame string is required (format: 123#AABBCC or 12345678#AABBCC)
|
||||||
|
|
||||||
|
# Invalid controller ID
|
||||||
|
twai> twai_init twai5 -t 4 -r 5
|
||||||
|
E (1678) cmd_twai_core: Invalid controller ID
|
||||||
|
```
|
4
examples/peripherals/twai/twai_utils/main/CMakeLists.txt
Normal file
4
examples/peripherals/twai/twai_utils/main/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
idf_component_register(SRCS "cmd_twai_dump.c" "cmd_twai_send.c" "cmd_twai_core.c" "cmd_twai.c" "twai_utils_main.c"
|
||||||
|
"twai_utils_parser.c"
|
||||||
|
REQUIRES esp_driver_twai esp_timer esp_driver_gpio console
|
||||||
|
INCLUDE_DIRS ".")
|
60
examples/peripherals/twai/twai_utils/main/Kconfig.projbuild
Normal file
60
examples/peripherals/twai/twai_utils/main/Kconfig.projbuild
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
menu "TWAI Configuration"
|
||||||
|
depends on SOC_TWAI_SUPPORTED
|
||||||
|
|
||||||
|
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
|
||||||
|
|
||||||
|
config EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
bool "Enable TWAI-FD Support"
|
||||||
|
depends on SOC_TWAI_SUPPORT_FD
|
||||||
|
default false
|
||||||
|
help
|
||||||
|
Enable TWAI-FD (Flexible Data-rate) support.
|
||||||
|
Allows up to 64 bytes of data per frame and dual bit rates.
|
||||||
|
Only available on chips that support TWAI-FD.
|
||||||
|
|
||||||
|
config EXAMPLE_DEFAULT_BITRATE
|
||||||
|
int "Default Arbitration Bitrate"
|
||||||
|
default 500000
|
||||||
|
help
|
||||||
|
Default arbitration bitrate in bits per second (bps).
|
||||||
|
|
||||||
|
config EXAMPLE_DEFAULT_FD_BITRATE
|
||||||
|
int "Default FD Data Bitrate"
|
||||||
|
depends on EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
default 1000000
|
||||||
|
help
|
||||||
|
Default data bitrate for TWAI-FD in bits per second (bps).
|
||||||
|
|
||||||
|
config EXAMPLE_TX_QUEUE_LEN
|
||||||
|
int "TX Queue Length"
|
||||||
|
range 1 100
|
||||||
|
default 10
|
||||||
|
help
|
||||||
|
Length of the transmission queue for TWAI messages.
|
||||||
|
|
||||||
|
config EXAMPLE_DUMP_QUEUE_SIZE
|
||||||
|
int "TWAI Dump Queue Size"
|
||||||
|
default 32
|
||||||
|
help
|
||||||
|
Size of the queue used to store received TWAI frames for dump task.
|
||||||
|
|
||||||
|
config EXAMPLE_DUMP_TASK_STACK_SIZE
|
||||||
|
int "TWAI Dump Task Stack Size"
|
||||||
|
default 4096
|
||||||
|
help
|
||||||
|
Stack size of the TWAI dump task.
|
||||||
|
|
||||||
|
config EXAMPLE_DUMP_TASK_PRIORITY
|
||||||
|
int "TWAI Dump Task Priority"
|
||||||
|
default 10
|
||||||
|
range 0 24
|
||||||
|
help
|
||||||
|
Priority of the TWAI dump task.
|
||||||
|
|
||||||
|
config EXAMPLE_DUMP_TASK_TIMEOUT_MS
|
||||||
|
int "TWAI Dump Task Timeout"
|
||||||
|
default 300
|
||||||
|
help
|
||||||
|
Timeout for the TWAI dump task.
|
||||||
|
|
||||||
|
endmenu
|
51
examples/peripherals/twai/twai_utils/main/cmd_twai.c
Normal file
51
examples/peripherals/twai/twai_utils/main/cmd_twai.c
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "cmd_twai.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "cmd_twai_internal.h"
|
||||||
|
|
||||||
|
static const char *TAG = "cmd_twai";
|
||||||
|
|
||||||
|
twai_controller_ctx_t g_twai_controller_ctx[SOC_TWAI_CONTROLLER_NUM];
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
* COMMAND REGISTRATION
|
||||||
|
* =============================================================================*/
|
||||||
|
|
||||||
|
twai_controller_ctx_t* get_controller_by_id(int controller_id)
|
||||||
|
{
|
||||||
|
if (controller_id < 0 || controller_id >= SOC_TWAI_CONTROLLER_NUM) {
|
||||||
|
ESP_LOGE(TAG, "Invalid controller ID: %d (valid range: 0-%d)",
|
||||||
|
controller_id, SOC_TWAI_CONTROLLER_NUM - 1);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return &g_twai_controller_ctx[controller_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_twai_commands(void)
|
||||||
|
{
|
||||||
|
register_twai_core_commands();
|
||||||
|
register_twai_send_commands();
|
||||||
|
register_twai_dump_commands();
|
||||||
|
ESP_LOGI(TAG, "TWAI commands registered successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregister_twai_commands(void)
|
||||||
|
{
|
||||||
|
unregister_twai_dump_commands();
|
||||||
|
unregister_twai_send_commands();
|
||||||
|
unregister_twai_core_commands();
|
||||||
|
ESP_LOGI(TAG, "TWAI commands unregistered successfully");
|
||||||
|
}
|
25
examples/peripherals/twai/twai_utils/main/cmd_twai.h
Normal file
25
examples/peripherals/twai/twai_utils/main/cmd_twai.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
* MACRO DEFINITIONS
|
||||||
|
* =============================================================================*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register TWAI commands with the console
|
||||||
|
*/
|
||||||
|
void register_twai_commands(void);
|
||||||
|
void unregister_twai_commands(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
749
examples/peripherals/twai/twai_utils/main/cmd_twai_core.c
Normal file
749
examples/peripherals/twai/twai_utils/main/cmd_twai_core.c
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "argtable3/argtable3.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_twai.h"
|
||||||
|
#include "esp_twai_onchip.h"
|
||||||
|
#include "cmd_twai_internal.h"
|
||||||
|
#include "esp_check.h"
|
||||||
|
#include "twai_utils_parser.h"
|
||||||
|
|
||||||
|
static const char *TAG = "cmd_twai_core";
|
||||||
|
|
||||||
|
/** @brief Command line arguments for twai_init command */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *controller;
|
||||||
|
struct arg_int *rate;
|
||||||
|
struct arg_lit *loopback;
|
||||||
|
struct arg_lit *self_test;
|
||||||
|
struct arg_lit *listen;
|
||||||
|
struct arg_int *fd_rate;
|
||||||
|
struct arg_int *tx_gpio;
|
||||||
|
struct arg_int *rx_gpio;
|
||||||
|
struct arg_int *clk_out_gpio;
|
||||||
|
struct arg_int *bus_off_gpio;
|
||||||
|
struct arg_end *end;
|
||||||
|
} twai_init_args;
|
||||||
|
|
||||||
|
/** @brief Command line arguments for twai_deinit command */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *controller;
|
||||||
|
struct arg_end *end;
|
||||||
|
} twai_deinit_args;
|
||||||
|
|
||||||
|
/** @brief Command line arguments for twai_info command */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *controller;
|
||||||
|
struct arg_end *end;
|
||||||
|
} twai_info_args;
|
||||||
|
|
||||||
|
/** @brief Command line arguments for twai_recover command */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *controller;
|
||||||
|
struct arg_int *timeout;
|
||||||
|
struct arg_end *end;
|
||||||
|
} twai_recover_args;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief State change callback for TWAI controller
|
||||||
|
*
|
||||||
|
* @param[in] handle TWAI node handle
|
||||||
|
* @param[in] edata Event data with state information
|
||||||
|
* @param[in] user_ctx Controller context pointer
|
||||||
|
*
|
||||||
|
* @return @c true if higher priority task woken, @c false otherwise
|
||||||
|
*/
|
||||||
|
static bool twai_state_change_callback(twai_node_handle_t handle, const twai_state_change_event_data_t *edata, void *user_ctx)
|
||||||
|
{
|
||||||
|
ESP_UNUSED(handle);
|
||||||
|
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)user_ctx;
|
||||||
|
bool higher_task_awoken = false;
|
||||||
|
|
||||||
|
if (edata->new_sta == TWAI_ERROR_BUS_OFF) {
|
||||||
|
int id = (int)(controller - g_twai_controller_ctx);
|
||||||
|
ESP_EARLY_LOGW(TAG, "TWAI%d entered Bus-Off state, use 'twai_recover twai%d' to recover", id, id);
|
||||||
|
} else if (edata->old_sta == TWAI_ERROR_BUS_OFF && edata->new_sta == TWAI_ERROR_ACTIVE) {
|
||||||
|
int id = (int)(controller - g_twai_controller_ctx);
|
||||||
|
ESP_EARLY_LOGI(TAG, "TWAI%d recovered from Bus-Off state", id);
|
||||||
|
}
|
||||||
|
return higher_task_awoken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create and configure a TWAI controller
|
||||||
|
*
|
||||||
|
* @param[in] controller Controller context to start
|
||||||
|
*
|
||||||
|
* @return TWAI node handle on success, @c NULL on failure
|
||||||
|
*/
|
||||||
|
static twai_node_handle_t twai_start(twai_controller_ctx_t *controller)
|
||||||
|
{
|
||||||
|
twai_node_handle_t res = NULL;
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
twai_core_ctx_t *ctx = &controller->core_ctx;
|
||||||
|
|
||||||
|
/* Check if the TWAI driver is already running */
|
||||||
|
if (atomic_load(&ctx->is_initialized)) {
|
||||||
|
ESP_LOGD(TAG, "TWAI driver is already running. Please stop it first.");
|
||||||
|
return controller->node_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller->node_handle) {
|
||||||
|
ESP_LOGW(TAG, "Cleaning up old TWAI node handle");
|
||||||
|
ret = twai_node_delete(controller->node_handle);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to delete old TWAI node: %s", esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
controller->node_handle = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
if (ctx->driver_config.data_timing.bitrate > 0) {
|
||||||
|
if (ctx->driver_config.data_timing.bitrate < ctx->driver_config.bit_timing.bitrate) {
|
||||||
|
ESP_LOGW(TAG, "TWAI-FD disabled: data bitrate (%" PRIu32 ") must be higher than arbitration bitrate (%" PRIu32 ")",
|
||||||
|
ctx->driver_config.data_timing.bitrate, ctx->driver_config.bit_timing.bitrate);
|
||||||
|
ctx->driver_config.data_timing.bitrate = 0; /* Disable FD */
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "TWAI-FD enabled: Arbitration=%" PRIu32 " bps, Data=%" PRIu32 " bps",
|
||||||
|
ctx->driver_config.bit_timing.bitrate, ctx->driver_config.data_timing.bitrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ESP_GOTO_ON_ERROR(twai_new_node_onchip(&(ctx->driver_config), &(controller->node_handle)),
|
||||||
|
err, TAG, "Failed to create TWAI node");
|
||||||
|
res = controller->node_handle;
|
||||||
|
|
||||||
|
/* Register event callbacks including our state change callback */
|
||||||
|
ctx->driver_cbs.on_state_change = twai_state_change_callback;
|
||||||
|
ESP_GOTO_ON_ERROR(twai_node_register_event_callbacks(controller->node_handle, &(ctx->driver_cbs), controller),
|
||||||
|
err_node, TAG, "Failed to register callbacks");
|
||||||
|
|
||||||
|
ESP_GOTO_ON_ERROR(twai_node_enable(controller->node_handle),
|
||||||
|
err_node, TAG, "Failed to enable node");
|
||||||
|
|
||||||
|
atomic_store(&ctx->is_initialized, true);
|
||||||
|
return res;
|
||||||
|
|
||||||
|
err_node:
|
||||||
|
if (controller->node_handle) {
|
||||||
|
ret = twai_node_delete(controller->node_handle);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to delete TWAI node during error cleanup: %s", esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
controller->node_handle = NULL;
|
||||||
|
}
|
||||||
|
err:
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop a TWAI controller, disable the node and delete it
|
||||||
|
*
|
||||||
|
* @param[in] controller Controller context to stop
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static esp_err_t twai_stop(twai_controller_ctx_t *controller)
|
||||||
|
{
|
||||||
|
twai_core_ctx_t *ctx = &controller->core_ctx;
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
|
||||||
|
if (!atomic_load(&ctx->is_initialized)) {
|
||||||
|
ESP_LOGI(TAG, "TWAI not running");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller->node_handle) {
|
||||||
|
ret = twai_node_disable(controller->node_handle);
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to disable TWAI node: %s", esp_err_to_name(ret));
|
||||||
|
ret = twai_node_delete(controller->node_handle);
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to delete TWAI node: %s", esp_err_to_name(ret));
|
||||||
|
controller->node_handle = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&ctx->is_initialized, false);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize and start TWAI controller `twai_init twai0 -t 4 -r 5 -b 500000` command handler
|
||||||
|
*
|
||||||
|
* @param[in] argc Argument count
|
||||||
|
* @param[in] argv Argument vector
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*
|
||||||
|
* @note Parses GPIO, timing, and mode configuration. -t=TX GPIO, -r=RX GPIO, -b=bitrate
|
||||||
|
*/
|
||||||
|
static int twai_init_handler(int argc, char **argv)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
int controller_id;
|
||||||
|
twai_controller_ctx_t *controller;
|
||||||
|
twai_core_ctx_t *ctx;
|
||||||
|
int tx_gpio, rx_gpio, clk_gpio, bus_off_gpio;
|
||||||
|
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&twai_init_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, twai_init_args.end, argv[0]);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check required arguments first */
|
||||||
|
if (twai_init_args.controller->count == 0) {
|
||||||
|
ESP_LOGE(TAG, "Controller argument is required (e.g., twai0, twai1)");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (twai_init_args.tx_gpio->count == 0) {
|
||||||
|
ESP_LOGE(TAG, "TX GPIO argument is required (-t <gpio>)");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (twai_init_args.rx_gpio->count == 0) {
|
||||||
|
ESP_LOGE(TAG, "RX GPIO argument is required (-r <gpio>)");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller_id = parse_controller_string(twai_init_args.controller->sval[0]);
|
||||||
|
ret = (controller_id >= 0) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid controller ID");
|
||||||
|
|
||||||
|
controller = get_controller_by_id(controller_id);
|
||||||
|
ret = (controller != NULL) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Controller %d not found", controller_id);
|
||||||
|
|
||||||
|
ctx = &controller->core_ctx;
|
||||||
|
ret = (!atomic_load(&ctx->is_initialized)) ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "TWAI%d already running", controller_id);
|
||||||
|
|
||||||
|
/* Configure TX GPIO */
|
||||||
|
tx_gpio = twai_init_args.tx_gpio->ival[0];
|
||||||
|
ret = (tx_gpio >= 0 && GPIO_IS_VALID_OUTPUT_GPIO(tx_gpio)) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid TX GPIO: %d", tx_gpio);
|
||||||
|
ctx->driver_config.io_cfg.tx = tx_gpio;
|
||||||
|
ESP_LOGI(TAG, "TX GPIO set to %d", tx_gpio);
|
||||||
|
|
||||||
|
/* Configure RX GPIO */
|
||||||
|
rx_gpio = twai_init_args.rx_gpio->ival[0];
|
||||||
|
ret = GPIO_IS_VALID_GPIO(rx_gpio) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid RX GPIO: %d", rx_gpio);
|
||||||
|
ctx->driver_config.io_cfg.rx = rx_gpio;
|
||||||
|
ESP_LOGI(TAG, "RX GPIO set to %d", rx_gpio);
|
||||||
|
|
||||||
|
/* Configure optional clock output GPIO */
|
||||||
|
if (twai_init_args.clk_out_gpio->count > 0) {
|
||||||
|
clk_gpio = twai_init_args.clk_out_gpio->ival[0];
|
||||||
|
if (clk_gpio >= 0) {
|
||||||
|
ret = GPIO_IS_VALID_OUTPUT_GPIO(clk_gpio) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid CLK out GPIO: %d", clk_gpio);
|
||||||
|
ctx->driver_config.io_cfg.quanta_clk_out = clk_gpio;
|
||||||
|
ESP_LOGI(TAG, "Clock output GPIO set to %d", clk_gpio);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx->driver_config.io_cfg.quanta_clk_out = -1;
|
||||||
|
ESP_LOGI(TAG, "Clock output disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Configure optional bus-off indicator GPIO */
|
||||||
|
if (twai_init_args.bus_off_gpio->count > 0) {
|
||||||
|
bus_off_gpio = twai_init_args.bus_off_gpio->ival[0];
|
||||||
|
if (bus_off_gpio >= 0) {
|
||||||
|
ret = GPIO_IS_VALID_OUTPUT_GPIO(bus_off_gpio) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid bus-off GPIO: %d", bus_off_gpio);
|
||||||
|
ctx->driver_config.io_cfg.bus_off_indicator = bus_off_gpio;
|
||||||
|
ESP_LOGI(TAG, "Bus-off indicator GPIO set to %d", bus_off_gpio);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx->driver_config.io_cfg.bus_off_indicator = -1;
|
||||||
|
ESP_LOGI(TAG, "Bus-off indicator disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verify required IO configuration */
|
||||||
|
ret = (ctx->driver_config.io_cfg.tx >= 0 && ctx->driver_config.io_cfg.rx >= 0) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Both TX and RX GPIO must be configured");
|
||||||
|
|
||||||
|
/* Update timing configuration */
|
||||||
|
if (twai_init_args.rate->count > 0) {
|
||||||
|
ctx->driver_config.bit_timing.bitrate = twai_init_args.rate->ival[0];
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Bitrate set to %" PRIu32 " bps", ctx->driver_config.bit_timing.bitrate);
|
||||||
|
|
||||||
|
ctx->driver_config.flags.enable_loopback = (twai_init_args.loopback->count > 0);
|
||||||
|
ctx->driver_config.flags.enable_self_test = (twai_init_args.self_test->count > 0);
|
||||||
|
ctx->driver_config.flags.enable_listen_only = (twai_init_args.listen->count > 0);
|
||||||
|
|
||||||
|
if (ctx->driver_config.flags.enable_loopback) {
|
||||||
|
ESP_LOGI(TAG, "Loopback mode enabled");
|
||||||
|
}
|
||||||
|
if (ctx->driver_config.flags.enable_self_test) {
|
||||||
|
ESP_LOGI(TAG, "Self-test mode enabled");
|
||||||
|
}
|
||||||
|
if (ctx->driver_config.flags.enable_listen_only) {
|
||||||
|
ESP_LOGI(TAG, "Listen-only mode enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
if (twai_init_args.fd_rate->count > 0) {
|
||||||
|
ctx->driver_config.data_timing.bitrate = twai_init_args.fd_rate->ival[0];
|
||||||
|
} else {
|
||||||
|
ctx->driver_config.data_timing.bitrate = CONFIG_EXAMPLE_DEFAULT_FD_BITRATE;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
ctx->driver_config.data_timing.bitrate = 0; /* FD disabled */
|
||||||
|
#endif
|
||||||
|
ESP_LOGI(TAG, "FD bitrate set to %" PRIu32, ctx->driver_config.data_timing.bitrate);
|
||||||
|
|
||||||
|
/* Start TWAI controller */
|
||||||
|
controller->node_handle = twai_start(controller);
|
||||||
|
ret = (controller->node_handle != NULL) ? ESP_OK : ESP_FAIL;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to start TWAI controller");
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
err:
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop and deinitialize TWAI controller command handler
|
||||||
|
*
|
||||||
|
* @param[in] argc Argument count
|
||||||
|
* @param[in] argv Argument vector
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*
|
||||||
|
* @note Stops dump monitoring and controller
|
||||||
|
*/
|
||||||
|
static int twai_deinit_handler(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int controller_id;
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&twai_deinit_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, twai_deinit_args.end, argv[0]);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twai_deinit_args.controller->count == 0) {
|
||||||
|
ESP_LOGE(TAG, "Controller ID is required");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller_id = parse_controller_string(twai_deinit_args.controller->sval[0]);
|
||||||
|
ret = (controller_id >= 0) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid controller ID");
|
||||||
|
|
||||||
|
twai_controller_ctx_t* controller = get_controller_by_id(controller_id);
|
||||||
|
ret = (controller != NULL) ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Controller %d not found", controller_id);
|
||||||
|
|
||||||
|
twai_core_ctx_t *ctx = &controller->core_ctx;
|
||||||
|
|
||||||
|
if (!atomic_load(&ctx->is_initialized)) {
|
||||||
|
ESP_LOGI(TAG, "TWAI%d not running", controller_id);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auto-stop dump monitoring if it's running */
|
||||||
|
esp_err_t dump_ret = twai_dump_stop_internal(controller_id);
|
||||||
|
if (dump_ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to stop dump for controller %d: %s", controller_id, esp_err_to_name(dump_ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = twai_stop(controller);
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to stop TWAI%d: %s", controller_id, esp_err_to_name(ret));
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
err:
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Recover from Bus-Off state `twai_recover twai0` command handler
|
||||||
|
*
|
||||||
|
* @param[in] argc Argument count
|
||||||
|
* @param[in] argv Argument vector
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*
|
||||||
|
* @note Supports async, blocking, and timeout modes
|
||||||
|
*/
|
||||||
|
static int twai_recover_handler(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int controller_id;
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&twai_recover_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, twai_recover_args.end, argv[0]);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twai_recover_args.controller->count == 0) {
|
||||||
|
ESP_LOGE(TAG, "Controller ID is required");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller_id = parse_controller_string(twai_recover_args.controller->sval[0]);
|
||||||
|
ESP_GOTO_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, err, TAG, "Invalid controller ID");
|
||||||
|
|
||||||
|
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
|
||||||
|
ESP_GOTO_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, err, TAG, "Controller %d not found", controller_id);
|
||||||
|
|
||||||
|
if (!controller->node_handle) {
|
||||||
|
ESP_LOGE(TAG, "TWAI%d not initialized", controller_id);
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* timeout: -1 = block, 0 = async, >0 = timeout (ms) */
|
||||||
|
int32_t timeout_ms = -1;
|
||||||
|
if (twai_recover_args.timeout->count > 0) {
|
||||||
|
timeout_ms = twai_recover_args.timeout->ival[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout_ms < -1) {
|
||||||
|
ESP_LOGE(TAG, "Invalid timeout value: %d (must be -1, 0 or positive)", timeout_ms);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
twai_node_status_t node_status;
|
||||||
|
ret = twai_node_get_info(controller->node_handle, &node_status, NULL);
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to get node%d status: %s", controller_id, esp_err_to_name(ret));
|
||||||
|
|
||||||
|
ret = (node_status.state == TWAI_ERROR_BUS_OFF) ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Recovery not needed - node is %s", twai_state_to_string(node_status.state));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Starting recovery from Bus-Off state...");
|
||||||
|
ret = twai_node_recover(controller->node_handle);
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to start recovery: %s", esp_err_to_name(ret));
|
||||||
|
|
||||||
|
if (timeout_ms == 0) {
|
||||||
|
ESP_LOGI(TAG, "Recovery initiated (async mode)");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Waiting for recovery to complete...");
|
||||||
|
uint32_t elapsed_ms = 0;
|
||||||
|
const uint32_t check_interval_ms = 100;
|
||||||
|
uint32_t limit_ms = (timeout_ms < 0) ? UINT32_MAX : (uint32_t)timeout_ms;
|
||||||
|
|
||||||
|
while (elapsed_ms < limit_ms) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(check_interval_ms));
|
||||||
|
elapsed_ms += check_interval_ms;
|
||||||
|
|
||||||
|
ret = twai_node_get_info(controller->node_handle, &node_status, NULL);
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to check recovery status: %s", esp_err_to_name(ret));
|
||||||
|
|
||||||
|
if (node_status.state == TWAI_ERROR_ACTIVE) {
|
||||||
|
ESP_LOGI(TAG, "Recovery completed successfully in %" PRIu32 " ms", elapsed_ms);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed_ms % 1000 == 0) {
|
||||||
|
ESP_LOGI(TAG, "Recovery in progress... (state: %s, elapsed: %" PRIu32 " ms)",
|
||||||
|
twai_state_to_string(node_status.state), elapsed_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Recovery timeout after %" PRIu32 " ms (current state: %s)",
|
||||||
|
limit_ms, twai_state_to_string(node_status.state));
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
|
||||||
|
err:
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Display controller information `twai_info twai0` command handler
|
||||||
|
*
|
||||||
|
* @param[in] argc Argument count
|
||||||
|
* @param[in] argv Argument vector
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*
|
||||||
|
* @note Shows status, configuration, and error counters
|
||||||
|
*/
|
||||||
|
static int twai_info_handler(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&twai_info_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, twai_info_args.end, argv[0]);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twai_info_args.controller->count == 0) {
|
||||||
|
ESP_LOGE(TAG, "Controller ID is required");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
int controller_id = parse_controller_string(twai_info_args.controller->sval[0]);
|
||||||
|
if (controller_id < 0) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
twai_controller_ctx_t* controller = get_controller_by_id(controller_id);
|
||||||
|
if (!controller) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
twai_core_ctx_t *ctx = &controller->core_ctx;
|
||||||
|
char tx_gpio_buf[16], rx_gpio_buf[16];
|
||||||
|
|
||||||
|
if (!atomic_load(&ctx->is_initialized)) {
|
||||||
|
printf("TWAI%d Status: Stopped\n", controller_id);
|
||||||
|
} else if (controller->node_handle) {
|
||||||
|
twai_node_status_t node_status;
|
||||||
|
if (twai_node_get_info(controller->node_handle, &node_status, NULL) == ESP_OK &&
|
||||||
|
node_status.state == TWAI_ERROR_BUS_OFF) {
|
||||||
|
printf("TWAI%d Status: Bus-Off\n", controller_id);
|
||||||
|
} else {
|
||||||
|
printf("TWAI%d Status: Running\n", controller_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf("TWAI%d Status: Initializing\n", controller_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node status and error counters */
|
||||||
|
if (controller->node_handle && atomic_load(&ctx->is_initialized)) {
|
||||||
|
twai_node_status_t node_status;
|
||||||
|
esp_err_t ret = twai_node_get_info(controller->node_handle, &node_status, NULL);
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
printf("Node State: %s\n", twai_state_to_string(node_status.state));
|
||||||
|
printf("Error Counters: TX=%u, RX=%u\n", node_status.tx_error_count, node_status.rx_error_count);
|
||||||
|
if (node_status.state == TWAI_ERROR_BUS_OFF) {
|
||||||
|
printf(" ! Use 'twai_recover twai%d' to recover from Bus-Off\n", controller_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf("Node State: Unable to read status\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf("Node State: Not initialized\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Configuration */
|
||||||
|
printf("Bitrate: %" PRIu32 " bps", ctx->driver_config.bit_timing.bitrate);
|
||||||
|
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
if (ctx->driver_config.data_timing.bitrate > 0) {
|
||||||
|
printf(" (FD: %" PRIu32 " bps)", ctx->driver_config.data_timing.bitrate);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
format_gpio_pin(ctx->driver_config.io_cfg.tx, tx_gpio_buf, sizeof(tx_gpio_buf));
|
||||||
|
format_gpio_pin(ctx->driver_config.io_cfg.rx, rx_gpio_buf, sizeof(rx_gpio_buf));
|
||||||
|
printf("GPIOs: TX=%s, RX=%s\n", tx_gpio_buf, rx_gpio_buf);
|
||||||
|
|
||||||
|
/* Special modes (only if not normal) */
|
||||||
|
if (ctx->driver_config.flags.enable_loopback ||
|
||||||
|
ctx->driver_config.flags.enable_self_test ||
|
||||||
|
ctx->driver_config.flags.enable_listen_only) {
|
||||||
|
printf("Modes: ");
|
||||||
|
bool first = true;
|
||||||
|
if (ctx->driver_config.flags.enable_self_test) {
|
||||||
|
printf("Self-Test");
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (ctx->driver_config.flags.enable_loopback) {
|
||||||
|
if (!first) {
|
||||||
|
printf(", ");
|
||||||
|
}
|
||||||
|
printf("Loopback");
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (ctx->driver_config.flags.enable_listen_only) {
|
||||||
|
if (!first) {
|
||||||
|
printf(", ");
|
||||||
|
}
|
||||||
|
printf("Listen-Only");
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register TWAI core commands with console
|
||||||
|
*
|
||||||
|
* @note Initializes controllers and registers all command handlers
|
||||||
|
*/
|
||||||
|
void register_twai_core_commands(void)
|
||||||
|
{
|
||||||
|
// Initialize all controllers
|
||||||
|
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
|
||||||
|
twai_controller_ctx_t* controller = &g_twai_controller_ctx[i];
|
||||||
|
twai_core_ctx_t *ctx = &controller->core_ctx;
|
||||||
|
|
||||||
|
ctx->driver_config = (twai_onchip_node_config_t) {
|
||||||
|
.io_cfg = {
|
||||||
|
.tx = GPIO_NUM_NC,
|
||||||
|
.rx = GPIO_NUM_NC,
|
||||||
|
.quanta_clk_out = GPIO_NUM_NC,
|
||||||
|
.bus_off_indicator = GPIO_NUM_NC,
|
||||||
|
},
|
||||||
|
.clk_src = 0,
|
||||||
|
.bit_timing = {
|
||||||
|
.bitrate = CONFIG_EXAMPLE_DEFAULT_BITRATE,
|
||||||
|
.sp_permill = 0,
|
||||||
|
.ssp_permill = 0,
|
||||||
|
},
|
||||||
|
.data_timing = {
|
||||||
|
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
.bitrate = CONFIG_EXAMPLE_DEFAULT_FD_BITRATE,
|
||||||
|
.sp_permill = 0,
|
||||||
|
.ssp_permill = 700,
|
||||||
|
#else
|
||||||
|
.bitrate = 0,
|
||||||
|
.sp_permill = 0,
|
||||||
|
.ssp_permill = 0,
|
||||||
|
#endif
|
||||||
|
},
|
||||||
|
.fail_retry_cnt = -1,
|
||||||
|
.tx_queue_depth = CONFIG_EXAMPLE_TX_QUEUE_LEN,
|
||||||
|
.intr_priority = 0,
|
||||||
|
.flags = {
|
||||||
|
.enable_self_test = false, /* Default: Self-test disabled */
|
||||||
|
.enable_loopback = false, /* Default: Loopback disabled */
|
||||||
|
.enable_listen_only = false, /* Default: Listen-only disabled */
|
||||||
|
.no_receive_rtr = 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
atomic_init(&ctx->is_initialized, false);
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Default config set for TWAI%d (TX=%d, RX=%d).",
|
||||||
|
i, ctx->driver_config.io_cfg.tx, ctx->driver_config.io_cfg.rx);
|
||||||
|
}
|
||||||
|
/* Register command arguments */
|
||||||
|
twai_init_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1, etc.)");
|
||||||
|
twai_init_args.tx_gpio = arg_int1("t", "tx", "<gpio>", "TX GPIO pin number (required, e.g., 4)");
|
||||||
|
twai_init_args.rx_gpio = arg_int1("r", "rx", "<gpio>", "RX GPIO pin number (required, e.g., 5)");
|
||||||
|
twai_init_args.rate = arg_int0("b", "bitrate", "<bps>", "Arbitration bitrate in bps (default: 500000)");
|
||||||
|
twai_init_args.loopback = arg_lit0(NULL, "loopback", "Enable loopback mode for testing");
|
||||||
|
twai_init_args.self_test = arg_lit0(NULL, "self-test", "Enable self-test mode for testing");
|
||||||
|
twai_init_args.listen = arg_lit0(NULL, "listen", "Enable listen-only mode (no transmission)");
|
||||||
|
twai_init_args.fd_rate = arg_int0("B", "fd-bitrate", "<bps>", "TWAI-FD data bitrate in bps (optional)");
|
||||||
|
twai_init_args.clk_out_gpio = arg_int0("c", "clk-out", "<gpio>", "Clock output GPIO pin (optional)");
|
||||||
|
twai_init_args.bus_off_gpio = arg_int0("o", "bus-off", "<gpio>", "Bus-off indicator GPIO pin (optional)");
|
||||||
|
twai_init_args.end = arg_end(20);
|
||||||
|
|
||||||
|
twai_deinit_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1)");
|
||||||
|
twai_deinit_args.end = arg_end(20);
|
||||||
|
|
||||||
|
twai_info_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1)");
|
||||||
|
twai_info_args.end = arg_end(20);
|
||||||
|
|
||||||
|
twai_recover_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1)");
|
||||||
|
twai_recover_args.timeout = arg_int0("t", "timeout", "<ms>", "Recovery timeout in milliseconds (default: -1=block)\n -1 = block until complete\n 0 = async (return immediately)\n >0 = timeout in ms");
|
||||||
|
twai_recover_args.end = arg_end(20);
|
||||||
|
|
||||||
|
/* Register commands */
|
||||||
|
const esp_console_cmd_t twai_init_cmd = {
|
||||||
|
.command = "twai_init",
|
||||||
|
.help = "Initialize and start the TWAI driver\n"
|
||||||
|
"Usage: twai_init <controller> -t <tx_gpio> -r <rx_gpio> [options]\n"
|
||||||
|
"Example: twai_init twai0 -t 4 -r 5 -b 500000\n"
|
||||||
|
"Example: twai_init twai0 -t 4 -r 5 --loopback --self-test",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &twai_init_handler,
|
||||||
|
.argtable = &twai_init_args
|
||||||
|
};
|
||||||
|
|
||||||
|
const esp_console_cmd_t twai_deinit_cmd = {
|
||||||
|
.command = "twai_deinit",
|
||||||
|
.help = "Stop and deinitialize the TWAI driver",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &twai_deinit_handler,
|
||||||
|
.argtable = &twai_deinit_args
|
||||||
|
};
|
||||||
|
|
||||||
|
const esp_console_cmd_t twai_info_cmd = {
|
||||||
|
.command = "twai_info",
|
||||||
|
.help = "Display TWAI controller information and status",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &twai_info_handler,
|
||||||
|
.argtable = &twai_info_args
|
||||||
|
};
|
||||||
|
|
||||||
|
const esp_console_cmd_t twai_recover_cmd = {
|
||||||
|
.command = "twai_recover",
|
||||||
|
.help = "Recover TWAI controller from Bus-Off error state\n"
|
||||||
|
"Usage:\n"
|
||||||
|
" twai_recover <controller> # Block until complete (default)\n"
|
||||||
|
" twai_recover <controller> -t 0 # Async recovery\n"
|
||||||
|
" twai_recover <controller> -t 5000 # 5 second timeout\n"
|
||||||
|
"\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" twai_recover twai0 # Block until complete\n"
|
||||||
|
" twai_recover twai0 -t 0 # Async recovery\n"
|
||||||
|
" twai_recover twai1 -t 15000 # 15 second timeout",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &twai_recover_handler,
|
||||||
|
.argtable = &twai_recover_args
|
||||||
|
};
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_init_cmd));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_deinit_cmd));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_info_cmd));
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_recover_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unregister TWAI core commands and cleanup resources
|
||||||
|
*/
|
||||||
|
void unregister_twai_core_commands(void)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
/* Cleanup all controllers */
|
||||||
|
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
|
||||||
|
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
|
||||||
|
twai_core_ctx_t *ctx = &controller->core_ctx;
|
||||||
|
|
||||||
|
/* Stop dump and other modules first to avoid callback issues */
|
||||||
|
ret = twai_dump_stop_internal(i);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to stop dump for controller %d: %s", i, esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable and delete TWAI node if it exists */
|
||||||
|
if (controller->node_handle) {
|
||||||
|
if (atomic_load(&ctx->is_initialized)) {
|
||||||
|
ret = twai_node_disable(controller->node_handle);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to disable TWAI node for controller %d: %s", i, esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = twai_node_delete(controller->node_handle);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to delete TWAI node for controller %d: %s", i, esp_err_to_name(ret));
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "Deleted TWAI node for controller %d", i);
|
||||||
|
}
|
||||||
|
controller->node_handle = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear initialization flag */
|
||||||
|
atomic_store(&ctx->is_initialized, false);
|
||||||
|
|
||||||
|
/* Clear callbacks */
|
||||||
|
memset(&ctx->driver_cbs, 0, sizeof(ctx->driver_cbs));
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "TWAI core commands unregistered and resources cleaned up");
|
||||||
|
}
|
569
examples/peripherals/twai/twai_utils/main/cmd_twai_dump.c
Normal file
569
examples/peripherals/twai/twai_utils/main/cmd_twai_dump.c
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
#include "argtable3/argtable3.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_twai.h"
|
||||||
|
#include "esp_twai_onchip.h"
|
||||||
|
#include "cmd_twai_internal.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "esp_check.h"
|
||||||
|
#include "twai_utils_parser.h"
|
||||||
|
|
||||||
|
#define DUMP_OUTPUT_LINE_SIZE 128
|
||||||
|
/**
|
||||||
|
* @brief Structure for queuing received frames with embedded buffer
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
twai_frame_t frame; /**< TWAI frame with embedded buffer */
|
||||||
|
int64_t timestamp_us; /**< Frame timestamp in microseconds */
|
||||||
|
uint8_t buffer[TWAI_FRAME_BUFFER_SIZE]; /**< Frame data buffer (supports both TWAI and TWAI-FD) */
|
||||||
|
} rx_queue_item_t;
|
||||||
|
|
||||||
|
/** @brief Command line arguments structure */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *controller_filter; /**< Format: <controller>[,<id>:<mask>[,<id>:<mask>...]] */
|
||||||
|
struct arg_lit *stop; /**< Stop option: --stop */
|
||||||
|
struct arg_str *timestamp; /**< Timestamp mode: -t <mode> */
|
||||||
|
struct arg_end *end;
|
||||||
|
} twai_dump_args;
|
||||||
|
|
||||||
|
static const char *TAG = "cmd_twai_dump";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse TWAI filters from a string and configure the controller
|
||||||
|
*
|
||||||
|
* @param[in] filter_str Filter string to parse
|
||||||
|
* @param[in] controller Controller context to configure
|
||||||
|
* @param[out] mask_count_out Number of mask filters configured
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static esp_err_t parse_twai_filters(const char *filter_str, twai_controller_ctx_t *controller, int *mask_count, int *range_count)
|
||||||
|
{
|
||||||
|
int mask_idx = 0;
|
||||||
|
#ifdef SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
int range_idx = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
size_t slen = strlen(filter_str);
|
||||||
|
if (filter_str && slen > 0) {
|
||||||
|
|
||||||
|
const char *start = filter_str;
|
||||||
|
const char *comma;
|
||||||
|
|
||||||
|
while (start && *start) {
|
||||||
|
comma = strchr(start, ',');
|
||||||
|
size_t tok_len = comma ? (size_t)(comma - start) : strlen(start);
|
||||||
|
|
||||||
|
if (tok_len == 0) {
|
||||||
|
start = comma ? comma + 1 : NULL;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t lhs, rhs;
|
||||||
|
size_t lhs_chars, rhs_chars;
|
||||||
|
bool is_mask_filter = false;
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
bool is_range_filter = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Try mask filter first: "id:mask" */
|
||||||
|
if (parse_pair_token(start, tok_len, ':', &lhs, &lhs_chars, &rhs, &rhs_chars) == PARSE_OK) {
|
||||||
|
ESP_RETURN_ON_FALSE(mask_idx < SOC_TWAI_MASK_FILTER_NUM, ESP_ERR_INVALID_ARG, TAG,
|
||||||
|
"Too many mask filters (max %d)", SOC_TWAI_MASK_FILTER_NUM);
|
||||||
|
is_mask_filter = true;
|
||||||
|
}
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
/* Try range filter: "low-high" */
|
||||||
|
else if (parse_pair_token(start, tok_len, '-', &lhs, &lhs_chars, &rhs, &rhs_chars) == PARSE_OK) {
|
||||||
|
ESP_RETURN_ON_FALSE(range_idx < SOC_TWAI_RANGE_FILTER_NUM, ESP_ERR_INVALID_ARG, TAG,
|
||||||
|
"Too many range filters (max %d)", SOC_TWAI_RANGE_FILTER_NUM);
|
||||||
|
is_range_filter = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
else {
|
||||||
|
ESP_LOGE(TAG, "Invalid filter token: %.*s", (int)tok_len, start);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common processing: determine if extended frame and validate */
|
||||||
|
bool is_ext = (lhs_chars > TWAI_STD_ID_CHAR_LEN) || (rhs_chars > TWAI_STD_ID_CHAR_LEN) ||
|
||||||
|
(lhs > TWAI_STD_ID_MASK) || (rhs > TWAI_STD_ID_MASK);
|
||||||
|
uint32_t id_domain = is_ext ? TWAI_EXT_ID_MASK : TWAI_STD_ID_MASK;
|
||||||
|
|
||||||
|
/* Validate values are within domain */
|
||||||
|
ESP_RETURN_ON_FALSE(lhs <= id_domain && rhs <= id_domain, ESP_ERR_INVALID_ARG, TAG,
|
||||||
|
"Filter values exceed %s domain", is_ext ? "extended" : "standard");
|
||||||
|
|
||||||
|
if (is_mask_filter) {
|
||||||
|
/* Configure mask filter */
|
||||||
|
twai_mask_filter_config_t *cfg = &controller->dump_ctx.mask_filter_configs[mask_idx];
|
||||||
|
cfg->id = lhs;
|
||||||
|
cfg->mask = rhs;
|
||||||
|
cfg->is_ext = is_ext;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Parsed mask filter %d: ID=0x%08" PRIX32 ", mask=0x%08" PRIX32 " (%s)",
|
||||||
|
mask_idx, cfg->id, cfg->mask, is_ext ? "extended" : "standard");
|
||||||
|
mask_idx++;
|
||||||
|
}
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
else if (is_range_filter) {
|
||||||
|
/* Additional validation for range filter */
|
||||||
|
ESP_RETURN_ON_FALSE(lhs <= rhs, ESP_ERR_INVALID_ARG, TAG,
|
||||||
|
"Range filter: low (0x%08" PRIX32 ") > high (0x%08" PRIX32 ")", lhs, rhs);
|
||||||
|
|
||||||
|
/* Configure range filter */
|
||||||
|
twai_range_filter_config_t *cfg = &controller->dump_ctx.range_filter_configs[range_idx];
|
||||||
|
cfg->range_low = lhs;
|
||||||
|
cfg->range_high = rhs;
|
||||||
|
cfg->is_ext = is_ext;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Parsed range filter %d: low=0x%08" PRIX32 ", high=0x%08" PRIX32 " (%s)",
|
||||||
|
range_idx, cfg->range_low, cfg->range_high, is_ext ? "extended" : "standard");
|
||||||
|
range_idx++;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
start = comma ? comma + 1 : NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*mask_count = mask_idx;
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
*range_count = range_idx;
|
||||||
|
#endif
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief TWAI receive done callback for dump functionality
|
||||||
|
*
|
||||||
|
* @param[in] handle TWAI node handle
|
||||||
|
* @param[in] event_data Receive event data
|
||||||
|
* @param[in] user_ctx Controller context pointer
|
||||||
|
*
|
||||||
|
* @return @c true if higher priority task woken, @c false otherwise
|
||||||
|
*/
|
||||||
|
static IRAM_ATTR bool twai_dump_rx_done_cb(twai_node_handle_t handle, const twai_rx_done_event_data_t *event_data, void *user_ctx)
|
||||||
|
{
|
||||||
|
ESP_UNUSED(handle);
|
||||||
|
ESP_UNUSED(event_data);
|
||||||
|
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)user_ctx;
|
||||||
|
BaseType_t higher_priority_task_woken = pdFALSE;
|
||||||
|
|
||||||
|
/* Validate user_ctx pointer */
|
||||||
|
if (controller == NULL || !atomic_load(&controller->dump_ctx.is_running)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if queue exists before using */
|
||||||
|
if (controller->dump_ctx.rx_queue == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rx_queue_item_t item = {0};
|
||||||
|
item.frame.buffer = item.buffer;
|
||||||
|
item.frame.buffer_len = sizeof(item.buffer);
|
||||||
|
|
||||||
|
if (ESP_OK == twai_node_receive_from_isr(handle, &item.frame)) {
|
||||||
|
item.timestamp_us = esp_timer_get_time();
|
||||||
|
|
||||||
|
/* Non-blocking queue send with explicit error handling */
|
||||||
|
if (xQueueSendFromISR(controller->dump_ctx.rx_queue, &item, &higher_priority_task_woken) != pdTRUE) {
|
||||||
|
/* Queue full - frame dropped silently to maintain ISR performance */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (higher_priority_task_woken == pdTRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Frame reception task for dump functionality
|
||||||
|
*
|
||||||
|
* @param[in] parameter Controller context pointer
|
||||||
|
*/
|
||||||
|
static void dump_task(void *parameter)
|
||||||
|
{
|
||||||
|
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)parameter;
|
||||||
|
twai_dump_ctx_t *dump_ctx = &(controller->dump_ctx);
|
||||||
|
int controller_id = controller - g_twai_controller_ctx;
|
||||||
|
char output_line[DUMP_OUTPUT_LINE_SIZE];
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Dump task started for controller %d", controller_id);
|
||||||
|
|
||||||
|
while (atomic_load(&dump_ctx->is_running)) {
|
||||||
|
rx_queue_item_t item;
|
||||||
|
if (xQueueReceive(dump_ctx->rx_queue, &item, pdMS_TO_TICKS(CONFIG_EXAMPLE_DUMP_TASK_TIMEOUT_MS)) == pdPASS) {
|
||||||
|
|
||||||
|
format_twaidump_frame(dump_ctx->timestamp_mode, &item.frame, item.timestamp_us,
|
||||||
|
dump_ctx->start_time_us, &dump_ctx->last_frame_time_us,
|
||||||
|
controller_id, output_line, sizeof(output_line));
|
||||||
|
printf("%s", output_line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean up our own resources */
|
||||||
|
vTaskSuspendAll();
|
||||||
|
dump_ctx->dump_task_handle = NULL;
|
||||||
|
xTaskResumeAll();
|
||||||
|
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize TWAI dump module for a controller
|
||||||
|
*
|
||||||
|
* @param[in] controller Controller context to initialize
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static esp_err_t twai_dump_init_controller(twai_controller_ctx_t *controller)
|
||||||
|
{
|
||||||
|
/* Just register the callback, resources will be created when dump starts */
|
||||||
|
controller->core_ctx.driver_cbs.on_rx_done = twai_dump_rx_done_cb;
|
||||||
|
|
||||||
|
/* Initialize atomic flags and handles */
|
||||||
|
atomic_init(&controller->dump_ctx.is_running, false);
|
||||||
|
controller->dump_ctx.rx_queue = NULL;
|
||||||
|
controller->dump_ctx.dump_task_handle = NULL;
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start dump for a controller - create resources and task
|
||||||
|
*
|
||||||
|
* @param[in] controller Controller context to start dump for
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static esp_err_t twai_dump_start_controller(twai_controller_ctx_t *controller)
|
||||||
|
{
|
||||||
|
int controller_id = controller - g_twai_controller_ctx;
|
||||||
|
twai_dump_ctx_t *dump_ctx = &controller->dump_ctx;
|
||||||
|
|
||||||
|
/* Create frame queue */
|
||||||
|
dump_ctx->rx_queue = xQueueCreate(CONFIG_EXAMPLE_DUMP_QUEUE_SIZE, sizeof(rx_queue_item_t));
|
||||||
|
if (!dump_ctx->rx_queue) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create frame queue for controller %d", controller_id);
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set running flag before creating task */
|
||||||
|
atomic_store(&dump_ctx->is_running, true);
|
||||||
|
|
||||||
|
/* Create dump task */
|
||||||
|
BaseType_t task_ret = xTaskCreate(
|
||||||
|
dump_task,
|
||||||
|
"twai_dump_task",
|
||||||
|
CONFIG_EXAMPLE_DUMP_TASK_STACK_SIZE,
|
||||||
|
controller, /* Pass controller as user data */
|
||||||
|
CONFIG_EXAMPLE_DUMP_TASK_PRIORITY,
|
||||||
|
&dump_ctx->dump_task_handle);
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
ESP_GOTO_ON_FALSE(task_ret == pdPASS, ESP_ERR_NO_MEM, err, TAG, "Failed to create dump task for controller %d", controller_id);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
err:
|
||||||
|
atomic_store(&dump_ctx->is_running, false);
|
||||||
|
if (dump_ctx->rx_queue != NULL) {
|
||||||
|
vQueueDelete(dump_ctx->rx_queue);
|
||||||
|
dump_ctx->rx_queue = NULL;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deinitialize TWAI dump module for a controller
|
||||||
|
*
|
||||||
|
* @param[in] controller Controller context to deinitialize
|
||||||
|
*/
|
||||||
|
static void twai_dump_deinit_controller(twai_controller_ctx_t *controller)
|
||||||
|
{
|
||||||
|
int controller_id = controller - g_twai_controller_ctx;
|
||||||
|
twai_dump_stop_internal(controller_id);
|
||||||
|
|
||||||
|
/* Clear callback */
|
||||||
|
controller->core_ctx.driver_cbs.on_rx_done = NULL;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Dump module deinitialized for controller %d", controller_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Command handler for twai_dump command
|
||||||
|
*
|
||||||
|
* @param[in] argc Argument count
|
||||||
|
* @param[in] argv Argument vector
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static int twai_dump_handler(int argc, char **argv)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&twai_dump_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, twai_dump_args.end, argv[0]);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stop dump */
|
||||||
|
if (twai_dump_args.stop->count > 0) {
|
||||||
|
/* For --stop option, controller ID is in the controller_filter argument */
|
||||||
|
const char *controller_str = twai_dump_args.controller_filter->sval[0];
|
||||||
|
int controller_id = parse_controller_string(controller_str);
|
||||||
|
ESP_RETURN_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, TAG, "Invalid controller ID: %s", controller_str);
|
||||||
|
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
|
||||||
|
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Failed to get controller for ID: %d", controller_id);
|
||||||
|
|
||||||
|
ret = twai_dump_stop_internal(controller_id);
|
||||||
|
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to stop dump on controller %d", controller_id);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Start dump */
|
||||||
|
const char *controller_str = twai_dump_args.controller_filter->sval[0];
|
||||||
|
|
||||||
|
/* Parse controller ID, e.g. "twai0" -> 0 */
|
||||||
|
int controller_id = -1;
|
||||||
|
const char *filter_str = NULL;
|
||||||
|
filter_str = parse_controller_id(controller_str, &controller_id);
|
||||||
|
ESP_RETURN_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to parse controller ID");
|
||||||
|
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
|
||||||
|
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Failed to get controller for ID: %d", controller_id);
|
||||||
|
|
||||||
|
/* Check if already running */
|
||||||
|
if (atomic_load(&controller->dump_ctx.is_running)) {
|
||||||
|
ESP_LOGW(TAG, "Dump already running for controller %d", controller_id); // Already running, no need to start again
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse filter string directly using simplified logic */
|
||||||
|
int mask_count = 0;
|
||||||
|
#ifdef SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
int range_count = 0;
|
||||||
|
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
|
||||||
|
|
||||||
|
/* Clear filter configs first */
|
||||||
|
memset(controller->dump_ctx.mask_filter_configs, 0, sizeof(controller->dump_ctx.mask_filter_configs));
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
memset(controller->dump_ctx.range_filter_configs, 0, sizeof(controller->dump_ctx.range_filter_configs));
|
||||||
|
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
|
||||||
|
|
||||||
|
/* Parse filters using the helper function */
|
||||||
|
#ifdef SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
ret = parse_twai_filters(filter_str, controller, &mask_count, &range_count);
|
||||||
|
#else
|
||||||
|
ret = parse_twai_filters(filter_str, controller, &mask_count, NULL);
|
||||||
|
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to parse filters: %s", esp_err_to_name(ret));
|
||||||
|
|
||||||
|
/* Check if controller is initialized */
|
||||||
|
if (!atomic_load(&controller->core_ctx.is_initialized)) {
|
||||||
|
ESP_LOGE(TAG, "TWAI%d not initialized", (controller - g_twai_controller_ctx));
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Configure filters */
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
if (mask_count > 0 || range_count > 0) {
|
||||||
|
#else
|
||||||
|
if (mask_count > 0) {
|
||||||
|
#endif
|
||||||
|
/* Always disable and reconfigure to apply new filter settings */
|
||||||
|
ret = twai_node_disable(controller->node_handle);
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to disable TWAI node%d for filter configuration: %s", controller_id, esp_err_to_name(ret));
|
||||||
|
|
||||||
|
ret = (SOC_TWAI_MASK_FILTER_NUM > 0) ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "TWAI%d does not support %d mask filters", controller_id, SOC_TWAI_MASK_FILTER_NUM);
|
||||||
|
if (mask_count > 0) {
|
||||||
|
for (int i = 0; i < mask_count; i++) {
|
||||||
|
ret = twai_node_config_mask_filter(controller->node_handle, i,
|
||||||
|
&controller->dump_ctx.mask_filter_configs[i]);
|
||||||
|
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to configure mask filter %d", i);
|
||||||
|
ESP_LOGD(TAG, "Configured mask filter %d: %08X : %08X", i,
|
||||||
|
controller->dump_ctx.mask_filter_configs[i].id,
|
||||||
|
controller->dump_ctx.mask_filter_configs[i].mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
ret = (SOC_TWAI_RANGE_FILTER_NUM > 0) ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "TWAI%d does not support %d range filters", controller_id, SOC_TWAI_RANGE_FILTER_NUM);
|
||||||
|
if (range_count > 0) {
|
||||||
|
for (int i = 0; i < range_count; i++) {
|
||||||
|
ret = twai_node_config_range_filter(controller->node_handle, i,
|
||||||
|
&controller->dump_ctx.range_filter_configs[i]);
|
||||||
|
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to configure range filter %d", i);
|
||||||
|
|
||||||
|
/* If no mask filter is configured, disable mask filter 0 which enabled by default */
|
||||||
|
if (mask_count == 0) {
|
||||||
|
twai_mask_filter_config_t mfilter_cfg = {
|
||||||
|
.id = 0xFFFFFFFF,
|
||||||
|
.mask = 0xFFFFFFFF,
|
||||||
|
};
|
||||||
|
esp_err_t mask_ret = twai_node_config_mask_filter(controller->node_handle, 0, &mfilter_cfg);
|
||||||
|
ESP_RETURN_ON_ERROR(mask_ret, TAG, "Failed to configure node%d default mask filter: %s", controller_id, esp_err_to_name(mask_ret));
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Configured range filter %d: %08X - %08X", i,
|
||||||
|
controller->dump_ctx.range_filter_configs[i].range_low,
|
||||||
|
controller->dump_ctx.range_filter_configs[i].range_high);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
|
||||||
|
esp_err_t enable_ret = twai_node_enable(controller->node_handle);
|
||||||
|
ESP_RETURN_ON_ERROR(enable_ret, TAG, "Failed to enable TWAI node%d after filter configuration: %s", controller_id, esp_err_to_name(enable_ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse timestamp mode */
|
||||||
|
controller->dump_ctx.timestamp_mode = TIMESTAMP_MODE_NONE;
|
||||||
|
if (twai_dump_args.timestamp->count > 0) {
|
||||||
|
char mode = twai_dump_args.timestamp->sval[0][0];
|
||||||
|
switch (mode) {
|
||||||
|
case 'a': case 'd': case 'z': case 'n':
|
||||||
|
controller->dump_ctx.timestamp_mode = (timestamp_mode_t)mode;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE(TAG, "Invalid timestamp mode: %c (use a/d/z/n)", mode);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initialize timestamp base time */
|
||||||
|
int64_t current_time = esp_timer_get_time();
|
||||||
|
controller->dump_ctx.start_time_us = current_time;
|
||||||
|
controller->dump_ctx.last_frame_time_us = current_time;
|
||||||
|
|
||||||
|
/* Start dump task and create resources */
|
||||||
|
ret = twai_dump_start_controller(controller);
|
||||||
|
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to start dump task");
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop dump and wait for task to exit naturally
|
||||||
|
*
|
||||||
|
* @param[in] controller_id Controller ID to stop dump for
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
esp_err_t twai_dump_stop_internal(int controller_id)
|
||||||
|
{
|
||||||
|
if (controller_id < 0 || controller_id >= SOC_TWAI_CONTROLLER_NUM) {
|
||||||
|
ESP_LOGE(TAG, "Invalid controller ID: %d", controller_id);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
|
||||||
|
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Invalid controller ID: %d", controller_id);
|
||||||
|
twai_dump_ctx_t *dump_ctx = &controller->dump_ctx;
|
||||||
|
|
||||||
|
if (!atomic_load(&dump_ctx->is_running)) {
|
||||||
|
ESP_LOGD(TAG, "Dump not running for controller %d", controller_id);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signal task to stop */
|
||||||
|
if (dump_ctx->dump_task_handle) {
|
||||||
|
atomic_store(&dump_ctx->is_running, false);
|
||||||
|
ESP_LOGD(TAG, "Signaled dump task to stop for controller %d", controller_id);
|
||||||
|
|
||||||
|
/* Wait for dump task to finish */
|
||||||
|
int timeout_ms = CONFIG_EXAMPLE_DUMP_TASK_TIMEOUT_MS * 2;
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(timeout_ms));
|
||||||
|
|
||||||
|
ESP_RETURN_ON_FALSE(dump_ctx->dump_task_handle == NULL, ESP_ERR_TIMEOUT, TAG,
|
||||||
|
"Dump task did not exit naturally, timeout after %d ms", timeout_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean up queue */
|
||||||
|
if (dump_ctx->rx_queue != NULL) {
|
||||||
|
vQueueDelete(dump_ctx->rx_queue);
|
||||||
|
dump_ctx->rx_queue = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register TWAI dump commands with console
|
||||||
|
*/
|
||||||
|
void register_twai_dump_commands(void)
|
||||||
|
{
|
||||||
|
/* Initialize all controller dump modules */
|
||||||
|
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
|
||||||
|
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
|
||||||
|
esp_err_t ret = twai_dump_init_controller(controller);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to initialize dump module for TWAI%d: %s", i, esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Register command */
|
||||||
|
twai_dump_args.controller_filter = arg_str1(NULL, NULL, "<controller>[,filter]",
|
||||||
|
"Controller ID and optional filters");
|
||||||
|
twai_dump_args.stop = arg_lit0(NULL, "stop",
|
||||||
|
"Stop monitoring the specified controller");
|
||||||
|
twai_dump_args.timestamp = arg_str0("t", "timestamp", "<mode>",
|
||||||
|
"Timestamp mode: a=absolute, d=delta, z=zero, n=none (default: n)");
|
||||||
|
twai_dump_args.end = arg_end(3);
|
||||||
|
|
||||||
|
const esp_console_cmd_t cmd = {
|
||||||
|
.command = "twai_dump",
|
||||||
|
.help = "Monitor TWAI bus messages with timestamps\n"
|
||||||
|
"Usage:\n"
|
||||||
|
" twai_dump [-t <mode>] <controller>[,filter...]\n"
|
||||||
|
" twai_dump <controller> --stop\n"
|
||||||
|
"\n"
|
||||||
|
"Options:\n"
|
||||||
|
" -t <mode> Timestamp mode: a=absolute, d=delta, z=zero, n=none (default: n)\n"
|
||||||
|
" --stop Stop monitoring the specified controller\n"
|
||||||
|
"\n"
|
||||||
|
"Filter formats:\n"
|
||||||
|
" id:mask Mask filter (e.g., 123:7FF)\n"
|
||||||
|
" low-high Range filter (e.g., a-15)\n"
|
||||||
|
"\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" twai_dump twai0 # Monitor without timestamps (default)\n"
|
||||||
|
" twai_dump -t a twai0 # Monitor with absolute timestamps\n"
|
||||||
|
" twai_dump -t d twai0 # Monitor with delta timestamps\n"
|
||||||
|
" twai_dump -t n twai0,123:7FF # Monitor ID 0x123 without timestamps\n"
|
||||||
|
" twai_dump twai0,a-15 # Monitor range: [0xa, 0x15]\n"
|
||||||
|
" twai_dump twai0,123:7FF,a-15 # Mix mask and range filters\n"
|
||||||
|
" twai_dump twai0,000-666 # Monitor range: [0x000, 0x666]\n"
|
||||||
|
" twai_dump twai0 --stop # Stop monitoring TWAI0\n"
|
||||||
|
,
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &twai_dump_handler,
|
||||||
|
.argtable = &twai_dump_args
|
||||||
|
};
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unregister dump commands and cleanup resources
|
||||||
|
*/
|
||||||
|
void unregister_twai_dump_commands(void)
|
||||||
|
{
|
||||||
|
/* Cleanup all controller dump modules */
|
||||||
|
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
|
||||||
|
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
|
||||||
|
twai_dump_deinit_controller(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "TWAI dump commands unregistered and resources cleaned up");
|
||||||
|
}
|
144
examples/peripherals/twai/twai_utils/main/cmd_twai_internal.h
Normal file
144
examples/peripherals/twai/twai_utils/main/cmd_twai_internal.h
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "stdbool.h"
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
#include "esp_twai.h"
|
||||||
|
#include "esp_twai_onchip.h"
|
||||||
|
|
||||||
|
/** @brief Frame buffer size based on TWAI-FD configuration */
|
||||||
|
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
#define TWAI_FRAME_BUFFER_SIZE TWAIFD_FRAME_MAX_LEN
|
||||||
|
#else
|
||||||
|
#define TWAI_FRAME_BUFFER_SIZE TWAI_FRAME_MAX_LEN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Time stamp mode for candump-style output
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
TIMESTAMP_MODE_ABSOLUTE = 'a', /**< Absolute time (default) */
|
||||||
|
TIMESTAMP_MODE_DELTA = 'd', /**< Delta time between frames */
|
||||||
|
TIMESTAMP_MODE_ZERO = 'z', /**< Relative time from start */
|
||||||
|
TIMESTAMP_MODE_NONE = 'n' /**< No timestamp */
|
||||||
|
} timestamp_mode_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Core TWAI driver context
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
twai_onchip_node_config_t driver_config; /**< Cached driver configuration */
|
||||||
|
twai_event_callbacks_t driver_cbs; /**< Driver event callbacks */
|
||||||
|
atomic_bool is_initialized; /**< Initialization flag */
|
||||||
|
} twai_core_ctx_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Context structure for the TWAI send command
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
SemaphoreHandle_t tx_done_sem; /**< Semaphore for TX completion signaling */
|
||||||
|
atomic_bool is_tx_pending; /**< Flag to indicate if TX is in progress */
|
||||||
|
twai_frame_t tx_frame; /**< TX frame structure */
|
||||||
|
uint8_t tx_frame_buffer[TWAI_FRAME_BUFFER_SIZE]; /**< TX frame buffer */
|
||||||
|
} twai_send_ctx_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief TWAI dump module context
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
atomic_bool is_running; /**< Dump running flag */
|
||||||
|
twai_mask_filter_config_t mask_filter_configs[SOC_TWAI_MASK_FILTER_NUM]; /**< Mask filter configurations */
|
||||||
|
#if SOC_TWAI_RANGE_FILTER_NUM
|
||||||
|
twai_range_filter_config_t range_filter_configs[SOC_TWAI_RANGE_FILTER_NUM]; /**< Range filter configurations */
|
||||||
|
#endif
|
||||||
|
QueueHandle_t rx_queue; /**< RX frame queue */
|
||||||
|
TaskHandle_t dump_task_handle; /**< Handle for dump task */
|
||||||
|
timestamp_mode_t timestamp_mode; /**< Time stamp mode */
|
||||||
|
int64_t start_time_us; /**< Start time in microseconds */
|
||||||
|
int64_t last_frame_time_us; /**< Last frame timestamp for delta */
|
||||||
|
} twai_dump_ctx_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Core state machine for the TWAI console
|
||||||
|
*
|
||||||
|
* This structure manages core driver resources, synchronization primitives,
|
||||||
|
* and resources for different functional modules (send, dump, player).
|
||||||
|
* It embeds twai_utils_status_t to handle bus status and statistics.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** @brief Core Driver Resources */
|
||||||
|
twai_core_ctx_t core_ctx; /**< Core driver context */
|
||||||
|
twai_node_handle_t node_handle; /**< TWAI node handle */
|
||||||
|
/** @brief Module Contexts */
|
||||||
|
twai_send_ctx_t send_ctx; /**< Send context for this controller */
|
||||||
|
twai_dump_ctx_t dump_ctx; /**< Dump module context */
|
||||||
|
} twai_controller_ctx_t;
|
||||||
|
|
||||||
|
/** @brief Global controller context array */
|
||||||
|
extern twai_controller_ctx_t g_twai_controller_ctx[SOC_TWAI_CONTROLLER_NUM];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get controller by ID
|
||||||
|
*
|
||||||
|
* @param[in] controller_id Controller ID
|
||||||
|
*
|
||||||
|
* @return Pointer to controller context, or NULL if invalid
|
||||||
|
*/
|
||||||
|
twai_controller_ctx_t* get_controller_by_id(int controller_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register TWAI core commands with console
|
||||||
|
*/
|
||||||
|
void register_twai_core_commands(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register TWAI send commands with console
|
||||||
|
*/
|
||||||
|
void register_twai_send_commands(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register TWAI dump commands with console
|
||||||
|
*/
|
||||||
|
void register_twai_dump_commands(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unregister TWAI core commands and cleanup resources
|
||||||
|
*/
|
||||||
|
void unregister_twai_core_commands(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unregister TWAI send commands and cleanup resources
|
||||||
|
*/
|
||||||
|
void unregister_twai_send_commands(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unregister TWAI dump commands and cleanup resources
|
||||||
|
*/
|
||||||
|
void unregister_twai_dump_commands(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop dump and wait for task to exit naturally
|
||||||
|
*
|
||||||
|
* @param[in] controller_id Controller ID to stop dump for
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
esp_err_t twai_dump_stop_internal(int controller_id);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
275
examples/peripherals/twai/twai_utils/main/cmd_twai_send.c
Normal file
275
examples/peripherals/twai/twai_utils/main/cmd_twai_send.c
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "argtable3/argtable3.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_check.h"
|
||||||
|
#include "esp_twai.h"
|
||||||
|
#include "esp_twai_onchip.h"
|
||||||
|
#include "cmd_twai_internal.h"
|
||||||
|
#include "twai_utils_parser.h"
|
||||||
|
|
||||||
|
/** @brief Log tag for this module */
|
||||||
|
static const char *TAG = "cmd_twai_send";
|
||||||
|
|
||||||
|
/** @brief Command line arguments for sending frames - supports positional and option formats */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *controller; /**< Controller ID (required) */
|
||||||
|
struct arg_str *frame; /**< Frame string (required) */
|
||||||
|
struct arg_end *end;
|
||||||
|
} twai_send_args;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief TX Callback for TWAI event handling
|
||||||
|
*
|
||||||
|
* @param[in] handle TWAI node handle
|
||||||
|
* @param[in] event_data TX done event data
|
||||||
|
* @param[in] user_ctx Controller context pointer
|
||||||
|
*
|
||||||
|
* @return @c true if higher priority task woken, @c false otherwise
|
||||||
|
*/
|
||||||
|
static bool twai_send_tx_done_cb(twai_node_handle_t handle, const twai_tx_done_event_data_t *event_data, void *user_ctx)
|
||||||
|
{
|
||||||
|
ESP_UNUSED(handle);
|
||||||
|
ESP_UNUSED(event_data);
|
||||||
|
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)user_ctx;
|
||||||
|
|
||||||
|
/* Signal TX completion */
|
||||||
|
if (atomic_load(&controller->send_ctx.is_tx_pending)) {
|
||||||
|
atomic_store(&controller->send_ctx.is_tx_pending, false);
|
||||||
|
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||||
|
xSemaphoreGiveFromISR(controller->send_ctx.tx_done_sem, &xHigherPriorityTaskWoken);
|
||||||
|
return xHigherPriorityTaskWoken == pdTRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the send module for a controller
|
||||||
|
*
|
||||||
|
* @param[in] controller Pointer to the controller context
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static esp_err_t twai_send_init_controller(twai_controller_ctx_t *controller)
|
||||||
|
{
|
||||||
|
int controller_id = controller - &g_twai_controller_ctx[0];
|
||||||
|
|
||||||
|
/* Create TX completion semaphore */
|
||||||
|
controller->send_ctx.tx_done_sem = xSemaphoreCreateBinary();
|
||||||
|
if (controller->send_ctx.tx_done_sem == NULL) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create TX semaphore for controller %d", controller_id);
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initialize TX pending flag */
|
||||||
|
atomic_init(&controller->send_ctx.is_tx_pending, false);
|
||||||
|
|
||||||
|
/* Register TX done callback */
|
||||||
|
twai_core_ctx_t *core_ctx = &controller->core_ctx;
|
||||||
|
core_ctx->driver_cbs.on_tx_done = twai_send_tx_done_cb;
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deinitialize the send module for a controller
|
||||||
|
*
|
||||||
|
* @param[in] controller Pointer to the controller context
|
||||||
|
*/
|
||||||
|
static void twai_send_deinit_controller(twai_controller_ctx_t *controller)
|
||||||
|
{
|
||||||
|
/* Clear pending flag */
|
||||||
|
atomic_store(&controller->send_ctx.is_tx_pending, false);
|
||||||
|
|
||||||
|
/* Delete TX completion semaphore */
|
||||||
|
if (controller->send_ctx.tx_done_sem) {
|
||||||
|
vSemaphoreDelete(controller->send_ctx.tx_done_sem);
|
||||||
|
controller->send_ctx.tx_done_sem = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear callback */
|
||||||
|
controller->core_ctx.driver_cbs.on_tx_done = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a TWAI frame with the provided parameters
|
||||||
|
*
|
||||||
|
* @param[in] controller Pointer to the TWAI controller context
|
||||||
|
* @param[in] frame Pointer to the TWAI frame to send
|
||||||
|
* @param[in] timeout_ms Timeout in milliseconds to wait for TX completion
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static esp_err_t send_frame_sync(twai_controller_ctx_t *controller, const twai_frame_t *frame, uint32_t timeout_ms)
|
||||||
|
{
|
||||||
|
if (!controller) {
|
||||||
|
ESP_LOGE(TAG, "Invalid controller pointer");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
int controller_id = controller - &g_twai_controller_ctx[0];
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
twai_core_ctx_t *ctx = &controller->core_ctx;
|
||||||
|
|
||||||
|
/* Check if TWAI driver is running */
|
||||||
|
ESP_RETURN_ON_FALSE(atomic_load(&ctx->is_initialized), ESP_ERR_INVALID_STATE, TAG, "TWAI%d not initialized", controller_id);
|
||||||
|
|
||||||
|
/* Mark TX as pending */
|
||||||
|
atomic_store(&controller->send_ctx.is_tx_pending, true);
|
||||||
|
|
||||||
|
/* Transmit the frame */
|
||||||
|
ret = twai_node_transmit(controller->node_handle, frame, timeout_ms);
|
||||||
|
ESP_GOTO_ON_ERROR(ret, err, TAG, "Node %d: Failed to queue TX frame: %s", controller_id, esp_err_to_name(ret));
|
||||||
|
|
||||||
|
/* Wait for TX completion or timeout */
|
||||||
|
ESP_GOTO_ON_FALSE(xSemaphoreTake(controller->send_ctx.tx_done_sem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE, ESP_ERR_TIMEOUT, err, TAG,
|
||||||
|
"Node %d: TX timed out after %"PRIu32" ms", controller_id, timeout_ms);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
err:
|
||||||
|
atomic_store(&controller->send_ctx.is_tx_pending, false);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Command handler for `twai_send twai0 123#AABBCC` command
|
||||||
|
*
|
||||||
|
* @param[in] argc Argument count
|
||||||
|
* @param[in] argv Argument vector
|
||||||
|
*
|
||||||
|
* @return @c ESP_OK on success, error code on failure
|
||||||
|
*/
|
||||||
|
static int twai_send_handler(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&twai_send_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, twai_send_args.end, argv[0]);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for mandatory arguments */
|
||||||
|
if (twai_send_args.controller->count == 0) {
|
||||||
|
ESP_LOGE(TAG, "Controller ID is required");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse controller id */
|
||||||
|
int controller_id = parse_controller_string(twai_send_args.controller->sval[0]);
|
||||||
|
ESP_RETURN_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, TAG, "Invalid controller ID: %s", twai_send_args.controller->sval[0]);
|
||||||
|
|
||||||
|
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
|
||||||
|
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Controller not found: %d", controller_id);
|
||||||
|
|
||||||
|
/* Prepare frame buffer on stack for synchronous transmission */
|
||||||
|
twai_frame_t frame = {0};
|
||||||
|
uint8_t data_buffer[TWAI_FRAME_BUFFER_SIZE] = {0};
|
||||||
|
frame.buffer = data_buffer;
|
||||||
|
|
||||||
|
/* Check if frame string is provided */
|
||||||
|
const char *frame_str = twai_send_args.frame->sval[0];
|
||||||
|
|
||||||
|
const char *sep = NULL;
|
||||||
|
int hash_count = 0;
|
||||||
|
bool is_fd = false;
|
||||||
|
int res = locate_hash(frame_str, &sep, &hash_count);
|
||||||
|
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Failed to locate '#' in frame string: %s", frame_str);
|
||||||
|
if (hash_count == 1) {
|
||||||
|
is_fd = false;
|
||||||
|
} else if (hash_count == 2) {
|
||||||
|
is_fd = true;
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Invalid '#' count in frame string: %s", frame_str);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse ID */
|
||||||
|
size_t id_len = (size_t)(sep - frame_str);
|
||||||
|
res = parse_twai_id(frame_str, id_len, &frame);
|
||||||
|
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Invalid ID: %.*s, error code: %d", (int)id_len, frame_str, res);
|
||||||
|
|
||||||
|
/* Parse frame body */
|
||||||
|
const char *body = sep + hash_count;
|
||||||
|
if (is_fd) {
|
||||||
|
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
|
||||||
|
frame.header.fdf = 1;
|
||||||
|
res = parse_twaifd_frame(body, &frame);
|
||||||
|
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Invalid TWAI-FD frame: %.*s, error code: %d", (int)id_len, frame_str, res);
|
||||||
|
#else
|
||||||
|
ESP_LOGE(TAG, "TWAI-FD not enabled in this build");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
res = parse_classic_frame(body, &frame);
|
||||||
|
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Invalid TWAI classic frame: %.*s, error code: %d", (int)id_len, frame_str, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send frame with 1 second timeout */
|
||||||
|
esp_err_t ret = send_frame_sync(controller, &frame, 1000);
|
||||||
|
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to send frame: %s", esp_err_to_name(ret));
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_twai_send_commands(void)
|
||||||
|
{
|
||||||
|
/* Initialize send context for all controllers */
|
||||||
|
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
|
||||||
|
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
|
||||||
|
esp_err_t ret = twai_send_init_controller(controller);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to initialize send module for TWAI%d: %s", i, esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Register command arguments */
|
||||||
|
twai_send_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (e.g. twai0)");
|
||||||
|
twai_send_args.frame = arg_str0(NULL, NULL, "<frame_str>", "Frame string in format 123#AABBCC (standard) or 12345678#AABBCC (extended)");
|
||||||
|
twai_send_args.end = arg_end(20);
|
||||||
|
|
||||||
|
/* Register command */
|
||||||
|
const esp_console_cmd_t twai_send_cmd = {
|
||||||
|
.command = "twai_send",
|
||||||
|
.help = "Send a TWAI frame using string format\n"
|
||||||
|
"Usage: twai_send <controller> <frame_str>\n"
|
||||||
|
"\n"
|
||||||
|
"Frame Formats:\n"
|
||||||
|
" Standard: 123#DEADBEEF (11-bit ID)\n"
|
||||||
|
" Extended: 12345678#CAFEBABE (29-bit ID)\n"
|
||||||
|
" RTR: 456#R or 456#R8 (Remote Transmission Request)\n"
|
||||||
|
" TWAI-FD: 123##1AABBCC (FD frame with flags)\n"
|
||||||
|
"\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" twai_send twai0 123#DEADBEEF # Standard frame\n"
|
||||||
|
" twai_send twai0 12345678#CAFEBABE # Extended frame\n"
|
||||||
|
" twai_send twai0 456#R8 # RTR frame\n"
|
||||||
|
" twai_send twai0 123##1DEADBEEFCAFEBABE # TWAI-FD frame\n"
|
||||||
|
,
|
||||||
|
.hint = "<controller> [<frame_str>]",
|
||||||
|
.func = &twai_send_handler,
|
||||||
|
.argtable = &twai_send_args
|
||||||
|
};
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_send_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregister_twai_send_commands(void)
|
||||||
|
{
|
||||||
|
/* Cleanup all controller send modules */
|
||||||
|
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
|
||||||
|
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
|
||||||
|
twai_send_deinit_controller(controller);
|
||||||
|
}
|
||||||
|
}
|
50
examples/peripherals/twai/twai_utils/main/twai_utils_main.c
Normal file
50
examples/peripherals/twai/twai_utils/main/twai_utils_main.c
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "cmd_twai.h"
|
||||||
|
|
||||||
|
static const char *TAG = "twai_example";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Main application entry point
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void app_main(void)
|
||||||
|
{
|
||||||
|
esp_console_repl_t *repl = NULL;
|
||||||
|
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
||||||
|
|
||||||
|
repl_config.prompt = "twai>";
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Initializing TWAI console example");
|
||||||
|
|
||||||
|
/* Initialize console REPL environment based on configuration */
|
||||||
|
#if CONFIG_ESP_CONSOLE_UART
|
||||||
|
esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
|
||||||
|
ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl));
|
||||||
|
#elif CONFIG_ESP_CONSOLE_USB_CDC
|
||||||
|
esp_console_dev_usb_cdc_config_t cdc_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
|
||||||
|
ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&cdc_config, &repl_config, &repl));
|
||||||
|
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
|
||||||
|
esp_console_dev_usb_serial_jtag_config_t usbjtag_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
|
||||||
|
ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&usbjtag_config, &repl_config, &repl));
|
||||||
|
#else
|
||||||
|
ESP_LOGE(TAG, "No console device configured");
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Register TWAI commands with console */
|
||||||
|
register_twai_commands();
|
||||||
|
|
||||||
|
/* Start console REPL */
|
||||||
|
ESP_ERROR_CHECK(esp_console_start_repl(repl));
|
||||||
|
}
|
335
examples/peripherals/twai/twai_utils/main/twai_utils_parser.c
Normal file
335
examples/peripherals/twai/twai_utils/main/twai_utils_parser.c
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "twai_utils_parser.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Format timestamp string based on the specified mode
|
||||||
|
*
|
||||||
|
* @param[in] timestamp_mode Timestamp mode configuration
|
||||||
|
* @param[in] frame_timestamp Frame timestamp in microseconds
|
||||||
|
* @param[in] start_time_us Start time for zero-based timestamps
|
||||||
|
* @param[in,out] last_frame_time_us Pointer to last frame time for delta mode (updated if delta mode)
|
||||||
|
* @param[out] timestamp_str Buffer to store formatted timestamp string
|
||||||
|
* @param[in] max_len Maximum length of timestamp string buffer
|
||||||
|
*/
|
||||||
|
void format_timestamp(timestamp_mode_t timestamp_mode, int64_t frame_timestamp,
|
||||||
|
int64_t start_time_us, int64_t *last_frame_time_us,
|
||||||
|
char *timestamp_str, size_t max_len)
|
||||||
|
{
|
||||||
|
if (timestamp_mode == TIMESTAMP_MODE_NONE) {
|
||||||
|
timestamp_str[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t timestamp_us;
|
||||||
|
|
||||||
|
switch (timestamp_mode) {
|
||||||
|
case TIMESTAMP_MODE_ABSOLUTE:
|
||||||
|
timestamp_us = frame_timestamp;
|
||||||
|
break;
|
||||||
|
case TIMESTAMP_MODE_DELTA:
|
||||||
|
timestamp_us = frame_timestamp - *last_frame_time_us;
|
||||||
|
*last_frame_time_us = frame_timestamp;
|
||||||
|
break;
|
||||||
|
case TIMESTAMP_MODE_ZERO:
|
||||||
|
timestamp_us = frame_timestamp - start_time_us;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
timestamp_str[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Format output: (seconds.microseconds) */
|
||||||
|
snprintf(timestamp_str, max_len, "(%lld.%06lld) ", timestamp_us / 1000000, timestamp_us % 1000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
int parse_hex_segment(const char *str, size_t len, uint32_t *out)
|
||||||
|
{
|
||||||
|
if (!str || len == 0 || len > TWAI_EXT_ID_CHAR_LEN || !out) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t result = 0;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
uint8_t nibble;
|
||||||
|
if (parse_nibble(str[i], &nibble) != PARSE_OK) {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
result = (result << 4) | nibble;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = result;
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse payload bytes (hex pairs) up to max length, skipping '.' separators
|
||||||
|
*
|
||||||
|
* This function reads up to max bytes from the ASCII hex string s,
|
||||||
|
* ignoring any '.' separators. Each pair of hex digits is converted
|
||||||
|
* into one byte and stored into buf.
|
||||||
|
*
|
||||||
|
* @param[in] s Null-terminated input string containing hex digits and optional '.' separators
|
||||||
|
* @param[out] buf Buffer to store parsed byte values
|
||||||
|
* @param[in] max Maximum number of bytes to parse (buffer capacity)
|
||||||
|
*
|
||||||
|
* @return On success, returns the number of bytes parsed (0..max).
|
||||||
|
* Returns PARSE_INVALID_ARG if input pointers are NULL or max <= 0.
|
||||||
|
* Returns PARSE_ERROR if a non-hex digit is encountered before parsing max bytes
|
||||||
|
*/
|
||||||
|
static inline int parse_payload(const char *s, uint8_t *buf, int max)
|
||||||
|
{
|
||||||
|
if (!s || !buf || max <= 0) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
int cnt = 0;
|
||||||
|
while (*s && cnt < max) {
|
||||||
|
if (*s == '.') {
|
||||||
|
s++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* Check if we have valid hex pair */
|
||||||
|
if (!isxdigit((unsigned char)s[0])) {
|
||||||
|
if (cnt == 0 && *s != '\0') {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!isxdigit((unsigned char)s[1])) {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
uint8_t high, low;
|
||||||
|
if (parse_nibble(s[0], &high) != PARSE_OK || parse_nibble(s[1], &low) != PARSE_OK) {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
buf[cnt++] = (high << 4) | low;
|
||||||
|
s += 2;
|
||||||
|
}
|
||||||
|
return cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse hex ID substring of given length
|
||||||
|
*
|
||||||
|
* @param[in] str Pointer to the start of the hex substring
|
||||||
|
* @param[in] len Number of characters in the hex substring (3 or 8)
|
||||||
|
* @param[out] out Pointer to the variable to receive the parsed ID value
|
||||||
|
* @param[out] is_ext Pointer to store whether the ID is extended format
|
||||||
|
*
|
||||||
|
* @return PARSE_OK on success;
|
||||||
|
* PARSE_INVALID_ARG if pointers are NULL or len is out of range;
|
||||||
|
* PARSE_ERROR if any character is not a valid hex digit or length mismatch
|
||||||
|
*/
|
||||||
|
static inline int parse_hex_id(const char *str, size_t len, uint32_t *out, bool *is_ext)
|
||||||
|
{
|
||||||
|
if (!str || !out || !is_ext || len == 0 || len > TWAI_EXT_ID_CHAR_LEN) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
int ret = parse_hex_segment(str, len, out);
|
||||||
|
if (ret != PARSE_OK) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
*is_ext = (len > TWAI_STD_ID_CHAR_LEN) || (*out > TWAI_STD_ID_MASK);
|
||||||
|
if ((*is_ext && *out > TWAI_EXT_ID_MASK) || (!*is_ext && *out > TWAI_STD_ID_MASK)) {
|
||||||
|
return PARSE_OUT_OF_RANGE;
|
||||||
|
}
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parse_twai_id(const char *str, size_t len, twai_frame_t *f)
|
||||||
|
{
|
||||||
|
if (!str || !f) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
bool is_ext = false;
|
||||||
|
uint32_t id = 0;
|
||||||
|
int res = parse_hex_id(str, len, &id, &is_ext);
|
||||||
|
if (res != PARSE_OK) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
f->header.id = id;
|
||||||
|
f->header.ide = is_ext ? 1 : 0;
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parse_classic_frame(const char *body, twai_frame_t *f)
|
||||||
|
{
|
||||||
|
if (!body || !f) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle RTR frame */
|
||||||
|
if (*body == 'R' || *body == 'r') {
|
||||||
|
f->header.rtr = true;
|
||||||
|
f->buffer_len = 0; // RTR frames have no data payload.
|
||||||
|
const char *dlc_str = body + 1;
|
||||||
|
uint8_t dlc = TWAI_RTR_DEFAULT_DLC; // Default DLC for RTR frame if not specified.
|
||||||
|
|
||||||
|
if (*dlc_str != '\0') {
|
||||||
|
// An explicit DLC is provided, e.g., "R8".
|
||||||
|
char *endptr;
|
||||||
|
dlc = (uint8_t)strtoul(dlc_str, &endptr, 16);
|
||||||
|
if (*endptr != '\0' || dlc > TWAI_FRAME_MAX_LEN) {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f->header.dlc = dlc;
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle data frame */
|
||||||
|
f->header.rtr = false; // Ensure RTR flag is cleared.
|
||||||
|
|
||||||
|
int dl = parse_payload(body, f->buffer, TWAI_FRAME_MAX_LEN);
|
||||||
|
if (dl < 0) {
|
||||||
|
return dl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for optional _dlc suffix */
|
||||||
|
const char *underscore = strchr(body, '_');
|
||||||
|
if (underscore && underscore[1] != '\0') {
|
||||||
|
uint8_t dlc = (uint8_t)strtoul(underscore + 1, NULL, 16);
|
||||||
|
if (dlc <= TWAI_FRAME_MAX_LEN) {
|
||||||
|
f->header.dlc = dlc;
|
||||||
|
} else {
|
||||||
|
f->header.dlc = TWAI_FRAME_MAX_LEN;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f->header.dlc = (uint8_t)dl;
|
||||||
|
}
|
||||||
|
f->buffer_len = dl;
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parse_twaifd_frame(const char *body, twai_frame_t *f)
|
||||||
|
{
|
||||||
|
if (!body || !f) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
uint8_t flags;
|
||||||
|
if (parse_nibble(*body++, &flags) != PARSE_OK || flags > TWAI_FD_FLAGS_MAX_VALUE) {
|
||||||
|
return PARSE_OUT_OF_RANGE;
|
||||||
|
}
|
||||||
|
f->header.fdf = true;
|
||||||
|
f->header.brs = !!(flags & TWAI_FD_BRS_FLAG_MASK);
|
||||||
|
f->header.esi = !!(flags & TWAI_FD_ESI_FLAG_MASK);
|
||||||
|
int dl = parse_payload(body, f->buffer, TWAIFD_FRAME_MAX_LEN);
|
||||||
|
if (dl < 0) {
|
||||||
|
return dl;
|
||||||
|
}
|
||||||
|
f->buffer_len = dl;
|
||||||
|
f->header.dlc = (uint8_t)twaifd_len2dlc((uint16_t)dl);
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parse_pair_token(const char *tok, size_t tok_len, char sep,
|
||||||
|
uint32_t *lhs, size_t *lhs_chars,
|
||||||
|
uint32_t *rhs, size_t *rhs_chars)
|
||||||
|
{
|
||||||
|
if (!tok || tok_len == 0 || !lhs || !rhs || !lhs_chars || !rhs_chars) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *mid = (const char *)memchr(tok, sep, tok_len);
|
||||||
|
if (!mid) {
|
||||||
|
return PARSE_NOT_FOUND; /* not this token kind */
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t l_len = (size_t)(mid - tok);
|
||||||
|
size_t r_len = tok_len - l_len - 1;
|
||||||
|
if (l_len == 0 || r_len == 0) {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rl = parse_hex_segment(tok, l_len, lhs);
|
||||||
|
int rr = parse_hex_segment(mid + 1, r_len, rhs);
|
||||||
|
if (rl != PARSE_OK || rr != PARSE_OK) {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
*lhs_chars = l_len;
|
||||||
|
*rhs_chars = r_len;
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *twai_state_to_string(twai_error_state_t state)
|
||||||
|
{
|
||||||
|
switch (state) {
|
||||||
|
case TWAI_ERROR_ACTIVE: return "Error Active";
|
||||||
|
case TWAI_ERROR_WARNING: return "Error Warning";
|
||||||
|
case TWAI_ERROR_PASSIVE: return "Error Passive";
|
||||||
|
case TWAI_ERROR_BUS_OFF: return "Bus Off";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int format_gpio_pin(int gpio_pin, char *buffer, size_t buffer_size)
|
||||||
|
{
|
||||||
|
if (gpio_pin == GPIO_NUM_NC || gpio_pin < 0) {
|
||||||
|
return snprintf(buffer, buffer_size, "Disabled");
|
||||||
|
} else {
|
||||||
|
return snprintf(buffer, buffer_size, "GPIO%d", gpio_pin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int parse_controller_string(const char *controller_str)
|
||||||
|
{
|
||||||
|
int controller_id;
|
||||||
|
const char *end = parse_controller_id(controller_str, &controller_id);
|
||||||
|
return end ? controller_id : PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
void format_twaidump_frame(timestamp_mode_t timestamp_mode, const twai_frame_t *frame,
|
||||||
|
int64_t frame_timestamp, int64_t start_time_us, int64_t *last_frame_time_us,
|
||||||
|
int controller_id, char *output_line, size_t max_len)
|
||||||
|
{
|
||||||
|
char timestamp_str[64] = {0};
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
/* Format timestamp */
|
||||||
|
format_timestamp(timestamp_mode, frame_timestamp, start_time_us, last_frame_time_us,
|
||||||
|
timestamp_str, sizeof(timestamp_str));
|
||||||
|
|
||||||
|
/* Add timestamp if enabled */
|
||||||
|
if (strlen(timestamp_str) > 0) {
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, "%s", timestamp_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add interface name (e.g. use twai0, twai1) */
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, "twai%d ", controller_id);
|
||||||
|
|
||||||
|
/* Format TWAI ID (formatted as: 3 digits for SFF, 8 digits for EFF) */
|
||||||
|
if (frame->header.ide) {
|
||||||
|
/* Extended frame: 8 hex digits */
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, "%08" PRIX32 " ", frame->header.id);
|
||||||
|
} else {
|
||||||
|
/* Standard frame: 3 hex digits (or less if ID is smaller) */
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, "%03" PRIX32 " ", frame->header.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame->header.rtr) {
|
||||||
|
/* RTR frame: add [R] and DLC */
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, "[R%d]", frame->header.dlc);
|
||||||
|
} else {
|
||||||
|
/* Data frame: add DLC and data bytes with spaces */
|
||||||
|
printf("frame->header.dlc: %d\n", frame->header.dlc);
|
||||||
|
int actual_len = twaifd_dlc2len(frame->header.dlc);
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, "[%d]", actual_len);
|
||||||
|
for (int i = 0; i < actual_len && i < frame->buffer_len && pos < max_len - 4; i++) {
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, " %02X", frame->buffer[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add newline */
|
||||||
|
if (pos < max_len - 1) {
|
||||||
|
pos += snprintf(output_line + pos, max_len - pos, "\n");
|
||||||
|
}
|
||||||
|
}
|
267
examples/peripherals/twai/twai_utils/main/twai_utils_parser.h
Normal file
267
examples/peripherals/twai/twai_utils/main/twai_utils_parser.h
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "esp_twai.h"
|
||||||
|
#include "cmd_twai_internal.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* TWAI frame constants */
|
||||||
|
#define TWAI_STD_ID_CHAR_LEN 3
|
||||||
|
#define TWAI_EXT_ID_CHAR_LEN 8
|
||||||
|
|
||||||
|
/** @brief Parser return codes */
|
||||||
|
#define PARSE_OK 0
|
||||||
|
#define PARSE_ERROR -1
|
||||||
|
#define PARSE_INVALID_ARG -2
|
||||||
|
#define PARSE_OUT_OF_RANGE -3
|
||||||
|
#define PARSE_TOO_LONG -4
|
||||||
|
#define PARSE_NOT_FOUND -5
|
||||||
|
|
||||||
|
/* Additional constants */
|
||||||
|
#define TWAI_RTR_DEFAULT_DLC 0
|
||||||
|
#define TWAI_FD_FLAGS_MAX_VALUE 15
|
||||||
|
#define TWAI_FD_BRS_FLAG_MASK 0x01
|
||||||
|
#define TWAI_FD_ESI_FLAG_MASK 0x02
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse TWAI ID from string
|
||||||
|
*
|
||||||
|
* @param[in] str Pointer to the start of the ID string
|
||||||
|
* @param[in] len Length of the ID string
|
||||||
|
* @param[out] f Pointer to frame structure to fill
|
||||||
|
*
|
||||||
|
* @return PARSE_OK on success;
|
||||||
|
* PARSE_INVALID_ARG if pointers are NULL;
|
||||||
|
* PARSE_ERROR or PARSE_OUT_OF_RANGE on format or range error
|
||||||
|
*/
|
||||||
|
int parse_twai_id(const char *str, size_t len, twai_frame_t *f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse TWAI-FD frames with flags and extended payload
|
||||||
|
*
|
||||||
|
* Body format: <flags>{data}
|
||||||
|
* flags: single hex nibble (0..F)
|
||||||
|
* data: up to 64 bytes hex pairs
|
||||||
|
*
|
||||||
|
* @param[in] body Pointing to the substring after '#'
|
||||||
|
* @param[out] f Pointer to frame structure to fill
|
||||||
|
*
|
||||||
|
* @return PARSE_OK on success;
|
||||||
|
* PARSE_INVALID_ARG if arguments are NULL;
|
||||||
|
* PARSE_ERROR or PARSE_OUT_OF_RANGE on format or range error
|
||||||
|
*/
|
||||||
|
int parse_twaifd_frame(const char *str, twai_frame_t *f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse Classical TWAI data and RTR frames
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* <twai_id>#{data} Data frame with up to 8 bytes
|
||||||
|
* <twai_id>#R{len} RTR frame with specified length
|
||||||
|
* <twai_id>#{data}_{dlc} Data frame with extended DLC (9..F)
|
||||||
|
*
|
||||||
|
* @param[in] body Pointing to the substring after '#'
|
||||||
|
* @param[out] f Pointer to frame structure to fill
|
||||||
|
*
|
||||||
|
* @return PARSE_OK on success;
|
||||||
|
* PARSE_INVALID_ARG if arguments are NULL;
|
||||||
|
* PARSE_ERROR or PARSE_OUT_OF_RANGE on format or range error
|
||||||
|
*/
|
||||||
|
int parse_classic_frame(const char *str, twai_frame_t *f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse controller string and return controller ID
|
||||||
|
*
|
||||||
|
* @param[in] controller_str Controller string (e.g., "twai0")
|
||||||
|
*
|
||||||
|
* @return Controller ID (0-9) on success, PARSE_ERROR on failure
|
||||||
|
*/
|
||||||
|
int parse_controller_string(const char *controller_str);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert TWAI state to string
|
||||||
|
*
|
||||||
|
* @param[in] state TWAI error state
|
||||||
|
*
|
||||||
|
* @return Pointer to the string representation of the state
|
||||||
|
*/
|
||||||
|
const char *twai_state_to_string(twai_error_state_t state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Format GPIO pin display
|
||||||
|
*
|
||||||
|
* @param[in] gpio_pin GPIO pin number
|
||||||
|
* @param[out] buffer Buffer to store the formatted string
|
||||||
|
* @param[in] buffer_size Size of the buffer
|
||||||
|
*
|
||||||
|
* @return Number of characters written to buffer
|
||||||
|
*/
|
||||||
|
int format_gpio_pin(int gpio_pin, char *buffer, size_t buffer_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse hex string with specified length (no null terminator required)
|
||||||
|
*
|
||||||
|
* @param[in] str Input string pointer
|
||||||
|
* @param[in] len Length of hex string to parse
|
||||||
|
* @param[out] out Output value pointer
|
||||||
|
*
|
||||||
|
* @return PARSE_OK on success, PARSE_ERROR on format error
|
||||||
|
*/
|
||||||
|
int parse_hex_segment(const char *str, size_t len, uint32_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse a "lhs <sep> rhs" token where both sides are hex strings.
|
||||||
|
*
|
||||||
|
* The function splits by @p sep, parses both halves as hex (no null terminators required),
|
||||||
|
* and returns their values and lengths.
|
||||||
|
*
|
||||||
|
* @param[in] tok Pointer to token start
|
||||||
|
* @param[in] tok_len Token length in bytes
|
||||||
|
* @param[in] sep Separator character (':' for mask, '-' for range)
|
||||||
|
* @param[out] lhs Parsed left-hand value
|
||||||
|
* @param[out] lhs_chars Characters consumed by left-hand substring
|
||||||
|
* @param[out] rhs Parsed right-hand value
|
||||||
|
* @param[out] rhs_chars Characters consumed by right-hand substring
|
||||||
|
*
|
||||||
|
* @return PARSE_OK on success;
|
||||||
|
* PARSE_INVALID_ARG for bad args;
|
||||||
|
* PARSE_ERROR if separator missing or hex parse fails.
|
||||||
|
*/
|
||||||
|
int parse_pair_token(const char *tok, size_t tok_len, char sep,
|
||||||
|
uint32_t *lhs, size_t *lhs_chars,
|
||||||
|
uint32_t *rhs, size_t *rhs_chars);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse a single hex nibble character
|
||||||
|
*
|
||||||
|
* @param[in] c Input character (0-9, A-F, a-f)
|
||||||
|
* @param[out] out Output pointer to store the parsed nibble value (0-15)
|
||||||
|
*
|
||||||
|
* @return PARSE_OK on success;
|
||||||
|
* PARSE_INVALID_ARG if out pointer is NULL;
|
||||||
|
* PARSE_ERROR if character is not a valid hex digit
|
||||||
|
*/
|
||||||
|
static inline int parse_nibble(char c, uint8_t *out)
|
||||||
|
{
|
||||||
|
if (!out) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
*out = (uint8_t)(c - '0');
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
if (c >= 'A' && c <= 'F') {
|
||||||
|
*out = (uint8_t)(c - 'A' + 10);
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
if (c >= 'a' && c <= 'f') {
|
||||||
|
*out = (uint8_t)(c - 'a' + 10);
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Locate first '#' and count consecutives
|
||||||
|
*
|
||||||
|
* @param[in] input Input string
|
||||||
|
* @param[out] sep Pointer to the separator
|
||||||
|
* @param[out] hash_count Pointer to the hash count
|
||||||
|
*
|
||||||
|
* @return PARSE_OK if successful, PARSE_INVALID_ARG if input is NULL, PARSE_ERROR if no '#' is found
|
||||||
|
*/
|
||||||
|
static inline int locate_hash(const char *input, const char **sep, int *hash_count)
|
||||||
|
{
|
||||||
|
if (!input || !sep || !hash_count) {
|
||||||
|
return PARSE_INVALID_ARG;
|
||||||
|
}
|
||||||
|
const char *s = strchr(input, '#');
|
||||||
|
if (!s) {
|
||||||
|
return PARSE_ERROR;
|
||||||
|
}
|
||||||
|
*sep = s;
|
||||||
|
*hash_count = 1;
|
||||||
|
while (s[*hash_count] == '#') {
|
||||||
|
(*hash_count)++;
|
||||||
|
}
|
||||||
|
return PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Format timestamp string based on the specified mode
|
||||||
|
*
|
||||||
|
* @param[in] timestamp_mode Timestamp mode configuration
|
||||||
|
* @param[in] frame_timestamp Frame timestamp in microseconds
|
||||||
|
* @param[in] start_time_us Start time for zero-based timestamps
|
||||||
|
* @param[in,out] last_frame_time_us Pointer to last frame time for delta mode (updated if delta mode)
|
||||||
|
* @param[out] timestamp_str Buffer to store formatted timestamp string
|
||||||
|
* @param[in] max_len Maximum length of timestamp string buffer
|
||||||
|
*/
|
||||||
|
void format_timestamp(timestamp_mode_t timestamp_mode, int64_t frame_timestamp,
|
||||||
|
int64_t start_time_us, int64_t *last_frame_time_us,
|
||||||
|
char *timestamp_str, size_t max_len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Format TWAI frame in twai_dump format
|
||||||
|
*
|
||||||
|
* @param[in] timestamp_mode Timestamp mode configuration
|
||||||
|
* @param[in] frame TWAI frame structure
|
||||||
|
* @param[in] frame_timestamp Frame timestamp in microseconds
|
||||||
|
* @param[in] start_time_us Start time for zero-based timestamps
|
||||||
|
* @param[in,out] last_frame_time_us Pointer to last frame time for delta mode (updated if delta mode)
|
||||||
|
* @param[in] controller_id Controller ID for interface name
|
||||||
|
* @param[out] output_line Buffer to store formatted output line
|
||||||
|
* @param[in] max_len Maximum length of output line buffer
|
||||||
|
*/
|
||||||
|
void format_twaidump_frame(timestamp_mode_t timestamp_mode, const twai_frame_t *frame,
|
||||||
|
int64_t frame_timestamp, int64_t start_time_us, int64_t *last_frame_time_us,
|
||||||
|
int controller_id, char *output_line, size_t max_len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse the controller ID string and return the end of the controller substring
|
||||||
|
*
|
||||||
|
* This function parses a controller string in the format "twai0", "twai1", ..., "twaix"
|
||||||
|
* and extracts the controller ID (0-x). It also supports controller strings with filters,
|
||||||
|
* such as "twai0,123:7FF", and returns a pointer to the end of the substring(e.g. the ',' or '\0').
|
||||||
|
*
|
||||||
|
* @param[in] controller_str Input controller string (e.g., "twai0" or "twai0,123:7FF")
|
||||||
|
* @param[out] controller_id Output pointer to store the parsed controller ID
|
||||||
|
*
|
||||||
|
* @return Pointer to the end of the controller substring (e.g., the ',' or '\0'), or NULL on error
|
||||||
|
*/
|
||||||
|
static inline const char *parse_controller_id(const char *controller_str, int *controller_id)
|
||||||
|
{
|
||||||
|
if (!controller_str || !controller_id) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Support "twai0" ~ "twaix" format (which is dependent on SOC_TWAI_CONTROLLER_NUM) */
|
||||||
|
if (strncmp(controller_str, "twai", 4) == 0 && strlen(controller_str) >= 5) {
|
||||||
|
char id_char = controller_str[4];
|
||||||
|
if (id_char >= '0' && id_char <= '9' && id_char < '0' + SOC_TWAI_CONTROLLER_NUM) {
|
||||||
|
*controller_id = id_char - '0';
|
||||||
|
/* Return pointer to character after the ID digit */
|
||||||
|
return controller_str + 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
743
examples/peripherals/twai/twai_utils/pytest_twai_utils.py
Normal file
743
examples/peripherals/twai/twai_utils/pytest_twai_utils.py
Normal file
@@ -0,0 +1,743 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from collections.abc import Generator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import can
|
||||||
|
import pexpect
|
||||||
|
import pytest
|
||||||
|
from pytest_embedded import Dut
|
||||||
|
from pytest_embedded_idf.utils import idf_parametrize
|
||||||
|
from pytest_embedded_idf.utils import soc_filtered_targets
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Constants / Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
PROMPTS = ['esp>', 'twai>', '>']
|
||||||
|
|
||||||
|
|
||||||
|
def _ctrl(controller_id: int) -> str:
|
||||||
|
return f'twai{controller_id}'
|
||||||
|
|
||||||
|
|
||||||
|
def _id_pattern(controller_str: str, can_id: int) -> str:
|
||||||
|
"""Return regex pattern for a dump line that contains this ctrl & CAN ID."""
|
||||||
|
hex_part = f'{can_id:08X}' if can_id > 0x7FF else f'{can_id:03X}'
|
||||||
|
return rf'{controller_str}\s+{hex_part}\s+\['
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig:
|
||||||
|
"""Test configuration"""
|
||||||
|
|
||||||
|
# Hardware configuration
|
||||||
|
DEFAULT_BITRATE = 500000
|
||||||
|
BITRATES = [125000, 250000, 500000, 1000000]
|
||||||
|
DEFAULT_TX_GPIO = 4
|
||||||
|
DEFAULT_RX_GPIO = 5
|
||||||
|
NO_TRANSCEIVER_GPIO = 4
|
||||||
|
|
||||||
|
# Test frame data
|
||||||
|
BASIC_FRAMES = [
|
||||||
|
('123#', 'Empty data'),
|
||||||
|
('124#AA', '1 byte'),
|
||||||
|
('125#DEADBEEF', '4 bytes'),
|
||||||
|
('126#DEADBEEFCAFEBABE', '8 bytes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
EXTENDED_FRAMES = [
|
||||||
|
('12345678#ABCD', 'Extended frame'),
|
||||||
|
('1FFFFFFF#AA55BB66', 'Max extended ID'),
|
||||||
|
]
|
||||||
|
|
||||||
|
RTR_FRAMES = [
|
||||||
|
('123#R', 'RTR default'),
|
||||||
|
('124#R8', 'RTR 8 bytes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# FD frames (if FD is supported)
|
||||||
|
FD_FRAMES = [
|
||||||
|
('123##0AABBCC', 'FD frame without BRS'),
|
||||||
|
('456##1DEADBEEF', 'FD frame with BRS'),
|
||||||
|
('789##2CAFEBABE', 'FD frame with ESI'),
|
||||||
|
('ABC##3112233', 'FD frame with BRS+ESI'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Boundary ID tests
|
||||||
|
BOUNDARY_ID_FRAMES = [
|
||||||
|
('7FF#AA', 'Max standard ID'),
|
||||||
|
('800#BB', 'Min extended ID (in extended format: 00000800)'),
|
||||||
|
('000#CC', 'Min ID'),
|
||||||
|
]
|
||||||
|
|
||||||
|
INVALID_FRAMES = [
|
||||||
|
('G123#DEAD', 'Invalid ID character'),
|
||||||
|
('123#GG', 'Invalid data character'),
|
||||||
|
('123', 'Missing separator'),
|
||||||
|
('123#DEADBEEFCAFEBABEAA', 'Too much data'),
|
||||||
|
('123###DEAD', 'Too many separators'),
|
||||||
|
('123##', 'FD frame without data or flags'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Filter tests (includes both basic and extended frame filtering)
|
||||||
|
FILTER_TESTS = [
|
||||||
|
# No filter - basic functionality
|
||||||
|
(
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
('123#DEAD', 0x123, True), # Standard frame passes
|
||||||
|
('12345678#CAFE', 0x12345678, True), # Extended frame passes
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Standard frame mask filter (is_ext=false by length=3, value<=0x7FF)
|
||||||
|
(
|
||||||
|
'123:7FF',
|
||||||
|
[
|
||||||
|
('123#DEAD', 0x123, True), # Standard frame matches
|
||||||
|
('456#BEEF', 0x456, False), # Standard frame doesn't match
|
||||||
|
('12345678#CAFE', 0x12345678, False), # Extended frame filtered out
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Extended frame mask filter (is_ext=true by length>3)
|
||||||
|
(
|
||||||
|
'12345678:1FFFFFFF',
|
||||||
|
[
|
||||||
|
('123#DEAD', 0x123, False), # Standard frame filtered out
|
||||||
|
('12345678#CAFE', 0x12345678, True), # Extended frame matches
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Extended frame mask filter (is_ext=true by value>0x7FF)
|
||||||
|
(
|
||||||
|
'800:1FFFFFFF',
|
||||||
|
[
|
||||||
|
('7FF#BEEF', 0x7FF, False), # Max standard ID filtered out
|
||||||
|
('800#CAFE', 0x800, True), # Extended ID matches exactly
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Range filter tests
|
||||||
|
RANGE_FILTER_TESTS = [
|
||||||
|
# Standard frame range filter
|
||||||
|
(
|
||||||
|
'a-15', # Test hex range parsing
|
||||||
|
[
|
||||||
|
('00a#DEAD', 0x00A, True), # Within range
|
||||||
|
('00f#BEEF', 0x00F, True), # Within range
|
||||||
|
('015#CAFE', 0x015, True), # At upper bound
|
||||||
|
('009#BABE', 0x009, False), # Below range
|
||||||
|
('016#FEED', 0x016, False), # Above range
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Extended frame range filter
|
||||||
|
(
|
||||||
|
'10000000-1FFFFFFF',
|
||||||
|
[
|
||||||
|
('123#DEAD', 0x123, False), # Standard frame filtered out
|
||||||
|
('0FFFFFFF#BEEF', 0x0FFFFFFF, False), # Below range
|
||||||
|
('10000000#CAFE', 0x10000000, True), # At lower bound
|
||||||
|
('1FFFFFFF#FEED', 0x1FFFFFFF, True), # At upper bound
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Rapid succession test frames
|
||||||
|
RAPID_FRAMES = ['123#AA', '124#BB', '125#CC', '126#DD', '127#EE']
|
||||||
|
|
||||||
|
# Basic send test frames
|
||||||
|
BASIC_SEND_FRAMES = ['123#DEADBEEF', '7FF#AA55', '12345678#CAFEBABE']
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TWAI helper (refactored)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TwaiTestHelper:
|
||||||
|
"""TWAI test helper built on small, reusable atomic operations."""
|
||||||
|
|
||||||
|
def __init__(self, dut: Dut) -> None:
|
||||||
|
self.dut = dut
|
||||||
|
self.timeout = 5
|
||||||
|
self._wait_ready()
|
||||||
|
|
||||||
|
# ------------------------- atomic I/O ops -------------------------
|
||||||
|
def _wait_ready(self) -> None:
|
||||||
|
try:
|
||||||
|
self.dut.expect(PROMPTS, timeout=10)
|
||||||
|
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF):
|
||||||
|
self.sendline('help')
|
||||||
|
self.expect(['Commands:'], timeout=5)
|
||||||
|
|
||||||
|
def sendline(self, cmd: str) -> None:
|
||||||
|
self.dut.write(f'\n{cmd}\n')
|
||||||
|
|
||||||
|
def expect(self, patterns: list[str] | str, timeout: float | None = None) -> bool:
|
||||||
|
timeout = timeout or self.timeout
|
||||||
|
try:
|
||||||
|
self.dut.expect(patterns, timeout=timeout)
|
||||||
|
return True
|
||||||
|
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run(self, cmd: str, expect: list[str] | str | None = None, timeout: float | None = None) -> bool:
|
||||||
|
self.sendline(cmd)
|
||||||
|
return self.expect(expect or PROMPTS, timeout)
|
||||||
|
|
||||||
|
# ------------------------- command builders -------------------------
|
||||||
|
def build_init(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
controller_id: int = 0,
|
||||||
|
tx_gpio: int | None = None,
|
||||||
|
rx_gpio: int | None = None,
|
||||||
|
bitrate: int | None = None,
|
||||||
|
clk_out_gpio: int | None = None,
|
||||||
|
bus_off_gpio: int | None = None,
|
||||||
|
fd_bitrate: int | None = None,
|
||||||
|
loopback: bool = False,
|
||||||
|
self_test: bool = False,
|
||||||
|
listen: bool = False,
|
||||||
|
) -> str:
|
||||||
|
ctrl = _ctrl(controller_id)
|
||||||
|
parts = [f'twai_init {ctrl}']
|
||||||
|
if tx_gpio is not None:
|
||||||
|
parts += [f'-t {tx_gpio}']
|
||||||
|
if rx_gpio is not None:
|
||||||
|
parts += [f'-r {rx_gpio}']
|
||||||
|
if bitrate is not None:
|
||||||
|
parts += [f'-b {bitrate}']
|
||||||
|
if fd_bitrate is not None:
|
||||||
|
parts += [f'-B {fd_bitrate}']
|
||||||
|
if clk_out_gpio is not None:
|
||||||
|
parts += [f'-c {clk_out_gpio}']
|
||||||
|
if bus_off_gpio is not None:
|
||||||
|
parts += [f'-o {bus_off_gpio}']
|
||||||
|
if loopback:
|
||||||
|
parts += ['--loopback']
|
||||||
|
if self_test:
|
||||||
|
parts += ['--self-test']
|
||||||
|
if listen:
|
||||||
|
parts += ['--listen']
|
||||||
|
return ' '.join(parts)
|
||||||
|
|
||||||
|
def build_dump_start(self, *, controller_id: int = 0, dump_filter: str | None = None) -> str:
|
||||||
|
cmd = f'twai_dump {_ctrl(controller_id)}'
|
||||||
|
if dump_filter:
|
||||||
|
cmd += f',{dump_filter}'
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
def build_dump_stop(self, *, controller_id: int = 0) -> str:
|
||||||
|
return f'twai_dump {_ctrl(controller_id)} --stop'
|
||||||
|
|
||||||
|
# ------------------------- high-level ops -------------------------
|
||||||
|
def init(self, controller_id: int = 0, **kwargs: Any) -> bool:
|
||||||
|
return self.run(self.build_init(controller_id=controller_id, **kwargs))
|
||||||
|
|
||||||
|
def deinit(self, controller_id: int = 0) -> bool:
|
||||||
|
return self.run(f'twai_deinit {_ctrl(controller_id)}')
|
||||||
|
|
||||||
|
def dump_start(self, controller_id: int = 0, dump_filter: str | None = None) -> bool:
|
||||||
|
return self.run(self.build_dump_start(controller_id=controller_id, dump_filter=dump_filter))
|
||||||
|
|
||||||
|
def dump_stop(self, controller_id: int = 0) -> tuple[bool, bool]:
|
||||||
|
"""Stop dump and return (stopped_ok, timeout_warning_seen)."""
|
||||||
|
self.sendline(self.build_dump_stop(controller_id=controller_id))
|
||||||
|
# If the dump task does not exit naturally, the implementation prints this warning.
|
||||||
|
warning_seen = self.expect(r'Dump task did not exit naturally, timeout', timeout=5)
|
||||||
|
# Whether or not warning appears, we should be back to a prompt.
|
||||||
|
prompt_ok = self.expect(PROMPTS, timeout=2) or True # relax
|
||||||
|
return prompt_ok, warning_seen
|
||||||
|
|
||||||
|
def send(self, frame_str: str, controller_id: int = 0) -> bool:
|
||||||
|
return self.run(f'twai_send {_ctrl(controller_id)} {frame_str}')
|
||||||
|
|
||||||
|
def info(self, controller_id: int = 0) -> bool:
|
||||||
|
return self.run(
|
||||||
|
f'twai_info {_ctrl(controller_id)}',
|
||||||
|
[rf'TWAI{controller_id} Status:', r'Node State:', r'Bitrate:'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def recover(self, controller_id: int = 0, timeout_ms: int | None = None) -> bool:
|
||||||
|
cmd = f'twai_recover {_ctrl(controller_id)}'
|
||||||
|
if timeout_ms is not None:
|
||||||
|
cmd += f' -t {timeout_ms}'
|
||||||
|
return self.run(cmd, ['Recovery not needed', 'node is Error Active', 'ESP_ERR_INVALID_STATE']) # any
|
||||||
|
|
||||||
|
def expect_info_format(self, controller_id: int = 0) -> bool:
|
||||||
|
self.sendline(f'twai_info {_ctrl(controller_id)}')
|
||||||
|
checks = [
|
||||||
|
rf'TWAI{controller_id} Status: \w+',
|
||||||
|
r'Node State: \w+',
|
||||||
|
r'Error Counters: TX=\d+, RX=\d+',
|
||||||
|
r'Bitrate: \d+ bps',
|
||||||
|
]
|
||||||
|
return all(self.expect(p, timeout=2) for p in checks)
|
||||||
|
|
||||||
|
def invalid_should_fail(self, cmd: str, timeout: float = 2.0) -> bool:
|
||||||
|
self.sendline(cmd)
|
||||||
|
return self.expect([r'Command returned non-zero error code:', r'ERROR', r'Failed', r'Invalid'], timeout=timeout)
|
||||||
|
|
||||||
|
def test_with_patterns(self, cmd: str, patterns: list[str], timeout: float = 3.0) -> bool:
|
||||||
|
self.sendline(cmd)
|
||||||
|
return all(self.expect(p, timeout=timeout) for p in patterns)
|
||||||
|
|
||||||
|
def send_and_expect_in_dump(
|
||||||
|
self,
|
||||||
|
frame_str: str,
|
||||||
|
expected_id: int,
|
||||||
|
controller_id: int = 0,
|
||||||
|
timeout: float = 3.0,
|
||||||
|
) -> bool:
|
||||||
|
ctrl = _ctrl(controller_id)
|
||||||
|
self.sendline(f'twai_send {ctrl} {frame_str}')
|
||||||
|
return self.expect(_id_pattern(ctrl, expected_id), timeout=timeout)
|
||||||
|
|
||||||
|
# ------------------------- context manager -------------------------
|
||||||
|
@contextmanager
|
||||||
|
def session(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
controller_id: int = 0,
|
||||||
|
mode: str = 'no_transceiver',
|
||||||
|
start_dump: bool = True,
|
||||||
|
dump_filter: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Generator['TwaiTestHelper', None, None]:
|
||||||
|
"""Manage init/dump lifecycle consistently.
|
||||||
|
|
||||||
|
- mode="no_transceiver": loopback + self_test on a single GPIO.
|
||||||
|
- mode="standard": caller must provide tx_gpio/rx_gpio (or we use defaults).
|
||||||
|
"""
|
||||||
|
# Build effective init args
|
||||||
|
init_args = dict(kwargs)
|
||||||
|
init_args['controller_id'] = controller_id
|
||||||
|
|
||||||
|
if mode == 'no_transceiver':
|
||||||
|
init_args |= dict(
|
||||||
|
tx_gpio=TestConfig.NO_TRANSCEIVER_GPIO,
|
||||||
|
rx_gpio=TestConfig.NO_TRANSCEIVER_GPIO,
|
||||||
|
bitrate=kwargs.get('bitrate', TestConfig.DEFAULT_BITRATE),
|
||||||
|
loopback=True,
|
||||||
|
self_test=True,
|
||||||
|
)
|
||||||
|
elif mode == 'standard':
|
||||||
|
init_args.setdefault('tx_gpio', TestConfig.DEFAULT_TX_GPIO)
|
||||||
|
init_args.setdefault('rx_gpio', TestConfig.DEFAULT_RX_GPIO)
|
||||||
|
init_args.setdefault('bitrate', kwargs.get('bitrate', TestConfig.DEFAULT_BITRATE))
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown mode: {mode}')
|
||||||
|
|
||||||
|
if not self.init(**init_args):
|
||||||
|
raise RuntimeError(f'Failed to initialize TWAI in {mode} mode')
|
||||||
|
|
||||||
|
dump_started = False
|
||||||
|
dump_timeout_flag = False
|
||||||
|
try:
|
||||||
|
if start_dump:
|
||||||
|
dump_started = self.dump_start(controller_id=controller_id, dump_filter=dump_filter)
|
||||||
|
yield self
|
||||||
|
finally:
|
||||||
|
if dump_started:
|
||||||
|
_, warning = self.dump_stop(controller_id=controller_id)
|
||||||
|
dump_timeout_flag = warning
|
||||||
|
|
||||||
|
self.deinit(controller_id=controller_id)
|
||||||
|
|
||||||
|
if dump_timeout_flag:
|
||||||
|
pytest.fail(f'Dump stop timed out for {_ctrl(controller_id)}')
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CAN bus manager (external hardware)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class CanBusManager:
|
||||||
|
"""CAN bus manager for external hardware tests"""
|
||||||
|
|
||||||
|
def __init__(self, interface: str = 'can0'):
|
||||||
|
self.interface = interface
|
||||||
|
self.bus: can.Bus | None = None
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def managed_bus(self, bitrate: int = 500000) -> Generator[can.Bus, None, None]:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['ip', '-details', 'link', 'show', self.interface], capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception(f'CAN interface {self.interface} not found')
|
||||||
|
|
||||||
|
interface_up = 'UP' in result.stdout
|
||||||
|
current_bitrate = None
|
||||||
|
m = re.search(r'bitrate (\d+)', result.stdout)
|
||||||
|
if m:
|
||||||
|
current_bitrate = int(m.group(1))
|
||||||
|
|
||||||
|
if current_bitrate != bitrate:
|
||||||
|
logging.info(
|
||||||
|
f'Configuring CAN interface: current_bitrate={current_bitrate}, required_bitrate={bitrate}'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if interface_up:
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', '-n', 'ip', 'link', 'set', self.interface, 'down'], check=True, capture_output=True
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
'sudo',
|
||||||
|
'-n',
|
||||||
|
'ip',
|
||||||
|
'link',
|
||||||
|
'set',
|
||||||
|
self.interface,
|
||||||
|
'up',
|
||||||
|
'type',
|
||||||
|
'can',
|
||||||
|
'bitrate',
|
||||||
|
str(bitrate),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
time.sleep(0.5)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
raise Exception(
|
||||||
|
f'Failed to configure CAN interface {self.interface}. '
|
||||||
|
f'Try: sudo ip link set {self.interface} down && '
|
||||||
|
f'sudo ip link set {self.interface} up type can bitrate {bitrate}'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bus = can.Bus(interface='socketcan', channel=self.interface)
|
||||||
|
yield self.bus
|
||||||
|
except Exception as e:
|
||||||
|
pytest.skip(f'CAN interface not available: {str(e)}')
|
||||||
|
finally:
|
||||||
|
if self.bus:
|
||||||
|
try:
|
||||||
|
self.bus.shutdown()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def twai(dut: Dut) -> TwaiTestHelper:
|
||||||
|
return TwaiTestHelper(dut)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def can_manager() -> CanBusManager:
|
||||||
|
return CanBusManager()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CORE TESTS
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_basic_operations(twai: TwaiTestHelper) -> None:
|
||||||
|
with twai.session(
|
||||||
|
mode='standard', tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO, start_dump=False
|
||||||
|
):
|
||||||
|
# Test basic send operation
|
||||||
|
assert twai.send('123#DEADBEEF'), 'Basic send operation failed'
|
||||||
|
|
||||||
|
# Test dump filter operations - first start should succeed
|
||||||
|
assert twai.dump_start(dump_filter='123:7FF'), 'First dump start failed'
|
||||||
|
|
||||||
|
# Second start should be handled gracefully (already running)
|
||||||
|
twai.dump_start(dump_filter='456:7FF') # Should handle "already running" case
|
||||||
|
|
||||||
|
# Stop should work normally
|
||||||
|
stopped_ok, warning = twai.dump_stop()
|
||||||
|
assert stopped_ok, 'Dump stop failed'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_bitrate_configuration(twai: TwaiTestHelper) -> None:
|
||||||
|
for bitrate in TestConfig.BITRATES:
|
||||||
|
with twai.session(
|
||||||
|
mode='standard', bitrate=bitrate, tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO
|
||||||
|
):
|
||||||
|
assert twai.info(), f'Info failed for bitrate {bitrate}'
|
||||||
|
|
||||||
|
# TWAI-FD bitrate validation (intentionally invalid: data bitrate < arbitration)
|
||||||
|
if twai.init(
|
||||||
|
tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO, bitrate=1_000_000, fd_bitrate=500_000
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
ok = twai.test_with_patterns(
|
||||||
|
f'twai_info {_ctrl(0)}',
|
||||||
|
[r'TWAI0 Status:', r'Bitrate: 1000000'],
|
||||||
|
)
|
||||||
|
assert ok, 'FD bitrate validation info failed'
|
||||||
|
finally:
|
||||||
|
twai.deinit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_frame_formats(twai: TwaiTestHelper) -> None:
|
||||||
|
with twai.session():
|
||||||
|
for frame_str, desc in TestConfig.BASIC_FRAMES:
|
||||||
|
can_id = int(frame_str.split('#')[0], 16)
|
||||||
|
assert twai.send_and_expect_in_dump(frame_str, can_id), f'Basic frame failed: {frame_str} ({desc})'
|
||||||
|
for frame_str, desc in TestConfig.EXTENDED_FRAMES:
|
||||||
|
can_id = int(frame_str.split('#')[0], 16)
|
||||||
|
assert twai.send_and_expect_in_dump(frame_str, can_id), f'Extended frame failed: {frame_str} ({desc})'
|
||||||
|
for frame_str, desc in TestConfig.RTR_FRAMES:
|
||||||
|
assert twai.send(frame_str), f'RTR frame failed: {frame_str} ({desc})'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_info_and_recovery(twai: TwaiTestHelper) -> None:
|
||||||
|
with twai.session():
|
||||||
|
assert twai.info(), 'Info command failed'
|
||||||
|
assert twai.expect_info_format(), 'Info format check failed'
|
||||||
|
|
||||||
|
assert twai.test_with_patterns(
|
||||||
|
f'twai_info {_ctrl(0)}',
|
||||||
|
[
|
||||||
|
r'TWAI0 Status: Running',
|
||||||
|
r'Node State: Error Active',
|
||||||
|
r'Error Counters: TX=0, RX=0',
|
||||||
|
],
|
||||||
|
), 'Expected status patterns not found'
|
||||||
|
|
||||||
|
assert twai.recover(), 'Recover status check failed'
|
||||||
|
assert twai.recover(timeout_ms=1000), 'Recover command with timeout failed'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_input_validation(twai: TwaiTestHelper) -> None:
|
||||||
|
with twai.session(start_dump=False):
|
||||||
|
for frame_str, desc in TestConfig.INVALID_FRAMES:
|
||||||
|
assert twai.invalid_should_fail(f'twai_send {_ctrl(0)} {frame_str}'), (
|
||||||
|
f'Invalid frame should be rejected: {frame_str} ({desc})'
|
||||||
|
)
|
||||||
|
|
||||||
|
invalid_commands = [
|
||||||
|
'twai_init', # Missing controller ID
|
||||||
|
f'twai_init {_ctrl(0)}', # Missing required GPIO
|
||||||
|
'twai_init twai99 -t 4 -r 5', # Invalid controller ID
|
||||||
|
f'twai_recover {_ctrl(0)} -t -5', # Invalid timeout value
|
||||||
|
f'twai_init {_ctrl(0)} -t -1 -r 5', # Negative TX GPIO
|
||||||
|
f'twai_init {_ctrl(0)} -t 99 -r 5', # High GPIO number
|
||||||
|
f'twai_init {_ctrl(0)} -t 4 -r 5 -c -1', # Negative clk_out GPIO
|
||||||
|
f'twai_init {_ctrl(0)} -t 4 -r 5 -b 0', # Zero bitrate
|
||||||
|
]
|
||||||
|
for cmd in invalid_commands:
|
||||||
|
assert twai.invalid_should_fail(cmd), f'Invalid command should fail: {cmd}'
|
||||||
|
|
||||||
|
uninitialized_ops = [f'twai_send {_ctrl(0)} 123#DEAD', f'twai_recover {_ctrl(0)}', f'twai_dump {_ctrl(0)}']
|
||||||
|
for cmd in uninitialized_ops:
|
||||||
|
assert twai.invalid_should_fail(cmd), f'Non-initialized operation should fail: {cmd}'
|
||||||
|
|
||||||
|
with twai.session(start_dump=False):
|
||||||
|
assert twai.invalid_should_fail(f'twai_init {_ctrl(0)} -t 4 -r 5'), (
|
||||||
|
'Duplicate initialization should be prevented'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_gpio_and_basic_send(twai: TwaiTestHelper) -> None:
|
||||||
|
with twai.session():
|
||||||
|
assert twai.send('123#DEADBEEF'), 'No-transceiver send failed'
|
||||||
|
|
||||||
|
with twai.session(mode='standard', tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO):
|
||||||
|
assert twai.info(), 'GPIO info failed'
|
||||||
|
assert twai.test_with_patterns(
|
||||||
|
f'twai_info {_ctrl(0)}',
|
||||||
|
[rf'GPIOs: TX=GPIO{TestConfig.DEFAULT_TX_GPIO}, RX=GPIO{TestConfig.DEFAULT_RX_GPIO}'],
|
||||||
|
)
|
||||||
|
for frame_str in TestConfig.BASIC_SEND_FRAMES:
|
||||||
|
assert twai.send(frame_str), f'Standard mode send failed: {frame_str}'
|
||||||
|
|
||||||
|
if twai.init(tx_gpio=4, rx_gpio=5, clk_out_gpio=6, bus_off_gpio=7):
|
||||||
|
try:
|
||||||
|
assert twai.info(), 'Optional GPIO info failed'
|
||||||
|
assert twai.test_with_patterns(f'twai_info {_ctrl(0)}', [r'TWAI0 Status:', r'GPIOs: TX=GPIO4']), (
|
||||||
|
'GPIO info format failed'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
twai.deinit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_send_various_frames(twai: TwaiTestHelper) -> None:
|
||||||
|
with twai.session():
|
||||||
|
for frame_str, desc in TestConfig.BOUNDARY_ID_FRAMES:
|
||||||
|
assert twai.send(frame_str), f'Boundary ID failed: {frame_str} ({desc})'
|
||||||
|
for frame_str in TestConfig.RAPID_FRAMES:
|
||||||
|
assert twai.send(frame_str), f'Rapid send failed: {frame_str}'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORT_FD == 1'), indirect=['target'])
|
||||||
|
def test_twai_fd_frames(twai: TwaiTestHelper) -> None:
|
||||||
|
with twai.session():
|
||||||
|
for frame_str, desc in TestConfig.FD_FRAMES:
|
||||||
|
assert twai.send(frame_str), f'FD frame failed: {frame_str} ({desc})'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_filtering(twai: TwaiTestHelper) -> None:
|
||||||
|
"""Test TWAI filtering including automatic extended frame detection."""
|
||||||
|
for filter_str, test_frames in TestConfig.FILTER_TESTS:
|
||||||
|
with twai.session(dump_filter=filter_str):
|
||||||
|
failed_cases: list[str] = []
|
||||||
|
for frame_str, expected_id, should_receive in test_frames:
|
||||||
|
received = twai.send_and_expect_in_dump(frame_str, expected_id, timeout=1.0)
|
||||||
|
if received != should_receive:
|
||||||
|
expected_action = 'receive' if should_receive else 'filter out'
|
||||||
|
actual_action = 'received' if received else 'filtered out'
|
||||||
|
failed_cases.append(f'{frame_str}: expected {expected_action}, got {actual_action}')
|
||||||
|
|
||||||
|
if failed_cases:
|
||||||
|
pytest.fail(
|
||||||
|
f'Filter test failed for filter "{filter_str or "no filter"}":\n'
|
||||||
|
+ '\n'.join(failed_cases)
|
||||||
|
+ '\n\nNote: Filters auto-detect extended frames by:'
|
||||||
|
'\n- String length > 3 chars or ID value > 0x7FF'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.generic
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_RANGE_FILTER_NUM > 0'), indirect=['target'])
|
||||||
|
def test_twai_range_filters(twai: TwaiTestHelper) -> None:
|
||||||
|
"""Test TWAI range filters (available on chips with range filter support)."""
|
||||||
|
for filter_str, test_frames in TestConfig.RANGE_FILTER_TESTS:
|
||||||
|
with twai.session(dump_filter=filter_str):
|
||||||
|
failed_cases: list[str] = []
|
||||||
|
for frame_str, expected_id, should_receive in test_frames:
|
||||||
|
received = twai.send_and_expect_in_dump(frame_str, expected_id, timeout=1.0)
|
||||||
|
if received != should_receive:
|
||||||
|
expected_action = 'receive' if should_receive else 'filter out'
|
||||||
|
actual_action = 'received' if received else 'filtered out'
|
||||||
|
failed_cases.append(f'{frame_str}: expected {expected_action}, got {actual_action}')
|
||||||
|
if failed_cases:
|
||||||
|
pytest.fail(f'Range filter failed for filter "{filter_str}":\n' + '\n'.join(failed_cases))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# EXTERNAL HARDWARE TESTS
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.twai_std
|
||||||
|
@pytest.mark.temp_skip_ci(targets=['esp32c5'], reason='no runner')
|
||||||
|
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
|
||||||
|
def test_twai_external_communication(twai: TwaiTestHelper, can_manager: CanBusManager) -> None:
|
||||||
|
"""
|
||||||
|
Test bidirectional communication with external CAN interface (hardware level).
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- ESP node connected to physical CAN transceiver, properly wired to PC's socketcan
|
||||||
|
interface (default can0) via CANH/CANL.
|
||||||
|
- PC has `python-can` and can0 is available.
|
||||||
|
- Bitrate matches TestConfig.DEFAULT_BITRATE (default 500 kbps).
|
||||||
|
"""
|
||||||
|
test_frames = [
|
||||||
|
('123#DEADBEEF', 0x123, bytes.fromhex('DEADBEEF'), False),
|
||||||
|
('7FF#AA55', 0x7FF, bytes.fromhex('AA55'), False),
|
||||||
|
('12345678#CAFEBABE', 0x12345678, bytes.fromhex('CAFEBABE'), True),
|
||||||
|
]
|
||||||
|
|
||||||
|
with can_manager.managed_bus(bitrate=TestConfig.DEFAULT_BITRATE) as can_bus:
|
||||||
|
with twai.session(
|
||||||
|
mode='standard',
|
||||||
|
tx_gpio=TestConfig.DEFAULT_TX_GPIO,
|
||||||
|
rx_gpio=TestConfig.DEFAULT_RX_GPIO,
|
||||||
|
bitrate=TestConfig.DEFAULT_BITRATE,
|
||||||
|
start_dump=False,
|
||||||
|
):
|
||||||
|
# --- ESP -> PC Connectivity Test ---
|
||||||
|
first_frame, test_id, test_data, test_extended = test_frames[0]
|
||||||
|
if not twai.send(first_frame):
|
||||||
|
pytest.skip(
|
||||||
|
f'ESP CAN send failed - check ESP GPIO '
|
||||||
|
f'{TestConfig.DEFAULT_TX_GPIO}/{TestConfig.DEFAULT_RX_GPIO} -> '
|
||||||
|
f'CAN transceiver connection'
|
||||||
|
)
|
||||||
|
|
||||||
|
deadline = time.time() + 3.0
|
||||||
|
got: can.Message | None = None
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
msg = can_bus.recv(timeout=0.2)
|
||||||
|
if msg and msg.arbitration_id == test_id:
|
||||||
|
got = msg
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f'PC CAN receive exception: {e}')
|
||||||
|
if got is None:
|
||||||
|
pytest.skip(
|
||||||
|
'ESP->PC communication failed - check CAN transceiver -> PC can0 connection. '
|
||||||
|
"Verify wiring and 'sudo ip link set can0 up type can bitrate 500000'"
|
||||||
|
)
|
||||||
|
if got is not None and bytes(got.data) != test_data:
|
||||||
|
pytest.fail(
|
||||||
|
f'ESP->PC data corruption detected: expected {test_data.hex()}, got {bytes(got.data).hex()}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Full ESP -> PC Test ---
|
||||||
|
for frame_str, expected_id, expected_data, is_extended in test_frames:
|
||||||
|
assert twai.send(frame_str), f'ESP->PC send failed: {frame_str}'
|
||||||
|
deadline = time.time() + 1.0
|
||||||
|
got = None
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
msg = can_bus.recv(timeout=0.1)
|
||||||
|
if msg and msg.arbitration_id == expected_id:
|
||||||
|
got = msg
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
assert got is not None, f'ESP->PC receive timeout for ID=0x{expected_id:X}'
|
||||||
|
assert bool(got.is_extended_id) == is_extended, (
|
||||||
|
f'ESP->PC extended flag mismatch for 0x{expected_id:X}: '
|
||||||
|
f'expected {is_extended}, got {got.is_extended_id}'
|
||||||
|
)
|
||||||
|
assert bytes(got.data) == expected_data, (
|
||||||
|
f'ESP->PC data mismatch for 0x{expected_id:X}: '
|
||||||
|
f'expected {expected_data.hex()}, got {bytes(got.data).hex()}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- PC -> ESP ---
|
||||||
|
assert twai.dump_start(), 'Failed to start twai_dump'
|
||||||
|
assert twai.info(), 'Failed to get twai_info'
|
||||||
|
|
||||||
|
test_msg = can.Message(arbitration_id=test_id, data=test_data, is_extended_id=test_extended)
|
||||||
|
try:
|
||||||
|
can_bus.send(test_msg)
|
||||||
|
time.sleep(0.2)
|
||||||
|
assert twai.expect(_id_pattern('twai0', test_id), timeout=2.0), (
|
||||||
|
f'PC->ESP frame not received: ID=0x{test_id:X}, data={test_data.hex()}'
|
||||||
|
)
|
||||||
|
for frame_str, expected_id, expected_data, is_extended in test_frames[1:]:
|
||||||
|
msg = can.Message(arbitration_id=expected_id, data=expected_data, is_extended_id=is_extended)
|
||||||
|
can_bus.send(msg)
|
||||||
|
time.sleep(0.1)
|
||||||
|
assert twai.expect(_id_pattern('twai0', expected_id), timeout=1.0), (
|
||||||
|
f'PC->ESP frame not received: ID=0x{expected_id:X}, data={expected_data.hex()}'
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
twai.dump_stop()
|
@@ -0,0 +1 @@
|
|||||||
|
CONFIG_EXAMPLE_ENABLE_TWAI_FD=y
|
Reference in New Issue
Block a user