Search code examples
rustserial-portprotocolsuart

Implementing a solar charger protocol - how to deal with register definitions


I would like to write a library that implements the serial (uart) communication protocol for a well known brand of solar chargers. Luckily the protocol specification is publicly available so I don't have to do any reverse engineering. The protocol is very easy: Each ascii encoded frame consists of a magic start, a register address, a command code (get or set), the payload (in case of 'get' this is omitted, otherwise the value to write) and a checksum.

My question is now: How should I deal with the dozens of register definitions? Just hardcode those as an array of structs like this:

pub struct Request {
    pub address: u16,
    pub command: u8,
    pub payload: Option<u16>,
    pub checksum: u8,
    pub description: &'static str
}

pub const REGS: &'static [Request] = &[
    Request {address: 0xabcd, command: 8, payload: Some(0x8000), checksum: 0xab, description: "Current battery voltage" },
    Request {address: 0xef23, command: 8, payload: Some(0x8080), checksum: 0xab, description: "Current (mA)"},
];

This way I could match on the address part of an incoming frame. Or maybe get those from a separate .json file via serde? But this would have the drawback that my library might be difficult to use on an embedded device (no_std) that doesn't have a file system. Any other ideas?


Solution

  • Lets take a look how the hyperium http crate defines some of its common HTTP headers, of which there are about 30-50 of.

    // Generate constants for all standard HTTP headers. This includes a static hash
    // code for the "fast hash" path. The hash code for static headers *do not* have
    // to match the text representation of those headers. This is because header
    // strings are always converted to the static values (when they match) before
    // being hashed. This means that it is impossible to compare the static hash
    // code of CONTENT_LENGTH with "content-length".
    standard_headers! {
        /* [...] */
        (Accept, ACCEPT, b"accept");
    
        /* [...] */
        (AcceptCharset, ACCEPT_CHARSET, b"accept-charset");
        /* [...] */
    }
    

    It uses a macro-rule to parse some predefined syntax, let's look at the standard-headers macro.

    macro_rules! standard_headers {
        (
            $(
                $(#[$docs:meta])*
                ($konst:ident, $upcase:ident, $name_bytes:literal);
            )+
        ) => {
            /* [...] */
    
            $(
                $(#[$docs])*
                pub const $upcase: HeaderName = HeaderName {
                    inner: Repr::Standard(StandardHeader::$konst),
                };
            )+
    
            /* [...] */
        }
    }
    

    Maybe we can use the existing code and rewrite it to generate the request constants.

    macro_rules! define_requests {
        (
            $(
                $(#[$docs:meta])*
                ($request_name:ident, $address:literal, $description:literal);
            )+
        ) => {
            $(
                $(#[$docs])*
                pub const $request_name: Request = Request {
                    address: $address,
                    command: 8, 
                    payload: Some(0x8000), 
                    checksum: 0xab, 
                    description: $description
                };
            )+
        }
    }
    

    Now we can define our constant request types using the macro.

    define_requests! {
        (GET_BATTERY_VOLTAGE, 0xabcd, "Current battery voltage");
        (GET_MILI_AMP, 0xef23, "Current (mA)");
    }
    

    Check out the code. To see the generated code, select from the upper left three dots the "HIR" representation and then press the button left to it.