Skip to main content

pico_de_gallo_ffi/
lib.rs

1//! C-compatible FFI bindings for the Pico de Gallo USB bridge.
2//!
3//! This crate wraps [`pico-de-gallo-lib`](https://docs.rs/pico-de-gallo-lib) in
4//! a C-compatible API using opaque pointers and integer status codes. It is
5//! compiled as a `cdylib` (shared library) and generates a C header via
6//! [`cbindgen`](https://docs.rs/cbindgen).
7//!
8//! # Usage from C
9//!
10//! ```c
11//! #include "pico_de_gallo.h"
12//!
13//! const PicoDeGallo *gallo = gallo_init();
14//! uint32_t id = 42;
15//! Status status = gallo_ping(gallo, &id);
16//! // id now contains the round-tripped value
17//! gallo_free(gallo);
18//! ```
19//!
20//! # Lifecycle
21//!
22//! 1. Call [`gallo_init`] (or [`gallo_init_with_serial_number`]) to create a context.
23//! 2. Use `gallo_*` functions, passing the context pointer.
24//! 3. Call [`gallo_free`] to release resources.
25//!
26//! # Thread Safety
27//!
28//! The context pointer is safe to share across threads — the inner type is
29//! `Send + Sync` (enforced by a compile-time assertion). Each function call
30//! creates its own async executor via [`futures::executor::block_on`], so
31//! concurrent calls from multiple threads are safe.
32//!
33//! # Status Codes
34//!
35//! All functions return a [`Status`] code. [`Status::Ok`] (0) indicates success;
36//! negative values indicate errors. See [`Status`] for the full list.
37
38use futures::executor::block_on;
39use pico_de_gallo_lib::{
40    self as lib, AdcChannel, AdcError, GpioError, I2cError, OneWireError, PicoDeGalloError,
41    PwmError, SpiError, UartError,
42};
43use std::ffi::CStr;
44use std::os::raw::c_char;
45
46/// Opaque handle to a Pico de Gallo device context.
47///
48/// Created by [`gallo_init`] or [`gallo_init_with_serial_number`] and released
49/// by [`gallo_free`]. This type is a thin wrapper around
50/// [`pico_de_gallo_lib::PicoDeGallo`] and must not be constructed directly
51/// from C code.
52pub struct PicoDeGallo(lib::PicoDeGallo);
53
54// Compile-time assertion: the FFI handle must be safe to share across
55// C threads. lib::PicoDeGallo is Send + Sync because HostClient is
56// internally Arc-wrapped.
57const _: () = {
58    const fn assert_send_sync<T: Send + Sync>() {}
59    assert_send_sync::<PicoDeGallo>();
60};
61
62// ----------------------------- Status Codes -----------------------------
63
64/// Status codes returned by all FFI functions.
65///
66/// [`Status::Ok`] (0) indicates success. All error codes are negative integers
67/// with stable values suitable for use in C `switch` statements.
68#[repr(i32)]
69#[derive(Debug, PartialEq)]
70pub enum Status {
71    /// Operation successful
72    Ok = 0,
73    /// I2c Read failed
74    I2cReadFailed = -1,
75    /// I2c Write failed
76    I2cWriteFailed = -2,
77    /// Firmware produced an invalid response
78    InvalidResponse = -3,
79    /// Library was not initialized
80    Uninitialized = -4,
81    /// Caller passed an invalid argument
82    InvalidArgument = -5,
83    /// Ping failed
84    PingFailed = -6,
85    /// Spi Read failed
86    SpiReadFailed = -7,
87    /// Spi Write failed
88    SpiWriteFailed = -8,
89    /// Spi Flush failed
90    SpiFlushFailed = -9,
91    /// Gpio get failed
92    GpioGetFailed = -10,
93    /// Gpio put failed
94    GpioPutFailed = -11,
95    /// Gpio wait failed
96    GpioWaitFailed = -12,
97    /// Set config failed
98    SetConfigFailed = -13,
99    /// Version failed
100    VersionFailed = -14,
101    /// I2c Write Read failed
102    I2cWriteReadFailed = -15,
103    /// I2c Set config failed
104    I2cSetConfigFailed = -16,
105    /// Spi Set config failed
106    SpiSetConfigFailed = -17,
107    /// I2C target did not acknowledge
108    I2cNack = -18,
109    /// I2C bus error
110    I2cBusError = -19,
111    /// I2C arbitration loss
112    I2cArbitrationLoss = -20,
113    /// I2C data overrun
114    I2cOverrun = -21,
115    /// Buffer exceeds firmware transfer limit
116    BufferTooLong = -22,
117    /// I2C address out of range
118    I2cAddressOutOfRange = -23,
119    /// GPIO pin number is invalid
120    GpioInvalidPin = -24,
121    /// USB communication failure
122    CommsFailed = -25,
123    /// I2C bus scan failed
124    I2cScanFailed = -26,
125    /// GPIO set config failed
126    GpioSetConfigFailed = -27,
127    /// GPIO pin configured in wrong direction for the requested operation
128    GpioWrongDirection = -28,
129    /// I2C get-config query failed
130    I2cGetConfigFailed = -29,
131    /// SPI get-config query failed
132    SpiGetConfigFailed = -30,
133    /// UART read failed
134    UartReadFailed = -31,
135    /// UART write failed
136    UartWriteFailed = -32,
137    /// UART flush failed
138    UartFlushFailed = -33,
139    /// UART receiver overrun
140    UartOverrun = -34,
141    /// UART break condition detected
142    UartBreak = -35,
143    /// UART parity error
144    UartParity = -36,
145    /// UART framing error
146    UartFraming = -37,
147    /// Invalid baud rate
148    UartInvalidBaudRate = -38,
149    /// UART set-config failed
150    UartSetConfigFailed = -39,
151    /// UART get-config query failed
152    UartGetConfigFailed = -40,
153    /// PWM set-duty-cycle failed
154    PwmSetDutyCycleFailed = -41,
155    /// PWM get-duty-cycle query failed
156    PwmGetDutyCycleFailed = -42,
157    /// PWM enable failed
158    PwmEnableFailed = -43,
159    /// PWM disable failed
160    PwmDisableFailed = -44,
161    /// PWM set-config failed
162    PwmSetConfigFailed = -45,
163    /// PWM get-config query failed
164    PwmGetConfigFailed = -46,
165    /// Invalid PWM channel
166    PwmInvalidChannel = -47,
167    /// Invalid PWM duty cycle
168    PwmInvalidDutyCycle = -48,
169    /// Invalid PWM configuration
170    PwmInvalidConfiguration = -49,
171    /// ADC read failed
172    AdcReadFailed = -50,
173    /// ADC get-config query failed
174    AdcGetConfigFailed = -51,
175    /// ADC conversion error
176    AdcConversionFailed = -52,
177    /// GPIO pin is currently being monitored (subscribed)
178    GpioPinMonitored = -53,
179    /// GPIO pin is not being monitored (not subscribed)
180    GpioPinNotMonitored = -54,
181    /// GPIO subscribe failed
182    GpioSubscribeFailed = -55,
183    /// GPIO unsubscribe failed
184    GpioUnsubscribeFailed = -56,
185    /// 1-Wire: no device responded to reset
186    OneWireNoPresence = -57,
187    /// 1-Wire: bus communication error
188    OneWireBusError = -58,
189    /// 1-Wire: read operation failed
190    OneWireReadFailed = -59,
191    /// 1-Wire: write operation failed
192    OneWireWriteFailed = -60,
193    /// 1-Wire: ROM search failed
194    OneWireSearchFailed = -61,
195    /// Device info query failed
196    DeviceInfoFailed = -62,
197    /// Schema version mismatch between host and firmware
198    SchemaMismatch = -63,
199    /// Firmware does not support the device/info endpoint
200    LegacyFirmware = -64,
201    /// Peripheral is not supported on this hardware revision
202    Unsupported = -65,
203}
204
205// ----------------------------- Error Mapping Helpers -----------------------------
206
207fn i2c_error_to_status(e: PicoDeGalloError<I2cError>) -> Status {
208    match e {
209        PicoDeGalloError::Endpoint(I2cError::NoAcknowledge) => Status::I2cNack,
210        PicoDeGalloError::Endpoint(I2cError::Bus) => Status::I2cBusError,
211        PicoDeGalloError::Endpoint(I2cError::ArbitrationLoss) => Status::I2cArbitrationLoss,
212        PicoDeGalloError::Endpoint(I2cError::Overrun) => Status::I2cOverrun,
213        PicoDeGalloError::Endpoint(I2cError::BufferTooLong) => Status::BufferTooLong,
214        PicoDeGalloError::Endpoint(I2cError::AddressOutOfRange) => Status::I2cAddressOutOfRange,
215        PicoDeGalloError::Endpoint(I2cError::Other) => Status::I2cReadFailed,
216        PicoDeGalloError::Comms(_) => Status::CommsFailed,
217    }
218}
219
220fn spi_error_to_status(e: PicoDeGalloError<SpiError>) -> Status {
221    match e {
222        PicoDeGalloError::Endpoint(SpiError::BufferTooLong) => Status::BufferTooLong,
223        PicoDeGalloError::Endpoint(SpiError::Other) => Status::SpiReadFailed,
224        PicoDeGalloError::Comms(_) => Status::CommsFailed,
225    }
226}
227
228fn gpio_error_to_status(e: PicoDeGalloError<GpioError>) -> Status {
229    match e {
230        PicoDeGalloError::Endpoint(GpioError::InvalidPin) => Status::GpioInvalidPin,
231        PicoDeGalloError::Endpoint(GpioError::WrongDirection) => Status::GpioWrongDirection,
232        PicoDeGalloError::Endpoint(GpioError::PinMonitored) => Status::GpioPinMonitored,
233        PicoDeGalloError::Endpoint(GpioError::PinNotMonitored) => Status::GpioPinNotMonitored,
234        PicoDeGalloError::Endpoint(GpioError::Other) => Status::GpioGetFailed,
235        PicoDeGalloError::Comms(_) => Status::CommsFailed,
236    }
237}
238
239fn uart_error_to_status(e: PicoDeGalloError<UartError>) -> Status {
240    match e {
241        PicoDeGalloError::Endpoint(UartError::BufferTooLong) => Status::BufferTooLong,
242        PicoDeGalloError::Endpoint(UartError::Overrun) => Status::UartOverrun,
243        PicoDeGalloError::Endpoint(UartError::Break) => Status::UartBreak,
244        PicoDeGalloError::Endpoint(UartError::Parity) => Status::UartParity,
245        PicoDeGalloError::Endpoint(UartError::Framing) => Status::UartFraming,
246        PicoDeGalloError::Endpoint(UartError::InvalidBaudRate) => Status::UartInvalidBaudRate,
247        PicoDeGalloError::Endpoint(UartError::Other) => Status::UartReadFailed,
248        PicoDeGalloError::Endpoint(UartError::Unsupported) => Status::Unsupported,
249        PicoDeGalloError::Comms(_) => Status::CommsFailed,
250    }
251}
252
253fn pwm_error_to_status(e: PicoDeGalloError<PwmError>) -> Status {
254    match e {
255        PicoDeGalloError::Endpoint(PwmError::InvalidChannel) => Status::PwmInvalidChannel,
256        PicoDeGalloError::Endpoint(PwmError::InvalidDutyCycle) => Status::PwmInvalidDutyCycle,
257        PicoDeGalloError::Endpoint(PwmError::InvalidConfiguration) => {
258            Status::PwmInvalidConfiguration
259        }
260        PicoDeGalloError::Endpoint(PwmError::Other) => Status::PwmSetDutyCycleFailed,
261        PicoDeGalloError::Comms(_) => Status::CommsFailed,
262    }
263}
264
265fn adc_error_to_status(e: PicoDeGalloError<AdcError>) -> Status {
266    match e {
267        PicoDeGalloError::Endpoint(AdcError::ConversionFailed) => Status::AdcConversionFailed,
268        PicoDeGalloError::Endpoint(AdcError::Other) => Status::AdcReadFailed,
269        PicoDeGalloError::Endpoint(AdcError::Unsupported) => Status::Unsupported,
270        PicoDeGalloError::Comms(_) => Status::CommsFailed,
271    }
272}
273
274fn onewire_error_to_status(e: PicoDeGalloError<OneWireError>) -> Status {
275    match e {
276        PicoDeGalloError::Endpoint(OneWireError::NoPresence) => Status::OneWireNoPresence,
277        PicoDeGalloError::Endpoint(OneWireError::BusError) => Status::OneWireBusError,
278        PicoDeGalloError::Endpoint(OneWireError::BufferTooLong) => Status::BufferTooLong,
279        PicoDeGalloError::Endpoint(OneWireError::Other) => Status::OneWireReadFailed,
280        PicoDeGalloError::Endpoint(OneWireError::Unsupported) => Status::Unsupported,
281        PicoDeGalloError::Comms(_) => Status::CommsFailed,
282    }
283}
284
285// ----------------------------- Library Lifetime -----------------------------
286
287/// gallo_init - Initialize the library context.
288///
289/// Returns an opaque representation of the underlying PicoDeGallo
290/// device.
291#[unsafe(no_mangle)]
292pub extern "C" fn gallo_init() -> *const PicoDeGallo {
293    let gallo = Box::new(PicoDeGallo(lib::PicoDeGallo::new()));
294
295    Box::into_raw(gallo) as *const PicoDeGallo
296}
297
298/// gallo_init_with_serial_number - Initialize the library context for
299/// a device with the given serial number.
300///
301/// Returns an opaque representation of the underlying PicoDeGallo
302/// device.
303///
304/// # Safety
305///
306/// `c_serial_number` must point to a valid c-string containing a
307/// valid Pico de Gallo serial number with a NULL-terminator.
308#[unsafe(no_mangle)]
309pub unsafe extern "C" fn gallo_init_with_serial_number(
310    c_serial_number: *const c_char,
311) -> *const PicoDeGallo {
312    if c_serial_number.is_null() {
313        eprintln!("NULL serial number received");
314        return std::ptr::null();
315    }
316
317    // Safety: Pointer is not null due to the check above. Caller must
318    // make sure to pass a null-terminated string.
319    let serial_number = unsafe { CStr::from_ptr(c_serial_number).to_str() };
320
321    if serial_number.is_err() {
322        eprintln!("Invalid UTF-8 string");
323        return std::ptr::null();
324    }
325
326    let gallo = Box::new(PicoDeGallo(lib::PicoDeGallo::new_with_serial_number(
327        serial_number.unwrap(),
328    )));
329
330    Box::into_raw(gallo) as *const PicoDeGallo
331}
332
333/// gallo_free - Releases and destroys the library context created by `gallo_init`.
334///
335/// # Safety
336///
337/// Caller must ensure that `gallo` is a valid, opaque pointer to
338/// `PicoDeGallo` returned by `gallo_init()`.
339#[unsafe(no_mangle)]
340pub unsafe extern "C" fn gallo_free(gallo: *const PicoDeGallo) {
341    if !gallo.is_null() {
342        // Safety: caller must ensure that `gallo` is a valid opaque
343        // pointer to `PicoDeGallo` returned by `gallo_init()`.
344        drop(unsafe { Box::from_raw(gallo as *mut PicoDeGallo) });
345    }
346}
347
348// ----------------------------- Ping endpoint -----------------------------
349
350/// gallo_ping - Ping the firmware and wait for a response
351///
352/// Returns the same `u32` passed as the first argument.
353///
354/// # Safety
355///
356/// Caller must ensure that `gallo` is a valid, opaque pointer to
357/// `PicoDeGallo` returned by `gallo_init()`.
358#[unsafe(no_mangle)]
359pub unsafe extern "C" fn gallo_ping(gallo: *mut PicoDeGallo, id: *mut u32) -> Status {
360    if gallo.is_null() {
361        eprintln!("Unexpected NULL context");
362        return Status::Uninitialized;
363    }
364
365    if id.is_null() {
366        eprintln!("Unexpected NULL id pointer");
367        return Status::InvalidArgument;
368    }
369
370    // Safety: caller must ensure that `gallo` is a valid opaque
371    // pointer to `PicoDeGallo` returned by `gallo_init()`.
372    let gallo = unsafe { &*gallo };
373
374    // Safety: null check above guarantees id is non-null.
375    let id_val = unsafe { *id };
376
377    let result = block_on(gallo.0.ping(id_val));
378    match result {
379        Ok(back) => {
380            unsafe { *id = back };
381            Status::Ok
382        }
383        Err(_) => Status::PingFailed,
384    }
385}
386
387// ----------------------------- I2c endpoints -----------------------------
388
389/// gallo_i2c_read - Read `len` bytes from the device at `address` into `buf`.
390///
391/// Returns `Status::Ok` in case of success or various error codes.
392///
393/// # Safety
394///
395/// Caller must ensure that `gallo` is a valid, opaque pointer to
396/// `PicoDeGallo` returned by `gallo_init()` and `buf` must be valid
397/// for `len` bytes.
398#[unsafe(no_mangle)]
399pub unsafe extern "C" fn gallo_i2c_read(
400    gallo: *mut PicoDeGallo,
401    address: u8,
402    buf: *mut u8,
403    len: usize,
404) -> Status {
405    if gallo.is_null() {
406        eprintln!("Unexpected NULL context");
407        return Status::Uninitialized;
408    }
409
410    if buf.is_null() {
411        eprintln!("Unexpected NULL buffer");
412        return Status::InvalidArgument;
413    }
414
415    if len > u16::MAX.into() {
416        eprintln!("Buffer is too large");
417        return Status::InvalidArgument;
418    }
419
420    // Safety: caller must ensure that `gallo` is a valid opaque
421    // pointer to `PicoDeGallo` returned by `gallo_init()`.
422    let gallo = unsafe { &*gallo };
423
424    // Safety: caller must ensure buf is valid for len bytes.
425    let buf = unsafe { std::slice::from_raw_parts_mut(buf, len) };
426
427    let result = block_on(gallo.0.i2c_read(address, len as u16));
428
429    match result {
430        Ok(data) => {
431            if data.len() != buf.len() {
432                eprintln!(
433                    "Firmware returned {} bytes, expected {}",
434                    data.len(),
435                    buf.len()
436                );
437                return Status::InvalidResponse;
438            }
439            buf.copy_from_slice(&data);
440            Status::Ok
441        }
442        Err(e) => i2c_error_to_status(e),
443    }
444}
445
446/// gallo_i2c_write - Write `len` bytes from `buf` to the device at `address`.
447///
448/// Returns `Status::Ok` in case of success or various error codes.
449///
450/// # Safety
451///
452/// Caller must ensure that `gallo` is a valid, opaque pointer to
453/// `PicoDeGallo` returned by `gallo_init()` and `buf` must be valid
454/// for `len` bytes.
455#[unsafe(no_mangle)]
456pub unsafe extern "C" fn gallo_i2c_write(
457    gallo: *mut PicoDeGallo,
458    address: u8,
459    buf: *const u8,
460    len: usize,
461) -> Status {
462    if gallo.is_null() {
463        eprintln!("Unexpected NULL context");
464        return Status::Uninitialized;
465    }
466
467    if buf.is_null() {
468        eprintln!("Unexpected NULL buffer");
469        return Status::InvalidArgument;
470    }
471
472    if len > u16::MAX.into() {
473        eprintln!("Buffer is too large");
474        return Status::InvalidArgument;
475    }
476
477    // Safety: caller must ensure that `gallo` is a valid opaque
478    // pointer to `PicoDeGallo` returned by `gallo_init()`.
479    let gallo = unsafe { &*gallo };
480
481    // Safety: caller must ensure buf is valid for len bytes.
482    let buf = unsafe { std::slice::from_raw_parts(buf, len) };
483
484    let result = block_on(gallo.0.i2c_write(address, buf));
485
486    match result {
487        Ok(()) => Status::Ok,
488        Err(e) => i2c_error_to_status(e),
489    }
490}
491
492/// gallo_i2c_write_read - Perform a write followed by a read.
493///
494/// Returns `Status::Ok` in case of success or various error codes.
495///
496/// # Safety
497///
498/// Caller must ensure that `gallo` is a valid, opaque pointer to
499/// `PicoDeGallo` returned by `gallo_init()`, `txbuf` must be valid
500/// for `txlen` bytes, and `rxbuf` must be valid for `rxlen` bytes.
501#[unsafe(no_mangle)]
502pub unsafe extern "C" fn gallo_i2c_write_read(
503    gallo: *mut PicoDeGallo,
504    address: u8,
505    txbuf: *const u8,
506    txlen: usize,
507    rxbuf: *mut u8,
508    rxlen: usize,
509) -> Status {
510    if gallo.is_null() {
511        eprintln!("Unexpected NULL context");
512        return Status::Uninitialized;
513    }
514
515    if txbuf.is_null() || rxbuf.is_null() {
516        eprintln!("Unexpected NULL buffer");
517        return Status::InvalidArgument;
518    }
519
520    if txlen > u16::MAX.into() || rxlen > u16::MAX.into() {
521        eprintln!("Buffer is too large");
522        return Status::InvalidArgument;
523    }
524
525    // Safety: caller must ensure that `gallo` is a valid opaque
526    // pointer to `PicoDeGallo` returned by `gallo_init()`.
527    let gallo = unsafe { &*gallo };
528
529    // Safety: caller must ensure txbuf is valid for txlen bytes.
530    let txbuf = unsafe { std::slice::from_raw_parts(txbuf, txlen) };
531
532    // Safety: caller must ensure rxbuf is valid for rxlen bytes.
533    let rxbuf = unsafe { std::slice::from_raw_parts_mut(rxbuf, rxlen) };
534
535    let result = block_on(gallo.0.i2c_write_read(address, txbuf, rxlen as u16));
536    match result {
537        Ok(data) => {
538            if data.len() != rxbuf.len() {
539                eprintln!(
540                    "Firmware returned {} bytes, expected {}",
541                    data.len(),
542                    rxbuf.len()
543                );
544                return Status::InvalidResponse;
545            }
546            rxbuf.copy_from_slice(&data);
547            Status::Ok
548        }
549        Err(e) => i2c_error_to_status(e),
550    }
551}
552
553/// gallo_i2c_scan - Scan the I2C bus for responding devices.
554///
555/// The firmware probes each 7-bit address. Addresses that ACK are written
556/// into `buf`. The actual number of devices found is written to `*found`.
557///
558/// When `include_reserved` is `false`, only the standard range (0x08–0x77)
559/// is probed; when `true`, the full range (0x00–0x7F) is scanned.
560///
561/// Returns `Status::Ok` in case of success or various error codes. If
562/// `buf_len` is smaller than the number of responding devices the buffer is
563/// filled to capacity and `*found` reflects the total count.
564///
565/// # Safety
566///
567/// Caller must ensure that `gallo` is a valid, opaque pointer to
568/// `PicoDeGallo` returned by `gallo_init()`, `buf` must be valid for
569/// `buf_len` bytes, and `found` must point to a valid `usize`.
570#[unsafe(no_mangle)]
571pub unsafe extern "C" fn gallo_i2c_scan(
572    gallo: *mut PicoDeGallo,
573    include_reserved: bool,
574    buf: *mut u8,
575    buf_len: usize,
576    found: *mut usize,
577) -> Status {
578    if gallo.is_null() {
579        eprintln!("Unexpected NULL context");
580        return Status::Uninitialized;
581    }
582
583    if buf.is_null() || found.is_null() {
584        eprintln!("Unexpected NULL pointer");
585        return Status::InvalidArgument;
586    }
587
588    // Safety: caller must ensure that `gallo` is a valid opaque
589    // pointer to `PicoDeGallo` returned by `gallo_init()`.
590    let gallo = unsafe { &*gallo };
591
592    // Safety: caller must ensure buf is valid for buf_len bytes.
593    let buf = unsafe { std::slice::from_raw_parts_mut(buf, buf_len) };
594
595    let result = block_on(gallo.0.i2c_scan(include_reserved));
596
597    match result {
598        Ok(addresses) => {
599            let copy_len = addresses.len().min(buf.len());
600            buf[..copy_len].copy_from_slice(&addresses[..copy_len]);
601            unsafe { *found = addresses.len() };
602            Status::Ok
603        }
604        Err(e) => i2c_error_to_status(e),
605    }
606}
607
608// ----------------------------- Spi endpoints -----------------------------
609
610/// gallo_spi_read - Read `len` bytes.
611///
612/// Returns `Status::Ok` in case of success or various error codes.
613///
614/// # Safety
615///
616/// Caller must ensure that `gallo` is a valid, opaque pointer to
617/// `PicoDeGallo` returned by `gallo_init()` and `buf` must be valid
618/// for `len` bytes.
619#[unsafe(no_mangle)]
620pub unsafe extern "C" fn gallo_spi_read(
621    gallo: *mut PicoDeGallo,
622    buf: *mut u8,
623    len: usize,
624) -> Status {
625    if gallo.is_null() {
626        eprintln!("Unexpected NULL context");
627        return Status::Uninitialized;
628    }
629
630    if buf.is_null() {
631        eprintln!("Unexpected NULL buffer");
632        return Status::InvalidArgument;
633    }
634
635    if len > u16::MAX.into() {
636        eprintln!("Buffer is too large");
637        return Status::InvalidArgument;
638    }
639
640    // Safety: caller must ensure that `gallo` is a valid opaque
641    // pointer to `PicoDeGallo` returned by `gallo_init()`.
642    let gallo = unsafe { &*gallo };
643
644    // Safety: caller must ensure buf is valid for len bytes.
645    let buf = unsafe { std::slice::from_raw_parts_mut(buf, len) };
646
647    let result = block_on(gallo.0.spi_read(len as u16));
648
649    match result {
650        Ok(data) => {
651            if data.len() != buf.len() {
652                eprintln!(
653                    "Firmware returned {} bytes, expected {}",
654                    data.len(),
655                    buf.len()
656                );
657                return Status::InvalidResponse;
658            }
659            buf.copy_from_slice(&data);
660            Status::Ok
661        }
662        Err(e) => spi_error_to_status(e),
663    }
664}
665
666/// gallo_spi_write - Write `len` bytes from `buf`.
667///
668/// Returns `Status::Ok` in case of success or various error codes.
669///
670/// # Safety
671///
672/// Caller must ensure that `gallo` is a valid, opaque pointer to
673/// `PicoDeGallo` returned by `gallo_init()` and `buf` must be valid
674/// for `len` bytes.
675#[unsafe(no_mangle)]
676pub unsafe extern "C" fn gallo_spi_write(
677    gallo: *mut PicoDeGallo,
678    buf: *const u8,
679    len: usize,
680) -> Status {
681    if gallo.is_null() {
682        eprintln!("Unexpected NULL context");
683        return Status::Uninitialized;
684    }
685
686    if buf.is_null() {
687        eprintln!("Unexpected NULL buffer");
688        return Status::InvalidArgument;
689    }
690
691    if len > u16::MAX.into() {
692        eprintln!("Buffer is too large");
693        return Status::InvalidArgument;
694    }
695
696    // Safety: caller must ensure that `gallo` is a valid opaque
697    // pointer to `PicoDeGallo` returned by `gallo_init()`.
698    let gallo = unsafe { &*gallo };
699
700    // Safety: caller must ensure buf is valid for len bytes.
701    let buf = unsafe { std::slice::from_raw_parts(buf, len) };
702
703    let result = block_on(gallo.0.spi_write(buf));
704
705    match result {
706        Ok(()) => Status::Ok,
707        Err(e) => spi_error_to_status(e),
708    }
709}
710
711/// gallo_spi_flush - Flush the SPI interface.
712///
713/// Returns `Status::Ok` in case of success or various error codes.
714///
715/// # Safety
716///
717/// Caller must ensure that `gallo` is a valid, opaque pointer to
718/// `PicoDeGallo` returned by `gallo_init()`.
719#[unsafe(no_mangle)]
720pub unsafe extern "C" fn gallo_spi_flush(gallo: *mut PicoDeGallo) -> Status {
721    if gallo.is_null() {
722        eprintln!("Unexpected NULL context");
723        return Status::Uninitialized;
724    }
725
726    // Safety: caller must ensure that `gallo` is a valid opaque
727    // pointer to `PicoDeGallo` returned by `gallo_init()`.
728    let gallo = unsafe { &*gallo };
729
730    let result = block_on(gallo.0.spi_flush());
731
732    match result {
733        Ok(()) => Status::Ok,
734        Err(e) => spi_error_to_status(e),
735    }
736}
737
738// ----------------------------- Gpio endpoints -----------------------------
739
740/// gallo_gpio_get - Get the state of a given GPIO pin.
741///
742/// Returns `Status::Ok` in case of success or various error codes.
743///
744/// # Safety
745///
746/// Caller must ensure that `gallo` is a valid, opaque pointer to
747/// `PicoDeGallo` returned by `gallo_init()`.
748#[unsafe(no_mangle)]
749pub unsafe extern "C" fn gallo_gpio_get(
750    gallo: *mut PicoDeGallo,
751    pin: u8,
752    state: *mut bool,
753) -> Status {
754    if gallo.is_null() {
755        eprintln!("Unexpected NULL context");
756        return Status::Uninitialized;
757    }
758
759    if state.is_null() {
760        eprintln!("Unexpected NULL state pointer");
761        return Status::InvalidArgument;
762    }
763
764    // Safety: caller must ensure that `gallo` is a valid opaque
765    // pointer to `PicoDeGallo` returned by `gallo_init()`.
766    let gallo = unsafe { &*gallo };
767
768    let result = block_on(gallo.0.gpio_get(pin));
769
770    match result {
771        Ok(s) => {
772            unsafe { *state = s == lib::GpioState::High };
773            Status::Ok
774        }
775        Err(e) => gpio_error_to_status(e),
776    }
777}
778
779/// gallo_gpio_put - Set the state of a given GPIO pin.
780///
781/// Returns `Status::Ok` in case of success or various error codes.
782///
783/// # Safety
784///
785/// Caller must ensure that `gallo` is a valid, opaque pointer to
786/// `PicoDeGallo` returned by `gallo_init()`.
787#[unsafe(no_mangle)]
788pub unsafe extern "C" fn gallo_gpio_put(gallo: *mut PicoDeGallo, pin: u8, state: bool) -> Status {
789    if gallo.is_null() {
790        eprintln!("Unexpected NULL context");
791        return Status::Uninitialized;
792    }
793
794    // Safety: caller must ensure that `gallo` is a valid opaque
795    // pointer to `PicoDeGallo` returned by `gallo_init()`.
796    let gallo = unsafe { &*gallo };
797
798    let s = if state {
799        lib::GpioState::High
800    } else {
801        lib::GpioState::Low
802    };
803    let result = block_on(gallo.0.gpio_put(pin, s));
804
805    match result {
806        Ok(()) => Status::Ok,
807        Err(e) => gpio_error_to_status(e),
808    }
809}
810
811/// gallo_gpio_wait_for_high - Waits for a high level on a given GPIO pin.
812///
813/// Returns `Status::Ok` in case of success or various error codes.
814///
815/// # Safety
816///
817/// Caller must ensure that `gallo` is a valid, opaque pointer to
818/// `PicoDeGallo` returned by `gallo_init()`.
819#[unsafe(no_mangle)]
820pub unsafe extern "C" fn gallo_gpio_wait_for_high(gallo: *mut PicoDeGallo, pin: u8) -> Status {
821    if gallo.is_null() {
822        eprintln!("Unexpected NULL context");
823        return Status::Uninitialized;
824    }
825
826    // Safety: caller must ensure that `gallo` is a valid opaque
827    // pointer to `PicoDeGallo` returned by `gallo_init()`.
828    let gallo = unsafe { &*gallo };
829
830    let result = block_on(gallo.0.gpio_wait_for_high(pin));
831
832    match result {
833        Ok(()) => Status::Ok,
834        Err(e) => gpio_error_to_status(e),
835    }
836}
837
838/// gallo_gpio_wait_for_low - Waits for a low level on a given GPIO pin.
839///
840/// Returns `Status::Ok` in case of success or various error codes.
841///
842/// # Safety
843///
844/// Caller must ensure that `gallo` is a valid, opaque pointer to
845/// `PicoDeGallo` returned by `gallo_init()`.
846#[unsafe(no_mangle)]
847pub unsafe extern "C" fn gallo_gpio_wait_for_low(gallo: *mut PicoDeGallo, pin: u8) -> Status {
848    if gallo.is_null() {
849        eprintln!("Unexpected NULL context");
850        return Status::Uninitialized;
851    }
852
853    // Safety: caller must ensure that `gallo` is a valid opaque
854    // pointer to `PicoDeGallo` returned by `gallo_init()`.
855    let gallo = unsafe { &*gallo };
856
857    let result = block_on(gallo.0.gpio_wait_for_low(pin));
858
859    match result {
860        Ok(()) => Status::Ok,
861        Err(e) => gpio_error_to_status(e),
862    }
863}
864
865/// gallo_gpio_wait_for_rising_edge - Waits for a rising edge on a given GPIO pin.
866///
867/// Returns `Status::Ok` in case of success or various error codes.
868///
869/// # Safety
870///
871/// Caller must ensure that `gallo` is a valid, opaque pointer to
872/// `PicoDeGallo` returned by `gallo_init()`.
873#[unsafe(no_mangle)]
874pub unsafe extern "C" fn gallo_gpio_wait_for_rising_edge(
875    gallo: *mut PicoDeGallo,
876    pin: u8,
877) -> Status {
878    if gallo.is_null() {
879        eprintln!("Unexpected NULL context");
880        return Status::Uninitialized;
881    }
882
883    // Safety: caller must ensure that `gallo` is a valid opaque
884    // pointer to `PicoDeGallo` returned by `gallo_init()`.
885    let gallo = unsafe { &*gallo };
886
887    let result = block_on(gallo.0.gpio_wait_for_rising_edge(pin));
888
889    match result {
890        Ok(()) => Status::Ok,
891        Err(e) => gpio_error_to_status(e),
892    }
893}
894
895/// gallo_gpio_wait_for_falling_edge - Waits for a falling edge on a given GPIO pin.
896///
897/// Returns `Status::Ok` in case of success or various error codes.
898///
899/// # Safety
900///
901/// Caller must ensure that `gallo` is a valid, opaque pointer to
902/// `PicoDeGallo` returned by `gallo_init()`.
903#[unsafe(no_mangle)]
904pub unsafe extern "C" fn gallo_gpio_wait_for_falling_edge(
905    gallo: *mut PicoDeGallo,
906    pin: u8,
907) -> Status {
908    if gallo.is_null() {
909        eprintln!("Unexpected NULL context");
910        return Status::Uninitialized;
911    }
912
913    // Safety: caller must ensure that `gallo` is a valid opaque
914    // pointer to `PicoDeGallo` returned by `gallo_init()`.
915    let gallo = unsafe { &*gallo };
916
917    let result = block_on(gallo.0.gpio_wait_for_falling_edge(pin));
918
919    match result {
920        Ok(()) => Status::Ok,
921        Err(e) => gpio_error_to_status(e),
922    }
923}
924
925/// gallo_gpio_wait_for_any_edge - Waits for a any edge on a given GPIO pin.
926///
927/// Returns `Status::Ok` in case of success or various error codes.
928///
929/// # Safety
930///
931/// Caller must ensure that `gallo` is a valid, opaque pointer to
932/// `PicoDeGallo` returned by `gallo_init()`.
933#[unsafe(no_mangle)]
934pub unsafe extern "C" fn gallo_gpio_wait_for_any_edge(gallo: *mut PicoDeGallo, pin: u8) -> Status {
935    if gallo.is_null() {
936        eprintln!("Unexpected NULL context");
937        return Status::Uninitialized;
938    }
939
940    // Safety: caller must ensure that `gallo` is a valid opaque
941    // pointer to `PicoDeGallo` returned by `gallo_init()`.
942    let gallo = unsafe { &*gallo };
943
944    let result = block_on(gallo.0.gpio_wait_for_any_edge(pin));
945
946    match result {
947        Ok(()) => Status::Ok,
948        Err(e) => gpio_error_to_status(e),
949    }
950}
951
952// ----------------------------- GPIO Set config endpoint -----------------------------
953
954/// gallo_gpio_set_config - Configure a GPIO pin's direction and pull resistor.
955///
956/// `direction`: 0 = Input, 1 = Output.
957/// `pull`: 0 = None, 1 = Pull-up, 2 = Pull-down.
958///
959/// After configuration, the pin enters explicit mode and get/put will no
960/// longer auto-switch direction. Calling `gallo_gpio_put` on an input pin
961/// (or `gallo_gpio_get`/wait on an output pin) returns
962/// `GpioWrongDirection`.
963///
964/// Returns `Status::Ok` in case of success or various error codes.
965///
966/// # Safety
967///
968/// Caller must ensure that `gallo` is a valid, opaque pointer to
969/// `PicoDeGallo` returned by `gallo_init()`.
970#[unsafe(no_mangle)]
971pub unsafe extern "C" fn gallo_gpio_set_config(
972    gallo: *mut PicoDeGallo,
973    pin: u8,
974    direction: u8,
975    pull: u8,
976) -> Status {
977    if gallo.is_null() {
978        eprintln!("Unexpected NULL context");
979        return Status::Uninitialized;
980    }
981
982    let dir = match direction {
983        0 => lib::GpioDirection::Input,
984        1 => lib::GpioDirection::Output,
985        _ => {
986            eprintln!("Invalid direction value: {direction}");
987            return Status::InvalidArgument;
988        }
989    };
990
991    let pull_cfg = match pull {
992        0 => lib::GpioPull::None,
993        1 => lib::GpioPull::Up,
994        2 => lib::GpioPull::Down,
995        _ => {
996            eprintln!("Invalid pull value: {pull}");
997            return Status::InvalidArgument;
998        }
999    };
1000
1001    // Safety: caller must ensure that `gallo` is a valid opaque
1002    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1003    let gallo = unsafe { &*gallo };
1004
1005    let result = block_on(gallo.0.gpio_set_config(pin, dir, pull_cfg));
1006
1007    match result {
1008        Ok(()) => Status::Ok,
1009        Err(e) => gpio_error_to_status(e),
1010    }
1011}
1012
1013// ----------------------------- GPIO Subscribe/Unsubscribe endpoints -----------------------------
1014
1015/// gallo_gpio_subscribe - Subscribe to GPIO edge events on a pin.
1016///
1017/// `edge`: 0 = Rising, 1 = Falling, 2 = Any. Any other value returns
1018/// `Status::InvalidArgument`.
1019///
1020/// While subscribed, other GPIO operations on this pin will return
1021/// `Status::GpioPinMonitored`. Use [`gallo_gpio_unsubscribe`] to release.
1022///
1023/// Returns `Status::Ok` on success.
1024///
1025/// # Safety
1026///
1027/// Caller must ensure that `gallo` is a valid, opaque pointer to
1028/// `PicoDeGallo` returned by `gallo_init()`.
1029#[unsafe(no_mangle)]
1030pub unsafe extern "C" fn gallo_gpio_subscribe(
1031    gallo: *mut PicoDeGallo,
1032    pin: u8,
1033    edge: u8,
1034) -> Status {
1035    if gallo.is_null() {
1036        eprintln!("Unexpected NULL context");
1037        return Status::Uninitialized;
1038    }
1039
1040    let edge_val = match edge {
1041        0 => lib::GpioEdge::Rising,
1042        1 => lib::GpioEdge::Falling,
1043        2 => lib::GpioEdge::Any,
1044        _ => {
1045            eprintln!("Invalid edge value: {edge}");
1046            return Status::InvalidArgument;
1047        }
1048    };
1049
1050    // Safety: caller must ensure that `gallo` is a valid opaque
1051    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1052    let gallo = unsafe { &*gallo };
1053
1054    let result = block_on(gallo.0.gpio_subscribe(pin, edge_val));
1055
1056    match result {
1057        Ok(()) => Status::Ok,
1058        Err(e) => gpio_error_to_status(e),
1059    }
1060}
1061
1062/// gallo_gpio_unsubscribe - Unsubscribe from GPIO edge events on a pin.
1063///
1064/// Stops monitoring and returns the pin to normal operation. Returns
1065/// `Status::GpioPinNotMonitored` if the pin is not currently subscribed.
1066///
1067/// Returns `Status::Ok` on success.
1068///
1069/// # Safety
1070///
1071/// Caller must ensure that `gallo` is a valid, opaque pointer to
1072/// `PicoDeGallo` returned by `gallo_init()`.
1073#[unsafe(no_mangle)]
1074pub unsafe extern "C" fn gallo_gpio_unsubscribe(gallo: *mut PicoDeGallo, pin: u8) -> Status {
1075    if gallo.is_null() {
1076        eprintln!("Unexpected NULL context");
1077        return Status::Uninitialized;
1078    }
1079
1080    // Safety: caller must ensure that `gallo` is a valid opaque
1081    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1082    let gallo = unsafe { &*gallo };
1083
1084    let result = block_on(gallo.0.gpio_unsubscribe(pin));
1085
1086    match result {
1087        Ok(()) => Status::Ok,
1088        Err(e) => gpio_error_to_status(e),
1089    }
1090}
1091
1092// ----------------------------- I2C Set config endpoint -----------------------------
1093
1094/// gallo_i2c_set_config - Sets the I2C bus configuration parameters.
1095///
1096/// `frequency`: 0 = Standard (100 kHz), 1 = Fast (400 kHz),
1097/// 2 = Fast+ (1 MHz). Any other value returns `Status::InvalidArgument`.
1098///
1099/// Returns `Status::Ok` in case of success or various error codes.
1100///
1101/// # Safety
1102///
1103/// Caller must ensure that `gallo` is a valid, opaque pointer to
1104/// `PicoDeGallo` returned by `gallo_init()`.
1105#[unsafe(no_mangle)]
1106pub unsafe extern "C" fn gallo_i2c_set_config(gallo: *mut PicoDeGallo, frequency: u8) -> Status {
1107    if gallo.is_null() {
1108        eprintln!("Unexpected NULL context");
1109        return Status::Uninitialized;
1110    }
1111
1112    let freq = match frequency {
1113        0 => lib::I2cFrequency::Standard,
1114        1 => lib::I2cFrequency::Fast,
1115        2 => lib::I2cFrequency::FastPlus,
1116        _ => return Status::InvalidArgument,
1117    };
1118
1119    // Safety: caller must ensure that `gallo` is a valid opaque
1120    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1121    let gallo = unsafe { &*gallo };
1122
1123    let result = block_on(gallo.0.i2c_set_config(freq));
1124
1125    match result {
1126        Ok(()) => Status::Ok,
1127        Err(e) => i2c_error_to_status(e),
1128    }
1129}
1130
1131// ----------------------------- SPI Set config endpoint -----------------------------
1132
1133/// gallo_spi_set_config - Sets the SPI bus configuration parameters.
1134///
1135/// `spi_phase`: false means "Capture on first transition" or CPHA=0,
1136/// true means "Capture on second transition" or CPHA=1.
1137///
1138/// `spi_polarity`: false means "Idle low" or CPOL=0, true means "Idle
1139/// high" or CPOL=1.
1140///
1141/// Returns `Status::Ok` in case of success or various error codes.
1142///
1143/// # Safety
1144///
1145/// Caller must ensure that `gallo` is a valid, opaque pointer to
1146/// `PicoDeGallo` returned by `gallo_init()`.
1147#[unsafe(no_mangle)]
1148pub unsafe extern "C" fn gallo_spi_set_config(
1149    gallo: *mut PicoDeGallo,
1150    frequency: u32,
1151    spi_phase: bool,
1152    spi_polarity: bool,
1153) -> Status {
1154    if gallo.is_null() {
1155        eprintln!("Unexpected NULL context");
1156        return Status::Uninitialized;
1157    }
1158
1159    // Safety: caller must ensure that `gallo` is a valid opaque
1160    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1161    let gallo = unsafe { &*gallo };
1162
1163    let phase = if spi_phase {
1164        lib::SpiPhase::CaptureOnSecondTransition
1165    } else {
1166        lib::SpiPhase::CaptureOnFirstTransition
1167    };
1168
1169    let polarity = if spi_polarity {
1170        lib::SpiPolarity::IdleHigh
1171    } else {
1172        lib::SpiPolarity::IdleLow
1173    };
1174
1175    let result = block_on(gallo.0.spi_set_config(frequency, phase, polarity));
1176
1177    match result {
1178        Ok(()) => Status::Ok,
1179        Err(e) => spi_error_to_status(e),
1180    }
1181}
1182
1183// ----------------------------- I2C Get config endpoint -----------------------------
1184
1185/// gallo_i2c_get_config - Queries the current I2C bus configuration.
1186///
1187/// On success, writes the current frequency to `*out_frequency`:
1188/// 0 = Standard (100 kHz), 1 = Fast (400 kHz), 2 = Fast+ (1 MHz).
1189///
1190/// Returns `Status::Ok` in case of success or various error codes.
1191///
1192/// # Safety
1193///
1194/// Caller must ensure that `gallo` is a valid, opaque pointer to
1195/// `PicoDeGallo` returned by `gallo_init()`, and that `out_frequency`
1196/// is a valid pointer to a `u8`.
1197#[unsafe(no_mangle)]
1198pub unsafe extern "C" fn gallo_i2c_get_config(
1199    gallo: *mut PicoDeGallo,
1200    out_frequency: *mut u8,
1201) -> Status {
1202    if gallo.is_null() {
1203        eprintln!("Unexpected NULL context");
1204        return Status::Uninitialized;
1205    }
1206
1207    if out_frequency.is_null() {
1208        eprintln!("Unexpected NULL out_frequency pointer");
1209        return Status::InvalidArgument;
1210    }
1211
1212    // Safety: caller must ensure that `gallo` is a valid opaque
1213    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1214    let gallo = unsafe { &*gallo };
1215
1216    let result = block_on(gallo.0.i2c_get_config());
1217
1218    match result {
1219        Ok(freq) => {
1220            unsafe {
1221                *out_frequency = freq as u8;
1222            }
1223            Status::Ok
1224        }
1225        Err(_) => Status::I2cGetConfigFailed,
1226    }
1227}
1228
1229// ----------------------------- SPI Get config endpoint -----------------------------
1230
1231/// gallo_spi_get_config - Queries the current SPI bus configuration.
1232///
1233/// On success, writes the current SPI parameters:
1234/// - `*out_frequency`: SPI clock frequency in Hz
1235/// - `*out_phase`: false = CPHA=0, true = CPHA=1
1236/// - `*out_polarity`: false = CPOL=0, true = CPOL=1
1237///
1238/// Returns `Status::Ok` in case of success or various error codes.
1239///
1240/// # Safety
1241///
1242/// Caller must ensure that `gallo` is a valid, opaque pointer to
1243/// `PicoDeGallo` returned by `gallo_init()`, and that all output
1244/// pointers are valid.
1245#[unsafe(no_mangle)]
1246pub unsafe extern "C" fn gallo_spi_get_config(
1247    gallo: *mut PicoDeGallo,
1248    out_frequency: *mut u32,
1249    out_phase: *mut bool,
1250    out_polarity: *mut bool,
1251) -> Status {
1252    if gallo.is_null() {
1253        eprintln!("Unexpected NULL context");
1254        return Status::Uninitialized;
1255    }
1256
1257    if out_frequency.is_null() || out_phase.is_null() || out_polarity.is_null() {
1258        eprintln!("Unexpected NULL output pointer");
1259        return Status::InvalidArgument;
1260    }
1261
1262    // Safety: caller must ensure that `gallo` is a valid opaque
1263    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1264    let gallo = unsafe { &*gallo };
1265
1266    let result = block_on(gallo.0.spi_get_config());
1267
1268    match result {
1269        Ok(info) => {
1270            unsafe {
1271                *out_frequency = info.spi_frequency;
1272                *out_phase = matches!(info.spi_phase, lib::SpiPhase::CaptureOnSecondTransition);
1273                *out_polarity = matches!(info.spi_polarity, lib::SpiPolarity::IdleHigh);
1274            }
1275            Status::Ok
1276        }
1277        Err(_) => Status::SpiGetConfigFailed,
1278    }
1279}
1280
1281// ----------------------------- UART Read endpoint -----------------------------
1282
1283/// gallo_uart_read - Read bytes from the UART bus.
1284///
1285/// Reads up to `count` bytes into `buf`. On success, writes the actual
1286/// number of bytes read to `*out_len`. If no data arrives within
1287/// `timeout_ms` milliseconds, sets `*out_len = 0` and returns
1288/// `Status::Ok`.
1289///
1290/// Returns `Status::Ok` in case of success or various error codes.
1291///
1292/// # Safety
1293///
1294/// Caller must ensure that `gallo` is a valid, opaque pointer to
1295/// `PicoDeGallo` returned by `gallo_init()`, that `buf` points to at
1296/// least `count` bytes, and `out_len` is a valid pointer.
1297#[unsafe(no_mangle)]
1298pub unsafe extern "C" fn gallo_uart_read(
1299    gallo: *mut PicoDeGallo,
1300    buf: *mut u8,
1301    count: u16,
1302    timeout_ms: u32,
1303    out_len: *mut u16,
1304) -> Status {
1305    if gallo.is_null() {
1306        eprintln!("Unexpected NULL context");
1307        return Status::Uninitialized;
1308    }
1309
1310    if buf.is_null() || out_len.is_null() {
1311        eprintln!("Unexpected NULL pointer");
1312        return Status::InvalidArgument;
1313    }
1314
1315    // Safety: caller must ensure that `gallo` is a valid opaque
1316    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1317    let gallo = unsafe { &*gallo };
1318
1319    let result = block_on(gallo.0.uart_read(count, timeout_ms));
1320
1321    match result {
1322        Ok(data) => {
1323            let len = data.len().min(count as usize);
1324            unsafe {
1325                std::ptr::copy_nonoverlapping(data.as_ptr(), buf, len);
1326                *out_len = len as u16;
1327            }
1328            Status::Ok
1329        }
1330        Err(e) => uart_error_to_status(e),
1331    }
1332}
1333
1334// ----------------------------- UART Write endpoint -----------------------------
1335
1336/// gallo_uart_write - Write bytes to the UART bus.
1337///
1338/// Queues `len` bytes from `buf` to the UART transmit buffer. Returns
1339/// once all bytes have been accepted. Use [`gallo_uart_flush`] to wait
1340/// for transmission to complete on the wire.
1341///
1342/// Returns `Status::Ok` in case of success or various error codes.
1343///
1344/// # Safety
1345///
1346/// Caller must ensure that `gallo` is a valid, opaque pointer to
1347/// `PicoDeGallo` returned by `gallo_init()`, and that `buf` points to
1348/// at least `len` bytes.
1349#[unsafe(no_mangle)]
1350pub unsafe extern "C" fn gallo_uart_write(
1351    gallo: *mut PicoDeGallo,
1352    buf: *const u8,
1353    len: u16,
1354) -> Status {
1355    if gallo.is_null() {
1356        eprintln!("Unexpected NULL context");
1357        return Status::Uninitialized;
1358    }
1359
1360    if buf.is_null() {
1361        eprintln!("Unexpected NULL buf pointer");
1362        return Status::InvalidArgument;
1363    }
1364
1365    // Safety: caller must ensure that `gallo` is a valid opaque
1366    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1367    let gallo = unsafe { &*gallo };
1368
1369    let data = unsafe { std::slice::from_raw_parts(buf, len as usize) };
1370
1371    let result = block_on(gallo.0.uart_write(data));
1372
1373    match result {
1374        Ok(()) => Status::Ok,
1375        Err(e) => uart_error_to_status(e),
1376    }
1377}
1378
1379// ----------------------------- UART Flush endpoint -----------------------------
1380
1381/// gallo_uart_flush - Flush the UART transmit buffer.
1382///
1383/// Blocks until all pending bytes have been transmitted on the wire.
1384///
1385/// Returns `Status::Ok` in case of success or various error codes.
1386///
1387/// # Safety
1388///
1389/// Caller must ensure that `gallo` is a valid, opaque pointer to
1390/// `PicoDeGallo` returned by `gallo_init()`.
1391#[unsafe(no_mangle)]
1392pub unsafe extern "C" fn gallo_uart_flush(gallo: *mut PicoDeGallo) -> Status {
1393    if gallo.is_null() {
1394        eprintln!("Unexpected NULL context");
1395        return Status::Uninitialized;
1396    }
1397
1398    // Safety: caller must ensure that `gallo` is a valid opaque
1399    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1400    let gallo = unsafe { &*gallo };
1401
1402    let result = block_on(gallo.0.uart_flush());
1403
1404    match result {
1405        Ok(()) => Status::Ok,
1406        Err(e) => uart_error_to_status(e),
1407    }
1408}
1409
1410// ----------------------------- UART Set config endpoint -----------------------------
1411
1412/// gallo_uart_set_config - Set the UART baud rate.
1413///
1414/// `baud_rate` must be greater than 0. Returns `Status::InvalidArgument`
1415/// for a zero baud rate.
1416///
1417/// Returns `Status::Ok` in case of success or various error codes.
1418///
1419/// # Safety
1420///
1421/// Caller must ensure that `gallo` is a valid, opaque pointer to
1422/// `PicoDeGallo` returned by `gallo_init()`.
1423#[unsafe(no_mangle)]
1424pub unsafe extern "C" fn gallo_uart_set_config(gallo: *mut PicoDeGallo, baud_rate: u32) -> Status {
1425    if gallo.is_null() {
1426        eprintln!("Unexpected NULL context");
1427        return Status::Uninitialized;
1428    }
1429
1430    if baud_rate == 0 {
1431        eprintln!("Invalid baud rate: 0");
1432        return Status::InvalidArgument;
1433    }
1434
1435    // Safety: caller must ensure that `gallo` is a valid opaque
1436    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1437    let gallo = unsafe { &*gallo };
1438
1439    let result = block_on(gallo.0.uart_set_config(baud_rate));
1440
1441    match result {
1442        Ok(()) => Status::Ok,
1443        Err(e) => uart_error_to_status(e),
1444    }
1445}
1446
1447// ----------------------------- UART Get config endpoint -----------------------------
1448
1449/// gallo_uart_get_config - Query the current UART configuration.
1450///
1451/// On success, writes the current baud rate to `*out_baud_rate`.
1452///
1453/// Returns `Status::Ok` in case of success or various error codes.
1454///
1455/// # Safety
1456///
1457/// Caller must ensure that `gallo` is a valid, opaque pointer to
1458/// `PicoDeGallo` returned by `gallo_init()`, and that `out_baud_rate`
1459/// is a valid pointer to a `u32`.
1460#[unsafe(no_mangle)]
1461pub unsafe extern "C" fn gallo_uart_get_config(
1462    gallo: *mut PicoDeGallo,
1463    out_baud_rate: *mut u32,
1464) -> Status {
1465    if gallo.is_null() {
1466        eprintln!("Unexpected NULL context");
1467        return Status::Uninitialized;
1468    }
1469
1470    if out_baud_rate.is_null() {
1471        eprintln!("Unexpected NULL out_baud_rate pointer");
1472        return Status::InvalidArgument;
1473    }
1474
1475    // Safety: caller must ensure that `gallo` is a valid opaque
1476    // pointer to `PicoDeGallo` returned by `gallo_init()`.
1477    let gallo = unsafe { &*gallo };
1478
1479    let result = block_on(gallo.0.uart_get_config());
1480
1481    match result {
1482        Ok(info) => {
1483            unsafe {
1484                *out_baud_rate = info.baud_rate;
1485            }
1486            Status::Ok
1487        }
1488        Err(e) => uart_error_to_status(e),
1489    }
1490}
1491
1492// ----------------------------- PWM endpoints -----------------------------
1493
1494/// gallo_pwm_set_duty_cycle - Set the raw duty cycle of a PWM channel.
1495///
1496/// `channel` is 0–3. `duty` is the raw compare value (0 to the current
1497/// `top` register). Use `gallo_pwm_get_duty_cycle` to discover the max.
1498///
1499/// # Safety
1500///
1501/// Caller must ensure that `gallo` is a valid, opaque pointer to
1502/// `PicoDeGallo` returned by `gallo_init()`.
1503#[unsafe(no_mangle)]
1504pub unsafe extern "C" fn gallo_pwm_set_duty_cycle(
1505    gallo: *mut PicoDeGallo,
1506    channel: u8,
1507    duty: u16,
1508) -> Status {
1509    if gallo.is_null() {
1510        eprintln!("Unexpected NULL context");
1511        return Status::Uninitialized;
1512    }
1513
1514    let gallo = unsafe { &*gallo };
1515    match block_on(gallo.0.pwm_set_duty_cycle(channel, duty)) {
1516        Ok(()) => Status::Ok,
1517        Err(e) => pwm_error_to_status(e),
1518    }
1519}
1520
1521/// gallo_pwm_get_duty_cycle - Query the current duty cycle of a PWM channel.
1522///
1523/// On success, writes the current raw compare value to `*out_duty` and
1524/// the maximum duty (top + 1) to `*out_max_duty`.
1525///
1526/// # Safety
1527///
1528/// Caller must ensure that `gallo` is a valid, opaque pointer to
1529/// `PicoDeGallo` returned by `gallo_init()`, and that `out_duty` and
1530/// `out_max_duty` are valid pointers.
1531#[unsafe(no_mangle)]
1532pub unsafe extern "C" fn gallo_pwm_get_duty_cycle(
1533    gallo: *mut PicoDeGallo,
1534    channel: u8,
1535    out_duty: *mut u16,
1536    out_max_duty: *mut u16,
1537) -> Status {
1538    if gallo.is_null() {
1539        eprintln!("Unexpected NULL context");
1540        return Status::Uninitialized;
1541    }
1542
1543    if out_duty.is_null() || out_max_duty.is_null() {
1544        eprintln!("Unexpected NULL output pointer");
1545        return Status::InvalidArgument;
1546    }
1547
1548    let gallo = unsafe { &*gallo };
1549    match block_on(gallo.0.pwm_get_duty_cycle(channel)) {
1550        Ok(info) => {
1551            unsafe {
1552                *out_duty = info.current_duty;
1553                *out_max_duty = info.max_duty;
1554            }
1555            Status::Ok
1556        }
1557        Err(e) => pwm_error_to_status(e),
1558    }
1559}
1560
1561/// gallo_pwm_enable - Enable the PWM slice that owns the given channel.
1562///
1563/// Channels 0–1 share a slice, channels 2–3 share another.
1564///
1565/// # Safety
1566///
1567/// Caller must ensure that `gallo` is a valid, opaque pointer to
1568/// `PicoDeGallo` returned by `gallo_init()`.
1569#[unsafe(no_mangle)]
1570pub unsafe extern "C" fn gallo_pwm_enable(gallo: *mut PicoDeGallo, channel: u8) -> Status {
1571    if gallo.is_null() {
1572        eprintln!("Unexpected NULL context");
1573        return Status::Uninitialized;
1574    }
1575
1576    let gallo = unsafe { &*gallo };
1577    match block_on(gallo.0.pwm_enable(channel)) {
1578        Ok(()) => Status::Ok,
1579        Err(e) => pwm_error_to_status(e),
1580    }
1581}
1582
1583/// gallo_pwm_disable - Disable the PWM slice that owns the given channel.
1584///
1585/// Channels 0–1 share a slice, channels 2–3 share another.
1586///
1587/// # Safety
1588///
1589/// Caller must ensure that `gallo` is a valid, opaque pointer to
1590/// `PicoDeGallo` returned by `gallo_init()`.
1591#[unsafe(no_mangle)]
1592pub unsafe extern "C" fn gallo_pwm_disable(gallo: *mut PicoDeGallo, channel: u8) -> Status {
1593    if gallo.is_null() {
1594        eprintln!("Unexpected NULL context");
1595        return Status::Uninitialized;
1596    }
1597
1598    let gallo = unsafe { &*gallo };
1599    match block_on(gallo.0.pwm_disable(channel)) {
1600        Ok(()) => Status::Ok,
1601        Err(e) => pwm_error_to_status(e),
1602    }
1603}
1604
1605/// gallo_pwm_set_config - Configure the PWM slice behind a channel.
1606///
1607/// Sets `frequency_hz` and `phase_correct` mode. The firmware computes
1608/// the `top` and `divider` registers automatically.
1609///
1610/// # Safety
1611///
1612/// Caller must ensure that `gallo` is a valid, opaque pointer to
1613/// `PicoDeGallo` returned by `gallo_init()`.
1614#[unsafe(no_mangle)]
1615pub unsafe extern "C" fn gallo_pwm_set_config(
1616    gallo: *mut PicoDeGallo,
1617    channel: u8,
1618    frequency_hz: u32,
1619    phase_correct: bool,
1620) -> Status {
1621    if gallo.is_null() {
1622        eprintln!("Unexpected NULL context");
1623        return Status::Uninitialized;
1624    }
1625
1626    let gallo = unsafe { &*gallo };
1627    match block_on(gallo.0.pwm_set_config(channel, frequency_hz, phase_correct)) {
1628        Ok(()) => Status::Ok,
1629        Err(e) => pwm_error_to_status(e),
1630    }
1631}
1632
1633/// gallo_pwm_get_config - Query the current PWM configuration.
1634///
1635/// On success, writes the effective frequency to `*out_frequency_hz`,
1636/// the phase-correct flag to `*out_phase_correct`, and the enabled
1637/// flag to `*out_enabled`.
1638///
1639/// # Safety
1640///
1641/// Caller must ensure that `gallo` is a valid, opaque pointer to
1642/// `PicoDeGallo` returned by `gallo_init()`, and that all output
1643/// pointers are valid.
1644#[unsafe(no_mangle)]
1645pub unsafe extern "C" fn gallo_pwm_get_config(
1646    gallo: *mut PicoDeGallo,
1647    channel: u8,
1648    out_frequency_hz: *mut u32,
1649    out_phase_correct: *mut bool,
1650    out_enabled: *mut bool,
1651) -> Status {
1652    if gallo.is_null() {
1653        eprintln!("Unexpected NULL context");
1654        return Status::Uninitialized;
1655    }
1656
1657    if out_frequency_hz.is_null() || out_phase_correct.is_null() || out_enabled.is_null() {
1658        eprintln!("Unexpected NULL output pointer");
1659        return Status::InvalidArgument;
1660    }
1661
1662    let gallo = unsafe { &*gallo };
1663    match block_on(gallo.0.pwm_get_config(channel)) {
1664        Ok(info) => {
1665            unsafe {
1666                *out_frequency_hz = info.frequency_hz;
1667                *out_phase_correct = info.phase_correct;
1668                *out_enabled = info.enabled;
1669            }
1670            Status::Ok
1671        }
1672        Err(e) => pwm_error_to_status(e),
1673    }
1674}
1675
1676// ----------------------------- ADC endpoints -----------------------------
1677
1678/// gallo_adc_read - Perform a single-shot ADC read.
1679///
1680/// On success, writes the raw 12-bit value (0–4095) to `*out_value`.
1681/// `channel` selects the input: 0–3 for GPIO26–29.
1682///
1683/// # Safety
1684///
1685/// Caller must ensure that `gallo` is a valid, opaque pointer to
1686/// `PicoDeGallo` returned by `gallo_init()`, and that `out_value` is a
1687/// valid pointer.
1688#[unsafe(no_mangle)]
1689pub unsafe extern "C" fn gallo_adc_read(
1690    gallo: *mut PicoDeGallo,
1691    channel: u8,
1692    out_value: *mut u16,
1693) -> Status {
1694    if gallo.is_null() {
1695        eprintln!("Unexpected NULL context");
1696        return Status::Uninitialized;
1697    }
1698
1699    if out_value.is_null() {
1700        eprintln!("Unexpected NULL output pointer");
1701        return Status::InvalidArgument;
1702    }
1703
1704    let adc_channel = match channel {
1705        0 => AdcChannel::Adc0,
1706        1 => AdcChannel::Adc1,
1707        2 => AdcChannel::Adc2,
1708        3 => AdcChannel::Adc3,
1709        _ => {
1710            eprintln!("Invalid ADC channel: {channel}");
1711            return Status::InvalidArgument;
1712        }
1713    };
1714
1715    let gallo = unsafe { &*gallo };
1716    match block_on(gallo.0.adc_read(adc_channel)) {
1717        Ok(raw) => {
1718            unsafe { *out_value = raw };
1719            Status::Ok
1720        }
1721        Err(e) => adc_error_to_status(e),
1722    }
1723}
1724
1725/// gallo_adc_get_config - Query the ADC configuration.
1726///
1727/// On success, writes resolution (bits), nominal reference voltage (mV),
1728/// number of GPIO channels.
1729///
1730/// # Safety
1731///
1732/// Caller must ensure that `gallo` is a valid, opaque pointer to
1733/// `PicoDeGallo` returned by `gallo_init()`, and that all output pointers
1734/// are valid.
1735#[unsafe(no_mangle)]
1736pub unsafe extern "C" fn gallo_adc_get_config(
1737    gallo: *mut PicoDeGallo,
1738    out_resolution_bits: *mut u8,
1739    out_nominal_reference_mv: *mut u16,
1740    out_num_gpio_channels: *mut u8,
1741) -> Status {
1742    if gallo.is_null() {
1743        eprintln!("Unexpected NULL context");
1744        return Status::Uninitialized;
1745    }
1746
1747    if out_resolution_bits.is_null()
1748        || out_nominal_reference_mv.is_null()
1749        || out_num_gpio_channels.is_null()
1750    {
1751        eprintln!("Unexpected NULL output pointer");
1752        return Status::InvalidArgument;
1753    }
1754
1755    let gallo = unsafe { &*gallo };
1756    match block_on(gallo.0.adc_get_config()) {
1757        Ok(info) => {
1758            unsafe {
1759                *out_resolution_bits = info.resolution_bits;
1760                *out_nominal_reference_mv = info.nominal_reference_mv;
1761                *out_num_gpio_channels = info.num_gpio_channels;
1762            }
1763            Status::Ok
1764        }
1765        Err(e) => adc_error_to_status(e),
1766    }
1767}
1768
1769// ----------------------------- 1-Wire endpoints -----------------------------
1770
1771#[unsafe(no_mangle)]
1772/// gallo_onewire_reset - Perform a 1-Wire bus reset.
1773///
1774/// On success, `*out_present` is set to `true` if device(s) responded with a
1775/// presence pulse, `false` otherwise.
1776///
1777/// # Safety
1778///
1779/// Caller must ensure that `gallo` is a valid, opaque pointer to
1780/// `PicoDeGallo` returned by `gallo_init()`.
1781pub unsafe extern "C" fn gallo_onewire_reset(
1782    gallo: *mut PicoDeGallo,
1783    out_present: *mut bool,
1784) -> Status {
1785    if gallo.is_null() {
1786        eprintln!("Unexpected NULL context");
1787        return Status::Uninitialized;
1788    }
1789
1790    if out_present.is_null() {
1791        eprintln!("Unexpected NULL output pointer");
1792        return Status::InvalidArgument;
1793    }
1794
1795    let gallo = unsafe { &*gallo };
1796    match block_on(gallo.0.onewire_reset()) {
1797        Ok(present) => {
1798            unsafe {
1799                *out_present = present;
1800            }
1801            Status::Ok
1802        }
1803        Err(e) => onewire_error_to_status(e),
1804    }
1805}
1806
1807#[unsafe(no_mangle)]
1808/// gallo_onewire_read - Read bytes from the 1-Wire bus.
1809///
1810/// Reads up to `len` bytes into `buf`. On success, `*out_len` is the number
1811/// of bytes actually read.
1812///
1813/// # Safety
1814///
1815/// - `buf` must point to at least `len` writable bytes.
1816/// - `out_len` must be a valid writable `u16` pointer.
1817pub unsafe extern "C" fn gallo_onewire_read(
1818    gallo: *mut PicoDeGallo,
1819    buf: *mut u8,
1820    len: u16,
1821    out_len: *mut u16,
1822) -> Status {
1823    if gallo.is_null() {
1824        eprintln!("Unexpected NULL context");
1825        return Status::Uninitialized;
1826    }
1827
1828    if buf.is_null() || out_len.is_null() {
1829        eprintln!("Unexpected NULL pointer");
1830        return Status::InvalidArgument;
1831    }
1832
1833    let gallo = unsafe { &*gallo };
1834    match block_on(gallo.0.onewire_read(len)) {
1835        Ok(data) => {
1836            let copy_len = data.len().min(len as usize);
1837            unsafe {
1838                std::ptr::copy_nonoverlapping(data.as_ptr(), buf, copy_len);
1839                *out_len = copy_len as u16;
1840            }
1841            Status::Ok
1842        }
1843        Err(e) => onewire_error_to_status(e),
1844    }
1845}
1846
1847#[unsafe(no_mangle)]
1848/// gallo_onewire_write - Write raw bytes to the 1-Wire bus.
1849///
1850/// # Safety
1851///
1852/// - `buf` must point to at least `len` readable bytes.
1853pub unsafe extern "C" fn gallo_onewire_write(
1854    gallo: *mut PicoDeGallo,
1855    buf: *const u8,
1856    len: u16,
1857) -> Status {
1858    if gallo.is_null() {
1859        eprintln!("Unexpected NULL context");
1860        return Status::Uninitialized;
1861    }
1862
1863    if buf.is_null() && len > 0 {
1864        eprintln!("Unexpected NULL buffer pointer");
1865        return Status::InvalidArgument;
1866    }
1867
1868    let gallo = unsafe { &*gallo };
1869    let data = if len > 0 {
1870        unsafe { std::slice::from_raw_parts(buf, len as usize) }
1871    } else {
1872        &[]
1873    };
1874
1875    match block_on(gallo.0.onewire_write(data)) {
1876        Ok(()) => Status::Ok,
1877        Err(e) => onewire_error_to_status(e),
1878    }
1879}
1880
1881#[unsafe(no_mangle)]
1882/// gallo_onewire_write_pullup - Write bytes then apply a strong pullup.
1883///
1884/// After writing `len` bytes from `buf`, the bus is held high for
1885/// `pullup_duration_ms` milliseconds to supply power to parasitic-power devices.
1886///
1887/// # Safety
1888///
1889/// - `buf` must point to at least `len` readable bytes.
1890pub unsafe extern "C" fn gallo_onewire_write_pullup(
1891    gallo: *mut PicoDeGallo,
1892    buf: *const u8,
1893    len: u16,
1894    pullup_duration_ms: u16,
1895) -> Status {
1896    if gallo.is_null() {
1897        eprintln!("Unexpected NULL context");
1898        return Status::Uninitialized;
1899    }
1900
1901    if buf.is_null() && len > 0 {
1902        eprintln!("Unexpected NULL buffer pointer");
1903        return Status::InvalidArgument;
1904    }
1905
1906    let gallo = unsafe { &*gallo };
1907    let data = if len > 0 {
1908        unsafe { std::slice::from_raw_parts(buf, len as usize) }
1909    } else {
1910        &[]
1911    };
1912
1913    match block_on(gallo.0.onewire_write_pullup(data, pullup_duration_ms)) {
1914        Ok(()) => Status::Ok,
1915        Err(e) => onewire_error_to_status(e),
1916    }
1917}
1918
1919#[unsafe(no_mangle)]
1920/// gallo_onewire_search - Search for all devices on the 1-Wire bus.
1921///
1922/// Discovers up to `max_count` ROM IDs and writes them to `out_rom_ids`.
1923/// On success, `*out_count` holds the number of devices found.
1924///
1925/// # Safety
1926///
1927/// - `out_rom_ids` must point to at least `max_count` writable `u64` elements.
1928/// - `out_count` must be a valid writable `u16` pointer.
1929pub unsafe extern "C" fn gallo_onewire_search(
1930    gallo: *mut PicoDeGallo,
1931    out_rom_ids: *mut u64,
1932    max_count: u16,
1933    out_count: *mut u16,
1934) -> Status {
1935    if gallo.is_null() {
1936        eprintln!("Unexpected NULL context");
1937        return Status::Uninitialized;
1938    }
1939
1940    if out_rom_ids.is_null() || out_count.is_null() {
1941        eprintln!("Unexpected NULL pointer");
1942        return Status::InvalidArgument;
1943    }
1944
1945    let gallo = unsafe { &*gallo };
1946
1947    // First search
1948    let first = match block_on(gallo.0.onewire_search()) {
1949        Ok(Some(id)) => id,
1950        Ok(None) => {
1951            unsafe {
1952                *out_count = 0;
1953            }
1954            return Status::Ok;
1955        }
1956        Err(e) => return onewire_error_to_status(e),
1957    };
1958
1959    unsafe {
1960        *out_rom_ids = first;
1961    }
1962    let mut count: u16 = 1;
1963
1964    // Continue searching
1965    while count < max_count {
1966        match block_on(gallo.0.onewire_search_next()) {
1967            Ok(Some(id)) => {
1968                unsafe {
1969                    *out_rom_ids.add(count as usize) = id;
1970                }
1971                count += 1;
1972            }
1973            Ok(None) => break,
1974            Err(e) => return onewire_error_to_status(e),
1975        }
1976    }
1977
1978    unsafe {
1979        *out_count = count;
1980    }
1981    Status::Ok
1982}
1983
1984// ----------------------------- Version endpoint -----------------------------
1985
1986#[unsafe(no_mangle)]
1987/// gallo_version - Gets the firmware version.
1988///
1989/// Returns `Status::Ok` in case of success or various error codes.
1990///
1991/// # Safety
1992///
1993/// Caller must ensure that `gallo` is a valid, opaque pointer to
1994/// `PicoDeGallo` returned by `gallo_init()`.
1995pub unsafe extern "C" fn gallo_version(
1996    gallo: *mut PicoDeGallo,
1997    major: *mut u16,
1998    minor: *mut u16,
1999    patch: *mut u32,
2000) -> Status {
2001    if gallo.is_null() {
2002        eprintln!("Unexpected NULL context");
2003        return Status::Uninitialized;
2004    }
2005
2006    if major.is_null() || minor.is_null() || patch.is_null() {
2007        eprintln!("Unexpected NULL version pointer");
2008        return Status::InvalidArgument;
2009    }
2010
2011    // Safety: caller must ensure that `gallo` is a valid opaque
2012    // pointer to `PicoDeGallo` returned by `gallo_init()`.
2013    let gallo = unsafe { &*gallo };
2014
2015    let result = block_on(gallo.0.version());
2016
2017    match result {
2018        Ok(lib::VersionInfo {
2019            major: a,
2020            minor: b,
2021            patch: c,
2022        }) => {
2023            unsafe {
2024                *major = a;
2025                *minor = b;
2026                *patch = c;
2027            }
2028
2029            Status::Ok
2030        }
2031        Err(_) => Status::VersionFailed,
2032    }
2033}
2034
2035// ----------------------------- Device Info endpoint -----------------------------
2036
2037/// I2C bus support (bit 0).
2038pub const GALLO_CAP_I2C: u64 = 1 << 0;
2039/// SPI bus support (bit 1).
2040pub const GALLO_CAP_SPI: u64 = 1 << 1;
2041/// UART support (bit 2).
2042pub const GALLO_CAP_UART: u64 = 1 << 2;
2043/// GPIO support (bit 3).
2044pub const GALLO_CAP_GPIO: u64 = 1 << 3;
2045/// PWM output support (bit 4).
2046pub const GALLO_CAP_PWM: u64 = 1 << 4;
2047/// ADC input support (bit 5).
2048pub const GALLO_CAP_ADC: u64 = 1 << 5;
2049/// 1-Wire bus support (bit 6).
2050pub const GALLO_CAP_ONEWIRE: u64 = 1 << 6;
2051
2052/// C-compatible device information struct.
2053///
2054/// Populated by [`gallo_get_device_info`]. Contains firmware version,
2055/// schema (wire protocol) version, hardware revision, and peripheral
2056/// capabilities as a `u64` bitfield.
2057///
2058/// Test individual capabilities with bitwise AND:
2059///
2060/// ```c
2061/// if (info.capabilities & GALLO_CAP_I2C) { /* I2C supported */ }
2062/// ```
2063#[repr(C)]
2064#[derive(Debug)]
2065pub struct GalloDeviceInfo {
2066    /// Firmware version — major.
2067    pub fw_major: u16,
2068    /// Firmware version — minor.
2069    pub fw_minor: u16,
2070    /// Firmware version — patch.
2071    pub fw_patch: u32,
2072    /// Schema (wire protocol) version — major.
2073    pub schema_major: u16,
2074    /// Schema (wire protocol) version — minor.
2075    pub schema_minor: u16,
2076    /// Schema (wire protocol) version — patch.
2077    pub schema_patch: u32,
2078    /// Hardware revision number.
2079    pub hw_version: u8,
2080    /// Peripheral capabilities bitfield.
2081    ///
2082    /// Each bit represents a peripheral; use the `GALLO_CAP_*` constants
2083    /// to test individual capabilities.
2084    pub capabilities: u64,
2085}
2086
2087#[unsafe(no_mangle)]
2088/// gallo_get_device_info - Gets extended device information.
2089///
2090/// Queries firmware version, schema version, hardware revision, and
2091/// peripheral capabilities in a single call. Also validates that the
2092/// schema version is compatible with this host library.
2093///
2094/// Returns `Status::Ok` on success, `Status::SchemaMismatch` if the
2095/// firmware's wire protocol is incompatible, `Status::LegacyFirmware`
2096/// if the firmware does not support this endpoint, or
2097/// `Status::DeviceInfoFailed` on communication error.
2098///
2099/// # Safety
2100///
2101/// Caller must ensure that `gallo` is a valid, opaque pointer to
2102/// `PicoDeGallo` returned by `gallo_init()`, and `out` points to a
2103/// valid `GalloDeviceInfo`.
2104pub unsafe extern "C" fn gallo_get_device_info(
2105    gallo: *mut PicoDeGallo,
2106    out: *mut GalloDeviceInfo,
2107) -> Status {
2108    if gallo.is_null() {
2109        eprintln!("Unexpected NULL context");
2110        return Status::Uninitialized;
2111    }
2112
2113    if out.is_null() {
2114        eprintln!("Unexpected NULL device info pointer");
2115        return Status::InvalidArgument;
2116    }
2117
2118    let gallo = unsafe { &*gallo };
2119
2120    let result = block_on(gallo.0.validate());
2121
2122    match result {
2123        Ok(info) => {
2124            unsafe {
2125                (*out).fw_major = info.fw_major;
2126                (*out).fw_minor = info.fw_minor;
2127                (*out).fw_patch = info.fw_patch;
2128                (*out).schema_major = info.schema_major;
2129                (*out).schema_minor = info.schema_minor;
2130                (*out).schema_patch = info.schema_patch;
2131                (*out).hw_version = info.hw_version;
2132                (*out).capabilities = info.capabilities.bits();
2133            }
2134            Status::Ok
2135        }
2136        Err(lib::ValidateError::SchemaMismatch { .. }) => Status::SchemaMismatch,
2137        Err(lib::ValidateError::LegacyFirmware) => Status::LegacyFirmware,
2138        Err(lib::ValidateError::Comms(_)) => Status::DeviceInfoFailed,
2139    }
2140}
2141
2142#[cfg(test)]
2143mod tests {
2144    use super::*;
2145    use std::collections::HashSet;
2146
2147    // ----------------------------- Status code invariants -----------------------------
2148
2149    #[test]
2150    fn ok_is_zero() {
2151        assert_eq!(Status::Ok as i32, 0);
2152    }
2153
2154    #[test]
2155    fn all_errors_are_negative() {
2156        let error_codes = [
2157            Status::I2cReadFailed as i32,
2158            Status::I2cWriteFailed as i32,
2159            Status::InvalidResponse as i32,
2160            Status::Uninitialized as i32,
2161            Status::InvalidArgument as i32,
2162            Status::PingFailed as i32,
2163            Status::SpiReadFailed as i32,
2164            Status::SpiWriteFailed as i32,
2165            Status::SpiFlushFailed as i32,
2166            Status::GpioGetFailed as i32,
2167            Status::GpioPutFailed as i32,
2168            Status::GpioWaitFailed as i32,
2169            Status::SetConfigFailed as i32,
2170            Status::VersionFailed as i32,
2171            Status::I2cWriteReadFailed as i32,
2172            Status::I2cSetConfigFailed as i32,
2173            Status::SpiSetConfigFailed as i32,
2174            Status::I2cNack as i32,
2175            Status::I2cBusError as i32,
2176            Status::I2cArbitrationLoss as i32,
2177            Status::I2cOverrun as i32,
2178            Status::BufferTooLong as i32,
2179            Status::I2cAddressOutOfRange as i32,
2180            Status::GpioInvalidPin as i32,
2181            Status::CommsFailed as i32,
2182            Status::I2cScanFailed as i32,
2183            Status::GpioSetConfigFailed as i32,
2184            Status::GpioWrongDirection as i32,
2185            Status::I2cGetConfigFailed as i32,
2186            Status::SpiGetConfigFailed as i32,
2187            Status::UartReadFailed as i32,
2188            Status::UartWriteFailed as i32,
2189            Status::UartFlushFailed as i32,
2190            Status::UartOverrun as i32,
2191            Status::UartBreak as i32,
2192            Status::UartParity as i32,
2193            Status::UartFraming as i32,
2194            Status::UartInvalidBaudRate as i32,
2195            Status::UartSetConfigFailed as i32,
2196            Status::UartGetConfigFailed as i32,
2197            Status::PwmSetDutyCycleFailed as i32,
2198            Status::PwmGetDutyCycleFailed as i32,
2199            Status::PwmEnableFailed as i32,
2200            Status::PwmDisableFailed as i32,
2201            Status::PwmSetConfigFailed as i32,
2202            Status::PwmGetConfigFailed as i32,
2203            Status::PwmInvalidChannel as i32,
2204            Status::PwmInvalidDutyCycle as i32,
2205            Status::PwmInvalidConfiguration as i32,
2206            Status::AdcReadFailed as i32,
2207            Status::AdcGetConfigFailed as i32,
2208            Status::AdcConversionFailed as i32,
2209            Status::GpioPinMonitored as i32,
2210            Status::GpioPinNotMonitored as i32,
2211            Status::GpioSubscribeFailed as i32,
2212            Status::GpioUnsubscribeFailed as i32,
2213            Status::OneWireNoPresence as i32,
2214            Status::OneWireBusError as i32,
2215            Status::OneWireReadFailed as i32,
2216            Status::OneWireWriteFailed as i32,
2217            Status::OneWireSearchFailed as i32,
2218            Status::DeviceInfoFailed as i32,
2219            Status::SchemaMismatch as i32,
2220            Status::LegacyFirmware as i32,
2221        ];
2222        for code in error_codes {
2223            assert!(code < 0, "error code {code} should be negative");
2224        }
2225    }
2226
2227    #[test]
2228    fn status_codes_are_distinct() {
2229        let codes = [
2230            Status::Ok as i32,
2231            Status::I2cReadFailed as i32,
2232            Status::I2cWriteFailed as i32,
2233            Status::InvalidResponse as i32,
2234            Status::Uninitialized as i32,
2235            Status::InvalidArgument as i32,
2236            Status::PingFailed as i32,
2237            Status::SpiReadFailed as i32,
2238            Status::SpiWriteFailed as i32,
2239            Status::SpiFlushFailed as i32,
2240            Status::GpioGetFailed as i32,
2241            Status::GpioPutFailed as i32,
2242            Status::GpioWaitFailed as i32,
2243            Status::SetConfigFailed as i32,
2244            Status::VersionFailed as i32,
2245            Status::I2cWriteReadFailed as i32,
2246            Status::I2cSetConfigFailed as i32,
2247            Status::SpiSetConfigFailed as i32,
2248            Status::I2cNack as i32,
2249            Status::I2cBusError as i32,
2250            Status::I2cArbitrationLoss as i32,
2251            Status::I2cOverrun as i32,
2252            Status::BufferTooLong as i32,
2253            Status::I2cAddressOutOfRange as i32,
2254            Status::GpioInvalidPin as i32,
2255            Status::CommsFailed as i32,
2256            Status::I2cScanFailed as i32,
2257            Status::GpioSetConfigFailed as i32,
2258            Status::GpioWrongDirection as i32,
2259            Status::I2cGetConfigFailed as i32,
2260            Status::SpiGetConfigFailed as i32,
2261            Status::UartReadFailed as i32,
2262            Status::UartWriteFailed as i32,
2263            Status::UartFlushFailed as i32,
2264            Status::UartOverrun as i32,
2265            Status::UartBreak as i32,
2266            Status::UartParity as i32,
2267            Status::UartFraming as i32,
2268            Status::UartInvalidBaudRate as i32,
2269            Status::UartSetConfigFailed as i32,
2270            Status::UartGetConfigFailed as i32,
2271            Status::PwmSetDutyCycleFailed as i32,
2272            Status::PwmGetDutyCycleFailed as i32,
2273            Status::PwmEnableFailed as i32,
2274            Status::PwmDisableFailed as i32,
2275            Status::PwmSetConfigFailed as i32,
2276            Status::PwmGetConfigFailed as i32,
2277            Status::PwmInvalidChannel as i32,
2278            Status::PwmInvalidDutyCycle as i32,
2279            Status::PwmInvalidConfiguration as i32,
2280            Status::AdcReadFailed as i32,
2281            Status::AdcGetConfigFailed as i32,
2282            Status::AdcConversionFailed as i32,
2283            Status::GpioPinMonitored as i32,
2284            Status::GpioPinNotMonitored as i32,
2285            Status::GpioSubscribeFailed as i32,
2286            Status::GpioUnsubscribeFailed as i32,
2287            Status::OneWireNoPresence as i32,
2288            Status::OneWireBusError as i32,
2289            Status::OneWireReadFailed as i32,
2290            Status::OneWireWriteFailed as i32,
2291            Status::OneWireSearchFailed as i32,
2292            Status::DeviceInfoFailed as i32,
2293            Status::SchemaMismatch as i32,
2294            Status::LegacyFirmware as i32,
2295        ];
2296        let unique: HashSet<i32> = codes.iter().copied().collect();
2297        assert_eq!(codes.len(), unique.len(), "duplicate status codes found");
2298    }
2299
2300    // ----------------------------- Null pointer checks -----------------------------
2301
2302    #[test]
2303    fn ping_null_device_returns_uninitialized() {
2304        let mut id = 42u32;
2305        let status = unsafe { gallo_ping(std::ptr::null_mut(), &mut id as *mut u32) };
2306        assert_eq!(status, Status::Uninitialized);
2307    }
2308
2309    #[test]
2310    fn i2c_read_null_device_returns_uninitialized() {
2311        let mut buf = [0u8; 4];
2312        let status =
2313            unsafe { gallo_i2c_read(std::ptr::null_mut(), 0x48, buf.as_mut_ptr(), buf.len()) };
2314        assert_eq!(status, Status::Uninitialized);
2315    }
2316
2317    #[test]
2318    fn i2c_write_null_device_returns_uninitialized() {
2319        let buf = [0u8; 4];
2320        let status =
2321            unsafe { gallo_i2c_write(std::ptr::null_mut(), 0x48, buf.as_ptr(), buf.len()) };
2322        assert_eq!(status, Status::Uninitialized);
2323    }
2324
2325    #[test]
2326    fn i2c_write_read_null_device_returns_uninitialized() {
2327        let txbuf = [0u8; 2];
2328        let mut rxbuf = [0u8; 4];
2329        let status = unsafe {
2330            gallo_i2c_write_read(
2331                std::ptr::null_mut(),
2332                0x48,
2333                txbuf.as_ptr(),
2334                txbuf.len(),
2335                rxbuf.as_mut_ptr(),
2336                rxbuf.len(),
2337            )
2338        };
2339        assert_eq!(status, Status::Uninitialized);
2340    }
2341
2342    #[test]
2343    fn spi_read_null_device_returns_uninitialized() {
2344        let mut buf = [0u8; 4];
2345        let status = unsafe { gallo_spi_read(std::ptr::null_mut(), buf.as_mut_ptr(), buf.len()) };
2346        assert_eq!(status, Status::Uninitialized);
2347    }
2348
2349    #[test]
2350    fn spi_write_null_device_returns_uninitialized() {
2351        let buf = [0u8; 4];
2352        let status = unsafe { gallo_spi_write(std::ptr::null_mut(), buf.as_ptr(), buf.len()) };
2353        assert_eq!(status, Status::Uninitialized);
2354    }
2355
2356    #[test]
2357    fn spi_flush_null_device_returns_uninitialized() {
2358        let status = unsafe { gallo_spi_flush(std::ptr::null_mut()) };
2359        assert_eq!(status, Status::Uninitialized);
2360    }
2361
2362    #[test]
2363    fn gpio_get_null_device_returns_uninitialized() {
2364        let mut state = false;
2365        let status = unsafe { gallo_gpio_get(std::ptr::null_mut(), 0, &mut state as *mut bool) };
2366        assert_eq!(status, Status::Uninitialized);
2367    }
2368
2369    #[test]
2370    fn gpio_put_null_device_returns_uninitialized() {
2371        let status = unsafe { gallo_gpio_put(std::ptr::null_mut(), 0, true) };
2372        assert_eq!(status, Status::Uninitialized);
2373    }
2374
2375    #[test]
2376    fn gpio_wait_for_high_null_device_returns_uninitialized() {
2377        let status = unsafe { gallo_gpio_wait_for_high(std::ptr::null_mut(), 0) };
2378        assert_eq!(status, Status::Uninitialized);
2379    }
2380
2381    #[test]
2382    fn gpio_wait_for_low_null_device_returns_uninitialized() {
2383        let status = unsafe { gallo_gpio_wait_for_low(std::ptr::null_mut(), 0) };
2384        assert_eq!(status, Status::Uninitialized);
2385    }
2386
2387    #[test]
2388    fn gpio_wait_for_rising_edge_null_device_returns_uninitialized() {
2389        let status = unsafe { gallo_gpio_wait_for_rising_edge(std::ptr::null_mut(), 0) };
2390        assert_eq!(status, Status::Uninitialized);
2391    }
2392
2393    #[test]
2394    fn gpio_wait_for_falling_edge_null_device_returns_uninitialized() {
2395        let status = unsafe { gallo_gpio_wait_for_falling_edge(std::ptr::null_mut(), 0) };
2396        assert_eq!(status, Status::Uninitialized);
2397    }
2398
2399    #[test]
2400    fn gpio_wait_for_any_edge_null_device_returns_uninitialized() {
2401        let status = unsafe { gallo_gpio_wait_for_any_edge(std::ptr::null_mut(), 0) };
2402        assert_eq!(status, Status::Uninitialized);
2403    }
2404
2405    #[test]
2406    fn gpio_set_config_null_device_returns_uninitialized() {
2407        let status = unsafe { gallo_gpio_set_config(std::ptr::null_mut(), 0, 0, 0) };
2408        assert_eq!(status, Status::Uninitialized);
2409    }
2410
2411    #[test]
2412    fn gpio_subscribe_null_device_returns_uninitialized() {
2413        let status = unsafe { gallo_gpio_subscribe(std::ptr::null_mut(), 0, 0) };
2414        assert_eq!(status, Status::Uninitialized);
2415    }
2416
2417    #[test]
2418    fn gpio_subscribe_invalid_edge_returns_uninitialized() {
2419        // null check happens before edge validation
2420        let status = unsafe { gallo_gpio_subscribe(std::ptr::null_mut(), 0, 99) };
2421        assert_eq!(status, Status::Uninitialized);
2422    }
2423
2424    #[test]
2425    fn gpio_unsubscribe_null_device_returns_uninitialized() {
2426        let status = unsafe { gallo_gpio_unsubscribe(std::ptr::null_mut(), 0) };
2427        assert_eq!(status, Status::Uninitialized);
2428    }
2429
2430    #[test]
2431    fn i2c_set_config_null_device_returns_uninitialized() {
2432        let status = unsafe { gallo_i2c_set_config(std::ptr::null_mut(), 1) };
2433        assert_eq!(status, Status::Uninitialized);
2434    }
2435
2436    #[test]
2437    fn i2c_set_config_invalid_frequency_returns_invalid_argument() {
2438        // We need a non-null pointer but it doesn't matter since validation
2439        // happens before dereference for the frequency parameter.
2440        // Use null to get Uninitialized first, then test with a valid-looking
2441        // but actually invalid frequency value — but null check comes first.
2442        // So we just verify the enum boundary at the API level.
2443        let status = unsafe { gallo_i2c_set_config(std::ptr::null_mut(), 99) };
2444        // null check happens first, so this returns Uninitialized
2445        assert_eq!(status, Status::Uninitialized);
2446    }
2447
2448    #[test]
2449    fn spi_set_config_null_device_returns_uninitialized() {
2450        let status = unsafe { gallo_spi_set_config(std::ptr::null_mut(), 1_000_000, false, false) };
2451        assert_eq!(status, Status::Uninitialized);
2452    }
2453
2454    #[test]
2455    fn i2c_get_config_null_device_returns_uninitialized() {
2456        let mut freq = 0u8;
2457        let status = unsafe { gallo_i2c_get_config(std::ptr::null_mut(), &mut freq as *mut u8) };
2458        assert_eq!(status, Status::Uninitialized);
2459    }
2460
2461    #[test]
2462    fn spi_get_config_null_device_returns_uninitialized() {
2463        let mut freq = 0u32;
2464        let mut phase = false;
2465        let mut polarity = false;
2466        let status = unsafe {
2467            gallo_spi_get_config(
2468                std::ptr::null_mut(),
2469                &mut freq as *mut u32,
2470                &mut phase as *mut bool,
2471                &mut polarity as *mut bool,
2472            )
2473        };
2474        assert_eq!(status, Status::Uninitialized);
2475    }
2476
2477    #[test]
2478    fn version_null_device_returns_uninitialized() {
2479        let mut major = 0u16;
2480        let mut minor = 0u16;
2481        let mut patch = 0u32;
2482        let status = unsafe {
2483            gallo_version(
2484                std::ptr::null_mut(),
2485                &mut major as *mut u16,
2486                &mut minor as *mut u16,
2487                &mut patch as *mut u32,
2488            )
2489        };
2490        assert_eq!(status, Status::Uninitialized);
2491    }
2492
2493    // ----------------------------- Null out-param checks -----------------------------
2494
2495    #[test]
2496    fn ping_null_id_returns_invalid_argument() {
2497        let status = unsafe { gallo_ping(std::ptr::null_mut(), std::ptr::null_mut()) };
2498        // gallo is null, so Uninitialized fires first
2499        assert_eq!(status, Status::Uninitialized);
2500    }
2501
2502    #[test]
2503    fn gpio_get_null_state_returns_invalid_argument() {
2504        let status = unsafe { gallo_gpio_get(std::ptr::null_mut(), 0, std::ptr::null_mut()) };
2505        assert_eq!(status, Status::Uninitialized);
2506    }
2507
2508    #[test]
2509    fn version_null_major_returns_invalid_argument() {
2510        let mut minor = 0u16;
2511        let mut patch = 0u32;
2512        let status = unsafe {
2513            gallo_version(
2514                std::ptr::null_mut(),
2515                std::ptr::null_mut(),
2516                &mut minor as *mut u16,
2517                &mut patch as *mut u32,
2518            )
2519        };
2520        assert_eq!(status, Status::Uninitialized);
2521    }
2522
2523    #[test]
2524    fn device_info_null_device_returns_uninitialized() {
2525        let mut info = std::mem::MaybeUninit::<GalloDeviceInfo>::uninit();
2526        let status = unsafe { gallo_get_device_info(std::ptr::null_mut(), info.as_mut_ptr()) };
2527        assert_eq!(status, Status::Uninitialized);
2528    }
2529
2530    #[test]
2531    fn device_info_null_out_returns_invalid_argument() {
2532        // gallo is null too, so Uninitialized fires first
2533        let status = unsafe { gallo_get_device_info(std::ptr::null_mut(), std::ptr::null_mut()) };
2534        assert_eq!(status, Status::Uninitialized);
2535    }
2536
2537    // ----------------------------- PWM null pointer checks -----------------------------
2538
2539    #[test]
2540    fn pwm_set_duty_cycle_null_device_returns_uninitialized() {
2541        let status = unsafe { gallo_pwm_set_duty_cycle(std::ptr::null_mut(), 0, 100) };
2542        assert_eq!(status, Status::Uninitialized);
2543    }
2544
2545    #[test]
2546    fn pwm_get_duty_cycle_null_device_returns_uninitialized() {
2547        let status = unsafe {
2548            gallo_pwm_get_duty_cycle(
2549                std::ptr::null_mut(),
2550                0,
2551                std::ptr::null_mut(),
2552                std::ptr::null_mut(),
2553            )
2554        };
2555        assert_eq!(status, Status::Uninitialized);
2556    }
2557
2558    #[test]
2559    fn pwm_enable_null_device_returns_uninitialized() {
2560        let status = unsafe { gallo_pwm_enable(std::ptr::null_mut(), 0) };
2561        assert_eq!(status, Status::Uninitialized);
2562    }
2563
2564    #[test]
2565    fn pwm_disable_null_device_returns_uninitialized() {
2566        let status = unsafe { gallo_pwm_disable(std::ptr::null_mut(), 0) };
2567        assert_eq!(status, Status::Uninitialized);
2568    }
2569
2570    #[test]
2571    fn pwm_set_config_null_device_returns_uninitialized() {
2572        let status = unsafe { gallo_pwm_set_config(std::ptr::null_mut(), 0, 1000, false) };
2573        assert_eq!(status, Status::Uninitialized);
2574    }
2575
2576    #[test]
2577    fn pwm_get_config_null_device_returns_uninitialized() {
2578        let status = unsafe {
2579            gallo_pwm_get_config(
2580                std::ptr::null_mut(),
2581                0,
2582                std::ptr::null_mut(),
2583                std::ptr::null_mut(),
2584                std::ptr::null_mut(),
2585            )
2586        };
2587        assert_eq!(status, Status::Uninitialized);
2588    }
2589
2590    #[test]
2591    fn adc_read_null_device_returns_uninitialized() {
2592        let status = unsafe { gallo_adc_read(std::ptr::null_mut(), 0, std::ptr::null_mut()) };
2593        assert_eq!(status, Status::Uninitialized);
2594    }
2595
2596    #[test]
2597    fn adc_get_config_null_device_returns_uninitialized() {
2598        let status = unsafe {
2599            gallo_adc_get_config(
2600                std::ptr::null_mut(),
2601                std::ptr::null_mut(),
2602                std::ptr::null_mut(),
2603                std::ptr::null_mut(),
2604            )
2605        };
2606        assert_eq!(status, Status::Uninitialized);
2607    }
2608
2609    // ----------------------------- Null buffer checks -----------------------------
2610
2611    #[test]
2612    fn i2c_read_null_buffer_returns_invalid_argument() {
2613        // Device is also null, so we get Uninitialized first.
2614        // This tests that the null-device check fires before the buffer check.
2615        let status = unsafe { gallo_i2c_read(std::ptr::null_mut(), 0x48, std::ptr::null_mut(), 4) };
2616        assert_eq!(status, Status::Uninitialized);
2617    }
2618
2619    #[test]
2620    fn spi_read_null_buffer_returns_invalid_argument() {
2621        let status = unsafe { gallo_spi_read(std::ptr::null_mut(), std::ptr::null_mut(), 4) };
2622        assert_eq!(status, Status::Uninitialized);
2623    }
2624
2625    // ----------------------------- Free safety -----------------------------
2626
2627    #[test]
2628    fn free_null_is_safe() {
2629        unsafe { gallo_free(std::ptr::null()) };
2630    }
2631
2632    #[test]
2633    fn init_with_null_serial_returns_null() {
2634        let ptr = unsafe { gallo_init_with_serial_number(std::ptr::null()) };
2635        assert!(ptr.is_null());
2636    }
2637
2638    // ----------------------------- UART null pointer checks -----------------------------
2639
2640    #[test]
2641    fn uart_read_null_device_returns_uninitialized() {
2642        let mut buf = [0u8; 4];
2643        let mut out_len = 0u16;
2644        let status = unsafe {
2645            gallo_uart_read(
2646                std::ptr::null_mut(),
2647                buf.as_mut_ptr(),
2648                4,
2649                1000,
2650                &mut out_len as *mut u16,
2651            )
2652        };
2653        assert_eq!(status, Status::Uninitialized);
2654    }
2655
2656    #[test]
2657    fn uart_read_null_buf_returns_uninitialized() {
2658        let status = unsafe {
2659            gallo_uart_read(
2660                std::ptr::null_mut(),
2661                std::ptr::null_mut(),
2662                4,
2663                1000,
2664                std::ptr::null_mut(),
2665            )
2666        };
2667        assert_eq!(status, Status::Uninitialized);
2668    }
2669
2670    #[test]
2671    fn uart_write_null_device_returns_uninitialized() {
2672        let data = [0x48, 0x65, 0x6c, 0x6c, 0x6f];
2673        let status = unsafe { gallo_uart_write(std::ptr::null_mut(), data.as_ptr(), 5) };
2674        assert_eq!(status, Status::Uninitialized);
2675    }
2676
2677    #[test]
2678    fn uart_write_null_buf_returns_uninitialized() {
2679        let status = unsafe { gallo_uart_write(std::ptr::null_mut(), std::ptr::null(), 5) };
2680        assert_eq!(status, Status::Uninitialized);
2681    }
2682
2683    #[test]
2684    fn uart_flush_null_device_returns_uninitialized() {
2685        let status = unsafe { gallo_uart_flush(std::ptr::null_mut()) };
2686        assert_eq!(status, Status::Uninitialized);
2687    }
2688
2689    #[test]
2690    fn uart_set_config_null_device_returns_uninitialized() {
2691        let status = unsafe { gallo_uart_set_config(std::ptr::null_mut(), 115200) };
2692        assert_eq!(status, Status::Uninitialized);
2693    }
2694
2695    #[test]
2696    fn uart_set_config_zero_baud_returns_uninitialized() {
2697        // null check fires first
2698        let status = unsafe { gallo_uart_set_config(std::ptr::null_mut(), 0) };
2699        assert_eq!(status, Status::Uninitialized);
2700    }
2701
2702    #[test]
2703    fn uart_get_config_null_device_returns_uninitialized() {
2704        let mut baud = 0u32;
2705        let status = unsafe { gallo_uart_get_config(std::ptr::null_mut(), &mut baud as *mut u32) };
2706        assert_eq!(status, Status::Uninitialized);
2707    }
2708
2709    #[test]
2710    fn uart_error_to_status_maps_all_variants() {
2711        assert_eq!(
2712            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::BufferTooLong)),
2713            Status::BufferTooLong
2714        );
2715        assert_eq!(
2716            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::Overrun)),
2717            Status::UartOverrun
2718        );
2719        assert_eq!(
2720            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::Break)),
2721            Status::UartBreak
2722        );
2723        assert_eq!(
2724            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::Parity)),
2725            Status::UartParity
2726        );
2727        assert_eq!(
2728            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::Framing)),
2729            Status::UartFraming
2730        );
2731        assert_eq!(
2732            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::InvalidBaudRate)),
2733            Status::UartInvalidBaudRate
2734        );
2735        assert_eq!(
2736            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::Other)),
2737            Status::UartReadFailed
2738        );
2739        assert_eq!(
2740            uart_error_to_status(PicoDeGalloError::Endpoint(UartError::Unsupported)),
2741            Status::Unsupported
2742        );
2743    }
2744
2745    #[test]
2746    fn adc_error_mapping() {
2747        assert_eq!(
2748            adc_error_to_status(PicoDeGalloError::Endpoint(AdcError::ConversionFailed)),
2749            Status::AdcConversionFailed
2750        );
2751        assert_eq!(
2752            adc_error_to_status(PicoDeGalloError::Endpoint(AdcError::Other)),
2753            Status::AdcReadFailed
2754        );
2755        assert_eq!(
2756            adc_error_to_status(PicoDeGalloError::Endpoint(AdcError::Unsupported)),
2757            Status::Unsupported
2758        );
2759    }
2760
2761    #[test]
2762    fn gpio_error_to_status_maps_pin_monitored() {
2763        assert_eq!(
2764            gpio_error_to_status(PicoDeGalloError::Endpoint(GpioError::PinMonitored)),
2765            Status::GpioPinMonitored
2766        );
2767    }
2768
2769    #[test]
2770    fn gpio_error_to_status_maps_pin_not_monitored() {
2771        assert_eq!(
2772            gpio_error_to_status(PicoDeGalloError::Endpoint(GpioError::PinNotMonitored)),
2773            Status::GpioPinNotMonitored
2774        );
2775    }
2776
2777    #[test]
2778    fn gpio_subscribe_status_codes_are_stable() {
2779        assert_eq!(Status::GpioPinMonitored as i32, -53);
2780        assert_eq!(Status::GpioPinNotMonitored as i32, -54);
2781        assert_eq!(Status::GpioSubscribeFailed as i32, -55);
2782        assert_eq!(Status::GpioUnsubscribeFailed as i32, -56);
2783    }
2784
2785    // ----------------------------- 1-Wire null pointer checks -----------------------------
2786
2787    #[test]
2788    fn onewire_reset_null_device_returns_uninitialized() {
2789        let status = unsafe { gallo_onewire_reset(std::ptr::null_mut(), std::ptr::null_mut()) };
2790        assert_eq!(status, Status::Uninitialized);
2791    }
2792
2793    #[test]
2794    fn onewire_read_null_device_returns_uninitialized() {
2795        let status = unsafe {
2796            gallo_onewire_read(
2797                std::ptr::null_mut(),
2798                std::ptr::null_mut(),
2799                9,
2800                std::ptr::null_mut(),
2801            )
2802        };
2803        assert_eq!(status, Status::Uninitialized);
2804    }
2805
2806    #[test]
2807    fn onewire_write_null_device_returns_uninitialized() {
2808        let data = [0xCC, 0x44];
2809        let status = unsafe { gallo_onewire_write(std::ptr::null_mut(), data.as_ptr(), 2) };
2810        assert_eq!(status, Status::Uninitialized);
2811    }
2812
2813    #[test]
2814    fn onewire_write_pullup_null_device_returns_uninitialized() {
2815        let data = [0xCC, 0x44];
2816        let status =
2817            unsafe { gallo_onewire_write_pullup(std::ptr::null_mut(), data.as_ptr(), 2, 750) };
2818        assert_eq!(status, Status::Uninitialized);
2819    }
2820
2821    #[test]
2822    fn onewire_search_null_device_returns_uninitialized() {
2823        let status = unsafe {
2824            gallo_onewire_search(
2825                std::ptr::null_mut(),
2826                std::ptr::null_mut(),
2827                10,
2828                std::ptr::null_mut(),
2829            )
2830        };
2831        assert_eq!(status, Status::Uninitialized);
2832    }
2833
2834    // ----------------------------- 1-Wire error mapping tests -----------------------------
2835
2836    #[test]
2837    fn onewire_error_mapping() {
2838        assert_eq!(
2839            onewire_error_to_status(PicoDeGalloError::Endpoint(OneWireError::NoPresence)),
2840            Status::OneWireNoPresence
2841        );
2842        assert_eq!(
2843            onewire_error_to_status(PicoDeGalloError::Endpoint(OneWireError::BusError)),
2844            Status::OneWireBusError
2845        );
2846        assert_eq!(
2847            onewire_error_to_status(PicoDeGalloError::Endpoint(OneWireError::BufferTooLong)),
2848            Status::BufferTooLong
2849        );
2850        assert_eq!(
2851            onewire_error_to_status(PicoDeGalloError::Endpoint(OneWireError::Other)),
2852            Status::OneWireReadFailed
2853        );
2854        assert_eq!(
2855            onewire_error_to_status(PicoDeGalloError::Endpoint(OneWireError::Unsupported)),
2856            Status::Unsupported
2857        );
2858    }
2859
2860    #[test]
2861    fn onewire_status_codes_are_stable() {
2862        assert_eq!(Status::OneWireNoPresence as i32, -57);
2863        assert_eq!(Status::OneWireBusError as i32, -58);
2864        assert_eq!(Status::OneWireReadFailed as i32, -59);
2865        assert_eq!(Status::OneWireWriteFailed as i32, -60);
2866        assert_eq!(Status::OneWireSearchFailed as i32, -61);
2867        assert_eq!(Status::DeviceInfoFailed as i32, -62);
2868        assert_eq!(Status::SchemaMismatch as i32, -63);
2869        assert_eq!(Status::LegacyFirmware as i32, -64);
2870        assert_eq!(Status::Unsupported as i32, -65);
2871    }
2872}