Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

pico-de-gallo-lib

pico-de-gallo-lib is the main Rust host library. It gives you a typed async client, PicoDeGallo, for every endpoint exposed by the firmware.

If you are writing a Rust application, this is usually the crate you want. gallo, the HAL crate, the FFI crate, and the Python bindings all build on top of it.

Connection Model

PicoDeGallo::new() and PicoDeGallo::new_with_serial_number() are synchronous constructors. They do not perform an async handshake up front; the client connects lazily in the background and operations fail only when you actually try to use the device.

That gives you a simple startup story:

  • create the client synchronously,
  • call async methods for real work,
  • optionally validate() once if you want a strict compatibility check.

Constructors and discovery

ItemWhat it does
PicoDeGallo::new()Targets the first matching board the host sees
PicoDeGallo::new_with_serial_number(serial)Targets one specific board by USB serial
list_devices()Returns DeviceDescription values for every attached board
wait_closed().awaitResolves when the underlying USB connection closes
use pico_de_gallo_lib::{PicoDeGallo, list_devices};

fn main() {
    for dev in list_devices() {
        println!(
            "serial={:?} manufacturer={:?} product={:?}",
            dev.serial_number,
            dev.manufacturer,
            dev.product,
        );
    }

    let _first = PicoDeGallo::new();
    let _named = PicoDeGallo::new_with_serial_number("E6633861A34B8C24");
}

Minimal Example

use pico_de_gallo_lib::PicoDeGallo;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gallo = PicoDeGallo::new();

    let echoed = gallo.ping(0x1234_5678).await?;
    println!("ping: 0x{echoed:08x}");

    let version = gallo.version().await?;
    println!(
        "firmware v{}.{}.{}",
        version.major,
        version.minor,
        version.patch,
    );

    Ok(())
}

Note

The library is async because USB I/O is async. The constructor is not. Put the client inside your async application and await the operations that actually hit the device.

Error Model

Most methods return Result<T, PicoDeGalloError<E>>.

That split is deliberate:

  • PicoDeGalloError::Comms(...) means the transport failed: disconnect, timeout, wire decode issue, closed connection, and similar host-side problems.
  • PicoDeGalloError::Endpoint(E) means the request made it to firmware and the endpoint itself reported an error.

The endpoint-specific E is one of the protocol error enums:

  • I2cError
  • SpiError
  • UartError
  • GpioError
  • PwmError
  • AdcError
  • OneWireError
  • plus I2cBatchError / SpiBatchError for batched operations

That means you can match exactly the layer you care about:

#![allow(unused)]
fn main() {
use pico_de_gallo_lib::{I2cError, PicoDeGallo, PicoDeGalloError};

async fn read_sensor(gallo: &PicoDeGallo) {
    match gallo.i2c_read(0x48, 2).await {
        Ok(bytes) => println!("got {bytes:?}"),
        Err(PicoDeGalloError::Endpoint(I2cError::NoAcknowledge)) => {
            eprintln!("device did not ACK");
        }
        Err(PicoDeGalloError::Comms(_)) => {
            eprintln!("USB or transport problem");
        }
        Err(err) => eprintln!("other error: {err}"),
    }
}
}

validate() and Schema Compatibility

validate().await is the strict compatibility gate.

It calls the device/info endpoint and checks the firmware’s schema version against the host library’s compiled-in schema version from pico-de-gallo-internal.

Pre-1.0, schema minor version must match. If host and firmware were built against different wire schemas, validate() fails instead of letting you debug mysterious decoding problems later.

validate() returns the DeviceInfo on success, so you can immediately inspect firmware version, hardware revision, and capability bits.

use pico_de_gallo_lib::PicoDeGallo;

#[tokio::main]
async fn main() {
    let gallo = PicoDeGallo::new();

    match gallo.validate().await {
        Ok(info) => println!(
            "fw {}.{}.{} schema {}.{}.{} hw={} capabilities={:?}",
            info.fw_major,
            info.fw_minor,
            info.fw_patch,
            info.schema_major,
            info.schema_minor,
            info.schema_patch,
            info.hw_version,
            info.capabilities,
        ),
        Err(err) => eprintln!("compatibility check failed: {err}"),
    }
}

