esp-hosted 0.1.7

Support for the ESP-Hosted firmware, with an STM32 host.
Documentation

Crate Docs

ESP Hosted

For connecting to an ESP-Hosted-MCU from a Host MCU with firmware written in rust.

Compatible with ESP-Hosted-MCU 2.0.6 and ESP IDF 5.4.1 (And likely anything newer), and any host MCU and architecture. For details on ESP-HOSTED-MCU's protocol see this document. For a list of Wi-Fi commands and dat structures available, reference the ESP32 IDF API Reference, Wi-Fi section. For BLE commands, reference the HCI docs.

This library includes two approaches: A high-level API using data structures from this library, and full access to the native protobuf structures (Wi-Fi) and HCI interface (BLE). The native API is easier to work with, but only implements a portion of functionality. The protobuf API is complete, but more cumbersome.

This library does not use an allocator. This makes integrating it simple, but it uses a significant amount of flash for static buffers. These are configured in the build_proto/src/main.rs script on a field-by-field basis.

It's transport agnostic; compatible with SPI, SDIO, and UART. It does this by allowing the application firmware to pass a generic write function, and reads are performed as functions that act on buffers passed by the firmware.

Example use:

use esp_hosted::{self, wifi};

fn init(buf: &mut [u8], uart: &mut Uart) {
    // Write could also be SPI, dma etc.
    let mut write = |buf: &[u8]| {
        uart.write(buf).map_err(|e| {
            println!("Uart write error: {:?}", e);
            EspError::Comms
        })
    };

    let heartbeat_cfg = RpcReqConfigHeartbeat {
        enable: true,
        duration: 10,
    };

    esp_hosted::cfg_heartbeat(buf, &mut write, 0, &heartbeat_cfg)?;
    
    // Configure Wi-Fi settings as-required, using functions in the `esp_hosted::wifi` module.
    wifi::start(buf, &mut write, 1).is_err()?;
}

In your UART, SPI etc reception handling (e.g. an interrupt handler), you can parse incoming messages from the Esp. The parse_msg function returns a MsgParsed enum of two varieties. One for Wi-Fi, the other for HCI (Bluetooth).

The Wi-Fi message containing the following:

  • The payload header. This contains generic data, and you may not need to use it.
  • A struct from this library containing the RPC header. This determines if a request, response, or event, and the Rpc ID being used.
  • The rpc payload, as a &[u8]
  • A struct generated by the micropb library, which contains the full RPC data, in a raw, but complete format.

The HCI (BLE) message contains a plain byte array of the payload received. We may change this later to parse it. For now, it's bring-your-own-HCI tools.

The auto-generated structs are rough: They don't include documentation, use numerical values directly vice enums, and use i32 for many integer types that are u8 in practice, and as defined by ESP-IDF.

This example demonstrates how to read messages sent by the ESP asynchronously.

#[interrupt]
fn USART2() {
    // Configure your I/O hardware here to start and stop transfers etc.
    // ...
    let msg = esp_hosted::parse_msg(buf)?;

    println!("\nHeader: {:?}", msg.header);
    println!("RPC: {:?}", msg.rpc);
    println!("Data buf: {:?}", msg.data_buf);
    
    match msg {
        MsgParsed::Wifi(wifi_msg) => {
            match wifi_msg.msg_id {
                // Example using native parsing and direct payload.
                RpcId::EventHeartbeat => {
                    println!("Heartbeat data: {:?}", rpc.data);
                }
                _ => ()
            }

            // For access to the full set of responses, parsed from the .proto file:
            if let Some(pl) = &wifi_msg.rpc_parsed.payload {
                match pl {
                    Rpc_::Payload::EventHeartbeat(hb) => {
                        println!("Heartbeat data: {:?}", hb.hb_num);
                    }
                    _ => (), // etc
                }
            }
        }
        MsgParsed::Hci(hci) => {}
    }
}

To perform specific actions, there are functions like wifi::get_protocol, wifi::start, wifi::get_mode etc. There take a write fn and uid as parameters, and others on a per-message basis. These are set up using structs that are part of this library.

To access the full functionality supported by ESP-Hosted, create a RpcP struct, then pass it, and a write fn to the write_rpc_proto. Constructing these RpcP structs is done IOC the micropb lib. Here's an example, using the same heartbeat config as above. This is more verbose than our high-level API, but is more flexible:

    use esp_hosted::{RpcP, RpcTypeP, RpcIdP, Rpc_};

    fn init(buf: &mut [u8], uart: &mut Uart) {
        // let write = ... (Same as above)
        
        let mut hb_msg = RpcP::default();
        hb_msg.uid = 0;
        hb_msg.msg_type = RpcTypeP::Req;
        hb_msg.msg_id = RpcIdP::ReqConfigHeartbeat;

        let mut hb_cfg = Rpc_Req_ConfigHeartbeat::default();
        hb_cfg.enable = true;
        hb_cfg.duration = 10;

        hb_msg.payload = Some(Rpc_::Payload::ReqConfigHeartbeat(hb_cfg));

        esp_hosted::write_rpc_proto(buf, &mut write, hb_msg)?;
    }

Example Wi-Fi initialization

You will run steps like this prior to using Wi-Fi functionality in many cases:

wifi::init(&mut buf, &mut write, 0, &wifi::InitConfig::default())?;

wifi::set_mode(&mut buf, &mut write, 0, WifiMode::Ap)?;

wifi::start(&mut buf, &mut write, 0)?;

Building the proto file

This is not required if installing from crates.io; only applicable if working with the source directly.

The module proto.rs is not included directly in the source code; it's built from esp_hosted_rpc.proto using Micropb

To build:

  • 1: Install the protoc application, and place its on your system's path.
  • 2: Run the build_proto sub application with cargo run from its directory. This will place esp_hosed_proto.rs in this program's src folder.