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.
This commit is contained in:
147
pkg/tools/i2c.go
Normal file
147
pkg/tools/i2c.go
Normal file
@@ -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
|
||||
}
|
||||
282
pkg/tools/i2c_linux.go
Normal file
282
pkg/tools/i2c_linux.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// I2C ioctl constants from Linux kernel headers (<linux/i2c-dev.h>, <linux/i2c.h>)
|
||||
const (
|
||||
i2cSlave = 0x0703 // Set slave address (fails if in use by driver)
|
||||
i2cFuncs = 0x0705 // Query adapter functionality bitmask
|
||||
i2cSmbus = 0x0720 // Perform SMBus transaction
|
||||
|
||||
// I2C_FUNC capability bits
|
||||
i2cFuncSmbusQuick = 0x00010000
|
||||
i2cFuncSmbusReadByte = 0x00020000
|
||||
|
||||
// SMBus transaction types
|
||||
i2cSmbusRead = 0
|
||||
i2cSmbusWrite = 1
|
||||
|
||||
// SMBus protocol sizes
|
||||
i2cSmbusQuick = 0
|
||||
i2cSmbusByte = 1
|
||||
)
|
||||
|
||||
// i2cSmbusData matches the kernel union i2c_smbus_data (34 bytes max).
|
||||
// For quick and byte transactions only the first byte is used (if at all).
|
||||
type i2cSmbusData [34]byte
|
||||
|
||||
// i2cSmbusArgs matches the kernel struct i2c_smbus_ioctl_data.
|
||||
type i2cSmbusArgs struct {
|
||||
readWrite uint8
|
||||
command uint8
|
||||
size uint32
|
||||
data *i2cSmbusData
|
||||
}
|
||||
|
||||
// smbusProbe performs a single SMBus probe at the given address.
|
||||
// Uses SMBus Quick Write (safest) or falls back to SMBus Read Byte for
|
||||
// EEPROM address ranges where quick write can corrupt AT24RF08 chips.
|
||||
// This matches i2cdetect's MODE_AUTO behavior.
|
||||
func smbusProbe(fd int, addr int, hasQuick bool) bool {
|
||||
// EEPROM ranges: use read byte (quick write can corrupt AT24RF08)
|
||||
useReadByte := (addr >= 0x30 && addr <= 0x37) || (addr >= 0x50 && addr <= 0x5F)
|
||||
|
||||
if !useReadByte && hasQuick {
|
||||
// SMBus Quick Write: [START] [ADDR|W] [ACK/NACK] [STOP]
|
||||
// Safest probe — no data transferred
|
||||
args := i2cSmbusArgs{
|
||||
readWrite: i2cSmbusWrite,
|
||||
command: 0,
|
||||
size: i2cSmbusQuick,
|
||||
data: nil,
|
||||
}
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args)))
|
||||
return errno == 0
|
||||
}
|
||||
|
||||
// SMBus Read Byte: [START] [ADDR|R] [ACK/NACK] [DATA] [STOP]
|
||||
var data i2cSmbusData
|
||||
args := i2cSmbusArgs{
|
||||
readWrite: i2cSmbusRead,
|
||||
command: 0,
|
||||
size: i2cSmbusByte,
|
||||
data: &data,
|
||||
}
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args)))
|
||||
return errno == 0
|
||||
}
|
||||
|
||||
// scan probes valid 7-bit addresses on a bus for connected devices.
|
||||
// Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO:
|
||||
// SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges.
|
||||
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||
bus, errResult := parseI2CBus(args)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
}
|
||||
|
||||
devPath := fmt.Sprintf("/dev/i2c-%s", bus)
|
||||
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to open %s: %v (check permissions and i2c-dev module)", devPath, err))
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
// Query adapter capabilities to determine available probe methods.
|
||||
// I2C_FUNCS writes an unsigned long, which is word-sized on Linux.
|
||||
var funcs uintptr
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cFuncs, uintptr(unsafe.Pointer(&funcs)))
|
||||
if errno != 0 {
|
||||
return ErrorResult(fmt.Sprintf("failed to query I2C adapter capabilities on %s: %v", devPath, errno))
|
||||
}
|
||||
|
||||
hasQuick := funcs&i2cFuncSmbusQuick != 0
|
||||
hasReadByte := funcs&i2cFuncSmbusReadByte != 0
|
||||
|
||||
if !hasQuick && !hasReadByte {
|
||||
return ErrorResult(fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath))
|
||||
}
|
||||
|
||||
type deviceEntry struct {
|
||||
Address string `json:"address"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
var found []deviceEntry
|
||||
// Scan 0x08-0x77, skipping I2C reserved addresses 0x00-0x07
|
||||
for addr := 0x08; addr <= 0x77; addr++ {
|
||||
// Set slave address — EBUSY means a kernel driver owns this address
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr))
|
||||
if errno != 0 {
|
||||
if errno == syscall.EBUSY {
|
||||
found = append(found, deviceEntry{
|
||||
Address: fmt.Sprintf("0x%02x", addr),
|
||||
Status: "busy (in use by kernel driver)",
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if smbusProbe(fd, addr, hasQuick) {
|
||||
found = append(found, deviceEntry{
|
||||
Address: fmt.Sprintf("0x%02x", addr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) == 0 {
|
||||
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"bus": devPath,
|
||||
"devices": found,
|
||||
"count": len(found),
|
||||
}, "", " ")
|
||||
return SilentResult(fmt.Sprintf("Scan of %s:\n%s", devPath, string(result)))
|
||||
}
|
||||
|
||||
// readDevice reads bytes from an I2C device, optionally at a specific register
|
||||
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
bus, errResult := parseI2CBus(args)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
}
|
||||
|
||||
addr, errResult := parseI2CAddress(args)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
}
|
||||
|
||||
length := 1
|
||||
if l, ok := args["length"].(float64); ok {
|
||||
length = int(l)
|
||||
}
|
||||
if length < 1 || length > 256 {
|
||||
return ErrorResult("length must be between 1 and 256")
|
||||
}
|
||||
|
||||
devPath := fmt.Sprintf("/dev/i2c-%s", bus)
|
||||
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err))
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
// Set slave address
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr))
|
||||
if errno != 0 {
|
||||
return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno))
|
||||
}
|
||||
|
||||
// If register is specified, write it first
|
||||
if regFloat, ok := args["register"].(float64); ok {
|
||||
reg := int(regFloat)
|
||||
if reg < 0 || reg > 255 {
|
||||
return ErrorResult("register must be between 0x00 and 0xFF")
|
||||
}
|
||||
_, err := syscall.Write(fd, []byte{byte(reg)})
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to write register 0x%02x: %v", reg, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Read data
|
||||
buf := make([]byte, length)
|
||||
n, err := syscall.Read(fd, buf)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to read from device 0x%02x: %v", addr, err))
|
||||
}
|
||||
|
||||
// Format as hex bytes
|
||||
hexBytes := make([]string, n)
|
||||
intBytes := make([]int, n)
|
||||
for i := 0; i < n; i++ {
|
||||
hexBytes[i] = fmt.Sprintf("0x%02x", buf[i])
|
||||
intBytes[i] = int(buf[i])
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"bus": devPath,
|
||||
"address": fmt.Sprintf("0x%02x", addr),
|
||||
"bytes": intBytes,
|
||||
"hex": hexBytes,
|
||||
"length": n,
|
||||
}, "", " ")
|
||||
return SilentResult(string(result))
|
||||
}
|
||||
|
||||
// writeDevice writes bytes to an I2C device, optionally at a specific register
|
||||
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
||||
confirm, _ := args["confirm"].(bool)
|
||||
if !confirm {
|
||||
return ErrorResult("write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.")
|
||||
}
|
||||
|
||||
bus, errResult := parseI2CBus(args)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
}
|
||||
|
||||
addr, errResult := parseI2CAddress(args)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
}
|
||||
|
||||
dataRaw, ok := args["data"].([]interface{})
|
||||
if !ok || len(dataRaw) == 0 {
|
||||
return ErrorResult("data is required for write (array of byte values 0-255)")
|
||||
}
|
||||
if len(dataRaw) > 256 {
|
||||
return ErrorResult("data too long: maximum 256 bytes per I2C transaction")
|
||||
}
|
||||
|
||||
data := make([]byte, 0, len(dataRaw)+1)
|
||||
|
||||
// If register is specified, prepend it to the data
|
||||
if regFloat, ok := args["register"].(float64); ok {
|
||||
reg := int(regFloat)
|
||||
if reg < 0 || reg > 255 {
|
||||
return ErrorResult("register must be between 0x00 and 0xFF")
|
||||
}
|
||||
data = append(data, byte(reg))
|
||||
}
|
||||
|
||||
for i, v := range dataRaw {
|
||||
f, ok := v.(float64)
|
||||
if !ok {
|
||||
return ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i))
|
||||
}
|
||||
b := int(f)
|
||||
if b < 0 || b > 255 {
|
||||
return ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b))
|
||||
}
|
||||
data = append(data, byte(b))
|
||||
}
|
||||
|
||||
devPath := fmt.Sprintf("/dev/i2c-%s", bus)
|
||||
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err))
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
// Set slave address
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr))
|
||||
if errno != 0 {
|
||||
return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno))
|
||||
}
|
||||
|
||||
// Write data
|
||||
n, err := syscall.Write(fd, data)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to write to device 0x%02x: %v", addr, err))
|
||||
}
|
||||
|
||||
return SilentResult(fmt.Sprintf("Wrote %d byte(s) to device 0x%02x on %s", n, addr, devPath))
|
||||
}
|
||||
18
pkg/tools/i2c_other.go
Normal file
18
pkg/tools/i2c_other.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build !linux
|
||||
|
||||
package tools
|
||||
|
||||
// scan is a stub for non-Linux platforms.
|
||||
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||
return ErrorResult("I2C is only supported on Linux")
|
||||
}
|
||||
|
||||
// readDevice is a stub for non-Linux platforms.
|
||||
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
return ErrorResult("I2C is only supported on Linux")
|
||||
}
|
||||
|
||||
// writeDevice is a stub for non-Linux platforms.
|
||||
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
||||
return ErrorResult("I2C is only supported on Linux")
|
||||
}
|
||||
156
pkg/tools/spi.go
Normal file
156
pkg/tools/spi.go
Normal file
@@ -0,0 +1,156 @@
|
||||
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, ""
|
||||
}
|
||||
196
pkg/tools/spi_linux.go
Normal file
196
pkg/tools/spi_linux.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// SPI ioctl constants from Linux kernel headers.
|
||||
// Calculated from _IOW('k', nr, size) macro:
|
||||
//
|
||||
// direction(1)<<30 | size<<16 | type(0x6B)<<8 | nr
|
||||
const (
|
||||
spiIocWrMode = 0x40016B01 // _IOW('k', 1, __u8)
|
||||
spiIocWrBitsPerWord = 0x40016B03 // _IOW('k', 3, __u8)
|
||||
spiIocWrMaxSpeedHz = 0x40046B04 // _IOW('k', 4, __u32)
|
||||
spiIocMessage1 = 0x40206B00 // _IOW('k', 0, struct spi_ioc_transfer) — 32 bytes
|
||||
)
|
||||
|
||||
// spiTransfer matches Linux kernel struct spi_ioc_transfer (32 bytes on all architectures).
|
||||
type spiTransfer struct {
|
||||
txBuf uint64
|
||||
rxBuf uint64
|
||||
length uint32
|
||||
speedHz uint32
|
||||
delayUsecs uint16
|
||||
bitsPerWord uint8
|
||||
csChange uint8
|
||||
txNbits uint8
|
||||
rxNbits uint8
|
||||
wordDelay uint8
|
||||
pad uint8
|
||||
}
|
||||
|
||||
// configureSPI opens an SPI device and sets mode, bits per word, and speed
|
||||
func configureSPI(devPath string, mode uint8, bits uint8, speed uint32) (int, *ToolResult) {
|
||||
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return -1, ErrorResult(fmt.Sprintf("failed to open %s: %v (check permissions and spidev module)", devPath, err))
|
||||
}
|
||||
|
||||
// Set SPI mode
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrMode, uintptr(unsafe.Pointer(&mode)))
|
||||
if errno != 0 {
|
||||
syscall.Close(fd)
|
||||
return -1, ErrorResult(fmt.Sprintf("failed to set SPI mode %d: %v", mode, errno))
|
||||
}
|
||||
|
||||
// Set bits per word
|
||||
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrBitsPerWord, uintptr(unsafe.Pointer(&bits)))
|
||||
if errno != 0 {
|
||||
syscall.Close(fd)
|
||||
return -1, ErrorResult(fmt.Sprintf("failed to set bits per word %d: %v", bits, errno))
|
||||
}
|
||||
|
||||
// Set max speed
|
||||
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrMaxSpeedHz, uintptr(unsafe.Pointer(&speed)))
|
||||
if errno != 0 {
|
||||
syscall.Close(fd)
|
||||
return -1, ErrorResult(fmt.Sprintf("failed to set SPI speed %d Hz: %v", speed, errno))
|
||||
}
|
||||
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// transfer performs a full-duplex SPI transfer
|
||||
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||
confirm, _ := args["confirm"].(bool)
|
||||
if !confirm {
|
||||
return ErrorResult("transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.")
|
||||
}
|
||||
|
||||
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
||||
if errMsg != "" {
|
||||
return ErrorResult(errMsg)
|
||||
}
|
||||
|
||||
dataRaw, ok := args["data"].([]interface{})
|
||||
if !ok || len(dataRaw) == 0 {
|
||||
return ErrorResult("data is required for transfer (array of byte values 0-255)")
|
||||
}
|
||||
if len(dataRaw) > 4096 {
|
||||
return ErrorResult("data too long: maximum 4096 bytes per SPI transfer")
|
||||
}
|
||||
|
||||
txBuf := make([]byte, len(dataRaw))
|
||||
for i, v := range dataRaw {
|
||||
f, ok := v.(float64)
|
||||
if !ok {
|
||||
return ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i))
|
||||
}
|
||||
b := int(f)
|
||||
if b < 0 || b > 255 {
|
||||
return ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b))
|
||||
}
|
||||
txBuf[i] = byte(b)
|
||||
}
|
||||
|
||||
devPath := fmt.Sprintf("/dev/spidev%s", dev)
|
||||
fd, errResult := configureSPI(devPath, mode, bits, speed)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
rxBuf := make([]byte, len(txBuf))
|
||||
|
||||
xfer := spiTransfer{
|
||||
txBuf: uint64(uintptr(unsafe.Pointer(&txBuf[0]))),
|
||||
rxBuf: uint64(uintptr(unsafe.Pointer(&rxBuf[0]))),
|
||||
length: uint32(len(txBuf)),
|
||||
speedHz: speed,
|
||||
bitsPerWord: bits,
|
||||
}
|
||||
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocMessage1, uintptr(unsafe.Pointer(&xfer)))
|
||||
runtime.KeepAlive(txBuf)
|
||||
runtime.KeepAlive(rxBuf)
|
||||
if errno != 0 {
|
||||
return ErrorResult(fmt.Sprintf("SPI transfer failed: %v", errno))
|
||||
}
|
||||
|
||||
// Format received bytes
|
||||
hexBytes := make([]string, len(rxBuf))
|
||||
intBytes := make([]int, len(rxBuf))
|
||||
for i, b := range rxBuf {
|
||||
hexBytes[i] = fmt.Sprintf("0x%02x", b)
|
||||
intBytes[i] = int(b)
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"device": devPath,
|
||||
"sent": len(txBuf),
|
||||
"received": intBytes,
|
||||
"hex": hexBytes,
|
||||
}, "", " ")
|
||||
return SilentResult(string(result))
|
||||
}
|
||||
|
||||
// readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed)
|
||||
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
||||
if errMsg != "" {
|
||||
return ErrorResult(errMsg)
|
||||
}
|
||||
|
||||
length := 0
|
||||
if l, ok := args["length"].(float64); ok {
|
||||
length = int(l)
|
||||
}
|
||||
if length < 1 || length > 4096 {
|
||||
return ErrorResult("length is required for read (1-4096)")
|
||||
}
|
||||
|
||||
devPath := fmt.Sprintf("/dev/spidev%s", dev)
|
||||
fd, errResult := configureSPI(devPath, mode, bits, speed)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
txBuf := make([]byte, length) // zeros
|
||||
rxBuf := make([]byte, length)
|
||||
|
||||
xfer := spiTransfer{
|
||||
txBuf: uint64(uintptr(unsafe.Pointer(&txBuf[0]))),
|
||||
rxBuf: uint64(uintptr(unsafe.Pointer(&rxBuf[0]))),
|
||||
length: uint32(length),
|
||||
speedHz: speed,
|
||||
bitsPerWord: bits,
|
||||
}
|
||||
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocMessage1, uintptr(unsafe.Pointer(&xfer)))
|
||||
runtime.KeepAlive(txBuf)
|
||||
runtime.KeepAlive(rxBuf)
|
||||
if errno != 0 {
|
||||
return ErrorResult(fmt.Sprintf("SPI read failed: %v", errno))
|
||||
}
|
||||
|
||||
hexBytes := make([]string, len(rxBuf))
|
||||
intBytes := make([]int, len(rxBuf))
|
||||
for i, b := range rxBuf {
|
||||
hexBytes[i] = fmt.Sprintf("0x%02x", b)
|
||||
intBytes[i] = int(b)
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"device": devPath,
|
||||
"bytes": intBytes,
|
||||
"hex": hexBytes,
|
||||
"length": len(rxBuf),
|
||||
}, "", " ")
|
||||
return SilentResult(string(result))
|
||||
}
|
||||
13
pkg/tools/spi_other.go
Normal file
13
pkg/tools/spi_other.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !linux
|
||||
|
||||
package tools
|
||||
|
||||
// transfer is a stub for non-Linux platforms.
|
||||
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||
return ErrorResult("SPI is only supported on Linux")
|
||||
}
|
||||
|
||||
// readDevice is a stub for non-Linux platforms.
|
||||
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
return ErrorResult("SPI is only supported on Linux")
|
||||
}
|
||||
Reference in New Issue
Block a user