Building CAN tools using Rust

I use CAN and CANopen quite a bit at work and I find myself wanting some functionality I can’t quite get out of rs-canopen-* or cansniffer / candump.

For example:

So basically I want a tool that can take an EDS file + Node ID and decode PDOs and SDO transfers, displaying in the terminal in a table format (that among other things).

Rust has some pretty nice tui libraries, so I decided to build my own CAN utilities.

Rust and CAN

First it is necessary to get CAN data using Rust. There is a socketcan crate, though it is a little out of date. I’ve forked this repo for now to apply some updates.

Since I’ll be writing code to handle CANopen data, I wanted to build the interface against the embedded-hal CAN traits. This would allow and device that can receive CAN data to handle CANopen.

Porting the socketcan crate to use embedded-hal was pretty simple, it’s just a matter of implementing the Frame trait and using some different data types.

I have a pull request for this here:

https://github.com/socketcan-rs/socketcan-rs/pull/24

Hoping to get this merged are some point. For now I’m using my own crate socketcan-hal.

tokio-socketcan

Given that this application will be primarily IO bound (waiting on CAN frames), I’ve decided to make it an async application. For this I’m using the tokio-socketcan library, specifically a fork that uses socketcan-hal.

No pull request for this yet, but I’d like to get that up soon.

Branch: https://github.com/nnarain/tokio-socketcan/tree/socketcan-embedded-hal

A simple example for read CAN frames:

use futures_util::StreamExt;
use tokio;
use tokio_socketcan::CANSocket;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut socket_rx = CANSocket::open("vcan0").unwrap();

    println!("Reading on vcan0");

    while let Some(next) = socket_rx.next().await {
        println!("{:#?}", next);
    }

    Ok(())
}

Reading CANopen data

CANopen is a suite of protocols for communicating data over CAN. The CANopen standard partitions the CAN ID for each of the different protocols. Each device on the bus has a node id use to identify that devices specific frames.

For example a PDO is data that streams to or from a CANopen device. TPDO1 is a frame that stream from the device and has a CAN ID of 180 + NODE-ID.

I’ve represented CANopen data on the bus using the following struct:

/// Parsed CANopen data
#[derive(Debug, Clone, Copy)]
pub enum CanOpenFrame {
    Sync,
    Heartbeat(NmtState),
    Pdo(Pdo, FrameData),
    Sdo(Sdo, FrameData),
}

A simple function to parse frame into CANopen frame + NODE ID:

pub fn parse<F: Frame>(frame: F) -> Result<(Option<NodeId>, CanOpenFrame), Error> {
    let id = utils::id_to_raw(&frame.id());

    let channel = id & !(0x7F);
    let node_id = NodeId((id & 0x7F) as u8);

    match channel {
        0x080 => Ok((None, CanOpenFrame::Sync)),
        0x700 => Ok((Some(node_id), CanOpenFrame::Heartbeat(NmtState::try_from(frame.data()[0])?))),
        0x180 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Tx1, FrameData::new(frame.data(), frame.dlc())))),
        0x200 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Rx1, FrameData::new(frame.data(), frame.dlc())))),
        0x280 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Tx2, FrameData::new(frame.data(), frame.dlc())))),
        0x300 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Rx2, FrameData::new(frame.data(), frame.dlc())))),
        0x380 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Tx3, FrameData::new(frame.data(), frame.dlc())))),
        0x400 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Rx3, FrameData::new(frame.data(), frame.dlc())))),
        0x480 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Tx4, FrameData::new(frame.data(), frame.dlc())))),
        0x500 => Ok((Some(node_id), CanOpenFrame::Pdo(Pdo::Rx4, FrameData::new(frame.data(), frame.dlc())))),
        0x580 => Ok((Some(node_id), CanOpenFrame::Sdo(Sdo::Tx, FrameData::new(frame.data(), frame.dlc())))),
        0x600 => Ok((Some(node_id), CanOpenFrame::Sdo(Sdo::Rx, FrameData::new(frame.data(), frame.dlc())))),
        _ => Err(Error::InvalidChannel(channel)),
    }
}

