feat(can_open_node): Added canopen protocol for twai driver (IEC-500)#704
feat(can_open_node): Added canopen protocol for twai driver (IEC-500)#704wanckl wants to merge 1 commit intoespressif:masterfrom
Conversation
|
|
There was a problem hiding this comment.
Pull request overview
Adds an ESP-IDF component that integrates CANopenNode with the ESP32 TWAI driver and provides initial target/driver scaffolding.
Changes:
- Introduces ESP32 target definitions (
CO_driver_target.h) and a TWAI-backed CANopenNode driver (CO_driver_esp32.c) - Adds component metadata/build configuration (
idf_component.yml,CMakeLists.txt) and initial documentation/license - Adds CANopenNode as a git submodule
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| esp_canopennode/src/CO_driver_target.h | ESP32/CANopenNode target types, macros, and synchronization placeholders |
| esp_canopennode/src/CO_driver_esp32.c | TWAI receive callback + CANopenNode CAN module init/send APIs |
| esp_canopennode/idf_component.yml | Component registry metadata and ESP-IDF dependency constraint |
| esp_canopennode/README.md | Component overview and placeholder quick start |
| esp_canopennode/LICENSE | Apache-2.0 license file |
| esp_canopennode/CMakeLists.txt | Component build wiring + CANopenNode sources inclusion |
| esp_canopennode/CANopenNode | Adds CANopenNode submodule pointer |
| .gitmodules | Registers CANopenNode submodule |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
57b7b9a to
be95521
Compare
canopennode/src/CO_driver_esp32.c
Outdated
|
|
||
| // Try to transmit immediately. If failed, queue the frame in the CANopenNode | ||
| // software buffer for later retry by CO_CANmodule_process. | ||
| printf("CO Sending %lx [%d]\n", buffer->tx_frame.header.id, buffer->tx_frame.header.dlc); |
Check warning
Code scanning / clang-tidy
format specifies type 'unsigned long' but the argument has type 'uint32_t' (aka 'unsigned int')
…driver Supports: NMT_Heartbeat, Emergency, OD
be95521 to
42e4dc7
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a new canopennode ESP-IDF extra component which integrates the upstream CANopenNode stack (via submodule) with the ESP TWAI driver, and adds a minimal heartbeat example plus component metadata/CI wiring needed for publishing.
Changes:
- Added an ESP-IDF/TWAI-specific CANopenNode driver target header and ESP32 driver implementation.
- Added a heartbeat example project (generated OD files + app) and basic component scaffolding (LICENSE/README/Kconfig/SBOM/manifest).
- Registered the new component in repo automation/configuration (submodule + upload workflow + issue template component list).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
canopennode/src/CO_driver_target.h |
Adds ESP-IDF/TWAI target types/macros and locking primitives for CANopenNode. |
canopennode/src/CO_driver_esp32.c |
Implements CAN module init, RX callbacks, TX buffering/sending, and processing hooks on top of TWAI. |
canopennode/sbom_canopennode.yml |
Adds SBOM metadata for the upstream CANopenNode dependency. |
canopennode/idf_component.yml |
Adds component manifest (version/IDF dependency/SBOM). |
canopennode/examples/heartbeat/main/idf_component.yml |
Adds example manifest and dependency on the new component. |
canopennode/examples/heartbeat/main/canopen_heartbeat.h |
Adds (currently minimal) example header scaffold. |
canopennode/examples/heartbeat/main/canopen_heartbeat.c |
Adds heartbeat demo app using TWAI on-chip node + CANopenNode init/process loop. |
canopennode/examples/heartbeat/main/OD.xpd |
Adds CANopenEditor project export for the example OD. |
canopennode/examples/heartbeat/main/OD.h |
Adds generated Object Dictionary header for the example. |
canopennode/examples/heartbeat/main/OD.c |
Adds generated Object Dictionary implementation for the example. |
canopennode/examples/heartbeat/main/CMakeLists.txt |
Adds example “main” component build definition. |
canopennode/examples/heartbeat/README.md |
Adds short description of the heartbeat example. |
canopennode/examples/heartbeat/CMakeLists.txt |
Adds top-level example project build file. |
canopennode/README.md |
Adds top-level component README stub. |
canopennode/LICENSE |
Adds Apache-2.0 license file for the component. |
canopennode/Kconfig |
Adds Kconfig option for OD build mode selection. |
canopennode/CMakeLists.txt |
Adds component build definition linking CANopenNode sources + TWAI dependency. |
.gitmodules |
Adds CANopenNode upstream as a git submodule under canopennode/CANopenNode. |
.github/workflows/upload_component.yml |
Adds canopennode to the upload workflow component list. |
.github/ISSUE_TEMPLATE/bug-report.yml |
Adds canopennode to the bug report component dropdown. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ESP_LOGI(TAG, "CO Sending %lx [%d]", buffer->tx_frame.header.id, buffer->tx_frame.header.dlc); | ||
| _lock_acquire(&CANmodule->tx_mutex); | ||
| if (ESP_OK != twai_node_transmit((twai_node_handle_t)CANmodule->CANptr, &buffer->tx_frame, 0U)) { | ||
| if (!buffer->bufferFull) { | ||
| CANmodule->CANtxCount++; | ||
| } | ||
| buffer->bufferFull = true; | ||
| return CO_ERROR_TX_BUSY; | ||
| } |
There was a problem hiding this comment.
CO_CANsend acquires tx_mutex, but on transmit failure it returns CO_ERROR_TX_BUSY without releasing the lock. This will deadlock all subsequent sends/processing that tries to take tx_mutex. Ensure the mutex is released on all return paths (e.g., release before returning or use a single exit path).
| ESP_ERROR_CHECK(twai_new_node_onchip(&node_config, &node_hdl)); | ||
|
|
||
| CO = CO_new(NULL, NULL); | ||
| CO_CANinit(CO, node_hdl, 500); |
There was a problem hiding this comment.
The example configures the TWAI bitrate to 200000, but passes 500 to CO_CANinit. Since the driver ignores CANbitRate, this mismatch is confusing and can lead to incorrect expectations when copying the example. Make these values consistent (either configure TWAI to 500000 or pass 200 (or whatever unit CO_CANinit expects)).
| CO_CANinit(CO, node_hdl, 500); | |
| CO_CANinit(CO, node_hdl, 200); |
| version: "0.1.0" | ||
| description: CANopenNode protocol implementation for ESP-IDF | ||
| dependencies: | ||
| idf: ">=6.1.0" | ||
| sbom: | ||
| manifests: | ||
| - path: sbom_canopennode.yml | ||
| dest: CANopenNode |
There was a problem hiding this comment.
The component manifest is missing the url field (and also lacks repository/issues fields that other components in this repo typically provide). This breaks consistency with the repository’s component manifests and the PR checklist item requiring url. Add url (and consider adding repository/issues as done in other components).
| static __attribute__((__unused__)) portMUX_TYPE co_can_lock = portMUX_INITIALIZER_UNLOCKED; | ||
| #define CO_LOCK_CAN_SEND(CAN_MODULE) portENTER_CRITICAL(&co_can_lock) | ||
| #define CO_UNLOCK_CAN_SEND(CAN_MODULE) portEXIT_CRITICAL(&co_can_lock) | ||
| #define CO_LOCK_EMCY(CAN_MODULE) portENTER_CRITICAL(&co_can_lock) | ||
| #define CO_UNLOCK_EMCY(CAN_MODULE) portEXIT_CRITICAL(&co_can_lock) | ||
| #define CO_LOCK_OD(CAN_MODULE) portENTER_CRITICAL(&co_can_lock) | ||
| #define CO_UNLOCK_OD(CAN_MODULE) portEXIT_CRITICAL(&co_can_lock) |
There was a problem hiding this comment.
co_can_lock is defined as a static variable in a header. Any source file including this header will get its own separate lock instance, so CO_LOCK_* macros won’t actually synchronize across translation units (and can lead to race conditions between stack modules). Move the lock definition into a single C file and declare it extern in the header, or change the macros to lock a per-module lock inside CO_CANmodule_t.
| static __attribute__((__unused__)) portMUX_TYPE co_can_lock = portMUX_INITIALIZER_UNLOCKED; | |
| #define CO_LOCK_CAN_SEND(CAN_MODULE) portENTER_CRITICAL(&co_can_lock) | |
| #define CO_UNLOCK_CAN_SEND(CAN_MODULE) portEXIT_CRITICAL(&co_can_lock) | |
| #define CO_LOCK_EMCY(CAN_MODULE) portENTER_CRITICAL(&co_can_lock) | |
| #define CO_UNLOCK_EMCY(CAN_MODULE) portEXIT_CRITICAL(&co_can_lock) | |
| #define CO_LOCK_OD(CAN_MODULE) portENTER_CRITICAL(&co_can_lock) | |
| #define CO_UNLOCK_OD(CAN_MODULE) portEXIT_CRITICAL(&co_can_lock) | |
| #define CO_LOCK_CAN_SEND(CAN_MODULE) portENTER_CRITICAL(&((CAN_MODULE)->state_lock)) | |
| #define CO_UNLOCK_CAN_SEND(CAN_MODULE) portEXIT_CRITICAL(&((CAN_MODULE)->state_lock)) | |
| #define CO_LOCK_EMCY(CAN_MODULE) portENTER_CRITICAL(&((CAN_MODULE)->state_lock)) | |
| #define CO_UNLOCK_EMCY(CAN_MODULE) portEXIT_CRITICAL(&((CAN_MODULE)->state_lock)) | |
| #define CO_LOCK_OD(CAN_MODULE) portENTER_CRITICAL(&((CAN_MODULE)->state_lock)) | |
| #define CO_UNLOCK_OD(CAN_MODULE) portEXIT_CRITICAL(&((CAN_MODULE)->state_lock)) |
| #include <stdbool.h> | ||
| #include <stddef.h> | ||
| #include <stdint.h> | ||
| #include "freertos/FreeRTOS.h" | ||
| #include "freertos/portmacro.h" | ||
| #include "esp_twai.h" | ||
|
|
There was a problem hiding this comment.
CO_CANmodule_t uses _lock_t, but this header doesn’t include <sys/lock.h> (where _lock_t is typically defined). This can cause build failures depending on include order. Include the defining header explicitly to make the type available.
| buffer->CANrx_callback = CANrx_callback; | ||
| buffer->ident = (ident & TWAI_STD_ID_MASK) | (rtr ? CO_ID_FLAG_RTR : 0U); | ||
| buffer->mask = (mask & TWAI_STD_ID_MASK) | CO_ID_FLAG_RTR; | ||
| return ESP_OK; |
There was a problem hiding this comment.
CO_CANrxBufferInit returns ESP_OK, but the function’s return type is CO_ReturnError_t. Even if the numeric value matches today, it mixes error domains and is brittle. Return the appropriate CANopenNode status (e.g., CO_ERROR_NO) instead.
| return ESP_OK; | |
| return CO_ERROR_NO; |
| ## IDF Component Manager Manifest File | ||
| dependencies: | ||
| idf: | ||
| version: ">=5.5.4" |
There was a problem hiding this comment.
The example’s idf_component.yml declares idf: ">=5.5.4", but the canopennode component manifest requires idf: ">=6.1.0". This will allow users to try building the example on unsupported IDF versions. Align the example’s minimum IDF version with the component’s requirement.
| version: ">=5.5.4" | |
| version: ">=6.1.0" |
| bool "Use custom OD.c/h (object directory) files" | ||
| default false | ||
| help | ||
| No help | ||
|
|
There was a problem hiding this comment.
Kconfig prompt says "object directory" (should be "object dictionary" in CANopen terminology) and the help text is a placeholder ("No help"). Update the prompt/help so users understand what enabling CO_MULTIPLE_OD changes and how OD.c/h are expected to be provided.
| bool "Use custom OD.c/h (object directory) files" | |
| default false | |
| help | |
| No help | |
| bool "Use custom OD.c/h (object dictionary) files" | |
| default false | |
| help | |
| Enable support for using application-specific CANopen object | |
| dictionary files (OD.c/OD.h) instead of a single default OD. | |
| When this option is enabled, the application or build system | |
| must provide the appropriate OD.c and OD.h files for each | |
| configuration that is built. |
| @@ -0,0 +1,5 @@ | |||
| idf_component_register( | |||
| SRCS "canopen_heartbeat.c" "OD.c" | |||
| REQUIRES | |||
There was a problem hiding this comment.
idf_component_register has an empty REQUIRES section. Depending on how the argument parsing works, this can be confusing and may be mis-parsed. Remove the empty keyword or specify the actual required components for this example.
| REQUIRES |
| CO_ReturnError_t CO_CANsend(CO_CANmodule_t *CANmodule, CO_CANtx_t *buffer) | ||
| { | ||
| ESP_RETURN_ON_FALSE(buffer, CO_ERROR_ILLEGAL_ARGUMENT, TAG, "CO_CANsend invalid args"); | ||
|
|
||
| // Try to transmit immediately. If failed, queue the frame in the CANopenNode | ||
| // software buffer for later retry by CO_CANmodule_process. | ||
| ESP_LOGI(TAG, "CO Sending %lx [%d]", buffer->tx_frame.header.id, buffer->tx_frame.header.dlc); | ||
| _lock_acquire(&CANmodule->tx_mutex); | ||
| if (ESP_OK != twai_node_transmit((twai_node_handle_t)CANmodule->CANptr, &buffer->tx_frame, 0U)) { |
There was a problem hiding this comment.
CO_CANsend dereferences CANmodule (tx_mutex/CANptr/CANtxCount) but only validates buffer. If CANmodule is NULL (or CANmodule->CANptr is NULL), this will crash. Validate CANmodule (and CANptr) in the argument check the same way other APIs in this file do.
Checklist
urlfield definedChange description
Please describe your change here