Skip to content

feat(can_open_node): Added canopen protocol for twai driver (IEC-500)#704

Open
wanckl wants to merge 1 commit intoespressif:masterfrom
wanckl:feat/twai_canopennode_support
Open

feat(can_open_node): Added canopen protocol for twai driver (IEC-500)#704
wanckl wants to merge 1 commit intoespressif:masterfrom
wanckl:feat/twai_canopennode_support

Conversation

@wanckl
Copy link
Copy Markdown
Contributor

@wanckl wanckl commented Mar 17, 2026

Checklist

  • Component contains License
  • Component contains README.md
  • Component contains idf_component.yml file with url field defined
  • Component was added to upload job
  • Component was added to build job
  • Optional: Component contains unit tests
  • CI passing

Change description

Please describe your change here

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions github-actions bot changed the title feat(can_open_node): Added canopen protocol for twai driver feat(can_open_node): Added canopen protocol for twai driver (IEC-500) Mar 17, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@wanckl wanckl force-pushed the feat/twai_canopennode_support branch 3 times, most recently from 57b7b9a to be95521 Compare April 2, 2026 09:19
@wanckl
Copy link
Copy Markdown
Contributor Author

wanckl commented Apr 2, 2026

@CLAassistant ?


// 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')

format specifies type 'unsigned long' but the argument has type 'uint32_t' (aka 'unsigned int')
…driver

Supports: NMT_Heartbeat, Emergency, OD
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +168 to +176
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;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
ESP_ERROR_CHECK(twai_new_node_onchip(&node_config, &node_hdl));

CO = CO_new(NULL, NULL);
CO_CANinit(CO, node_hdl, 500);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
CO_CANinit(CO, node_hdl, 500);
CO_CANinit(CO, node_hdl, 200);

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +8
version: "0.1.0"
description: CANopenNode protocol implementation for ESP-IDF
dependencies:
idf: ">=6.1.0"
sbom:
manifests:
- path: sbom_canopennode.yml
dest: CANopenNode
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +87
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)
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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))

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +17
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "esp_twai.h"

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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;
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return ESP_OK;
return CO_ERROR_NO;

Copilot uses AI. Check for mistakes.
## IDF Component Manager Manifest File
dependencies:
idf:
version: ">=5.5.4"
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
version: ">=5.5.4"
version: ">=6.1.0"

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +8
bool "Use custom OD.c/h (object directory) files"
default false
help
No help

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "canopen_heartbeat.c" "OD.c"
REQUIRES
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
REQUIRES

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +170
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)) {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants