Files
picoclaw/pkg/tools/spi.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

157 lines
4.6 KiB
Go

package tools
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"runtime"
)
// SPITool provides SPI bus interaction for high-speed peripheral communication.
type SPITool struct{}
func NewSPITool() *SPITool {
return &SPITool{}
}
func (t *SPITool) Name() string {
return "spi"
}
func (t *SPITool) Description() string {
return "Interact with SPI bus devices for high-speed peripheral communication. Actions: list (find SPI devices), transfer (full-duplex send/receive), read (receive bytes). Linux only."
}
func (t *SPITool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"list", "transfer", "read"},
"description": "Action to perform: list (find available SPI devices), transfer (full-duplex send/receive), read (receive bytes by sending zeros)",
},
"device": map[string]interface{}{
"type": "string",
"description": "SPI device identifier (e.g. \"2.0\" for /dev/spidev2.0). Required for transfer/read.",
},
"speed": map[string]interface{}{
"type": "integer",
"description": "SPI clock speed in Hz. Default: 1000000 (1 MHz).",
},
"mode": map[string]interface{}{
"type": "integer",
"description": "SPI mode (0-3). Default: 0. Mode sets CPOL and CPHA: 0=0,0 1=0,1 2=1,0 3=1,1.",
},
"bits": map[string]interface{}{
"type": "integer",
"description": "Bits per word. Default: 8.",
},
"data": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "integer"},
"description": "Bytes to send (0-255 each). Required for transfer action.",
},
"length": map[string]interface{}{
"type": "integer",
"description": "Number of bytes to read (1-4096). Required for read action.",
},
"confirm": map[string]interface{}{
"type": "boolean",
"description": "Must be true for transfer operations. Safety guard to prevent accidental writes.",
},
},
"required": []string{"action"},
}
}
func (t *SPITool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if runtime.GOOS != "linux" {
return ErrorResult("SPI is only supported on Linux. This tool requires /dev/spidev* device files.")
}
action, ok := args["action"].(string)
if !ok {
return ErrorResult("action is required")
}
switch action {
case "list":
return t.list()
case "transfer":
return t.transfer(args)
case "read":
return t.readDevice(args)
default:
return ErrorResult(fmt.Sprintf("unknown action: %s (valid: list, transfer, read)", action))
}
}
// list finds available SPI devices by globbing /dev/spidev*
func (t *SPITool) list() *ToolResult {
matches, err := filepath.Glob("/dev/spidev*")
if err != nil {
return ErrorResult(fmt.Sprintf("failed to scan for SPI devices: %v", err))
}
if len(matches) == 0 {
return SilentResult("No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded")
}
type devInfo struct {
Path string `json:"path"`
Device string `json:"device"`
}
devices := make([]devInfo, 0, len(matches))
re := regexp.MustCompile(`/dev/spidev(\d+\.\d+)`)
for _, m := range matches {
if sub := re.FindStringSubmatch(m); sub != nil {
devices = append(devices, devInfo{Path: m, Device: sub[1]})
}
}
result, _ := json.MarshalIndent(devices, "", " ")
return SilentResult(fmt.Sprintf("Found %d SPI device(s):\n%s", len(devices), string(result)))
}
// parseSPIArgs extracts and validates common SPI parameters
func parseSPIArgs(args map[string]interface{}) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
dev, ok := args["device"].(string)
if !ok || dev == "" {
return "", 0, 0, 0, "device is required (e.g. \"2.0\" for /dev/spidev2.0)"
}
matched, _ := regexp.MatchString(`^\d+\.\d+$`, dev)
if !matched {
return "", 0, 0, 0, "invalid device identifier: must be in format \"X.Y\" (e.g. \"2.0\")"
}
speed = 1000000 // default 1 MHz
if s, ok := args["speed"].(float64); ok {
if s < 1 || s > 125000000 {
return "", 0, 0, 0, "speed must be between 1 Hz and 125 MHz"
}
speed = uint32(s)
}
mode = 0
if m, ok := args["mode"].(float64); ok {
if int(m) < 0 || int(m) > 3 {
return "", 0, 0, 0, "mode must be 0-3"
}
mode = uint8(m)
}
bits = 8
if b, ok := args["bits"].(float64); ok {
if int(b) < 1 || int(b) > 32 {
return "", 0, 0, 0, "bits must be between 1 and 32"
}
bits = uint8(b)
}
return dev, speed, mode, bits, ""
}