This function identifies the CANopen protocol and copies the data into a new frame.

EDS file parsing

I created a separate package for parsing CANopen EDS files. This file is just an INI file that describes the CANopen object dictionary.

Not going to go into detail on the parsing, but I think overall I’ve learned a bit about Rust error handling and how to structure Error types. I found using the thiserror particularly useful (if not necessary) for building clean error interfaces.

CANopen is a flat data model. All data is simply a object in the dictionary that is referenced using a index and a subindex (its COB-ID). The EDS parser reads each variable in the dictionary and stores it in a HashMap keyed by the COB-ID

/// EDS file representation
pub struct Eds {
    /// Objects in the dictionary
    objects: HashMap<CobId, Object>,
    /// Metadata objects such as Arrays and Records.
    metadata: HashMap<CobId, Object>,
}

After the dictionary is loaded, specific CANopen objects can be read to determine PDO mapping (among other things).

PDO Mapping

CANopen uses standard data type such as uint8, int8, uint16, etc. The smallest datatype being 8 bits in length. This means a PDO can at most have 8 mapped objects.

Mapped PDO in the EDS might look like:

[1A00sub1]
ParameterName=PDO 1 Mapping for a process data variable 1
ObjectType=0x7
DataType=0x0007
AccessType=rw
DefaultValue=0x60010120
PDOMapping=0

The value 0x60010120 breaks down to:

I used the following to represent mapped PDO data:

/// A mapped PDO item, with its data length
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MappedPdo(CobId, u8);

/// PDO Mapping
#[derive(Debug)]
pub struct PdoMapping {
    /// A maximum of 8 objects can be mapped into a single PDO
    pub slots: [Option<MappedPdo>; 8],
}

Getting the PDO mapping involves reading a number of variables from either an EDS Array or Record and then interpreting the Mapped PDO value.

fn get_pdo_mapping(&self, cobid: CobId) -> Option<PdoMapping> {
    // Get TPDO mapping as array
    self.get_array(&cobid)
        // Get each mapped PDO item variable
        .map(|tpdos| tpdos.items )
        .or_else(|| self.get_record(&cobid).map(|r| r.items.values().cloned().collect()))
        // Get the value of each variable
        // In the form:
        //   IIII SSLL
        // where:
        //   I - Index
        //   S - subindex
        //   L - data length
        .map(|vars| vars.iter().filter_map(|var| var.default_value.to_unsigned_int())
        // Collect as vector of integers
        .collect::<Vec<usize>>())
        // Map the integers into MappedPdo structs
        .map(|values| {
            values.iter()
                    .map(|value| {
                        let index = ((value & 0xFFFF_0000) >> 16) as u16;
                        let subindex = ((value & 0x0000_FF00) >> 8) as u8;
                        let bit_len = (value & 0x0000_00FF) as u8;

                        // println!("{:04X}.{:02X} ({})", index, subindex, data_len);

                        MappedPdo(CobId(index, subindex), bit_len / 8)
                    })
                    .collect::<Vec<MappedPdo>>()
        })
        .map(PdoMapping::from)
}

Getting the mapping for each PDO:

pub fn get_tpdo1_mapping(&self) -> Option<PdoMapping> {
    self.get_pdo_mapping(CobId(0x1A00, 0x00))
}

pub fn get_tpdo2_mapping(&self) -> Option<PdoMapping> {
    self.get_pdo_mapping(CobId(0x1A01, 0x00))
}

pub fn get_tpdo3_mapping(&self) -> Option<PdoMapping> {
    self.get_pdo_mapping(CobId(0x1A02, 0x00))
}

pub fn get_tpdo4_mapping(&self) -> Option<PdoMapping> {
    self.get_pdo_mapping(CobId(0x1A03, 0x00))
}

PDO Decoder

