From 6730db4ed552cd8f92907bfa8d8b69e972737060 Mon Sep 17 00:00:00 2001 From: len0rd Date: Fri, 13 Dec 2024 17:20:31 -0500 Subject: [PATCH] add post about logging canopen traffic --- posts/canbus_dbc_format.rst | 2 +- posts/canopen_spying.rst | 157 ++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 posts/canopen_spying.rst diff --git a/posts/canbus_dbc_format.rst b/posts/canbus_dbc_format.rst index 366c1de..3304321 100644 --- a/posts/canbus_dbc_format.rst +++ b/posts/canbus_dbc_format.rst @@ -4,7 +4,7 @@ Defining CANBus messages with a DBC file ======================================== .. post:: 03, December 2024 - :tags: embedded, development, toolchain, + :tags: embedded, development, toolchain :category: Projects :author: len0rd diff --git a/posts/canopen_spying.rst b/posts/canopen_spying.rst new file mode 100644 index 0000000..1fefcb9 --- /dev/null +++ b/posts/canopen_spying.rst @@ -0,0 +1,157 @@ + +Spying on a CANOpen bus +======================= + +.. post:: 13, December 2024 + :tags: embedded, development + :category: Projects + :author: len0rd + +`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: + +.. code-block:: bash + + # 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): + +.. code-block:: python + :linenos: + + import can # using v4.4.2 at time of writing + from datetime import datetime + from pathlib import Path + + channel = "can0" + now_str = f"{datetime.now():%Y_%m_%d-%H_%M_%S%z}" + SCRIPT_ROOT = Path(__file__).parent.resolve() + file_desc = input("Enter description for filenames: ") + root_filename = f"canopen_{now_str}_{file_desc}" + + # create python canbus and log packets to a TRC logfile + canbus = can.ThreadSafeBus(interface="socketcan", channel=channel) + trc_logfile = SCRIPT_ROOT / "logs" / f"{root_filename}.trc" + trc_logger = can.TRCWriter(trc_logfile) + # canbus automatically logs packets to the shell and trc file + notifier = can.Notifier(canbus, [can.Printer(), trc_logger]) + + user_in = input("Press enter to stop capture... ") + print("Closing canbus/logs") + notifier.stop() + canbus.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: + +.. csv-table:: + :header: "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] --> [\x07\x01]" + + +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: + + +.. code-block:: ini + + + [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: https://github.com/len0rd/canopen-message-interpreter/tree/feature/len0rd/eds_sdo + +And here's an example of what the CSV would look like with the new columns added. + +.. csv-table:: + :header: "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] --> [\x00\x01]","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] --> [FUU\x00]","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: + +.. code-block:: python + :linenos: + + import can # using v4.4.2 at time of writing + from datetime import datetime + from canopen_msg_interpreter import interpret + from pathlib import Path + + channel = "can0" + now_str = f"{datetime.now():%Y_%m_%d-%H_%M_%S%z}" + SCRIPT_ROOT = Path(__file__).parent.resolve() + file_desc = input("Enter description for filenames: ") + root_filename = f"canopen_{now_str}_{file_desc}" + + # create python canbus and log packets to a TRC logfile + canbus = can.ThreadSafeBus(interface="socketcan", channel=channel) + trc_logfile = SCRIPT_ROOT / "logs" / f"{root_filename}.trc" + trc_logger = can.TRCWriter(trc_logfile) + # canbus automatically log packets to the shell and trc file + notifier = can.Notifier(canbus, [can.Printer(), trc_logger]) + + user_in = input("Press enter to stop capture... ") + print("Closing canbus/logs") + notifier.stop() + canbus.shutdown() + + # now create the CSV from the logfile + print("Run log through CANOpen analyzer...") + + interpret.analyze( + trc_logfile, SCRIPT_ROOT.parent / "can_database" / "my_eds_file.eds" + )