Skip to content
AI-driventedge-dot is developed largely by AI coding agents, and it is alpha software. Check what it does before you rely on it, and report what you find.

CAN bus and CANopen

tedge-dot has two connectors for CAN, and both use Linux SocketCAN:

  • canbus listens to frames on the bus and decodes the signals you name. The signals are defined in a DBC file. This connector is push-based: a signal is published when its frame arrives.
  • canopen polls a CANopen node’s object dictionary with expedited SDO transfers (up to 4 bytes), and writes to it the same way.

What you’ll build: both connectors running against the demo’s simulators on one virtual bus, vcan0. canbus-sim broadcasts ENGINE_STATUS frames at 10 Hz, and canopen-sim is a CANopen node with node ID 1. Their CAN IDs don’t collide, just as on real hardware.

  1. Start the CAN simulators. Both run with host networking and privileges, because a CAN interface belongs to the host’s network namespace. canbus-sim creates vcan0 if it does not exist yet:

    Terminal window
    docker compose -f /usr/share/tedge-dot/demo/docker-compose.yaml up -d canbus-sim canopen-sim

    On podman, create the interface yourself and start the containers directly:

    Terminal window
    sudo modprobe vcan
    sudo ip link add dev vcan0 type vcan
    sudo ip link set up vcan0
    sudo podman run -d --name canbus-sim --network host --privileged \
    --restart unless-stopped ghcr.io/thin-edge/tedge-dot/canbus-sim:latest
    sudo podman run -d --name canopen-sim --network host --privileged \
    --restart unless-stopped ghcr.io/thin-edge/tedge-dot/canopen-sim:latest
  2. Enable the demo configs. The package ships them, together with the demo DBC file at /usr/share/tedge-dot/demo/can/test.dbc:

    Terminal window
    sudo cp /usr/share/tedge-dot/demo/canbus.toml /usr/share/tedge-dot/demo/canopen.toml /etc/tedge/plugins/ot/

    The service picks up new files within a few seconds.

  3. Watch the measurements.

    Terminal window
    tedge mqtt sub 'te/+/+/+/+/m/+'

    You should see rpm and coolant_temp from the engine device, and analog_in and temperature from the CANopen device plc1.

A canbus device is a SocketCAN interface plus the DBC file that describes its frames:

demo/config/canbus.toml
[[device]]
name = "engine"
protocol_address = { interface = "vcan0", dbc_file = "/usr/share/tedge-dot/demo/can/test.dbc" }
default_mode = "typed"
points_from = ["demo-sim"]
View demo/config/canbus.toml on GitHub

A point names a DBC message and a signal. The DBC supplies the CAN ID, bit position, length and byte order, so a CAN point address is short:

demo/points.d/canbus/demo-sim.toml
# Point library for the demo CAN bus simulator (contract §3.4).
#
# The signals of a *device type*, with no connection information in it: the
# SocketCAN interface and the DBC file belong to the instance
# (`protocol_address`), these signals belong to every instance. Reference it
# from a device with:
#
# points_from = ["demo-sim"]
#
# Packages install this at /usr/share/tedge-dot/points.d/canbus/demo-sim.toml.
#
# The addresses name a message and a signal from the DBC the connector loads;
# bit offsets, lengths, scaling and endianness all come from that DBC, which is
# why a CAN point address is this short. See connectors/canbus/sim/test.dbc.
[library]
protocol = "canbus"
# The device *type* this list describes (§3.1): it names the parameter sets of every
# device that references this library, which is what keeps two device types on the
# same protocol from sharing a (tenant-wide) Cumulocity identifier.
type = "canbus-demo-sim"
description = "Demo CAN sender (canbus-sim in demo/docker-compose.yaml), signals from test.dbc"
version = "1"
[[point]]
id = "rpm"
name = "Engine speed"
description = "Crankshaft speed from the ENGINE_STATUS frame"
datatype = "uint16"
access = "read"
address = { message_name = "ENGINE_STATUS", signal_name = "RPM" }
[[point]]
id = "coolant_temp"
name = "Coolant temperature"
description = "Engine coolant temperature from the ENGINE_STATUS frame"
datatype = "int8"
access = "read"
address = { message_name = "ENGINE_STATUS", signal_name = "COOLANT_TEMP" }
# Writable: the connector sends the frame with this signal set.
[[point]]
id = "brake_active"
name = "Brake active"
description = "Brake pedal flag in the ENGINE_STATUS frame"
datatype = "bool"
access = "read_write"
address = { message_name = "ENGINE_STATUS", signal_name = "BRAKE_ACTIVE" }
View demo/points.d/canbus/demo-sim.toml on GitHub

