pub mod decode;
use nusb::DeviceInfo as NusbDeviceInfo;
use pico_de_gallo_internal::{
AdcGetConfiguration, AdcRead, AdcReadRequest, GetDeviceInfo, GpioEventTopic, GpioGet, GpioGetRequest, GpioPut,
GpioPutRequest, GpioSetConfiguration, GpioSetConfigurationRequest, GpioSubscribe, GpioSubscribeRequest,
GpioUnsubscribe, GpioUnsubscribeRequest, GpioWaitForAny, GpioWaitForFalling, GpioWaitForHigh, GpioWaitForLow,
GpioWaitForRising, GpioWaitRequest, I2cBatch, I2cBatchRequest, I2cGetConfiguration, I2cRead, I2cReadRequest,
I2cScan, I2cScanRequest, I2cSetConfiguration, I2cSetConfigurationRequest, I2cWrite, I2cWriteRead,
I2cWriteReadRequest, I2cWriteRequest, MICROSOFT_VID, OneWireRead, OneWireReadRequest, OneWireReset, OneWireSearch,
OneWireSearchNext, OneWireWrite, OneWireWritePullup, OneWireWritePullupRequest, OneWireWriteRequest,
PICO_DE_GALLO_PID, PwmDisable, PwmDisableRequest, PwmEnable, PwmEnableRequest, PwmGetConfiguration,
PwmGetConfigurationRequest, PwmGetDutyCycle, PwmGetDutyCycleRequest, PwmSetConfiguration,
PwmSetConfigurationRequest, PwmSetDutyCycle, PwmSetDutyCycleRequest, SCHEMA_VERSION_MINOR, SpiBatch,
SpiBatchRequest, SpiFlush, SpiGetConfiguration, SpiRead, SpiReadRequest, SpiSetConfiguration,
SpiSetConfigurationRequest, SpiTransfer, SpiTransferRequest, SpiWrite, SpiWriteRequest, UartFlush,
UartGetConfiguration, UartRead, UartReadRequest, UartSetConfiguration, UartSetConfigurationRequest, UartWrite,
UartWriteRequest, Version,
};
pub use pico_de_gallo_internal::{
AdcChannel, AdcConfigurationInfo, Capabilities, DeviceInfo, GpioDirection, GpioEdge, GpioEvent, GpioPull,
GpioState, I2cBatchOp, I2cFrequency, PwmConfigurationInfo, PwmDutyCycleInfo, SpiBatchOp, SpiConfigurationInfo,
SpiPhase, SpiPolarity, UartConfigurationInfo, VersionInfo,
};
pub use pico_de_gallo_internal::{
AdcError, GpioError, I2cBatchError, I2cError, OneWireError, PwmError, SpiBatchError, SpiError, UartError,
};
pub use pico_de_gallo_internal::{
encode_i2c_batch_ops, encode_spi_batch_ops, i2c_batch_response_len, spi_batch_response_len,
};
pub use postcard_rpc::host_client::{IoClosed, MultiSubscription};
use postcard_rpc::{
header::VarSeqKind,
host_client::{HostClient, HostErr},
standard_icd::{ERROR_PATH, PingEndpoint, WireError},
};
use std::convert::Infallible;
#[derive(Debug, Clone)]
pub struct DeviceDescription {
pub serial_number: Option<String>,
pub manufacturer: Option<String>,
pub product: Option<String>,
}
pub fn list_devices() -> Vec<DeviceDescription> {
let devices = match nusb::list_devices() {
Ok(iter) => iter,
Err(_) => return Vec::new(),
};
devices
.filter(|dev| dev.vendor_id() == MICROSOFT_VID && dev.product_id() == PICO_DE_GALLO_PID)
.map(|dev| DeviceDescription {
serial_number: dev.serial_number().map(String::from),
manufacturer: dev.manufacturer_string().map(String::from),
product: dev.product_string().map(String::from),
})
.collect()
}
#[derive(Debug)]
pub enum PicoDeGalloError<E> {
Comms(HostErr<WireError>),
Endpoint(E),
}
impl<E: core::fmt::Display> core::fmt::Display for PicoDeGalloError<E> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Comms(e) => write!(f, "communication error: {e:?}"),
Self::Endpoint(e) => write!(f, "endpoint error: {e}"),
}
}
}
impl<E: core::fmt::Debug + core::fmt::Display> std::error::Error for PicoDeGalloError<E> {}
impl<E> From<HostErr<WireError>> for PicoDeGalloError<E> {
fn from(value: HostErr<WireError>) -> Self {
Self::Comms(value)
}
}
#[derive(Debug)]
pub enum ValidateError {
Comms(HostErr<WireError>),
LegacyFirmware,
SchemaMismatch {
expected_minor: u16,
actual_minor: u16,
},
}
impl core::fmt::Display for ValidateError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Comms(e) => write!(f, "communication error: {e:?}"),
Self::LegacyFirmware => write!(
f,
"firmware does not support the device/info endpoint — upgrade firmware"
),
Self::SchemaMismatch {
expected_minor,
actual_minor,
} => write!(
f,
"schema version mismatch: host expects 0.{expected_minor}.x \
but firmware reports 0.{actual_minor}.x — upgrade both together"
),
}
}
}
impl std::error::Error for ValidateError {}
#[derive(Clone)]
pub struct PicoDeGallo {
client: HostClient<WireError>,
}
impl Default for PicoDeGallo {
fn default() -> Self {
Self::new()
}
}
impl PicoDeGallo {
pub fn new() -> Self {
Self::new_inner(|dev| dev.vendor_id() == MICROSOFT_VID && dev.product_id() == PICO_DE_GALLO_PID)
}
pub fn new_with_serial_number(serial_number: &str) -> Self {
Self::new_inner(|dev| {
dev.vendor_id() == MICROSOFT_VID
&& dev.product_id() == PICO_DE_GALLO_PID
&& dev.serial_number() == Some(serial_number)
})
}
fn new_inner<F: FnMut(&NusbDeviceInfo) -> bool>(func: F) -> Self {
let client = HostClient::new_raw_nusb(func, ERROR_PATH, 8, VarSeqKind::Seq2);
Self { client }
}
pub async fn wait_closed(&self) {
self.client.wait_closed().await;
}
pub async fn ping(&self, id: u32) -> Result<u32, PicoDeGalloError<Infallible>> {
Ok(self.client.send_resp::<PingEndpoint>(&id).await?)
}
pub async fn i2c_read(&self, address: u8, count: u16) -> Result<Vec<u8>, PicoDeGalloError<I2cError>> {
self.client
.send_resp::<I2cRead>(&I2cReadRequest { address, count })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn i2c_write(&self, address: u8, contents: &[u8]) -> Result<(), PicoDeGalloError<I2cError>> {
self.client
.send_resp::<I2cWrite>(&I2cWriteRequest { address, contents })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn i2c_write_read(
&self,
address: u8,
contents: &[u8],
count: u16,
) -> Result<Vec<u8>, PicoDeGalloError<I2cError>> {
self.client
.send_resp::<I2cWriteRead>(&I2cWriteReadRequest {
address,
contents,
count,
})
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn i2c_scan(&self, include_reserved: bool) -> Result<Vec<u8>, PicoDeGalloError<I2cError>> {
self.client
.send_resp::<I2cScan>(&I2cScanRequest { include_reserved })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn i2c_batch(
&self,
address: u8,
ops: &[I2cBatchOp<'_>],
) -> Result<Vec<u8>, PicoDeGalloError<I2cBatchError>> {
let encoded = encode_i2c_batch_ops(ops);
self.client
.send_resp::<I2cBatch>(&I2cBatchRequest {
address,
count: ops.len() as u16,
ops: &encoded,
})
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn spi_read(&self, count: u16) -> Result<Vec<u8>, PicoDeGalloError<SpiError>> {
self.client
.send_resp::<SpiRead>(&SpiReadRequest { count })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn spi_write(&self, contents: &[u8]) -> Result<(), PicoDeGalloError<SpiError>> {
self.client
.send_resp::<SpiWrite>(&SpiWriteRequest { contents })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn spi_flush(&self) -> Result<(), PicoDeGalloError<SpiError>> {
self.client
.send_resp::<SpiFlush>(&())
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn spi_transfer(&self, write_data: &[u8]) -> Result<Vec<u8>, PicoDeGalloError<SpiError>> {
self.client
.send_resp::<SpiTransfer>(&SpiTransferRequest { contents: write_data })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn spi_batch(
&self,
cs_pin: u8,
ops: &[SpiBatchOp<'_>],
) -> Result<Vec<u8>, PicoDeGalloError<SpiBatchError>> {
let encoded = encode_spi_batch_ops(ops);
self.client
.send_resp::<SpiBatch>(&SpiBatchRequest {
cs_pin,
count: ops.len() as u16,
ops: &encoded,
})
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn uart_read(&self, count: u16, timeout_ms: u32) -> Result<Vec<u8>, PicoDeGalloError<UartError>> {
self.client
.send_resp::<UartRead>(&UartReadRequest { count, timeout_ms })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn uart_write(&self, contents: &[u8]) -> Result<(), PicoDeGalloError<UartError>> {
self.client
.send_resp::<UartWrite>(&UartWriteRequest { contents })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn uart_flush(&self) -> Result<(), PicoDeGalloError<UartError>> {
self.client
.send_resp::<UartFlush>(&())
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_get(&self, pin: u8) -> Result<GpioState, PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioGet>(&GpioGetRequest { pin })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_put(&self, pin: u8, state: GpioState) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioPut>(&GpioPutRequest { pin, state })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_wait_for_high(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioWaitForHigh>(&GpioWaitRequest { pin })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_wait_for_low(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioWaitForLow>(&GpioWaitRequest { pin })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_wait_for_rising_edge(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioWaitForRising>(&GpioWaitRequest { pin })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_wait_for_falling_edge(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioWaitForFalling>(&GpioWaitRequest { pin })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_wait_for_any_edge(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioWaitForAny>(&GpioWaitRequest { pin })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_set_config(
&self,
pin: u8,
direction: GpioDirection,
pull: GpioPull,
) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioSetConfiguration>(&GpioSetConfigurationRequest { pin, direction, pull })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_subscribe(&self, pin: u8, edge: GpioEdge) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioSubscribe>(&GpioSubscribeRequest { pin, edge })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn gpio_unsubscribe(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
self.client
.send_resp::<GpioUnsubscribe>(&GpioUnsubscribeRequest { pin })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn subscribe_gpio_events(
&self,
depth: usize,
) -> Result<MultiSubscription<GpioEvent>, PicoDeGalloError<Infallible>> {
self.client
.subscribe_multi::<GpioEventTopic>(depth)
.await
.map_err(|_| PicoDeGalloError::Comms(HostErr::Closed))
}
pub async fn i2c_set_config(&self, frequency: I2cFrequency) -> Result<(), PicoDeGalloError<I2cError>> {
self.client
.send_resp::<I2cSetConfiguration>(&I2cSetConfigurationRequest { frequency })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn spi_set_config(
&self,
spi_frequency: u32,
spi_phase: SpiPhase,
spi_polarity: SpiPolarity,
) -> Result<(), PicoDeGalloError<SpiError>> {
self.client
.send_resp::<SpiSetConfiguration>(&SpiSetConfigurationRequest {
spi_frequency,
spi_phase,
spi_polarity,
})
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn version(&self) -> Result<VersionInfo, PicoDeGalloError<Infallible>> {
Ok(self.client.send_resp::<Version>(&()).await?)
}
pub async fn device_info(&self) -> Result<DeviceInfo, PicoDeGalloError<Infallible>> {
Ok(self.client.send_resp::<GetDeviceInfo>(&()).await?)
}
pub async fn validate(&self) -> Result<DeviceInfo, ValidateError> {
let info = self
.client
.send_resp::<GetDeviceInfo>(&())
.await
.map_err(|e| match &e {
HostErr::Closed => ValidateError::Comms(e),
_ => ValidateError::LegacyFirmware,
})?;
if info.schema_minor != SCHEMA_VERSION_MINOR {
return Err(ValidateError::SchemaMismatch {
expected_minor: SCHEMA_VERSION_MINOR,
actual_minor: info.schema_minor,
});
}
Ok(info)
}
pub async fn i2c_get_config(&self) -> Result<I2cFrequency, PicoDeGalloError<Infallible>> {
Ok(self.client.send_resp::<I2cGetConfiguration>(&()).await?)
}
pub async fn spi_get_config(&self) -> Result<SpiConfigurationInfo, PicoDeGalloError<Infallible>> {
Ok(self.client.send_resp::<SpiGetConfiguration>(&()).await?)
}
pub async fn uart_set_config(&self, baud_rate: u32) -> Result<(), PicoDeGalloError<UartError>> {
self.client
.send_resp::<UartSetConfiguration>(&UartSetConfigurationRequest { baud_rate })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn uart_get_config(&self) -> Result<UartConfigurationInfo, PicoDeGalloError<UartError>> {
self.client
.send_resp::<UartGetConfiguration>(&())
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn pwm_set_duty_cycle(&self, channel: u8, duty: u16) -> Result<(), PicoDeGalloError<PwmError>> {
self.client
.send_resp::<PwmSetDutyCycle>(&PwmSetDutyCycleRequest { channel, duty })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn pwm_get_duty_cycle(&self, channel: u8) -> Result<PwmDutyCycleInfo, PicoDeGalloError<PwmError>> {
self.client
.send_resp::<PwmGetDutyCycle>(&PwmGetDutyCycleRequest { channel })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn pwm_enable(&self, channel: u8) -> Result<(), PicoDeGalloError<PwmError>> {
self.client
.send_resp::<PwmEnable>(&PwmEnableRequest { channel })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn pwm_disable(&self, channel: u8) -> Result<(), PicoDeGalloError<PwmError>> {
self.client
.send_resp::<PwmDisable>(&PwmDisableRequest { channel })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn pwm_set_config(
&self,
channel: u8,
frequency_hz: u32,
phase_correct: bool,
) -> Result<(), PicoDeGalloError<PwmError>> {
self.client
.send_resp::<PwmSetConfiguration>(&PwmSetConfigurationRequest {
channel,
frequency_hz,
phase_correct,
})
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn pwm_get_config(&self, channel: u8) -> Result<PwmConfigurationInfo, PicoDeGalloError<PwmError>> {
self.client
.send_resp::<PwmGetConfiguration>(&PwmGetConfigurationRequest { channel })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn adc_read(&self, channel: AdcChannel) -> Result<u16, PicoDeGalloError<AdcError>> {
self.client
.send_resp::<AdcRead>(&AdcReadRequest { channel })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn adc_get_config(&self) -> Result<AdcConfigurationInfo, PicoDeGalloError<AdcError>> {
self.client
.send_resp::<AdcGetConfiguration>(&())
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn onewire_reset(&self) -> Result<bool, PicoDeGalloError<OneWireError>> {
self.client
.send_resp::<OneWireReset>(&())
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn onewire_read(&self, len: u16) -> Result<Vec<u8>, PicoDeGalloError<OneWireError>> {
self.client
.send_resp::<OneWireRead>(&OneWireReadRequest { len })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn onewire_write(&self, data: &[u8]) -> Result<(), PicoDeGalloError<OneWireError>> {
self.client
.send_resp::<OneWireWrite>(&OneWireWriteRequest { data })
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn onewire_write_pullup(
&self,
data: &[u8],
pullup_duration_ms: u16,
) -> Result<(), PicoDeGalloError<OneWireError>> {
self.client
.send_resp::<OneWireWritePullup>(&OneWireWritePullupRequest {
data,
pullup_duration_ms,
})
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn onewire_search(&self) -> Result<Option<u64>, PicoDeGalloError<OneWireError>> {
self.client
.send_resp::<OneWireSearch>(&())
.await?
.map_err(PicoDeGalloError::Endpoint)
}
pub async fn onewire_search_next(&self) -> Result<Option<u64>, PicoDeGalloError<OneWireError>> {
self.client
.send_resp::<OneWireSearchNext>(&())
.await?
.map_err(PicoDeGalloError::Endpoint)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn endpoint_error_wraps_inner() {
let err: PicoDeGalloError<&str> = PicoDeGalloError::Endpoint("endpoint failed");
match err {
PicoDeGalloError::Endpoint(e) => assert_eq!(e, "endpoint failed"),
PicoDeGalloError::Comms(_) => panic!("expected Endpoint, got Comms"),
}
}
#[test]
fn map_err_converts_ok() {
let result: Result<u32, &str> = Ok(42);
let mapped: Result<u32, PicoDeGalloError<&str>> = result.map_err(PicoDeGalloError::Endpoint);
assert_eq!(mapped.unwrap(), 42);
}
#[test]
fn map_err_converts_err() {
let result: Result<(), I2cError> = Err(I2cError::NoAcknowledge);
let mapped = result.map_err(PicoDeGalloError::Endpoint);
match mapped {
Err(PicoDeGalloError::Endpoint(I2cError::NoAcknowledge)) => {}
_ => panic!("expected Endpoint(I2cError::NoAcknowledge)"),
}
}
#[test]
fn host_err_converts_to_comms_error() {
let host_err: HostErr<WireError> = HostErr::Closed;
let err: PicoDeGalloError<Infallible> = PicoDeGalloError::from(host_err);
match err {
PicoDeGalloError::Comms(HostErr::Closed) => {}
_ => panic!("expected Comms(Closed)"),
}
}
#[test]
fn error_debug_format_is_readable() {
let err: PicoDeGalloError<I2cError> = PicoDeGalloError::Endpoint(I2cError::Bus);
let debug = format!("{:?}", err);
assert!(debug.contains("Endpoint"));
assert!(debug.contains("Bus"));
let comms_err: PicoDeGalloError<Infallible> = PicoDeGalloError::Comms(HostErr::Closed);
let debug = format!("{:?}", comms_err);
assert!(debug.contains("Comms"));
}
#[test]
fn error_display_endpoint() {
let err: PicoDeGalloError<&str> = PicoDeGalloError::Endpoint("sensor timeout");
let msg = format!("{err}");
assert!(msg.contains("endpoint error"));
assert!(msg.contains("sensor timeout"));
}
#[test]
fn error_display_comms() {
let err: PicoDeGalloError<&str> = PicoDeGalloError::Comms(HostErr::Closed);
let msg = format!("{err}");
assert!(msg.contains("communication error"));
}
#[test]
fn error_is_std_error() {
fn assert_error<E: std::error::Error>() {}
assert_error::<PicoDeGalloError<&str>>();
}
#[test]
fn list_devices_returns_vec() {
let devices = list_devices();
for dev in &devices {
assert!(dev.serial_number.is_some() || dev.serial_number.is_none());
}
let _ = devices;
}
#[test]
fn device_description_is_clone_and_debug() {
let desc = DeviceDescription {
serial_number: Some("ABC123".to_string()),
manufacturer: Some("Microsoft".to_string()),
product: Some("Pico de Gallo".to_string()),
};
let cloned = desc.clone();
assert_eq!(format!("{:?}", desc), format!("{:?}", cloned));
}
}