Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
registry.Register(tools.NewWebSearchTool(braveAPIKey, cfg.Tools.Web.Search.MaxResults))
registry.Register(tools.NewWebFetchTool(50000))

// Hardware tools (I2C, SPI) - Linux only, returns error on other platforms
registry.Register(tools.NewI2CTool())
registry.Register(tools.NewSPITool())

// Message tool - available to both agent and subagent
// Subagent uses it to communicate directly with user
messageTool := tools.NewMessageTool()
Expand Down
147 changes: 147 additions & 0 deletions pkg/tools/i2c.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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
}
Loading