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}