Files
picoclaw/pkg/tools/i2c.go
karan 2720fa71c7 add I2C and SPI tools for hardware interaction (#140)
* add I2C and SPI tools for hardware interaction

- Implemented I2CTool for I2C bus interaction, including device scanning, reading, and writing.
- Implemented SPITool for SPI bus communication, supporting device listing, data transfer, and reading.
- Added platform-specific implementations for Linux and stubs for non-Linux platforms.
- Updated agent loop to register new I2C and SPI tools.
- Created documentation for hardware skills, including usage examples and pinmux setup instructions.

* Remove build constraints for Linux from I2C and SPI tool files.
2026-02-14 16:53:17 +08:00

148 lines
4.6 KiB
Go

package tools
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"runtime"
)
// I2CTool provides I2C bus interaction for reading sensors and controlling peripherals.
type I2CTool struct{}
func NewI2CTool() *I2CTool {
return &I2CTool{}
}
func (t *I2CTool) Name() string {
return "i2c"
}
func (t *I2CTool) Description() string {
return "Interact with I2C bus devices for reading sensors and controlling peripherals. Actions: detect (list buses), scan (find devices on a bus), read (read bytes from device), write (send bytes to device). Linux only."
}
func (t *I2CTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"detect", "scan", "read", "write"},
"description": "Action to perform: detect (list available I2C buses), scan (find devices on a bus), read (read bytes from a device), write (send bytes to a device)",
},
"bus": map[string]interface{}{
"type": "string",
"description": "I2C bus number (e.g. \"1\" for /dev/i2c-1). Required for scan/read/write.",
},
"address": map[string]interface{}{
"type": "integer",
"description": "7-bit I2C device address (0x03-0x77). Required for read/write.",
},
"register": map[string]interface{}{
"type": "integer",
"description": "Register address to read from or write to. If set, sends register byte before read/write.",
},
"data": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "integer"},
"description": "Bytes to write (0-255 each). Required for write action.",
},
"length": map[string]interface{}{
"type": "integer",
"description": "Number of bytes to read (1-256). Default: 1. Used with read action.",
},
"confirm": map[string]interface{}{
"type": "boolean",
"description": "Must be true for write operations. Safety guard to prevent accidental writes.",
},
},
"required": []string{"action"},
}
}
func (t *I2CTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if runtime.GOOS != "linux" {
return ErrorResult("I2C is only supported on Linux. This tool requires /dev/i2c-* device files.")
}
action, ok := args["action"].(string)
if !ok {
return ErrorResult("action is required")
}
switch action {
case "detect":
return t.detect()
case "scan":
return t.scan(args)
case "read":
return t.readDevice(args)
case "write":
return t.writeDevice(args)
default:
return ErrorResult(fmt.Sprintf("unknown action: %s (valid: detect, scan, read, write)", action))
}
}
// detect lists available I2C buses by globbing /dev/i2c-*
func (t *I2CTool) detect() *ToolResult {
matches, err := filepath.Glob("/dev/i2c-*")
if err != nil {
return ErrorResult(fmt.Sprintf("failed to scan for I2C buses: %v", err))
}
if len(matches) == 0 {
return SilentResult("No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)")
}
type busInfo struct {
Path string `json:"path"`
Bus string `json:"bus"`
}
buses := make([]busInfo, 0, len(matches))
re := regexp.MustCompile(`/dev/i2c-(\d+)`)
for _, m := range matches {
if sub := re.FindStringSubmatch(m); sub != nil {
buses = append(buses, busInfo{Path: m, Bus: sub[1]})
}
}
result, _ := json.MarshalIndent(buses, "", " ")
return SilentResult(fmt.Sprintf("Found %d I2C bus(es):\n%s", len(buses), string(result)))
}
// isValidBusID checks that a bus identifier is a simple number (prevents path injection)
func isValidBusID(id string) bool {
matched, _ := regexp.MatchString(`^\d+$`, id)
return matched
}
// parseI2CAddress extracts and validates an I2C address from args
func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
addrFloat, ok := args["address"].(float64)
if !ok {
return 0, ErrorResult("address is required (e.g. 0x38 for AHT20)")
}
addr := int(addrFloat)
if addr < 0x03 || addr > 0x77 {
return 0, ErrorResult("address must be in valid 7-bit range (0x03-0x77)")
}
return addr, nil
}
// parseI2CBus extracts and validates an I2C bus from args
func parseI2CBus(args map[string]interface{}) (string, *ToolResult) {
bus, ok := args["bus"].(string)
if !ok || bus == "" {
return "", ErrorResult("bus is required (e.g. \"1\" for /dev/i2c-1)")
}
if !isValidBusID(bus) {
return "", ErrorResult("invalid bus identifier: must be a number (e.g. \"1\")")
}
return bus, nil
}