107-Arduino-Cyphal: Bringing Cyphal to Arduino

Hello everyone :coffee: :wave:

I’d like to announce the availability of 107-Arduino-Cyphal via Arduino Library Manager.

This library is the successor of 107-Arduino-UAVCAN.

It’s purpose is to provide a libcanard based OpenCyphal implementation for various Arduino platforms and was made necessary due to the renaming of the project. Furthermore the occasion was used to facelift the library, i.e. update to the latest versions of libcanard/o1heap as well as various other minor improvements (see changelog).

Cheers, Alex

4 Likes

Bravo! Thank you @aentinger for this exciting contribution.

1 Like

Hi everyone :coffee: :wave:

Just a short heads-up that adding a convenient register API is underway (#158) for 107-Arduino-Cyphal. Here a short sneak-preview:

CC @generationmake

2 Likes

Hi
I have already tested it and it seems to work for read and write operations. So best to configure your nodes.
examples will follow!
Bernhard

2 Likes

pretty cool! Cant wait to test it

1 Like

With the release of 107-Arduino-Cyphal:v2.2.0 a convenient C++ API for dealing with the OpenCyphal Register feature has been added to the Arduino library.

Here’s how to use 107-Arduino-Cyphal’s register API:

First instantiate a couple of registers (plus a RegisterList object) which you’d like to use:

RegisterNatural8  reg_rw_uavcan_node_id          ("uavcan.node.id",           Register::Access::ReadWrite, Register::Persistent::No, OPEN_CYPHAL_NODE_ID, [&node_hdl](uint8_t const & val) { node_hdl.setNodeId(val); });
RegisterString    reg_ro_uavcan_node_description ("uavcan.node.description",  Register::Access::ReadWrite, Register::Persistent::No, "OpenCyphal-ToF-Distance-Sensor-Node");
RegisterNatural16 reg_ro_uavcan_pub_distance_id  ("uavcan.pub.distance.id",   Register::Access::ReadWrite, Register::Persistent::No, OPEN_CYPHAL_ID_DISTANCE_DATA, [&pub_distance_id](uint16_t const & val) { pub_distance_id = val; });
RegisterString    reg_ro_uavcan_pub_distance_type("uavcan.pub.distance.type", Register::Access::ReadOnly,  Register::Persistent::No, "uavcan.primitive.scalar.Real32.1.0");
RegisterList      reg_list;

Secondly add those registers to the register list and subscribe to the relevant topics:

reg_list.add(reg_rw_uavcan_node_id);
reg_list.add(reg_ro_uavcan_node_description);
reg_list.add(reg_ro_uavcan_pub_distance_id);
reg_list.add(reg_ro_uavcan_pub_distance_type);
reg_list.subscribe(node_hdl);

Now you can access those registers using i.e. yakut. A short video of a couple of register operations is shown below:

A fully implemented example for the register API can be found here.

Enjoy!

If there are any questions, don’t hesitate to fire off an :email: or ask an question within this forum.

Cheers, Alex

PS: If you are using 107-Arduino-MCP2515 to get your OpenCyphal CAN frames on the wire please updates to 107-Arduino-MCP2515:v1.3.5 as this version contains numerous OpenCyphal/CAN related bug fixes.

3 Likes

Great contribution Alexander! Thanks for keeping us in Arduino.

2 Likes

Breaking News: 107-Arduino-Cyphal v3.0.0 w/ modern API and full Cyphal compliance has just been released!

This release represents a significant overhaul of the entire library. As a result, it is possible to create a fully Cyphal-compliant application by leveraging a powerful, compact, and concise API. These are the main characteristics:

  • ROS/ROS2 style API with type erasure for creating Publisher's, Subscriptions's, ServiceServer's and ServiceClient's.

  • Run-time configurable parameters, such as

  • The library is free of Microcontroller/Arduino specific idioms, the library therefore compiles for any architecture (incl. your PC):

  • nunavut pre-generated C++ headers.

  • Support for both CAN and CAN FD PHY layers.

  • Implementation of the Register API supporting both RO and RW registers, as well as array and scalar types.

  • Implementation of the NodeInfo API for providing generic node information for i.e. yakut.

  • Provision of a uavcan/node/port/List.1.0 publisher which periodically publishes information on publisher and subscribed topics, as well as service servers and clients.

  • Self-explaining examples for Publisher, Subscriptions, ServiceServer and ServiceClient.

  • A number of full Cyphal compliant reference implementations.

  • There are also external support libraries that provide Cyphal-specific key functionality:

    • 107-Arduino-UniqueId provides a 16-Byte unique id over various Arduino platforms. This is needed for the NodeInfo API.
    • 107-Arduino-CriticalSection provides RAII-style critical sections for preventing race conditions when calling library APIs within both interrupts and normal execution context.

Let’s take a closer look at each of these features:

Declaration/Usage of a Cyphal-Node

The basic API entry point is a Node class instance, which must be provided with heap as well as a function pointer to a CAN transmit function:

Node::Heap<Node::DEFAULT_O1HEAP_SIZE> node_heap;
Node node_hdl(node_heap.data(), node_heap.size(), micros, [] (CanardFrame const & frame) { return mcp2515.transmit(frame); });

Processing happens inside spinSome which needs to be called regularly:

void loop()
{
  /* Process all pending OpenCyphal actions. */
  {
    CriticalSection crit_sec;
    node_hdl.spinSome();
  }
  /* ... */

Received CAN frames are passed to the application via onCanFrameReceived:

void onReceiveBufferFull(CanardFrame const & frame)
{
  node_hdl.onCanFrameReceived(frame);
}

Publisher

A Publisher can be created using the create_publisher API:

/* Create a publisher with a fixed port id: */
const auto heartbeat_pub = node_hdl.create_publisher<uavcan::node::Heartbeat_1_0>
  (1*1000*1000UL /* = transmit timeout / usec */);

/* Create a publisher with a custom port id: */
CanardPortID const ANGLE_ID = 1001U;
const auto angle_pub = node_hdl.create_publisher<uavcan::si::unit::angle::Scalar_1_0>
  (ANGLE_ID, 1*1000*1000UL /* = transmit timeout / usec */);

which can then be used to publish messages:

uavcan::node::Heartbeat_1_0 msg;

msg.uptime = now / 1000;
msg.health.value = uavcan::node::Health_1_0::NOMINAL;
msg.mode.value = uavcan::node::Mode_1_0::OPERATIONAL;
msg.vendor_specific_status_code = 0;

heartbeat_pub->publish(msg);

or

uavcan::si::unit::angle::Scalar_1_0 angle_scalar;
angle_scalar.radian = (b_angle_deg - b_angle_offset_deg) * M_PI / 180.0f;
angle_pub->publish(angle_scalar);

Subscription

A Subscription can be created using the create_subscription API:

/* Create a subscription with a fixed port id: */
const auto heartbeat_subscription = node_hdl.create_subscription<Heartbeat_1_0>(onHeartbeat_1_0_Received);

/* Create a subscription with a custom port id: */
static CanardPortID const BIT_PORT_ID   = 1620U;
const auto bit_subscription = node_hdl.create_subscription<Bit_1_0>(BIT_PORT_ID, onBit_1_0_Received);

Upon reception of such a transfer the callback provided during creation of the Subscription object is invoked:

void onBit_1_0_Received(Bit_1_0 const & msg)
{
  if(msg.value)
    digitalWrite(LED_BUILTIN, HIGH);
  else
    digitalWrite(LED_BUILTIN, LOW);
}

Service Server

A ServiceServer can be created using the create_service_server API:

const auto ServiceServer execute_command_srv = node_hdl.create_service_server<ExecuteCommand::Request_1_1, ExecuteCommand::Response_1_1>(
  2*1000*1000UL,
  onExecuteCommand_1_1_Request_Received);

Upon reception of a service clients request the callback provided during creation of the ServiceServer object is invoked:

ExecuteCommand::Response_1_1 onExecuteCommand_1_1_Request_Received(ExecuteCommand::Request_1_1 const & req)
{
  ExecuteCommand::Response_1_1 rsp;

  if (req.command == 0xCAFE)
    rsp.status = ExecuteCommand::Response_1_1::STATUS_SUCCESS;
  else
    rsp.status = ExecuteCommand::Response_1_1::STATUS_NOT_AUTHORIZED;

  return rsp;
}

Service Client

A ServiceClient can be created using the create_service_client API:

const auto srv_client = node_hdl.create_service_client<ExecuteCommand::Request_1_1, ExecuteCommand::Response_1_1>(
  2*1000*1000UL,
  onExecuteCommand_1_1_Response_Received);

Send an asynchronous request:

/* Request some coffee. */
std::string const cmd_param("I want a double espresso with cream!");
ExecuteCommand::Request_1_1 req;
req.command = 0xCAFE;
std::copy_n(cmd_param.begin(),
            std::min(cmd_param.length(), req.parameter.capacity()),
            req.parameter.begin());

if (!srv_client->request(27 /* remote node id */, req))
  Serial.println("Coffee request failed.");

Upon reception of a service servers response the callback provided during creation of the ServiceClient object is invoked:

void onExecuteCommand_1_1_Response_Received(ExecuteCommand::Response_1_1 const & rsp)
{
  if (rsp.status == ExecuteCommand::Response_1_1::STATUS_SUCCESS)
    Serial.println("Coffee successfully retrieved");
  else
    Serial.println("Error when retrieving coffee");
}

Register API

The Register API enables node configuration:

/* R/W register value. */
CanardNodeID node_id = node_hdl.getNodeId();
CanardPortID counter_port_id = DEFAULT_COUNTER_PORT_ID;
uint16_t counter_update_period_ms = DEFAULT_COUNTER_UPDATE_PERIOD_ms;

/* Creating a registry. */
const auto node_registry = node_hdl.create_registry();

/* Creating RO and RW registers. */
const auto reg_ro_node_description             = node_registry->route ("cyphal.node.description", {true}, []() { return "basic-cyphal-node"; });
const auto reg_ro_pub_counter_type             = node_registry->route ("cyphal.pub.counter.type", {true}, []() { return "uavcan.primitive.scalar.Integer8.1.0"; });
const auto reg_rw_node_id                      = node_registry->expose("cyphal.node.id", {}, node_id);
const auto reg_rw_pub_counter_id               = node_registry->expose("cyphal.pub.counter.id", {}, counter_port_id);
const auto reg_rw_pub_counter_update_period_ms = node_registry->expose("cyphal.pub.counter.update_period_ms", {}, counter_update_period_ms);

NodeInfo API

The NodeInfo API provides relevant node information:

static auto node_info = node_hdl.create_node_info
(
  /* uavcan.node.Version.1.0 protocol_version */
  1, 0,
  /* uavcan.node.Version.1.0 hardware_version */
  1, 0,
  /* uavcan.node.Version.1.0 software_version */
  0, 1,
  /* saturated uint64 software_vcs_revision_id */
#ifdef CYPHAL_NODE_INFO_GIT_VERSION
  CYPHAL_NODE_INFO_GIT_VERSION,
#else
  0,
#endif
  /* saturated uint8[16] unique_id */
  OpenCyphalUniqueId(),
  /* saturated uint8[<=50] name */
  "107-systems.l3xz-leg-ctrl"
);

Note: This release contains many breaking changes. If you are a corporation needing help to upgrade your 107-Arduino-Cyphal based application feel free to reach out to me (e-mail).

CC @pavel.kirienko @scottdixon @generationmake

4 Likes

Outstanding. Hoping to see Cyphal/UDP support one day :wink:

1 Like

(clears weekend plans to play with Arduino!)

1 Like

@generationmake has a board to get you started :wink: : OpenCyphalPicoBase :bowing_man: .

1 Like

I like that this board has a reserved pin for a “radiation detector” :wink:

Anyone want to do a group buy on this board?

:laughing: That’s a leftover from the origins of this board, which started out as an auxiliary controller for the L3X-Z hexapod robot. One requirement of our selected scenario at ELROB 2023 was to be able to detect radioactive sources. Hence I purchased a simple (yet expensive) radiation sensor which provides a counts/minute (CPM) signal that was captured by that pin =) Clearly the documentation needs to be cleaned up a bit @generationmake :wink: .

For reference, here’s already a thread dedicated to the OpenCyphalPicoBase board: https://forum.opencyphal.org/t/opencyphalpicobase-a-node-with-raspberry-pi-pico-and-mcp2515 .

Hi everyone :coffee: :wave:

I’d hereby like to announce the release of

107-Arduino-Cyphal-Support

a Arduino library which provides convenient plumbing to build a fully functional Cyphal node in combination with 107-Arduino-Cyphal.

Here’s a list of its current features:

  • API for obtaining a unique 64-bit ID.
auto /* std::array<uint8_t, 16> */ const UNIQUE_ID = cyphal::support::UniqueId::instance().value();
  • API for persistent register storage and retrieval.
/* Declaration of key/value storage. */
cyphal::support::platform::storage::littlefs::KeyValueStorage kv_storage(filesystem);

/* Load persistently stored registers from a non-volatile memory (EEPROM, flash, etc.). */
if (auto const opt_err = cyphal::support::load(kv_storage, *node_registry); opt_err.has_value())
{
  Serial.print("load failed with error code ");
  Serial.println(static_cast<int>(opt_err.value()));
}

/* Store persistent registers to a non-volatile memory (EEPROM, flash, etc.). */
if (auto const opt_err = cyphal::support::save(kv_storage, *node_registry); opt_err.has_value())
{
  Serial.print("save failed with error code ");
  Serial.println(static_cast<int>(opt_err.value()));
}
  • API for performing synchronous and asynchronous resets.
/* Synchronous reset: */
cyphal::support::platform::reset_sync(std::chrono::milliseconds(5000));
/* Asynchronous reset: */
cyphal::support::platform::reset_async(std::chrono::milliseconds(5000));

Video-Demo of node configuration via yakut utilizing persistent register storage, unique ID and reset API

Resources

The following hardware and firmware were used for producing the demo:

What can be seen on that video?

Using yakut the node id is changed multiple times using yakut. After changing the node id the node is restarted (again via yakut) and can be observed coming back online with a the freshly configured node-id. Note that the node’s unique ID does never change (its a per MCU unique ID).

  • After startup the initial node id in the video is 20.
  • The node id is changed from 20 to 42, the changes are written persistent storage and the node is restarted.
  • The node comes up again with a node ID of 42.
  • The node id is changed from 42 to 1, the changes are written persistent storage and the node is restarted.
  • The node comes up again with a node ID of 1.

CC @generationmake FYI @pavel.kirienko @scottdixon

4 Likes

This is really cool to see! Thanks!

1 Like

Hi everyone :coffee: :wave:

I want to take the opportunity and announce the release of 107-Arduino-Cyphal:v3.3.0.

The most notable difference to the previous release is that now the new Arduino Core for Renesas (ArduinoCore-renesas) is also supported. Simply put, this means that 107-Arduino-Cyphal can now be used with

and custom designs employing those chips.

This has been possible because ArduinoCore-renesas supports C++17 and is only the second of all (official and 3rd party) Arduino Cores to do so (107-Arduino-Cyphal requires C++17 since that’s a requirement for using the nunavut generated C++ headers with @pavel.kirienko register API code - otherwise C++14 would suffice).

Thanks to @generationmake for pushing me to extend library support for the new platform :bowing_man: .

Note: You will also need to update to

4 Likes

It was a pleasure! :wink:

1 Like

Good Morning :coffee: :wave:

107-Arduino-Cyphal:v3.4.0 has just been released.

It contains a breaking change insofar that all elements except the pre-generated DSDL are now encapsulated within the cyphal namespace. This prevents naming conflicts in larger software projects (let’s face it: Publisher, Subscription, ServiceServer, etc. are pretty generic names).

For examples on how to upgrade from the previous version of this library you can take a look at any of those PRs:

2 Likes

Rejoice everyone :coffee: :wave: for 107-Arduino-Cyphal:v3.5.0 has just been released.

Most changes are improvement in documentation and updating GitHub CI actions. There’s also fixing a couple of imperfections leading to a compilation error on Clang (#259) and both libcanard (#248) and libo1heap (#250) are now integrated via git subtree (sadly no submodules in Arduino libraries) as well as allowing to provide a CRC parameter to the NodeInfo service (#258).

Enjoy :wink:

1 Like