Spying on a CANOpen bus#

CANOpen is a protocol stack that sits on top of a normal CANBus. Recently I found myself needing to monitor the traffic between 2 devices on a CANOpen network. Monitoring a CANbus by itself is easy to do with a little Python. Where I ran into difficulty was interpreting the raw CANbus frames into CANOpen messages. Here’s how I solved that.

Hardware Setup#

First, I had to insert a CAN<->USB adaptor into the network as a silent listener. I’ve used the expensive IXXAT USB-to-CAN adaptor and would not recommend it. It doesnt provide enough value for the price. CANable (and its cheap knockoffs) have worked just fine for my needs. Any adaptor that supports Linux socketcan will work in this demonstration

Once the adapter was installed on the CAN network and properly terminated, I needed to configure it on Linux to connect to the bus in listen-only mode, allowing me to be a silent observer. For instance:

# note: listen-only disables TX
sudo ip link set can0 type can bitrate 1000000 listen-only on
sudo ip link set can0 up

Software Setup#

As mentioned, logging raw CAN frames is easy with the python-can package. For instance (using the can0 network created in the previous bash block):

 1import can # using v4.4.2 at time of writing
 2from datetime import datetime
 3from pathlib import Path
 4
 5channel = "can0"
 6now_str = f"{datetime.now():%Y_%m_%d-%H_%M_%S%z}"
 7SCRIPT_ROOT = Path(__file__).parent.resolve()
 8file_desc = input("Enter description for filenames: ")
 9root_filename = f"canopen_{now_str}_{file_desc}"
10
11# create python canbus and log packets to a TRC logfile
12canbus = can.ThreadSafeBus(interface="socketcan", channel=channel)
13trc_logfile = SCRIPT_ROOT / "logs" / f"{root_filename}.trc"
14trc_logger = can.TRCWriter(trc_logfile)
15# canbus automatically logs packets to the shell and trc file
16notifier = can.Notifier(canbus, [can.Printer(), trc_logger])
17
18user_in = input("Press enter to stop capture... ")
19print("Closing canbus/logs")
20notifier.stop()
21canbus.shutdown()

Now I have a file with raw CAN frames. Interpreting those to CANopen messages would be pretty tedious. I had a hard time finding any tools that could do this for me until I eventually stumbled on the tiny canopen-message-interpreter python project. This takes a CANbus logfile, parses valid CANopen messages from it, and saves the results in a CSV file:

Message Number

Time [ms]

ID

DLC

Data Bytes

CANopen

Node

Index

Subindex

Interpretation

0

0.000

0x0080

0

[]

EMCY

SYNC

1

0.486

0x0181

8

[0x19 0xc4 0xfc 0xff 0xd6 0xf4 0xff 0xff]

PDO1_T

1

Transmit PDO1

2

143.334

0x0581

8

[0x4b 0x40 0x60 0x00 0x07 0x01 0x00 0x00]

SDO_T

1

0x6040

0

server: upload response = [0x07 0x01] –> [x07x01]

This is a great start. I can now see an interpretation of the CANOpen packets being sent on the network. As expected, there’s a series of SDO, PDO, NMT, SYNC, etc messages getting passed around.

But I want to take it a step further. CANopen has a spec for an “Electronic Data Sheet” or “EDS” file. EDS can be used to define the objects that can be passed around a CANopen network. I believe CAN-In-Automation, the maintainers of the CANOpen spec, require vendors to create one of these files to be CANOpen certified. A number of tools exist to create/modify these files and generate code from them. Under the hood an EDS file is just a big .ini file. For instance, here’s a portion of EDS file I was using:

[1000]
ParameterName=Device type
ObjectType=0x7
;StorageLocation=PERSIST_COMM
DataType=0x0007
AccessType=ro
DefaultValue=0x00000000
PDOMapping=0

[1001]
ParameterName=Error register
ObjectType=0x7
;StorageLocation=RAM
DataType=0x0005
AccessType=ro
DefaultValue=0x00
PDOMapping=1

Given the simple format, it was easy to extend canopen-message-interpreter to support reading in an EDS, then filling in information about SDO’s: their name and an interpretation of the value they were reading/writing.

Here’s the resulting code: len0rd/canopen-message-interpreter

And here’s an example of what the CSV would look like with the new columns added.

Message Number

Time [ms]

ID

DLC

Data Bytes

CANopen

Node

Index

Subindex

Interpretation

SDO Name

SDO Value (hex)

SDO Value (int)

26

3856.139

0x0601

8

[0x2b 0x40 0x60 0x00 0x00 0x01 0x00 0x00]

SDO_R

1

0x6040

0

client: download request = [0x00 0x01] –> [x00x01]

ControlWord

0x100

256

60

3899.052

0x0601

8

[0x23 0x81 0x60 0x00 0x46 0x55 0x55 0x00]

SDO_R

1

0x6081

0

client: download request = [0x46 0x55 0x55 0x00] –> [FUUx00]

Profile Velocity

0x555546

5592390

Much more helpful. This made parsing logs much easier. I also updated the package so a CSV analysis could be run right after a log was captured. Continuing the python code from earlier:

 1import can # using v4.4.2 at time of writing
 2from datetime import datetime
 3from canopen_msg_interpreter import interpret
 4from pathlib import Path
 5
 6channel = "can0"
 7now_str = f"{datetime.now():%Y_%m_%d-%H_%M_%S%z}"
 8SCRIPT_ROOT = Path(__file__).parent.resolve()
 9file_desc = input("Enter description for filenames: ")
10root_filename = f"canopen_{now_str}_{file_desc}"
11
12# create python canbus and log packets to a TRC logfile
13canbus = can.ThreadSafeBus(interface="socketcan", channel=channel)
14trc_logfile = SCRIPT_ROOT / "logs" / f"{root_filename}.trc"
15trc_logger = can.TRCWriter(trc_logfile)
16# canbus automatically log packets to the shell and trc file
17notifier = can.Notifier(canbus, [can.Printer(), trc_logger])
18
19user_in = input("Press enter to stop capture... ")
20print("Closing canbus/logs")
21notifier.stop()
22canbus.shutdown()
23
24# now create the CSV from the logfile
25print("Run log through CANOpen analyzer...")
26
27interpret.analyze(
28    trc_logfile, SCRIPT_ROOT.parent / "can_database" / "my_eds_file.eds"
29)