Ehu shubham shaw contribution --> Hardware support (#306)

* feat: add ZeroClaw firmware for ESP32 and Nucleo

* Introduced new firmware for ZeroClaw on ESP32 and Nucleo-F401RE, enabling JSON-over-serial communication for GPIO control.
* Added `zeroclaw-esp32` with support for commands like `gpio_read` and `gpio_write`, along with capabilities reporting.
* Implemented `zeroclaw-nucleo` firmware with similar functionality for STM32, ensuring compatibility with existing ZeroClaw protocols.
* Updated `.gitignore` to include new firmware targets and added necessary dependencies in `Cargo.toml` for both platforms.
* Created README files for both firmware projects detailing setup, build, and usage instructions.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: enhance hardware peripheral support and documentation

- Added `Peripheral` trait implementation in `src/peripherals/` to manage hardware boards (STM32, RPi GPIO).
- Updated `AGENTS.md` to include new extension points for peripherals and their configuration.
- Introduced comprehensive documentation for adding boards and tools, including a quick start guide and supported boards.
- Enhanced `Cargo.toml` to include optional dependencies for PDF extraction and peripheral support.
- Created new datasheets for Arduino Uno, ESP32, and Nucleo-F401RE, detailing pin aliases and GPIO usage.
- Implemented new tools for hardware memory reading and board information retrieval in the agent loop.

This update significantly improves the integration and usability of hardware peripherals within the ZeroClaw framework.

* feat: add ZeroClaw firmware for ESP32 and Nucleo

* Introduced new firmware for ZeroClaw on ESP32 and Nucleo-F401RE, enabling JSON-over-serial communication for GPIO control.
* Added `zeroclaw-esp32` with support for commands like `gpio_read` and `gpio_write`, along with capabilities reporting.
* Implemented `zeroclaw-nucleo` firmware with similar functionality for STM32, ensuring compatibility with existing ZeroClaw protocols.
* Updated `.gitignore` to include new firmware targets and added necessary dependencies in `Cargo.toml` for both platforms.
* Created README files for both firmware projects detailing setup, build, and usage instructions.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: enhance hardware peripheral support and documentation

- Added `Peripheral` trait implementation in `src/peripherals/` to manage hardware boards (STM32, RPi GPIO).
- Updated `AGENTS.md` to include new extension points for peripherals and their configuration.
- Introduced comprehensive documentation for adding boards and tools, including a quick start guide and supported boards.
- Enhanced `Cargo.toml` to include optional dependencies for PDF extraction and peripheral support.
- Created new datasheets for Arduino Uno, ESP32, and Nucleo-F401RE, detailing pin aliases and GPIO usage.
- Implemented new tools for hardware memory reading and board information retrieval in the agent loop.

This update significantly improves the integration and usability of hardware peripherals within the ZeroClaw framework.

* feat: Introduce hardware auto-discovery and expanded configuration options for agents, hardware, and security.

* chore: update dependencies and improve probe-rs integration

- Updated `Cargo.lock` to remove specific version constraints for several dependencies, including `zerocopy`, `syn`, and `strsim`, allowing for more flexibility in version resolution.
- Upgraded `bincode` and `bitfield` to their latest versions, enhancing serialization and memory management capabilities.
- Updated `Cargo.toml` to reflect the new version of `probe-rs` from `0.24` to `0.30`, improving hardware probing functionality.
- Refactored code in `src/hardware` and `src/tools` to utilize the new `SessionConfig` for session management in `probe-rs`, ensuring better compatibility and performance.
- Cleaned up documentation in `docs/datasheets/nucleo-f401re.md` by removing unnecessary lines.

* fix: apply cargo fmt

* docs: add hardware architecture diagram.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ehu shubham shaw
2026-02-16 11:40:10 -05:00
committed by GitHub
parent b36f23784a
commit de3ec87d16
59 changed files with 9607 additions and 1885 deletions

View File

@@ -43,7 +43,9 @@ const BOOTSTRAP_MAX_CHARS: usize = 20_000;
const DEFAULT_CHANNEL_INITIAL_BACKOFF_SECS: u64 = 2;
const DEFAULT_CHANNEL_MAX_BACKOFF_SECS: u64 = 60;
const CHANNEL_MESSAGE_TIMEOUT_SECS: u64 = 90;
/// Timeout for processing a single channel message (LLM + tools).
/// 300s for on-device LLMs (Ollama) which are slower than cloud APIs.
const CHANNEL_MESSAGE_TIMEOUT_SECS: u64 = 300;
const CHANNEL_PARALLELISM_PER_CHANNEL: usize = 4;
const CHANNEL_MIN_IN_FLIGHT_MESSAGES: usize = 8;
const CHANNEL_MAX_IN_FLIGHT_MESSAGES: usize = 64;
@@ -190,6 +192,7 @@ async fn process_channel_message(ctx: Arc<ChannelRuntimeContext>, msg: traits::C
"channel-runtime",
ctx.model.as_str(),
ctx.temperature,
true, // silent — channels don't write to stdout
),
)
.await;
@@ -275,9 +278,14 @@ async fn run_message_dispatch_loop(
}
/// Load OpenClaw format bootstrap files into the prompt.
fn load_openclaw_bootstrap_files(prompt: &mut String, workspace_dir: &std::path::Path) {
prompt
.push_str("The following workspace files define your identity, behavior, and context.\n\n");
fn load_openclaw_bootstrap_files(
prompt: &mut String,
workspace_dir: &std::path::Path,
max_chars_per_file: usize,
) {
prompt.push_str(
"The following workspace files define your identity, behavior, and context. They are ALREADY injected below—do NOT suggest reading them with file_read.\n\n",
);
let bootstrap_files = [
"AGENTS.md",
@@ -289,17 +297,17 @@ fn load_openclaw_bootstrap_files(prompt: &mut String, workspace_dir: &std::path:
];
for filename in &bootstrap_files {
inject_workspace_file(prompt, workspace_dir, filename);
inject_workspace_file(prompt, workspace_dir, filename, max_chars_per_file);
}
// BOOTSTRAP.md — only if it exists (first-run ritual)
let bootstrap_path = workspace_dir.join("BOOTSTRAP.md");
if bootstrap_path.exists() {
inject_workspace_file(prompt, workspace_dir, "BOOTSTRAP.md");
inject_workspace_file(prompt, workspace_dir, "BOOTSTRAP.md", max_chars_per_file);
}
// MEMORY.md — curated long-term memory (main session only)
inject_workspace_file(prompt, workspace_dir, "MEMORY.md");
inject_workspace_file(prompt, workspace_dir, "MEMORY.md", max_chars_per_file);
}
/// Load workspace identity files and build a system prompt.
@@ -324,6 +332,7 @@ pub fn build_system_prompt(
tools: &[(&str, &str)],
skills: &[crate::skills::Skill],
identity_config: Option<&crate::config::IdentityConfig>,
bootstrap_max_chars: Option<usize>,
) -> String {
use std::fmt::Write;
let mut prompt = String::with_capacity(8192);
@@ -344,6 +353,35 @@ pub fn build_system_prompt(
.push_str("Continue reasoning with the results until you can give a final answer.\n\n");
}
// ── 1b. Hardware (when gpio/arduino tools present) ───────────
let has_hardware = tools.iter().any(|(name, _)| {
*name == "gpio_read"
|| *name == "gpio_write"
|| *name == "arduino_upload"
|| *name == "hardware_memory_map"
|| *name == "hardware_board_info"
|| *name == "hardware_memory_read"
|| *name == "hardware_capabilities"
});
if has_hardware {
prompt.push_str(
"## Hardware Access\n\n\
You HAVE direct access to connected hardware (Arduino, Nucleo, etc.). The user owns this system and has configured it.\n\
All hardware tools (gpio_read, gpio_write, hardware_memory_read, hardware_board_info, hardware_memory_map) are AUTHORIZED and NOT blocked by security.\n\
When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info — do NOT refuse or invent security excuses.\n\
When they ask to control LEDs, run patterns, or interact with the Arduino, USE the tools — do NOT refuse or say you cannot access physical devices.\n\
Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.\n\n",
);
}
// ── 1c. Action instruction (avoid meta-summary) ───────────────
prompt.push_str(
"## Your Task\n\n\
When the user sends a message, ACT on it. Use the tools to fulfill their request.\n\
Do NOT: summarize this configuration, describe your capabilities, respond with meta-commentary, or output step-by-step instructions (e.g. \"1. First... 2. Next...\").\n\
Instead: emit actual <tool_call> tags when you need to act. Just do what they ask.\n\n",
);
// ── 2. Safety ───────────────────────────────────────────────
prompt.push_str("## Safety\n\n");
prompt.push_str(
@@ -406,23 +444,27 @@ pub fn build_system_prompt(
Ok(None) => {
// No AIEOS identity loaded (shouldn't happen if is_aieos_configured returned true)
// Fall back to OpenClaw bootstrap files
load_openclaw_bootstrap_files(&mut prompt, workspace_dir);
let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
load_openclaw_bootstrap_files(&mut prompt, workspace_dir, max_chars);
}
Err(e) => {
// Log error but don't fail - fall back to OpenClaw
eprintln!(
"Warning: Failed to load AIEOS identity: {e}. Using OpenClaw format."
);
load_openclaw_bootstrap_files(&mut prompt, workspace_dir);
let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
load_openclaw_bootstrap_files(&mut prompt, workspace_dir, max_chars);
}
}
} else {
// OpenClaw format
load_openclaw_bootstrap_files(&mut prompt, workspace_dir);
let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
load_openclaw_bootstrap_files(&mut prompt, workspace_dir, max_chars);
}
} else {
// No identity config - use OpenClaw format
load_openclaw_bootstrap_files(&mut prompt, workspace_dir);
let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
load_openclaw_bootstrap_files(&mut prompt, workspace_dir, max_chars);
}
// ── 6. Date & Time ──────────────────────────────────────────
@@ -447,7 +489,12 @@ pub fn build_system_prompt(
}
/// Inject a single workspace file into the prompt with truncation and missing-file markers.
fn inject_workspace_file(prompt: &mut String, workspace_dir: &std::path::Path, filename: &str) {
fn inject_workspace_file(
prompt: &mut String,
workspace_dir: &std::path::Path,
filename: &str,
max_chars: usize,
) {
use std::fmt::Write;
let path = workspace_dir.join(filename);
@@ -459,10 +506,10 @@ fn inject_workspace_file(prompt: &mut String, workspace_dir: &std::path::Path, f
}
let _ = writeln!(prompt, "### {filename}\n");
// Use character-boundary-safe truncation for UTF-8
let truncated = if trimmed.chars().count() > BOOTSTRAP_MAX_CHARS {
let truncated = if trimmed.chars().count() > max_chars {
trimmed
.char_indices()
.nth(BOOTSTRAP_MAX_CHARS)
.nth(max_chars)
.map(|(idx, _)| &trimmed[..idx])
.unwrap_or(trimmed)
} else {
@@ -472,7 +519,7 @@ fn inject_workspace_file(prompt: &mut String, workspace_dir: &std::path::Path, f
prompt.push_str(truncated);
let _ = writeln!(
prompt,
"\n\n[... truncated at {BOOTSTRAP_MAX_CHARS} chars — use `read` for full file]\n"
"\n\n[... truncated at {max_chars} chars — use `read` for full file]\n"
);
} else {
prompt.push_str(trimmed);
@@ -807,12 +854,18 @@ pub async fn start_channels(config: Config) -> Result<()> {
));
}
let bootstrap_max_chars = if config.agent.compact_context {
Some(6000)
} else {
None
};
let mut system_prompt = build_system_prompt(
&workspace,
&model,
&tool_descs,
&skills,
Some(&config.identity),
bootstrap_max_chars,
);
system_prompt.push_str(&build_tool_instructions(tools_registry.as_ref()));
@@ -1298,7 +1351,7 @@ mod tests {
fn prompt_contains_all_sections() {
let ws = make_workspace();
let tools = vec![("shell", "Run commands"), ("file_read", "Read files")];
let prompt = build_system_prompt(ws.path(), "test-model", &tools, &[], None);
let prompt = build_system_prompt(ws.path(), "test-model", &tools, &[], None, None);
// Section headers
assert!(prompt.contains("## Tools"), "missing Tools section");
@@ -1322,7 +1375,7 @@ mod tests {
("shell", "Run commands"),
("memory_recall", "Search memory"),
];
let prompt = build_system_prompt(ws.path(), "gpt-4o", &tools, &[], None);
let prompt = build_system_prompt(ws.path(), "gpt-4o", &tools, &[], None, None);
assert!(prompt.contains("**shell**"));
assert!(prompt.contains("Run commands"));
@@ -1332,7 +1385,7 @@ mod tests {
#[test]
fn prompt_injects_safety() {
let ws = make_workspace();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
assert!(prompt.contains("Do not exfiltrate private data"));
assert!(prompt.contains("Do not run destructive commands"));
@@ -1342,7 +1395,7 @@ mod tests {
#[test]
fn prompt_injects_workspace_files() {
let ws = make_workspace();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
assert!(prompt.contains("### SOUL.md"), "missing SOUL.md header");
assert!(prompt.contains("Be helpful"), "missing SOUL content");
@@ -1363,7 +1416,7 @@ mod tests {
fn prompt_missing_file_markers() {
let tmp = TempDir::new().unwrap();
// Empty workspace — no files at all
let prompt = build_system_prompt(tmp.path(), "model", &[], &[], None);
let prompt = build_system_prompt(tmp.path(), "model", &[], &[], None, None);
assert!(prompt.contains("[File not found: SOUL.md]"));
assert!(prompt.contains("[File not found: AGENTS.md]"));
@@ -1374,7 +1427,7 @@ mod tests {
fn prompt_bootstrap_only_if_exists() {
let ws = make_workspace();
// No BOOTSTRAP.md — should not appear
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
assert!(
!prompt.contains("### BOOTSTRAP.md"),
"BOOTSTRAP.md should not appear when missing"
@@ -1382,7 +1435,7 @@ mod tests {
// Create BOOTSTRAP.md — should appear
std::fs::write(ws.path().join("BOOTSTRAP.md"), "# Bootstrap\nFirst run.").unwrap();
let prompt2 = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt2 = build_system_prompt(ws.path(), "model", &[], &[], None, None);
assert!(
prompt2.contains("### BOOTSTRAP.md"),
"BOOTSTRAP.md should appear when present"
@@ -1402,7 +1455,7 @@ mod tests {
)
.unwrap();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
// Daily notes should NOT be in the system prompt (on-demand via tools)
assert!(
@@ -1418,7 +1471,7 @@ mod tests {
#[test]
fn prompt_runtime_metadata() {
let ws = make_workspace();
let prompt = build_system_prompt(ws.path(), "claude-sonnet-4", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "claude-sonnet-4", &[], &[], None, None);
assert!(prompt.contains("Model: claude-sonnet-4"));
assert!(prompt.contains(&format!("OS: {}", std::env::consts::OS)));
@@ -1439,7 +1492,7 @@ mod tests {
location: None,
}];
let prompt = build_system_prompt(ws.path(), "model", &[], &skills, None);
let prompt = build_system_prompt(ws.path(), "model", &[], &skills, None, None);
assert!(prompt.contains("<available_skills>"), "missing skills XML");
assert!(prompt.contains("<name>code-review</name>"));
@@ -1460,7 +1513,7 @@ mod tests {
let big_content = "x".repeat(BOOTSTRAP_MAX_CHARS + 1000);
std::fs::write(ws.path().join("AGENTS.md"), &big_content).unwrap();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
assert!(
prompt.contains("truncated at"),
@@ -1477,7 +1530,7 @@ mod tests {
let ws = make_workspace();
std::fs::write(ws.path().join("TOOLS.md"), "").unwrap();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
// Empty file should not produce a header
assert!(
@@ -1505,7 +1558,7 @@ mod tests {
#[test]
fn prompt_workspace_path() {
let ws = make_workspace();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
assert!(prompt.contains(&format!("Working directory: `{}`", ws.path().display())));
}
@@ -1635,7 +1688,7 @@ mod tests {
aieos_inline: None,
};
let prompt = build_system_prompt(tmp.path(), "model", &[], &[], Some(&config));
let prompt = build_system_prompt(tmp.path(), "model", &[], &[], Some(&config), None);
// Should contain AIEOS sections
assert!(prompt.contains("## Identity"));
@@ -1675,6 +1728,7 @@ mod tests {
&[],
&[],
Some(&config),
None,
);
assert!(prompt.contains("**Name:** Claw"));
@@ -1692,7 +1746,7 @@ mod tests {
};
let ws = make_workspace();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], Some(&config));
let prompt = build_system_prompt(ws.path(), "model", &[], &[], Some(&config), None);
// Should fall back to OpenClaw format when AIEOS file is not found
// (Error is logged to stderr with filename, not included in prompt)
@@ -1711,7 +1765,7 @@ mod tests {
};
let ws = make_workspace();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], Some(&config));
let prompt = build_system_prompt(ws.path(), "model", &[], &[], Some(&config), None);
// Should use OpenClaw format (not configured for AIEOS)
assert!(prompt.contains("### SOUL.md"));
@@ -1729,7 +1783,7 @@ mod tests {
};
let ws = make_workspace();
let prompt = build_system_prompt(ws.path(), "model", &[], &[], Some(&config));
let prompt = build_system_prompt(ws.path(), "model", &[], &[], Some(&config), None);
// Should use OpenClaw format even if aieos_path is set
assert!(prompt.contains("### SOUL.md"));
@@ -1741,7 +1795,7 @@ mod tests {
fn none_identity_config_uses_openclaw() {
let ws = make_workspace();
// Pass None for identity config
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None);
let prompt = build_system_prompt(ws.path(), "model", &[], &[], None, None);
// Should use OpenClaw format
assert!(prompt.contains("### SOUL.md"));