* feat(hardware): add RPi GPIO, Aardvark I2C/SPI/GPIO, and hardware plugin system
Extends the hardware subsystem with three clusters of functionality,
all feature-gated (hardware / peripheral-rpi) with no impact on default builds.
Raspberry Pi native support:
- src/hardware/rpi.rs: board self-discovery (model, serial, revision),
sysfs GPIO pin read/write, and ACT LED control
- scripts/99-act-led.rules: udev rule for non-root ACT LED access
- scripts/deploy-rpi.sh, scripts/rpi-config.toml, scripts/zeroclaw.service:
one-shot deployment helper and systemd service template
Total Phase Aardvark USB adapter (I2C / SPI / GPIO):
- crates/aardvark-sys/: new workspace crate with FFI bindings loaded at
runtime via libloading; graceful stub fallback when .so is absent or
arch mismatches (Rosetta 2 detection)
- src/hardware/aardvark.rs: AardvarkTransport implementing Transport trait
- src/hardware/aardvark_tools.rs: agent tools i2c_scan, i2c_read,
i2c_write, spi_transfer, gpio_aardvark
- src/hardware/datasheet.rs: datasheet search/download for detected devices
- docs/aardvark-integration.md, examples/hardware/aardvark/: guide + examples
Hardware plugin / ToolRegistry system:
- src/hardware/tool_registry.rs: ToolRegistry for hardware module tool sets
- src/hardware/loader.rs, src/hardware/manifest.rs: manifest-driven loader
- src/hardware/subprocess.rs: subprocess execution helper for board I/O
- src/gateway/hardware_context.rs: POST /api/hardware/reload endpoint
- src/hardware/mod.rs: exports all new modules; merge_hardware_tools and
load_hardware_context_prompt helpers
Integration hooks (minimal surface):
- src/hardware/device.rs: DeviceKind::Aardvark, DeviceRuntime::Aardvark,
has_aardvark / resolve_aardvark_device on DeviceRegistry
- src/hardware/transport.rs: TransportKind::Aardvark
- src/peripherals/mod.rs: gate create_board_info_tools behind hardware feature
- src/agent/loop_.rs: TOOL_CHOICE_OVERRIDE task-local for Anthropic provider
- src/providers/anthropic.rs: read TOOL_CHOICE_OVERRIDE; add tool_choice field
- Cargo.toml: add aardvark-sys to workspace and as dependency
- firmware/zeroclaw-nucleo/: update Cargo.toml and Cargo.lock
Non-goals:
- No changes to agent orchestration, channels, providers, or security policy
- No new config keys outside existing [hardware] / [peripherals] sections
- No CI workflow changes
Risk: Low. All new paths are feature-gated; aardvark.so loads at runtime
only when present. No schema migrations or persistent state introduced.
Rollback: revert this single commit.
* fix(hardware): resolve clippy and rustfmt CI failures
- struct_excessive_bools: allow on DeviceCapabilities (7 bool fields needed)
- unnecessary_debug_formatting: use .display() instead of {:?} for paths
- stable_sort_primitive: replace .sort() with .sort_unstable() on &str slices
* fix(hardware): add missing serial/uf2/pico modules declared in mod.rs
cargo fmt was exiting with code 1 because mod.rs declared pub mod serial,
uf2, pico_flash, pico_code but those files were missing from the branch.
Also apply auto-formatting to loader.rs.
* fix(hardware): apply rustfmt 1.92.0 formatting (matches CI toolchain)
* docs(scripts): add RPi deployment and interaction guide
* push
* feat(firmware): add initial Pico firmware and serial device handling
- Introduced main.py for ZeroClaw Pico firmware with a placeholder for MicroPython implementation.
- Added binary UF2 file for Pico deployment.
- Implemented serial device enumeration and validation in the hardware module, enhancing security by restricting allowed serial paths.
- Updated related modules to integrate new serial device functionality.
---------
Co-authored-by: ehushubhamshaw <eshaw1@wpi.edu>
149 lines
4.7 KiB
Rust
149 lines
4.7 KiB
Rust
//! ZeroClaw serial JSON protocol — the firmware contract.
|
|
//!
|
|
//! These types define the newline-delimited JSON wire format shared between
|
|
//! the ZeroClaw host and device firmware (Pico, Arduino, ESP32, Nucleo).
|
|
//!
|
|
//! Wire format:
|
|
//! Host → Device: `{"cmd":"gpio_write","params":{"pin":25,"value":1}}\n`
|
|
//! Device → Host: `{"ok":true,"data":{"pin":25,"value":1,"state":"HIGH"}}\n`
|
|
//!
|
|
//! Both sides MUST agree on these struct definitions. Any change here is a
|
|
//! breaking firmware contract change.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Host-to-device command.
|
|
///
|
|
/// Serialized as one JSON line terminated by `\n`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct ZcCommand {
|
|
/// Command name (e.g. `"gpio_read"`, `"ping"`, `"reboot_bootsel"`).
|
|
pub cmd: String,
|
|
/// Command parameters — schema depends on the command.
|
|
#[serde(default)]
|
|
pub params: serde_json::Value,
|
|
}
|
|
|
|
impl ZcCommand {
|
|
/// Create a new command with the given name and parameters.
|
|
pub fn new(cmd: impl Into<String>, params: serde_json::Value) -> Self {
|
|
Self {
|
|
cmd: cmd.into(),
|
|
params,
|
|
}
|
|
}
|
|
|
|
/// Create a parameterless command (e.g. `ping`, `capabilities`).
|
|
pub fn simple(cmd: impl Into<String>) -> Self {
|
|
Self {
|
|
cmd: cmd.into(),
|
|
params: serde_json::Value::Object(serde_json::Map::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Device-to-host response.
|
|
///
|
|
/// Serialized as one JSON line terminated by `\n`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct ZcResponse {
|
|
/// Whether the command succeeded.
|
|
pub ok: bool,
|
|
/// Response payload — schema depends on the command executed.
|
|
#[serde(default)]
|
|
pub data: serde_json::Value,
|
|
/// Human-readable error message when `ok` is false.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
impl ZcResponse {
|
|
/// Create a success response with data.
|
|
pub fn success(data: serde_json::Value) -> Self {
|
|
Self {
|
|
ok: true,
|
|
data,
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
/// Create an error response.
|
|
pub fn error(message: impl Into<String>) -> Self {
|
|
Self {
|
|
ok: false,
|
|
data: serde_json::Value::Null,
|
|
error: Some(message.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn zc_command_serialization_roundtrip() {
|
|
let cmd = ZcCommand::new("gpio_write", json!({"pin": 25, "value": 1}));
|
|
let json = serde_json::to_string(&cmd).unwrap();
|
|
let parsed: ZcCommand = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed.cmd, "gpio_write");
|
|
assert_eq!(parsed.params["pin"], 25);
|
|
assert_eq!(parsed.params["value"], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn zc_command_simple_has_empty_params() {
|
|
let cmd = ZcCommand::simple("ping");
|
|
assert_eq!(cmd.cmd, "ping");
|
|
assert!(cmd.params.is_object());
|
|
}
|
|
|
|
#[test]
|
|
fn zc_response_success_roundtrip() {
|
|
let resp = ZcResponse::success(json!({"value": 1}));
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
let parsed: ZcResponse = serde_json::from_str(&json).unwrap();
|
|
assert!(parsed.ok);
|
|
assert_eq!(parsed.data["value"], 1);
|
|
assert!(parsed.error.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn zc_response_error_roundtrip() {
|
|
let resp = ZcResponse::error("pin not available");
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
let parsed: ZcResponse = serde_json::from_str(&json).unwrap();
|
|
assert!(!parsed.ok);
|
|
assert_eq!(parsed.error.as_deref(), Some("pin not available"));
|
|
}
|
|
|
|
#[test]
|
|
fn zc_command_wire_format_matches_spec() {
|
|
// Verify the exact JSON shape the firmware expects.
|
|
let cmd = ZcCommand::new("gpio_write", json!({"pin": 25, "value": 1}));
|
|
let v: serde_json::Value = serde_json::to_value(&cmd).unwrap();
|
|
assert!(v.get("cmd").is_some());
|
|
assert!(v.get("params").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn zc_response_from_firmware_json() {
|
|
// Simulate a raw firmware response line.
|
|
let raw = r#"{"ok":true,"data":{"pin":25,"value":1,"state":"HIGH"}}"#;
|
|
let resp: ZcResponse = serde_json::from_str(raw).unwrap();
|
|
assert!(resp.ok);
|
|
assert_eq!(resp.data["state"], "HIGH");
|
|
}
|
|
|
|
#[test]
|
|
fn zc_response_missing_optional_fields() {
|
|
// Firmware may omit `data` and `error` on success.
|
|
let raw = r#"{"ok":true}"#;
|
|
let resp: ZcResponse = serde_json::from_str(raw).unwrap();
|
|
assert!(resp.ok);
|
|
assert!(resp.data.is_null());
|
|
assert!(resp.error.is_none());
|
|
}
|
|
}
|