The failure modes are explicit:

  • ValidateError::Comms — the host could not talk to the device,
  • ValidateError::LegacyFirmware — firmware is too old for device/info,
  • ValidateError::SchemaMismatch — host and firmware do not agree on the wire schema.

GPIO Topic Subscriptions

GPIO edge events are push-based topics, not request/response endpoints.

The flow is:

  1. open a host-side subscription with subscribe_gpio_events(depth).await,
  2. tell firmware which pin to monitor with gpio_subscribe(pin, edge).await,
  3. receive GpioEvent values from the returned MultiSubscription<GpioEvent>,
  4. call gpio_unsubscribe(pin).await when you are done.
use pico_de_gallo_lib::{GpioEdge, PicoDeGallo};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gallo = PicoDeGallo::new();
    let mut events = gallo.subscribe_gpio_events(16).await?;

    gallo.gpio_subscribe(0, GpioEdge::Any).await?;

    if let Ok(event) = events.recv().await {
        println!("pin {} -> {:?}", event.pin, event.edge);
    }

    gallo.gpio_unsubscribe(0).await?;
    Ok(())
}

Tip

Open the topic subscription before you start monitoring pins. That way the host already has a buffer waiting when the first edge arrives.

Endpoint Catalog

The library exposes one typed async method per firmware capability.

MethodArgumentsPurpose
pingidEcho a u32 back from firmware
i2c_readaddress, countRead bytes from an I2C target
i2c_writeaddress, contentsWrite bytes to an I2C target
i2c_write_readaddress, contents, countWrite, then read with a repeated start
i2c_scaninclude_reservedScan the I2C bus for responding addresses
i2c_batchaddress, opsExecute several I2C operations in one USB transfer
i2c_set_configfrequencySet the I2C clock frequency
i2c_get_configRead back the active I2C frequency
spi_readcountRead bytes from the SPI bus
spi_writecontentsWrite bytes to the SPI bus
spi_transfercontentsFull-duplex SPI transfer
spi_flushFlush pending SPI traffic
spi_batchcs_pin, opsExecute atomic multi-step SPI traffic under chip-select
spi_set_configspi_frequency, spi_phase, spi_polaritySet SPI timing and mode
spi_get_configRead back the active SPI configuration
uart_readcount, timeout_msRead up to count bytes with timeout
uart_writecontentsQueue bytes for UART transmit
uart_flushWait until UART TX has drained
uart_set_configbaud_rateSet UART baud rate
uart_get_configRead back the active UART configuration
gpio_getpinRead a GPIO level
gpio_putpin, stateDrive a GPIO high or low
gpio_wait_for_highpinWait until a pin reads high
gpio_wait_for_lowpinWait until a pin reads low
gpio_wait_for_rising_edgepinWait for a rising edge
gpio_wait_for_falling_edgepinWait for a falling edge
gpio_wait_for_any_edgepinWait for either edge
gpio_set_configpin, direction, pullSet GPIO direction and pull resistor
gpio_subscribepin, edgeAsk firmware to monitor a pin for edge events
gpio_unsubscribepinStop firmware-side monitoring
versionRead the firmware version
device_infoRead firmware version, schema version, HW revision, and capabilities
validatePerform a strict schema compatibility check and return DeviceInfo
pwm_set_duty_cyclechannel, dutySet a raw PWM duty-cycle value
pwm_get_duty_cyclechannelRead current and maximum PWM duty
pwm_enablechannelEnable the PWM slice behind a channel
pwm_disablechannelDisable the PWM slice behind a channel
pwm_set_configchannel, frequency_hz, phase_correctSet PWM frequency and phase-correct mode
pwm_get_configchannelRead the active PWM configuration
adc_readchannelRead one ADC sample
adc_get_configRead ADC capabilities and constants
onewire_resetReset the 1-Wire bus and detect presence
onewire_readlenRead raw 1-Wire bytes
onewire_writedataWrite raw 1-Wire bytes
onewire_write_pullupdata, pullup_duration_msWrite, then hold the line high for parasitic-power devices
onewire_searchStart ROM search and return the first device
onewire_search_nextContinue the current ROM search

For the full API surface, field docs, and current signatures, use the crate reference on docs.rs.