Skip to main content

pico_de_gallo_lib/
lib.rs

1//! Host-side library for communicating with a Pico de Gallo USB bridge.
2//!
3//! This crate provides [`PicoDeGallo`], an async client for interacting with
4//! the Pico de Gallo firmware over USB. It supports I2C reads/writes, SPI
5//! operations (including full-duplex transfers), UART reads/writes, GPIO
6//! control, PWM output, ADC sampling, 1-Wire bus operations, and device
7//! configuration — all via [postcard-rpc](https://docs.rs/postcard-rpc)
8//! endpoints.
9//!
10//! # Quick Start
11//!
12//! ```no_run
13//! use pico_de_gallo_lib::PicoDeGallo;
14//!
15//! #[tokio::main]
16//! async fn main() {
17//!     let gallo = PicoDeGallo::new();
18//!     let version = gallo.version().await.unwrap();
19//!     println!("Firmware v{}.{}.{}", version.major, version.minor, version.patch);
20//! }
21//! ```
22//!
23//! # Multiple Devices
24//!
25//! When multiple Pico de Gallo boards are connected, use [`list_devices`] to
26//! enumerate them and [`PicoDeGallo::new_with_serial_number`] to connect to a
27//! specific board:
28//!
29//! ```no_run
30//! use pico_de_gallo_lib::{PicoDeGallo, list_devices};
31//!
32//! #[tokio::main]
33//! async fn main() {
34//!     for dev in list_devices() {
35//!         println!("Found: {:?}", dev.serial_number);
36//!     }
37//!     let gallo = PicoDeGallo::new_with_serial_number("ABCD1234");
38//! }
39//! ```
40//!
41//! # Error Handling
42//!
43//! All methods return [`Result<T, PicoDeGalloError<E>>`](PicoDeGalloError)
44//! where `E` is the endpoint-specific error type. Errors are either
45//! communication failures ([`PicoDeGalloError::Comms`]) or endpoint-level
46//! errors ([`PicoDeGalloError::Endpoint`]).
47
48pub mod decode;
49
50use nusb::DeviceInfo as NusbDeviceInfo;
51use pico_de_gallo_internal::{
52    AdcGetConfiguration, AdcRead, AdcReadRequest, GetDeviceInfo, GpioEventTopic, GpioGet, GpioGetRequest, GpioPut,
53    GpioPutRequest, GpioSetConfiguration, GpioSetConfigurationRequest, GpioSubscribe, GpioSubscribeRequest,
54    GpioUnsubscribe, GpioUnsubscribeRequest, GpioWaitForAny, GpioWaitForFalling, GpioWaitForHigh, GpioWaitForLow,
55    GpioWaitForRising, GpioWaitRequest, I2cBatch, I2cBatchRequest, I2cGetConfiguration, I2cRead, I2cReadRequest,
56    I2cScan, I2cScanRequest, I2cSetConfiguration, I2cSetConfigurationRequest, I2cWrite, I2cWriteRead,
57    I2cWriteReadRequest, I2cWriteRequest, MICROSOFT_VID, OneWireRead, OneWireReadRequest, OneWireReset, OneWireSearch,
58    OneWireSearchNext, OneWireWrite, OneWireWritePullup, OneWireWritePullupRequest, OneWireWriteRequest,
59    PICO_DE_GALLO_PID, PwmDisable, PwmDisableRequest, PwmEnable, PwmEnableRequest, PwmGetConfiguration,
60    PwmGetConfigurationRequest, PwmGetDutyCycle, PwmGetDutyCycleRequest, PwmSetConfiguration,
61    PwmSetConfigurationRequest, PwmSetDutyCycle, PwmSetDutyCycleRequest, SCHEMA_VERSION_MINOR, SpiBatch,
62    SpiBatchRequest, SpiFlush, SpiGetConfiguration, SpiRead, SpiReadRequest, SpiSetConfiguration,
63    SpiSetConfigurationRequest, SpiTransfer, SpiTransferRequest, SpiWrite, SpiWriteRequest, UartFlush,
64    UartGetConfiguration, UartRead, UartReadRequest, UartSetConfiguration, UartSetConfigurationRequest, UartWrite,
65    UartWriteRequest, Version,
66};
67
68pub use pico_de_gallo_internal::{
69    AdcChannel, AdcConfigurationInfo, Capabilities, DeviceInfo, GpioDirection, GpioEdge, GpioEvent, GpioPull,
70    GpioState, I2cBatchOp, I2cFrequency, PwmConfigurationInfo, PwmDutyCycleInfo, SpiBatchOp, SpiConfigurationInfo,
71    SpiPhase, SpiPolarity, UartConfigurationInfo, VersionInfo,
72};
73pub use pico_de_gallo_internal::{
74    AdcError, GpioError, I2cBatchError, I2cError, OneWireError, PwmError, SpiBatchError, SpiError, UartError,
75};
76pub use pico_de_gallo_internal::{
77    encode_i2c_batch_ops, encode_spi_batch_ops, i2c_batch_response_len, spi_batch_response_len,
78};
79
80pub use postcard_rpc::host_client::{IoClosed, MultiSubscription};
81use postcard_rpc::{
82    header::VarSeqKind,
83    host_client::{HostClient, HostErr},
84    standard_icd::{ERROR_PATH, PingEndpoint, WireError},
85};
86use std::convert::Infallible;
87
88/// Description of a connected Pico de Gallo device.
89#[derive(Debug, Clone)]
90pub struct DeviceDescription {
91    /// USB serial number (unique per board, derived from chip ID).
92    pub serial_number: Option<String>,
93    /// USB manufacturer string.
94    pub manufacturer: Option<String>,
95    /// USB product string.
96    pub product: Option<String>,
97}
98
99/// List all connected Pico de Gallo devices.
100///
101/// Returns a description for each device found on the USB bus matching the
102/// Pico de Gallo VID/PID. Use the serial number with
103/// [`PicoDeGallo::new_with_serial_number`] to connect to a specific device.
104pub fn list_devices() -> Vec<DeviceDescription> {
105    let devices = match nusb::list_devices() {
106        Ok(iter) => iter,
107        Err(_) => return Vec::new(),
108    };
109    devices
110        .filter(|dev| dev.vendor_id() == MICROSOFT_VID && dev.product_id() == PICO_DE_GALLO_PID)
111        .map(|dev| DeviceDescription {
112            serial_number: dev.serial_number().map(String::from),
113            manufacturer: dev.manufacturer_string().map(String::from),
114            product: dev.product_string().map(String::from),
115        })
116        .collect()
117}
118
119/// Error type for Pico de Gallo operations.
120///
121/// Every method on [`PicoDeGallo`] returns this error type, parameterized by the
122/// endpoint-specific error `E`. In practice, `E` is a rich error enum like
123/// [`I2cError`], [`SpiError`], or [`GpioError`].
124#[derive(Debug)]
125pub enum PicoDeGalloError<E> {
126    /// A transport-level communication error (USB disconnect, timeout, wire format error).
127    Comms(HostErr<WireError>),
128    /// The firmware processed the request but returned an error.
129    Endpoint(E),
130}
131
132impl<E: core::fmt::Display> core::fmt::Display for PicoDeGalloError<E> {
133    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134        match self {
135            Self::Comms(e) => write!(f, "communication error: {e:?}"),
136            Self::Endpoint(e) => write!(f, "endpoint error: {e}"),
137        }
138    }
139}
140
141impl<E: core::fmt::Debug + core::fmt::Display> std::error::Error for PicoDeGalloError<E> {}
142
143impl<E> From<HostErr<WireError>> for PicoDeGalloError<E> {
144    fn from(value: HostErr<WireError>) -> Self {
145        Self::Comms(value)
146    }
147}
148
149/// Error returned by [`PicoDeGallo::validate()`] when the connected firmware
150/// is incompatible with this host library.
151#[derive(Debug)]
152pub enum ValidateError {
153    /// Could not communicate with the device (USB disconnect, timeout, etc.).
154    Comms(HostErr<WireError>),
155    /// The firmware does not support the `device/info` endpoint (legacy firmware).
156    LegacyFirmware,
157    /// The schema (wire protocol) version does not match.
158    ///
159    /// The host and firmware were compiled against different versions of
160    /// `pico-de-gallo-internal`. They must be upgraded together.
161    SchemaMismatch {
162        /// Schema minor version expected by this host library.
163        expected_minor: u16,
164        /// Schema minor version reported by the firmware.
165        actual_minor: u16,
166    },
167}
168
169impl core::fmt::Display for ValidateError {
170    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171        match self {
172            Self::Comms(e) => write!(f, "communication error: {e:?}"),
173            Self::LegacyFirmware => write!(
174                f,
175                "firmware does not support the device/info endpoint — upgrade firmware"
176            ),
177            Self::SchemaMismatch {
178                expected_minor,
179                actual_minor,
180            } => write!(
181                f,
182                "schema version mismatch: host expects 0.{expected_minor}.x \
183                 but firmware reports 0.{actual_minor}.x — upgrade both together"
184            ),
185        }
186    }
187}
188
189impl std::error::Error for ValidateError {}
190
191/// Async client for a Pico de Gallo USB bridge device.
192///
193/// This is the primary type for interacting with the hardware. It wraps a
194/// [`postcard_rpc::host_client::HostClient`] and provides typed async methods
195/// for every firmware endpoint. The client is cheaply cloneable (the inner
196/// transport is reference-counted) and safe to share across tasks.
197///
198/// Connection happens lazily in the background — constructing a `PicoDeGallo`
199/// does not block or fail. If the device is not connected, methods will return
200/// errors when called.
201#[derive(Clone)]
202pub struct PicoDeGallo {
203    client: HostClient<WireError>,
204}
205
206impl Default for PicoDeGallo {
207    fn default() -> Self {
208        Self::new()
209    }
210}
211
212impl PicoDeGallo {
213    /// Create a new instance for the Pico de Gallo device.
214    ///
215    /// NOTICE:
216    ///
217    /// This constructor will return the first matching device in case
218    /// there are more than one connected.
219    ///
220    /// If you want more control, please use `new_with_serial_number`
221    /// instead.
222    pub fn new() -> Self {
223        Self::new_inner(|dev| dev.vendor_id() == MICROSOFT_VID && dev.product_id() == PICO_DE_GALLO_PID)
224    }
225
226    /// Create a new instance for the Pico de Gallo device with the
227    /// given serial number.
228    pub fn new_with_serial_number(serial_number: &str) -> Self {
229        Self::new_inner(|dev| {
230            dev.vendor_id() == MICROSOFT_VID
231                && dev.product_id() == PICO_DE_GALLO_PID
232                && dev.serial_number() == Some(serial_number)
233        })
234    }
235
236    fn new_inner<F: FnMut(&NusbDeviceInfo) -> bool>(func: F) -> Self {
237        let client = HostClient::new_raw_nusb(func, ERROR_PATH, 8, VarSeqKind::Seq2);
238        Self { client }
239    }
240
241    /// Wait until the client has closed the connection.
242    pub async fn wait_closed(&self) {
243        self.client.wait_closed().await;
244    }
245
246    /// Ping endpoint.
247    ///
248    /// Only used for testing purposes. Send a `u32` and get the same
249    /// `u32` as a response.
250    pub async fn ping(&self, id: u32) -> Result<u32, PicoDeGalloError<Infallible>> {
251        Ok(self.client.send_resp::<PingEndpoint>(&id).await?)
252    }
253
254    /// Read `count` bytes from the I2C device at `address`.
255    ///
256    /// The firmware buffer is limited to [`pico_de_gallo_internal::MAX_TRANSFER_SIZE`]
257    /// (4096) bytes. Reads exceeding this limit will be truncated.
258    pub async fn i2c_read(&self, address: u8, count: u16) -> Result<Vec<u8>, PicoDeGalloError<I2cError>> {
259        self.client
260            .send_resp::<I2cRead>(&I2cReadRequest { address, count })
261            .await?
262            .map_err(PicoDeGalloError::Endpoint)
263    }
264
265    /// Write `contents` to the I2C device at `address`.
266    pub async fn i2c_write(&self, address: u8, contents: &[u8]) -> Result<(), PicoDeGalloError<I2cError>> {
267        self.client
268            .send_resp::<I2cWrite>(&I2cWriteRequest { address, contents })
269            .await?
270            .map_err(PicoDeGalloError::Endpoint)
271    }
272
273    /// Write `contents` to the I2C device at `address` and read back `count` bytes.
274    ///
275    /// The firmware buffer is limited to [`pico_de_gallo_internal::MAX_TRANSFER_SIZE`]
276    /// (4096) bytes. Reads exceeding this limit will be truncated.
277    pub async fn i2c_write_read(
278        &self,
279        address: u8,
280        contents: &[u8],
281        count: u16,
282    ) -> Result<Vec<u8>, PicoDeGalloError<I2cError>> {
283        self.client
284            .send_resp::<I2cWriteRead>(&I2cWriteReadRequest {
285                address,
286                contents,
287                count,
288            })
289            .await?
290            .map_err(PicoDeGalloError::Endpoint)
291    }
292
293    /// Scan the I2C bus and return the addresses of all responding devices.
294    ///
295    /// The firmware probes each 7-bit address by attempting a 1-byte read.
296    /// Addresses that ACK are returned in ascending order. When
297    /// `include_reserved` is `false`, only the standard range (0x08–0x77) is
298    /// probed; when `true`, the full range (0x00–0x7F) is scanned.
299    pub async fn i2c_scan(&self, include_reserved: bool) -> Result<Vec<u8>, PicoDeGalloError<I2cError>> {
300        self.client
301            .send_resp::<I2cScan>(&I2cScanRequest { include_reserved })
302            .await?
303            .map_err(PicoDeGalloError::Endpoint)
304    }
305
306    /// Execute a batch of I2C operations in a single USB transfer.
307    ///
308    /// Pass a slice of [`I2cBatchOp`] values directly — they are encoded
309    /// internally. On success, returns the concatenated read data from
310    /// all Read operations in order.
311    ///
312    /// This is much faster than issuing individual I2C calls when
313    /// performing multi-step sequences (e.g., EEPROM programming).
314    pub async fn i2c_batch(
315        &self,
316        address: u8,
317        ops: &[I2cBatchOp<'_>],
318    ) -> Result<Vec<u8>, PicoDeGalloError<I2cBatchError>> {
319        let encoded = encode_i2c_batch_ops(ops);
320        self.client
321            .send_resp::<I2cBatch>(&I2cBatchRequest {
322                address,
323                count: ops.len() as u16,
324                ops: &encoded,
325            })
326            .await?
327            .map_err(PicoDeGalloError::Endpoint)
328    }
329
330    /// Read `count` bytes from the SPI bus.
331    ///
332    /// The firmware buffer is limited to [`pico_de_gallo_internal::MAX_TRANSFER_SIZE`]
333    /// (4096) bytes. Reads exceeding this limit will be truncated.
334    pub async fn spi_read(&self, count: u16) -> Result<Vec<u8>, PicoDeGalloError<SpiError>> {
335        self.client
336            .send_resp::<SpiRead>(&SpiReadRequest { count })
337            .await?
338            .map_err(PicoDeGalloError::Endpoint)
339    }
340
341    /// Write `contents` to the SPI bus.
342    pub async fn spi_write(&self, contents: &[u8]) -> Result<(), PicoDeGalloError<SpiError>> {
343        self.client
344            .send_resp::<SpiWrite>(&SpiWriteRequest { contents })
345            .await?
346            .map_err(PicoDeGalloError::Endpoint)
347    }
348
349    /// Flush the SPI interface.
350    pub async fn spi_flush(&self) -> Result<(), PicoDeGalloError<SpiError>> {
351        self.client
352            .send_resp::<SpiFlush>(&())
353            .await?
354            .map_err(PicoDeGalloError::Endpoint)
355    }
356
357    /// Perform a full-duplex SPI transfer.
358    ///
359    /// Simultaneously sends `write_data` and receives the same number of bytes.
360    /// The firmware buffer is limited to [`pico_de_gallo_internal::MAX_TRANSFER_SIZE`]
361    /// bytes. Transfers exceeding this limit will be rejected.
362    pub async fn spi_transfer(&self, write_data: &[u8]) -> Result<Vec<u8>, PicoDeGalloError<SpiError>> {
363        self.client
364            .send_resp::<SpiTransfer>(&SpiTransferRequest { contents: write_data })
365            .await?
366            .map_err(PicoDeGalloError::Endpoint)
367    }
368
369    /// Execute a batch of SPI operations atomically under chip-select.
370    ///
371    /// Pass a slice of [`SpiBatchOp`] values directly — they are encoded
372    /// internally. The firmware asserts CS on `cs_pin` before the first
373    /// operation and deasserts it after the last (or on error). On success,
374    /// returns concatenated data from all Read and Transfer operations
375    /// in order.
376    ///
377    /// This is much faster than issuing individual SPI calls when
378    /// performing multi-step sequences.
379    pub async fn spi_batch(
380        &self,
381        cs_pin: u8,
382        ops: &[SpiBatchOp<'_>],
383    ) -> Result<Vec<u8>, PicoDeGalloError<SpiBatchError>> {
384        let encoded = encode_spi_batch_ops(ops);
385        self.client
386            .send_resp::<SpiBatch>(&SpiBatchRequest {
387                cs_pin,
388                count: ops.len() as u16,
389                ops: &encoded,
390            })
391            .await?
392            .map_err(PicoDeGalloError::Endpoint)
393    }
394
395    /// Read up to `count` bytes from the UART bus.
396    ///
397    /// The firmware reads up to `count` bytes from the UART receive buffer.
398    /// If no data is immediately available, it waits up to `timeout_ms`
399    /// milliseconds for at least one byte. Returns whatever bytes are
400    /// available (1 to `count`), or an empty `Vec` on timeout.
401    ///
402    /// The firmware buffer is limited to [`pico_de_gallo_internal::MAX_TRANSFER_SIZE`]
403    /// (4096) bytes.
404    pub async fn uart_read(&self, count: u16, timeout_ms: u32) -> Result<Vec<u8>, PicoDeGalloError<UartError>> {
405        self.client
406            .send_resp::<UartRead>(&UartReadRequest { count, timeout_ms })
407            .await?
408            .map_err(PicoDeGalloError::Endpoint)
409    }
410
411    /// Write `contents` to the UART bus.
412    ///
413    /// Bytes are queued to the firmware's UART transmit buffer. The call
414    /// returns once all bytes have been accepted by the TX buffer (not
415    /// necessarily transmitted on the wire). Use [`uart_flush`](Self::uart_flush)
416    /// to wait for transmission to complete.
417    pub async fn uart_write(&self, contents: &[u8]) -> Result<(), PicoDeGalloError<UartError>> {
418        self.client
419            .send_resp::<UartWrite>(&UartWriteRequest { contents })
420            .await?
421            .map_err(PicoDeGalloError::Endpoint)
422    }
423
424    /// Flush the UART transmit buffer.
425    ///
426    /// Blocks until all pending bytes have been transmitted on the wire.
427    pub async fn uart_flush(&self) -> Result<(), PicoDeGalloError<UartError>> {
428        self.client
429            .send_resp::<UartFlush>(&())
430            .await?
431            .map_err(PicoDeGalloError::Endpoint)
432    }
433
434    /// Get the current state of GPIO numbered by `pin`.
435    ///
436    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
437    pub async fn gpio_get(&self, pin: u8) -> Result<GpioState, PicoDeGalloError<GpioError>> {
438        self.client
439            .send_resp::<GpioGet>(&GpioGetRequest { pin })
440            .await?
441            .map_err(PicoDeGalloError::Endpoint)
442    }
443
444    /// Set the GPIO numbered by `pin` to state `state`.
445    ///
446    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
447    pub async fn gpio_put(&self, pin: u8, state: GpioState) -> Result<(), PicoDeGalloError<GpioError>> {
448        self.client
449            .send_resp::<GpioPut>(&GpioPutRequest { pin, state })
450            .await?
451            .map_err(PicoDeGalloError::Endpoint)
452    }
453
454    /// Wait for GPIO numbered by `pin` to reach `High` state.
455    ///
456    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
457    pub async fn gpio_wait_for_high(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
458        self.client
459            .send_resp::<GpioWaitForHigh>(&GpioWaitRequest { pin })
460            .await?
461            .map_err(PicoDeGalloError::Endpoint)
462    }
463
464    /// Wait for GPIO numbered by `pin` to reach `Low` state.
465    ///
466    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
467    pub async fn gpio_wait_for_low(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
468        self.client
469            .send_resp::<GpioWaitForLow>(&GpioWaitRequest { pin })
470            .await?
471            .map_err(PicoDeGalloError::Endpoint)
472    }
473
474    /// Wait for a rising edge on the GPIO numbered by `pin`.
475    ///
476    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
477    pub async fn gpio_wait_for_rising_edge(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
478        self.client
479            .send_resp::<GpioWaitForRising>(&GpioWaitRequest { pin })
480            .await?
481            .map_err(PicoDeGalloError::Endpoint)
482    }
483
484    /// Wait for a falling edge on the GPIO numbered by `pin`.
485    ///
486    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
487    pub async fn gpio_wait_for_falling_edge(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
488        self.client
489            .send_resp::<GpioWaitForFalling>(&GpioWaitRequest { pin })
490            .await?
491            .map_err(PicoDeGalloError::Endpoint)
492    }
493
494    /// Wait for either a rising edge or a falling edge on the GPIO
495    /// numbered by `pin`.
496    ///
497    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
498    pub async fn gpio_wait_for_any_edge(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
499        self.client
500            .send_resp::<GpioWaitForAny>(&GpioWaitRequest { pin })
501            .await?
502            .map_err(PicoDeGalloError::Endpoint)
503    }
504
505    /// Configure a GPIO pin's direction and internal pull resistor.
506    ///
507    /// After configuration, the pin enters explicit mode: `gpio_get` and
508    /// `gpio_put` will no longer auto-switch direction. Calling `gpio_put`
509    /// on an input pin (or `gpio_get`/wait on an output pin) will return
510    /// [`GpioError::WrongDirection`].
511    ///
512    /// Pico de Gallo offers 4 total GPIOs, numbered 0 through 3.
513    pub async fn gpio_set_config(
514        &self,
515        pin: u8,
516        direction: GpioDirection,
517        pull: GpioPull,
518    ) -> Result<(), PicoDeGalloError<GpioError>> {
519        self.client
520            .send_resp::<GpioSetConfiguration>(&GpioSetConfigurationRequest { pin, direction, pull })
521            .await?
522            .map_err(PicoDeGalloError::Endpoint)
523    }
524
525    /// Subscribe to GPIO edge events on a pin.
526    ///
527    /// Starts push-based monitoring for the specified edge type. While subscribed,
528    /// the pin cannot be used by other GPIO operations (they will return
529    /// [`GpioError::PinMonitored`]). Use [`gpio_unsubscribe`](Self::gpio_unsubscribe)
530    /// to release the pin.
531    ///
532    /// Call [`subscribe_gpio_events`](Self::subscribe_gpio_events) to receive the
533    /// event stream.
534    pub async fn gpio_subscribe(&self, pin: u8, edge: GpioEdge) -> Result<(), PicoDeGalloError<GpioError>> {
535        self.client
536            .send_resp::<GpioSubscribe>(&GpioSubscribeRequest { pin, edge })
537            .await?
538            .map_err(PicoDeGalloError::Endpoint)
539    }
540
541    /// Unsubscribe from GPIO edge events on a pin.
542    ///
543    /// Stops monitoring and returns the pin to normal operation. Returns
544    /// [`GpioError::PinNotMonitored`] if the pin is not currently subscribed.
545    pub async fn gpio_unsubscribe(&self, pin: u8) -> Result<(), PicoDeGalloError<GpioError>> {
546        self.client
547            .send_resp::<GpioUnsubscribe>(&GpioUnsubscribeRequest { pin })
548            .await?
549            .map_err(PicoDeGalloError::Endpoint)
550    }
551
552    /// Subscribe to the GPIO event topic stream.
553    ///
554    /// Returns a [`MultiSubscription`] that yields [`GpioEvent`] messages as edges
555    /// are detected on any subscribed pin. Call this *before* or *after*
556    /// [`gpio_subscribe`](Self::gpio_subscribe) — events are buffered up to
557    /// `depth` messages.
558    ///
559    /// Edge detection is best-effort: if the pin changes faster than the
560    /// firmware monitor loop cadence, intermediate transitions may be missed.
561    pub async fn subscribe_gpio_events(
562        &self,
563        depth: usize,
564    ) -> Result<MultiSubscription<GpioEvent>, PicoDeGalloError<Infallible>> {
565        self.client
566            .subscribe_multi::<GpioEventTopic>(depth)
567            .await
568            .map_err(|_| PicoDeGalloError::Comms(HostErr::Closed))
569    }
570
571    /// Set I2C bus configuration parameters.
572    ///
573    /// Changes the I2C bus clock frequency. Takes effect immediately before
574    /// the next I2C operation.
575    pub async fn i2c_set_config(&self, frequency: I2cFrequency) -> Result<(), PicoDeGalloError<I2cError>> {
576        self.client
577            .send_resp::<I2cSetConfiguration>(&I2cSetConfigurationRequest { frequency })
578            .await?
579            .map_err(PicoDeGalloError::Endpoint)
580    }
581
582    /// Set SPI bus configuration parameters.
583    ///
584    /// Changes the SPI bus clock frequency, phase, and polarity. Takes effect
585    /// immediately before the next SPI operation.
586    pub async fn spi_set_config(
587        &self,
588        spi_frequency: u32,
589        spi_phase: SpiPhase,
590        spi_polarity: SpiPolarity,
591    ) -> Result<(), PicoDeGalloError<SpiError>> {
592        self.client
593            .send_resp::<SpiSetConfiguration>(&SpiSetConfigurationRequest {
594                spi_frequency,
595                spi_phase,
596                spi_polarity,
597            })
598            .await?
599            .map_err(PicoDeGalloError::Endpoint)
600    }
601
602    /// Get the firmware version from the Pico de Gallo device.
603    pub async fn version(&self) -> Result<VersionInfo, PicoDeGalloError<Infallible>> {
604        Ok(self.client.send_resp::<Version>(&()).await?)
605    }
606
607    /// Get extended device information including firmware version, schema
608    /// (wire protocol) version, hardware revision, and peripheral capabilities.
609    pub async fn device_info(&self) -> Result<DeviceInfo, PicoDeGalloError<Infallible>> {
610        Ok(self.client.send_resp::<GetDeviceInfo>(&()).await?)
611    }
612
613    /// Validate that the connected firmware is wire-compatible with this
614    /// host library.
615    ///
616    /// Queries the `device/info` endpoint and checks that the schema minor
617    /// version matches (pre-1.0 semver: minor bumps are breaking). Returns
618    /// the [`DeviceInfo`] on success so callers can inspect capabilities
619    /// without an extra round-trip.
620    ///
621    /// # Errors
622    ///
623    /// - [`ValidateError::Comms`] — could not reach the device.
624    /// - [`ValidateError::LegacyFirmware`] — firmware does not support
625    ///   `device/info` (upgrade firmware).
626    /// - [`ValidateError::SchemaMismatch`] — firmware and host disagree on
627    ///   the wire protocol version.
628    pub async fn validate(&self) -> Result<DeviceInfo, ValidateError> {
629        let info = self
630            .client
631            .send_resp::<GetDeviceInfo>(&())
632            .await
633            .map_err(|e| match &e {
634                HostErr::Closed => ValidateError::Comms(e),
635                _ => ValidateError::LegacyFirmware,
636            })?;
637
638        // Pre-1.0: minor version must match (0.x bumps are breaking).
639        // Post-1.0: switch to major version matching.
640        if info.schema_minor != SCHEMA_VERSION_MINOR {
641            return Err(ValidateError::SchemaMismatch {
642                expected_minor: SCHEMA_VERSION_MINOR,
643                actual_minor: info.schema_minor,
644            });
645        }
646
647        Ok(info)
648    }
649
650    /// Query the current I2C bus configuration.
651    ///
652    /// Returns the [`I2cFrequency`] value that is currently active on the
653    /// firmware. The default is [`I2cFrequency::Standard`] (100 kHz).
654    pub async fn i2c_get_config(&self) -> Result<I2cFrequency, PicoDeGalloError<Infallible>> {
655        Ok(self.client.send_resp::<I2cGetConfiguration>(&()).await?)
656    }
657
658    /// Query the current SPI bus configuration.
659    ///
660    /// Returns a [`SpiConfigurationInfo`] struct with the active SPI
661    /// frequency, phase, and polarity. The defaults are 1 MHz,
662    /// `CaptureOnFirstTransition`, and `IdleLow`.
663    pub async fn spi_get_config(&self) -> Result<SpiConfigurationInfo, PicoDeGalloError<Infallible>> {
664        Ok(self.client.send_resp::<SpiGetConfiguration>(&()).await?)
665    }
666
667    /// Set UART bus configuration parameters.
668    ///
669    /// Changes the UART baud rate. Takes effect immediately before the next
670    /// UART operation. The default baud rate is 115200.
671    pub async fn uart_set_config(&self, baud_rate: u32) -> Result<(), PicoDeGalloError<UartError>> {
672        self.client
673            .send_resp::<UartSetConfiguration>(&UartSetConfigurationRequest { baud_rate })
674            .await?
675            .map_err(PicoDeGalloError::Endpoint)
676    }
677
678    /// Query the current UART bus configuration.
679    ///
680    /// Returns a [`UartConfigurationInfo`] struct with the active baud rate.
681    /// The default is 115200.
682    ///
683    /// Returns [`UartError::Unsupported`] if the firmware's hardware revision
684    /// does not support UART.
685    pub async fn uart_get_config(&self) -> Result<UartConfigurationInfo, PicoDeGalloError<UartError>> {
686        self.client
687            .send_resp::<UartGetConfiguration>(&())
688            .await?
689            .map_err(PicoDeGalloError::Endpoint)
690    }
691
692    // -----------------------------------------------------------------------
693    // PWM
694    // -----------------------------------------------------------------------
695
696    /// Set the raw duty cycle of a PWM channel (0–3).
697    ///
698    /// `duty` is a raw compare value in the range `0..=top`. Use
699    /// [`pwm_get_duty_cycle`](Self::pwm_get_duty_cycle) to discover `max_duty`
700    /// (which equals the current `top` value).
701    ///
702    /// Channels 0–1 share PWM slice 6, channels 2–3 share PWM slice 7.
703    pub async fn pwm_set_duty_cycle(&self, channel: u8, duty: u16) -> Result<(), PicoDeGalloError<PwmError>> {
704        self.client
705            .send_resp::<PwmSetDutyCycle>(&PwmSetDutyCycleRequest { channel, duty })
706            .await?
707            .map_err(PicoDeGalloError::Endpoint)
708    }
709
710    /// Query the current duty cycle of a PWM channel (0–3).
711    ///
712    /// Returns a [`PwmDutyCycleInfo`] with `current_duty` (the raw compare
713    /// value) and `max_duty` (the `top` register + 1, i.e., the full-scale
714    /// value).
715    pub async fn pwm_get_duty_cycle(&self, channel: u8) -> Result<PwmDutyCycleInfo, PicoDeGalloError<PwmError>> {
716        self.client
717            .send_resp::<PwmGetDutyCycle>(&PwmGetDutyCycleRequest { channel })
718            .await?
719            .map_err(PicoDeGalloError::Endpoint)
720    }
721
722    /// Enable the PWM slice that owns `channel` (0–3).
723    ///
724    /// Because PWM slices drive two channels, enabling channel 0 also
725    /// enables channel 1 (and vice versa). Same for channels 2/3.
726    pub async fn pwm_enable(&self, channel: u8) -> Result<(), PicoDeGalloError<PwmError>> {
727        self.client
728            .send_resp::<PwmEnable>(&PwmEnableRequest { channel })
729            .await?
730            .map_err(PicoDeGalloError::Endpoint)
731    }
732
733    /// Disable the PWM slice that owns `channel` (0–3).
734    ///
735    /// Because PWM slices drive two channels, disabling channel 0 also
736    /// disables channel 1 (and vice versa). Same for channels 2/3.
737    pub async fn pwm_disable(&self, channel: u8) -> Result<(), PicoDeGalloError<PwmError>> {
738        self.client
739            .send_resp::<PwmDisable>(&PwmDisableRequest { channel })
740            .await?
741            .map_err(PicoDeGalloError::Endpoint)
742    }
743
744    /// Configure the PWM slice behind `channel` (0–3).
745    ///
746    /// Sets the output frequency and phase-correct mode. The firmware
747    /// computes `top` and `divider` automatically. Existing duty-cycle
748    /// compare values are scaled proportionally to the new `top`.
749    ///
750    /// Channels 0–1 share a slice, so configuring channel 0 also affects
751    /// channel 1 (and vice versa). Same for channels 2/3.
752    pub async fn pwm_set_config(
753        &self,
754        channel: u8,
755        frequency_hz: u32,
756        phase_correct: bool,
757    ) -> Result<(), PicoDeGalloError<PwmError>> {
758        self.client
759            .send_resp::<PwmSetConfiguration>(&PwmSetConfigurationRequest {
760                channel,
761                frequency_hz,
762                phase_correct,
763            })
764            .await?
765            .map_err(PicoDeGalloError::Endpoint)
766    }
767
768    /// Query the current configuration of the PWM slice behind `channel` (0–3).
769    ///
770    /// Returns a [`PwmConfigurationInfo`] with the effective frequency,
771    /// phase-correct flag, and enabled state.
772    pub async fn pwm_get_config(&self, channel: u8) -> Result<PwmConfigurationInfo, PicoDeGalloError<PwmError>> {
773        self.client
774            .send_resp::<PwmGetConfiguration>(&PwmGetConfigurationRequest { channel })
775            .await?
776            .map_err(PicoDeGalloError::Endpoint)
777    }
778
779    // ---- ADC methods ----
780
781    /// Perform a single-shot ADC read on the specified channel.
782    ///
783    /// Returns a raw 12-bit value (0–4095). Convert to voltage with:
784    /// `V ≈ raw × 3.3 / 4096` (approximate — depends on ADC_AVDD).
785    pub async fn adc_read(&self, channel: AdcChannel) -> Result<u16, PicoDeGalloError<AdcError>> {
786        self.client
787            .send_resp::<AdcRead>(&AdcReadRequest { channel })
788            .await?
789            .map_err(PicoDeGalloError::Endpoint)
790    }
791
792    /// Query the ADC configuration (resolution, reference, channel count).
793    ///
794    /// Returns an [`AdcConfigurationInfo`] with fixed values for the RP2350
795    /// ADC. Useful for host-side discovery.
796    ///
797    /// Returns [`AdcError::Unsupported`] if the firmware's hardware revision
798    /// does not support ADC.
799    pub async fn adc_get_config(&self) -> Result<AdcConfigurationInfo, PicoDeGalloError<AdcError>> {
800        self.client
801            .send_resp::<AdcGetConfiguration>(&())
802            .await?
803            .map_err(PicoDeGalloError::Endpoint)
804    }
805
806    // ---- 1-Wire ----
807
808    /// Perform a 1-Wire bus reset and detect device presence.
809    ///
810    /// Returns `true` if one or more devices responded with a presence pulse.
811    pub async fn onewire_reset(&self) -> Result<bool, PicoDeGalloError<OneWireError>> {
812        self.client
813            .send_resp::<OneWireReset>(&())
814            .await?
815            .map_err(PicoDeGalloError::Endpoint)
816    }
817
818    /// Read `len` bytes from the 1-Wire bus.
819    ///
820    /// The firmware sends `0xFF` read slots and captures the device's response bits.
821    pub async fn onewire_read(&self, len: u16) -> Result<Vec<u8>, PicoDeGalloError<OneWireError>> {
822        self.client
823            .send_resp::<OneWireRead>(&OneWireReadRequest { len })
824            .await?
825            .map_err(PicoDeGalloError::Endpoint)
826    }
827
828    /// Write raw bytes to the 1-Wire bus.
829    pub async fn onewire_write(&self, data: &[u8]) -> Result<(), PicoDeGalloError<OneWireError>> {
830        self.client
831            .send_resp::<OneWireWrite>(&OneWireWriteRequest { data })
832            .await?
833            .map_err(PicoDeGalloError::Endpoint)
834    }
835
836    /// Write bytes to the 1-Wire bus, then apply a strong pullup for the given duration.
837    ///
838    /// This is needed for parasitic-power devices like the DS18B20 during temperature
839    /// conversion. The bus is held high for `pullup_duration_ms` milliseconds after
840    /// the last bit is sent.
841    pub async fn onewire_write_pullup(
842        &self,
843        data: &[u8],
844        pullup_duration_ms: u16,
845    ) -> Result<(), PicoDeGalloError<OneWireError>> {
846        self.client
847            .send_resp::<OneWireWritePullup>(&OneWireWritePullupRequest {
848                data,
849                pullup_duration_ms,
850            })
851            .await?
852            .map_err(PicoDeGalloError::Endpoint)
853    }
854
855    /// Start a new 1-Wire ROM search and return the first device address.
856    ///
857    /// Returns `Some(rom_id)` for the first device found, or `None` if no devices
858    /// are on the bus. Call [`onewire_search_next`](Self::onewire_search_next) to
859    /// continue enumerating.
860    pub async fn onewire_search(&self) -> Result<Option<u64>, PicoDeGalloError<OneWireError>> {
861        self.client
862            .send_resp::<OneWireSearch>(&())
863            .await?
864            .map_err(PicoDeGalloError::Endpoint)
865    }
866
867    /// Continue the current 1-Wire ROM search.
868    ///
869    /// Returns the next device's 64-bit ROM ID, or `None` when all devices have
870    /// been enumerated.
871    pub async fn onewire_search_next(&self) -> Result<Option<u64>, PicoDeGalloError<OneWireError>> {
872        self.client
873            .send_resp::<OneWireSearchNext>(&())
874            .await?
875            .map_err(PicoDeGalloError::Endpoint)
876    }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882
883    // --- PicoDeGalloError tests ---
884
885    #[test]
886    fn endpoint_error_wraps_inner() {
887        let err: PicoDeGalloError<&str> = PicoDeGalloError::Endpoint("endpoint failed");
888        match err {
889            PicoDeGalloError::Endpoint(e) => assert_eq!(e, "endpoint failed"),
890            PicoDeGalloError::Comms(_) => panic!("expected Endpoint, got Comms"),
891        }
892    }
893
894    #[test]
895    fn map_err_converts_ok() {
896        let result: Result<u32, &str> = Ok(42);
897        let mapped: Result<u32, PicoDeGalloError<&str>> = result.map_err(PicoDeGalloError::Endpoint);
898        assert_eq!(mapped.unwrap(), 42);
899    }
900
901    #[test]
902    fn map_err_converts_err() {
903        let result: Result<(), I2cError> = Err(I2cError::NoAcknowledge);
904        let mapped = result.map_err(PicoDeGalloError::Endpoint);
905        match mapped {
906            Err(PicoDeGalloError::Endpoint(I2cError::NoAcknowledge)) => {}
907            _ => panic!("expected Endpoint(I2cError::NoAcknowledge)"),
908        }
909    }
910
911    // --- PicoDeGalloError From impl ---
912
913    #[test]
914    fn host_err_converts_to_comms_error() {
915        let host_err: HostErr<WireError> = HostErr::Closed;
916        let err: PicoDeGalloError<Infallible> = PicoDeGalloError::from(host_err);
917        match err {
918            PicoDeGalloError::Comms(HostErr::Closed) => {}
919            _ => panic!("expected Comms(Closed)"),
920        }
921    }
922
923    // --- PicoDeGalloError Debug ---
924
925    #[test]
926    fn error_debug_format_is_readable() {
927        let err: PicoDeGalloError<I2cError> = PicoDeGalloError::Endpoint(I2cError::Bus);
928        let debug = format!("{:?}", err);
929        assert!(debug.contains("Endpoint"));
930        assert!(debug.contains("Bus"));
931
932        let comms_err: PicoDeGalloError<Infallible> = PicoDeGalloError::Comms(HostErr::Closed);
933        let debug = format!("{:?}", comms_err);
934        assert!(debug.contains("Comms"));
935    }
936
937    // --- PicoDeGalloError Display ---
938
939    #[test]
940    fn error_display_endpoint() {
941        // Use a simple Display-implementing type
942        let err: PicoDeGalloError<&str> = PicoDeGalloError::Endpoint("sensor timeout");
943        let msg = format!("{err}");
944        assert!(msg.contains("endpoint error"));
945        assert!(msg.contains("sensor timeout"));
946    }
947
948    #[test]
949    fn error_display_comms() {
950        let err: PicoDeGalloError<&str> = PicoDeGalloError::Comms(HostErr::Closed);
951        let msg = format!("{err}");
952        assert!(msg.contains("communication error"));
953    }
954
955    #[test]
956    fn error_is_std_error() {
957        fn assert_error<E: std::error::Error>() {}
958        assert_error::<PicoDeGalloError<&str>>();
959    }
960
961    // --- Device enumeration ---
962
963    #[test]
964    fn list_devices_returns_vec() {
965        // Without hardware this returns an empty vec, but should not panic
966        let devices = list_devices();
967        // Each returned device must have the correct VID/PID (already filtered)
968        for dev in &devices {
969            assert!(dev.serial_number.is_some() || dev.serial_number.is_none());
970        }
971        // Mainly verifying the function doesn't panic
972        let _ = devices;
973    }
974
975    #[test]
976    fn device_description_is_clone_and_debug() {
977        let desc = DeviceDescription {
978            serial_number: Some("ABC123".to_string()),
979            manufacturer: Some("Microsoft".to_string()),
980            product: Some("Pico de Gallo".to_string()),
981        };
982        let cloned = desc.clone();
983        assert_eq!(format!("{:?}", desc), format!("{:?}", cloned));
984    }
985}