Pico de Gallo: Release Announcement
Pico de Gallo turns an RP2350 into a USB-attached bridge that lets a host program drive real I²C, SPI, GPIO, PWM, ADC, UART, and 1-Wire from std Rust, C, or Python — so you can write and test device drivers on your laptop instead of cross-compiling for an MCU every time. Today’s release moves the whole ecosystem forward at once.
This is a lockstep release. The wire protocol in pico-de-gallo-internal went from schema 0.5 to 0.6, and under the pre-1.0 schema-versioning rule that is a breaking change. Firmware and host must be upgraded together — a 0.10 firmware will reject an older host’s RPCs, and a new host will refuse to talk to old firmware rather than silently mis-decode bytes on the wire. The upgrade notes at the bottom have the full version table.
Most of what landed here comes out of a reliability review that turned up a handful of real bugs: a GPIO wait could wedge the entire firmware dispatcher, a hung handler had no recovery path, GPIO subscriptions leaked when a host process crashed, and a bumped schema major could slip past validation and corrupt decoding silently. Each crate’s share of the fix is below.
pico-de-gallo-internal 0.6.0 — the wire protocol
This is the crate every other one depends on, and the breaking schema bump that drives the lockstep.
GpioWaitRequestgained atimeout_ms: u32field, used by all fivegpio/wait-*endpoints (wait-high,wait-low,wait-rising,wait-falling,wait-any). A value of0preserves the old wait-forever behavior; a non-zero value bounds the firmware-side wait and returns the newGpioError::Timeouton expiry. This is the wire half of the fix for the dispatcher wedge — a wait on a pin that never transitions used to block every other endpoint until you power-cycled the board.- New
system/reset-subscriptionsendpoint (request(), responseu8count). GPIO subscriptions are server-side state that outlives the USB transport, so a host that crashed without sendinggpio/unsubscribewould strand those pins until a power cycle. This endpoint is the recovery path.
Both changes are append-only on the wire, but the schema-version bump itself is what requires the coordinated upgrade.
pico-de-gallo-lib 0.6.0 — the Rust host library
- New
gpio_wait_for_{high,low,rising_edge,falling_edge,any_edge}_with_timeoutmethods take astd::time::Durationand returnErr(PicoDeGalloError::Endpoint(GpioError::Timeout))on expiry. The existing two-argument methods keep waiting forever by sendingtimeout_ms: 0. - New
system_reset_subscriptions()method returns the number of subscriptions it reset. The recommended connect sequence is nownew()→validate().await?→system_reset_subscriptions().await?. validate()now checksschema_majorin addition toschema_minor. Previously a firmware reporting a bumped major with a matching minor would pass validation and the host would then mis-decode wire bytes — silent garbage out.ValidateError::SchemaMismatchnow carriesexpected_major/actual_major, and itsDisplayshows the fullMAJOR.MINOR.xskew.- Fixed:
validate()no longer mis-classifies transport, postcard-decode, and frame-size errors asValidateError::LegacyFirmware. Only the postcard-rpc “no handler for that key” signals (UnknownKey,KeyTooSmall) map toLegacyFirmware; everything else routes toComms, so you stop being told to upgrade firmware that is already current. MAX_BATCH_OPSandMAX_TRANSFER_SIZEare now re-exported, so you don’t have to depend on the wire crate just to validate batch sizes.
pico-de-gallo-hal 0.6.0 — the embedded-hal layer
- New
Hal::new_validated()andHal::new_validated_with_serial_number()constructors callvalidate()before returning, failing loudly on a disconnected device or a schema mismatch. The lazyHal::new()still defers failures to the first RPC if that’s what you want. A standaloneHal::validate()accessor lets you check after the fact. - New
Hal::system_reset_subscriptions() -> Result<u8, SystemHalError>exposes the subscription teardown that previously required dropping down topico-de-gallo-lib. Recommended right afternew_validated()in any app that uses GPIO subscriptions. - New
Gpio::wait_for_*_with_timeoutinherent async methods accept aDurationand returnGpioError::Timeouton expiry. They’re inherent methods rather than trait methods becauseembedded-hal-async’sWaittrait has no notion of a timeout; the trait methods keep their wait-forever semantics. AdcChannel,AdcConfigurationInfo,GpioDirection,GpioEdge, andGpioPullare now re-exported — driver authors no longer needpico-de-gallo-libin theirCargo.tomljust for these types.- New
HalInitErrorandSystemHalErrortypes, and a fix for a stale doc-comment that referenced aHal::uart_set_configmethod that never existed.
pico-de-gallo-ffi 0.7.0 — the C bindings
- New
gallo_init_strict()andgallo_init_strict_with_serial_number()callvalidate()internally and returnNULLon device-not-found, schema mismatch, or legacy firmware. Prefer these over the lazygallo_initin production C — failures surface at construct time instead of on the first RPC. - New
gallo_gpio_wait_for_{high,low,rising_edge,falling_edge,any_edge}_with_timeout_msfunctions.timeout_ms == 0keeps the wait-forever behavior; non-zero bounds it and returnsStatus::GpioTimeout(-70). These need firmware schema 0.6+; older firmware returnsStatus::SchemaMismatch. - New
gallo_system_reset_subscriptions()withSystemResetSubscriptionsFailed(-69). - The high-throughput primitives
gallo_spi_transfer,gallo_spi_batch, andgallo_i2c_batchare now reachable from C, via the tagged structsGalloSpiBatchOp/GalloI2cBatchOp. On a per-operation failure an optionalout_failed_opreceives the zero-based index of the failing op. New status codes:I2cBatchFailed(-66),SpiBatchFailed(-67),SpiTransferFailed(-68). The wire protocol is unchanged here — this is pure FFI surface over existing endpoints. - All
gallo_*functions now takeconst PicoDeGallo *for the device handle. The ABI is unchanged, but C consumers that previously cast awayconston every call can drop those casts, and headers built with-Wcast-qualstop warning. The handle remainsSend + Syncand interior-mutable.
pyco-de-gallo 0.4.2 — the Python bindings
- New
pyco_de_gallo.open_strict()andopen_strict_with_serial_number(serial_number)callvalidate()before returning the handle and raiseRuntimeErroron device-not-found, schema mismatch, or legacy firmware. Prefer these over the lazyopen()in production Python. - New
gpio_wait_for_*_with_timeout(timeout_ms: int)methods —0waits forever, non-zero raisesRuntimeErroronGpioError::Timeout. Requires firmware schema 0.6+. - New
system_reset_subscriptions()returns anint.
gallo (CLI) 0.7.0 — the command-line tool
gallonow callsvalidate()at the top of every subcommand exceptlistandversion. A schema-version mismatch is reported up front with an actionable message that points atgallo versionand tells you to re-flash the firmware or install a matchinggallo, instead of surfacing as a confusingCommsFailedon the first RPC.listis exempt because it doesn’t touch a connected device;versionis exempt because it is the diagnostic that reports schema skew.- Everything else is unchanged. The existing
gpiosubcommands (get,put,set-config,monitor) keep working. The CLI doesn’t exposegpio wait-for-*subcommands, so bounded waits stay available through the Rust, C, and Python libraries.
pico-de-gallo-firmware 0.10.0 — the device
gpio_wait_for_*handlers now honor the per-requesttimeout_ms. Non-zero values wrap embassy’swait_for_*_edge()future inembassy_time::with_timeout(...)and returnGpioError::Timeouton expiry;0keeps the pre-0.6 wait-forever behavior.- An embassy-rp watchdog is now enabled at a 2-second timeout, fed every 800 ms by a dedicated
watchdog_feeder_task. It’s a separate task on purpose — a wedged handler can’t be trusted to feed a handler-based scheme — so the device recovers from any future handler hang.pause_on_debug(true)keeps debugger sessions from resetting the chip. i2c_scan_handlernow wraps each per-address probe in a 50 ms timeout, so one slow-to-NAK address no longer burns the whole scan budget.- New
system/reset-subscriptionshandler iterates the GPIO monitor slots, signals each live one to stop, awaits the pin back from its monitor task, and returns it to the context. It’s idempotent and cheap when nothing is subscribed — the device-side half of the subscription-leak recovery.
Together these close the dispatcher-wedge regression (a gpio_wait on a never-transitioning pin blocking every other endpoint), the no-recovery-from-a-hung-handler gap, and the worst-case impact of a flaky I²C bus on i2c_scan.
Upgrade and compatibility
Because the wire schema changed, flash the new firmware and update your host crate in the same step. Mixed versions won’t talk to each other — by design, the new validation refuses rather than mis-decodes.
| Crate | Old | New |
|---|---|---|
pico-de-gallo-internal | 0.5.0 | 0.6.0 |
pico-de-gallo-lib | 0.5.0 | 0.6.0 |
pico-de-gallo-hal | 0.5.0 | 0.6.0 |
pico-de-gallo-ffi | 0.6.0 | 0.7.0 |
gallo (CLI) | 0.6.0 | 0.7.0 |
pyco-de-gallo | 0.2.0 | 0.4.2 |
pico-de-gallo-firmware | 0.9.0 | 0.10.0 |
After flashing, point the host at the device and confirm the schema lines up:
$ gallo versionFor new code, reach for the validating entry points so a version skew or a missing board fails at construct time rather than on the first call:
- Rust library:
PicoDeGallo::new()→validate().await?→system_reset_subscriptions().await? - HAL:
Hal::new_validated(), thensystem_reset_subscriptions() - C:
gallo_init_strict() - Python:
pyco_de_gallo.open_strict()
If you hit a schema-mismatch error after upgrading only one side, that mismatch is the new validation doing its job. Re-flash or re-install so both ends report the same schema, and you’re good.