Points to know:

  • Names are resolved when the config loads. If the DBC file doesn’t contain the named message or signal, the configuration is rejected.
  • The connector doesn’t apply DBC factor and offset. Scale values with the point’s transform, or in a flow.
  • The bit rate is set on the interface, not by the connector. For a real bus, bring the interface up first, for example with ip link set can0 type can bitrate 500000. bitrate in protocol_address is only logged.
  • Writes are read-modify-write. Writing a signal (access = "read_write", like brake_active) sends its frame with that signal changed. The connector takes the rest of the frame from the last copy it received, or from zeros if it hasn’t received one.
  • No heartbeat. A CAN signal can’t be read on demand, so report.max_interval has no effect. A signal is published only when its frame arrives. Because buses repeat frames periodically, report = { on_change = true } or a deadband usually saves the most.

The interface is shared by every node, so it goes in [connection]. Each node is a [[device]] with its node_id (1–127):

demo/config/canopen.toml
[connection]
interface = "vcan0"
View demo/config/canopen.toml on GitHub
demo/config/canopen.toml
[[device]]
name = "plc1"
protocol_address = { node_id = 1 }
default_mode = "typed"
points_from = ["demo-sim"]
View demo/config/canopen.toml on GitHub

A point addresses an object dictionary entry by index and subindex, as in the demo node’s point library:

demo/points.d/canopen/demo-sim.toml
# Point library for the demo CANopen simulator (contract §3.4).
#
# The object dictionary entries of a *device type*, with no connection
# information in it: the interface (`[connection]`) and the node id
# (`protocol_address`) belong to the instance, these objects belong to every
# node of this type. Reference it from a device with:
#
# points_from = ["demo-sim"]
#
# Packages install this at /usr/share/tedge-dot/points.d/canopen/demo-sim.toml.
#
# Addresses are object-dictionary index/subindex pairs, read and written with
# expedited SDO transfers.
[library]
protocol = "canopen"
# The device *type* this list describes (§3.1): it names the parameter sets of every
# device that references this library, which is what keeps two device types on the
# same protocol from sharing a (tenant-wide) Cumulocity identifier.
type = "canopen-demo-sim"
description = "Demo CANopen node (canopen-sim in demo/docker-compose.yaml)"
version = "1"
[[point]]
id = "analog_in"
name = "Analog input"
description = "Raw analog input word at object 0x2000"
datatype = "uint16"
access = "read"
address = { index = 0x2000, subindex = 0 }
[[point]]
id = "temperature"
name = "Temperature"
description = "Signed temperature word at object 0x2001"
datatype = "int16"
access = "read"
address = { index = 0x2001, subindex = 0 }
# Writable, so it appears as a Cumulocity device parameter (RFC 0003).
[[point]]
id = "digital_out"
name = "Digital outputs"
description = "Output byte at object 0x2002, one bit per channel"
datatype = "uint8"
access = "read_write"
address = { index = 0x2002, subindex = 0 }
View demo/points.d/canopen/demo-sim.toml on GitHub

For a real drive, the packaged default /etc/tedge/plugins/ot/canopen.toml has commented CiA 402 examples (velocity actual value 0x606C, controlword 0x6040). The connector polls every poll_interval. Writes use SDO download, and a writable point becomes a Cumulocity device parameter (see Writing to devices from the cloud).

Change vcan0 to your interface (for example can0), point dbc_file at your own DBC, and replace points_from = ["demo-sim"] with your points or with a point library for your device type. The packaged defaults in /etc/tedge/plugins/ot/ contain commented examples.