Overview

I am trying to teach myself Rust, and I am a pretty visual/hands-on learner.
So that means I need a project to get stuck into vs demo/lab-based learning.
It is just how my brain works!

Since I am interested in reading stats from my car via CanBus.
I figured a good first project was a basic CanBus frame parser in Rust. So let us get into it!

Cargo.toml

One of my favorite things about Rust is how Cargo takes care of everything from dependencies, dev dependencies, cross-platform building... pretty much everything.
So let us take a look at my cargo.toml file for this project.

[package]
name = "canmatch"
version = "0.0.1"
edition = "2021"
authors = ["Brendan Horan <brendan@horan.hk>"]
description = "Demo to read CanBus data frames with frame ID matching support"

[dependencies]
tokio-socketcan = "0.3.1"
tokio = { version = "1", features = ["full"] }
futures-util = "0.3"
anyhow = "1.0"
procfs = "0.14.1"
clap = { version = "4.0.15", features = ["derive"] }

This cargo.toml is pretty basic but it pulls in all the dependencies we need to read from the CanBus using tokio to ensure our app runs in full async mode.
It also pulls in error handling via anyhow and CLI parsing via clap.

main.rs

Now let us take a look at the actual code.
To simulate Can frames I am using the CLI application cansend.
In this demo, I send a frame that contains 123#1122334455667788.

// Ignore some of clippy's warnings
#![allow(clippy::never_loop)]
#![allow(clippy::needless_return)]

use anyhow::Result;
use clap::Parser;
use futures_util::StreamExt;
use std::process;
use tokio_socketcan::CANSocket;


// Struct for the Can frame
#[derive(Debug, PartialEq)]
struct CanFrame {
    /// Can ID
    id: u32,
    /// Data section of the Can frame
    data: Vec<u8>,
}

// Struct for the CLI arguments
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct ClI {
    /// Can interface name. Eg; can0
    device: String,
    /// ID of the frame to match, Eg; 291
    frame_id: u32,
}

#[tokio::main]
async fn main() -> Result<(), &'static str> {
    // Parse the CLI arguments
    let cli = ClI::parse();

    // Use the given CLI arguments
    let device = cli.device;
    let frame_id = cli.frame_id;

    // Check to see if the interface is up
    check_interface_is_up(&device);

    // print Can frame only when we find a match on the frame ID
    let data = get_can_frame_with_id(&device, frame_id)
        .await
        .ok_or("No CanFrame found")?;
    println!("------------------------------------------------------------------");
    println!("Matched can ID: {:?}", data.id);
    println!("Can data is: {:?}", data.data);
    println!("------------------------------------------------------------------");

    Ok(())
}

fn check_interface_is_up(device: &str) {
    println!("Checking if {:?} interface is up...", device);
    // Get all interface dev stats from procfs, this should never fail
    // as proctfs should at least contain the loopback interface, thus use unwrap()
    let dev_stats = procfs::net::dev_status().unwrap();

    // Search the dev stats output for the device name given to the function
    if dev_stats.contains_key(device) {
        match dev_stats.get(device) {
            // If we match on the given device name, print the stats of the interface
            Some(interface_stats) => println!(
                "Interface stats for: {:?},
             {:?}",
                device, interface_stats
            ),
            // This arm should never match
            _ => println!("Interface stats not found"),
        }
    } else {
        // If we don't have a match on the given interface name, exit the program with a helpful message
        println!("Interface: {:?} not found or down.", device);
        println!("Can not continue, exiting");
        // Exit the application, return code 1
        process::exit(1);
    }
}

async fn get_can_frame_with_id(device: &str, can_id: u32) -> Option<CanFrame> {
    // Try open the can interface, this should be up as we checked via the check_interface_is_up()
    // function, thus use unwrap()
    let mut socket_rx = CANSocket::open(device).expect("Unable to open Can Interface");

    // Keep getting Can frames
    while let Some(Ok(frame)) = socket_rx.next().await {
        let _can_id = frame.id();
        let _can_data = frame.data();

        // insert can frame data into our struct
        let can_frame = CanFrame {
            id: _can_id,
            data: _can_data.to_vec(),
        };

        // match on the frame ID and return the struct on match
        if frame.id() == can_id {
            return Some(can_frame);
        }
    }

    return None;
}

Let us break this down...

First, we have two struct's CLI and CanFrame.
The CLI struct holds the command line arguments we give to the application.
The CanFrame struct holds the data of the Canframe that we find on the interface.

Now let us take a look at the functions.

The first function we use is check_interface_is_up(), this function takes one argument a string that holds the interface name.
We use the procfs crate to check the interface stats for the given interface. If the interface is not present or not up it won't have interface stats to display.
If the interface is not found the application will exit. process::exit(1);
As we can not continue if the interface is down or not found.

Next up we have get_can_frame_with_id() this function takes two arguments, a string that holds the interface name and u32 that holds the number of the frame ID we want to match on.
This function iterates over each Can frame that is presented via tokio_socketcan until it matches on the given ID that was passed as an argument to the function.

Example run.

I run cargo run vcan0 291 in another terminal.
The first CLI option is the network device to listen on, followed by the Can frame ID to search for.

In another terminal, I run the command cansend vcan0 123#1122334455667788

$ cargo run vcan0 291
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/canmatch vcan0 291`
Checking if "vcan0" interface is up...
Interface stats for: "vcan0",
             DeviceStatus { name: "vcan0", recv_bytes: 40, recv_packets: 5, recv_errs: 0, recv_drop: 0, recv_fifo: 0, recv_frame: 0, recv_compressed: 0, recv_multicast: 0, sent_bytes: 40, sent_packets: 5, sent_errs: 0, sent_drop: 0, sent_fifo: 0, sent_colls: 0, sent_carrier: 0, sent_compressed: 0 }
------------------------------------------------------------------
Matched can ID: 291
Can data is: [17, 34, 51, 68, 85, 102, 119, 136]
------------------------------------------------------------------
$