Using the PDO mapping a PDO decoder can be created to interpret bytes in the CAN frame.

The PdoDecoder will decode up to 8 objects from the PDO.

/// PDO Decoder
pub struct PdoDecoder {
    pub mapping: [Option<(MappedPdo, DataType)>; 8],
}

impl PdoDecoder {
    pub fn decode(&self, data: &[u8]) -> [Option<(CobId, ValueType)>; 8] {
        let mut values: [Option<(CobId, ValueType)>; 8] = Default::default();

        let mut offset: usize = 0;

        for (item, pdo) in values.iter_mut().zip(self.mapping.iter().filter(|item| item.is_some())) {
            if let Some((pdo, data_type)) = pdo {
                let start = offset;
                let end = start + pdo.1 as usize;

                offset = end;

                if let Some(value_type) = value_type_from_bytes(&data[start..end], data_type.clone()) {
                    *item = Some((pdo.0, value_type));
                }
            }
        }

        values
    }
}

fn value_type_from_bytes(src: &[u8], data_type: DataType) -> Option<ValueType> {
    match (data_type, src.len()) {
        (DataType::Bool, 1) => Some(ValueType::Bool(src[0] != 0)),
        (DataType::U8, 1) => Some(ValueType::U8(src[0])),
        (DataType::U16, 2) => Some(ValueType::U16(u16::from_le_bytes(src.try_into().unwrap()))),
        (DataType::U32, 4) => Some(ValueType::U32(u32::from_le_bytes(src.try_into().unwrap()))),
        (DataType::I8, 1) => Some(ValueType::I8(src[0] as i8)),
        (DataType::I16, 2) => Some(ValueType::I16(i16::from_le_bytes(src.try_into().unwrap()))),
        (DataType::I32, 4) => Some(ValueType::I32(i32::from_le_bytes(src.try_into().unwrap()))),
        _ => None,
    }
}

TUI

I’m using the crate tui for the terminal interface. This is to create a simple layout to display the CANopen data in a table.

The ui function renders a table with the object COB-ID, parameter name, value and data type.

fn ui<B: Backend>(f: &mut UiFrame<B>, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints(
            [
                Constraint::Percentage(100),
            ]
            .as_ref(),
        )
        .split(f.size());

    // let format_mode = app.format_mode;

    let headers_cells = ["COB-ID", "Parameter Name", "Value", "Data Type"]
        .iter().map(|h| Cell::from(*h).style(Style::default().fg(Color::Green)));
    let header = Row::new(headers_cells);

    let rows = app.objects.iter().map(|(cobid, value)| {
        // let (index, subindex) = cobid.clone().into_parts();

        let parameter_name = app.name_lookup.get(cobid).map(|s| s.clone()).unwrap_or(String::from("unknown"));

        let type_str = match value {
            ValueType::Bool(_) => "bool",
            ValueType::U8(_) => "uint8",
            ValueType::I8(_) => "int8",
            ValueType::U16(_) => "uint16",
            ValueType::I16(_) => "int16",
            ValueType::U32(_) => "uint32",
            ValueType::I32(_) => "int32",
            ValueType::F32(_) => "float32",
            ValueType::OString(_) => "Octet String",
            ValueType::VString(_) => "V String",
        };

        let cell0 = Cell::from(format!("{}", cobid));
        let cell1 = Cell::from(parameter_name);
        let cell2 = Cell::from(format!("{}", value));
        let cell3 = Cell::from(type_str);

        Row::new([cell0, cell1, cell2, cell3])
    });

    let title = format!("{:?} on {}", app.node_id, app.device_name);

    let t = Table::new(rows)
        .header(header)
        .block(Block::default().borders(Borders::ALL).title(title))
        .widths(&[
            Constraint::Percentage(25),
            Constraint::Percentage(25),
            Constraint::Percentage(25),
            Constraint::Percentage(25)
        ]);

    f.render_widget(t, chunks[0]);

}

Output

This is the output from the default EDS file I have for a motor controller:

image not found!