* 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.
148 lines
4.6 KiB
Go
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
|
|
}
|