diff --git a/cmd/clawdroid/main.go b/cmd/clawdroid/main.go index 7013cc6b6..38eaaa80a 100644 --- a/cmd/clawdroid/main.go +++ b/cmd/clawdroid/main.go @@ -27,15 +27,12 @@ import ( "github.com/KarakuriAgent/clawdroid/pkg/channels" "github.com/KarakuriAgent/clawdroid/pkg/config" "github.com/KarakuriAgent/clawdroid/pkg/cron" - "github.com/KarakuriAgent/clawdroid/pkg/devices" "github.com/KarakuriAgent/clawdroid/pkg/health" "github.com/KarakuriAgent/clawdroid/pkg/heartbeat" "github.com/KarakuriAgent/clawdroid/pkg/logger" "github.com/KarakuriAgent/clawdroid/pkg/providers" "github.com/KarakuriAgent/clawdroid/pkg/skills" - "github.com/KarakuriAgent/clawdroid/pkg/state" "github.com/KarakuriAgent/clawdroid/pkg/tools" - "github.com/KarakuriAgent/clawdroid/pkg/voice" ) //go:generate cp -r ../../workspace . @@ -518,33 +515,6 @@ func gatewayCmd() { // Inject channel manager into agent loop for command handling agentLoop.SetChannelManager(channelManager) - var transcriber *voice.GroqTranscriber - if cfg.STT.APIKey != "" { - transcriber = voice.NewGroqTranscriber(cfg.STT.APIKey) - logger.InfoC("voice", "Groq voice transcription enabled") - } - - if transcriber != nil { - if telegramChannel, ok := channelManager.GetChannel("telegram"); ok { - if tc, ok := telegramChannel.(*channels.TelegramChannel); ok { - tc.SetTranscriber(transcriber) - logger.InfoC("voice", "Groq transcription attached to Telegram channel") - } - } - if discordChannel, ok := channelManager.GetChannel("discord"); ok { - if dc, ok := discordChannel.(*channels.DiscordChannel); ok { - dc.SetTranscriber(transcriber) - logger.InfoC("voice", "Groq transcription attached to Discord channel") - } - } - if slackChannel, ok := channelManager.GetChannel("slack"); ok { - if sc, ok := slackChannel.(*channels.SlackChannel); ok { - sc.SetTranscriber(transcriber) - logger.InfoC("voice", "Groq transcription attached to Slack channel") - } - } - } - enabledChannels := channelManager.GetEnabledChannels() if len(enabledChannels) > 0 { fmt.Printf("✓ Channels enabled: %s\n", enabledChannels) @@ -568,18 +538,6 @@ func gatewayCmd() { } fmt.Println("✓ Heartbeat service started") - stateManager := state.NewManager(cfg.DataPath()) - deviceService := devices.NewService(devices.Config{ - Enabled: cfg.Devices.Enabled, - MonitorUSB: cfg.Devices.MonitorUSB, - }, stateManager) - deviceService.SetBus(msgBus) - if err := deviceService.Start(ctx); err != nil { - fmt.Printf("Error starting device service: %v\n", err) - } else if cfg.Devices.Enabled { - fmt.Println("✓ Device event service started") - } - if err := channelManager.StartAll(ctx); err != nil { fmt.Printf("Error starting channels: %v\n", err) } @@ -601,7 +559,6 @@ func gatewayCmd() { fmt.Println("\nShutting down...") cancel() healthServer.Stop(context.Background()) - deviceService.Stop() heartbeatService.Stop() cronService.Stop() agentLoop.Stop() @@ -649,9 +606,6 @@ func statusCmd() { if cfg.LLM.BaseURL != "" { fmt.Printf("Base URL: %s\n", cfg.LLM.BaseURL) } - if cfg.STT.APIKey != "" { - fmt.Println("STT API Key: ✓") - } } } diff --git a/config/config.example.json b/config/config.example.json index 765dfbfc9..ba52592a8 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -4,9 +4,6 @@ "api_key": "", "base_url": "" }, - "stt": { - "api_key": "" - }, "agents": { "defaults": { "workspace": "~/.clawdroid/workspace", @@ -96,12 +93,6 @@ "exec": { "enabled": false }, - "i2c": { - "enabled": false - }, - "spi": { - "enabled": false - }, "android": { "enabled": true }, @@ -140,10 +131,6 @@ "enabled": true, "interval": 30 }, - "devices": { - "enabled": false, - "monitor_usb": true - }, "gateway": { "host": "127.0.0.1", "port": 18790 diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index e7c8a05b1..8722ba0a0 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -106,14 +106,6 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg } registry.Register(tools.NewWebFetchTool(50000)) - // Hardware tools (I2C, SPI) - disabled by default for security - if cfg.Tools.I2C.Enabled { - registry.Register(tools.NewI2CTool()) - } - if cfg.Tools.SPI.Enabled { - registry.Register(tools.NewSPITool()) - } - // Android device control tool sendCallbackWithType := func(channel, chatID, content, msgType string) error { msgBus.PublishOutbound(bus.OutboundMessage{ diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index dbe862929..55bf8b739 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -443,180 +443,6 @@ func TestCreateToolRegistry_ExecEnabled(t *testing.T) { } } -// TestCreateToolRegistry_I2CDisabled verifies I2C tool is NOT registered when disabled -func TestCreateToolRegistry_I2CDisabled(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "agent-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - cfg := &config.Config{ - LLM: config.LLMConfig{ - Model: "test-model", - }, - Agents: config.AgentsConfig{ - Defaults: config.AgentDefaults{ - Workspace: tmpDir, - DataDir: tmpDir, - MaxTokens: 4096, - ContextWindow: 128000, - MaxToolIterations: 10, - }, - }, - Tools: config.ToolsConfig{ - I2C: config.I2CToolsConfig{Enabled: false}, - }, - } - - msgBus := bus.NewMessageBus() - provider := &mockProvider{} - al := NewAgentLoop(cfg, msgBus, provider) - - info := al.GetStartupInfo() - toolsInfo := info["tools"].(map[string]interface{}) - toolsList := toolsInfo["names"].([]string) - - for _, name := range toolsList { - if name == "i2c" { - t.Error("i2c tool should NOT be registered when I2C.Enabled is false") - } - } -} - -// TestCreateToolRegistry_I2CEnabled verifies I2C tool IS registered when enabled -func TestCreateToolRegistry_I2CEnabled(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "agent-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - cfg := &config.Config{ - LLM: config.LLMConfig{ - Model: "test-model", - }, - Agents: config.AgentsConfig{ - Defaults: config.AgentDefaults{ - Workspace: tmpDir, - DataDir: tmpDir, - MaxTokens: 4096, - ContextWindow: 128000, - MaxToolIterations: 10, - }, - }, - Tools: config.ToolsConfig{ - I2C: config.I2CToolsConfig{Enabled: true}, - }, - } - - msgBus := bus.NewMessageBus() - provider := &mockProvider{} - al := NewAgentLoop(cfg, msgBus, provider) - - info := al.GetStartupInfo() - toolsInfo := info["tools"].(map[string]interface{}) - toolsList := toolsInfo["names"].([]string) - - found := false - for _, name := range toolsList { - if name == "i2c" { - found = true - break - } - } - if !found { - t.Error("i2c tool should be registered when I2C.Enabled is true") - } -} - -// TestCreateToolRegistry_SPIDisabled verifies SPI tool is NOT registered when disabled -func TestCreateToolRegistry_SPIDisabled(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "agent-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - cfg := &config.Config{ - LLM: config.LLMConfig{ - Model: "test-model", - }, - Agents: config.AgentsConfig{ - Defaults: config.AgentDefaults{ - Workspace: tmpDir, - DataDir: tmpDir, - MaxTokens: 4096, - ContextWindow: 128000, - MaxToolIterations: 10, - }, - }, - Tools: config.ToolsConfig{ - SPI: config.SPIToolsConfig{Enabled: false}, - }, - } - - msgBus := bus.NewMessageBus() - provider := &mockProvider{} - al := NewAgentLoop(cfg, msgBus, provider) - - info := al.GetStartupInfo() - toolsInfo := info["tools"].(map[string]interface{}) - toolsList := toolsInfo["names"].([]string) - - for _, name := range toolsList { - if name == "spi" { - t.Error("spi tool should NOT be registered when SPI.Enabled is false") - } - } -} - -// TestCreateToolRegistry_SPIEnabled verifies SPI tool IS registered when enabled -func TestCreateToolRegistry_SPIEnabled(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "agent-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - cfg := &config.Config{ - LLM: config.LLMConfig{ - Model: "test-model", - }, - Agents: config.AgentsConfig{ - Defaults: config.AgentDefaults{ - Workspace: tmpDir, - DataDir: tmpDir, - MaxTokens: 4096, - ContextWindow: 128000, - MaxToolIterations: 10, - }, - }, - Tools: config.ToolsConfig{ - SPI: config.SPIToolsConfig{Enabled: true}, - }, - } - - msgBus := bus.NewMessageBus() - provider := &mockProvider{} - al := NewAgentLoop(cfg, msgBus, provider) - - info := al.GetStartupInfo() - toolsInfo := info["tools"].(map[string]interface{}) - toolsList := toolsInfo["names"].([]string) - - found := false - for _, name := range toolsList { - if name == "spi" { - found = true - break - } - } - if !found { - t.Error("spi tool should be registered when SPI.Enabled is true") - } -} - // TestAgentLoop_Stop verifies Stop() sets running to false func TestAgentLoop_Stop(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-test-*") diff --git a/pkg/agent/status.go b/pkg/agent/status.go index a0ca2eea9..c8857871f 100644 --- a/pkg/agent/status.go +++ b/pkg/agent/status.go @@ -62,10 +62,6 @@ func statusLabel(toolName string, args map[string]interface{}) string { return "アシスタント終了中..." case "mcp": return mcpStatusLabel(args) - case "i2c": - return i2cStatusLabel(args) - case "spi": - return spiStatusLabel(args) default: return "処理中..." } @@ -180,49 +176,6 @@ func mcpStatusLabel(args map[string]interface{}) string { } } -func i2cStatusLabel(args map[string]interface{}) string { - switch strArg(args, "action") { - case "detect": - return "I2Cバス検出中..." - case "scan": - if b := strArg(args, "bus"); b != "" { - return fmt.Sprintf("I2Cデバイススキャン中...(bus %s)", b) - } - return "I2Cデバイススキャン中..." - case "read": - if addr := intArg(args, "address"); addr > 0 { - return fmt.Sprintf("センサー読み取り中...(0x%02X)", addr) - } - return "センサー読み取り中..." - case "write": - if addr := intArg(args, "address"); addr > 0 { - return fmt.Sprintf("デバイス書き込み中...(0x%02X)", addr) - } - return "デバイス書き込み中..." - default: - return "I2C操作中..." - } -} - -func spiStatusLabel(args map[string]interface{}) string { - switch strArg(args, "action") { - case "list": - return "SPIデバイス一覧取得中..." - case "transfer": - if d := strArg(args, "device"); d != "" { - return fmt.Sprintf("SPI通信中...(%s)", d) - } - return "SPI通信中..." - case "read": - if d := strArg(args, "device"); d != "" { - return fmt.Sprintf("SPI読み取り中...(%s)", d) - } - return "SPI読み取り中..." - default: - return "SPI操作中..." - } -} - // strArg extracts a string argument from a tool arguments map. func strArg(args map[string]interface{}, key string) string { if v, ok := args[key]; ok { @@ -233,19 +186,6 @@ func strArg(args map[string]interface{}, key string) string { return "" } -// intArg extracts an integer argument from a tool arguments map. -func intArg(args map[string]interface{}, key string) int { - if v, ok := args[key]; ok { - switch n := v.(type) { - case float64: - return int(n) - case int: - return n - } - } - return 0 -} - // truncLabel truncates a string to maxRunes runes, appending "..." if truncated. func truncLabel(s string, maxRunes int) string { if utf8.RuneCountInString(s) <= maxRunes { diff --git a/pkg/config/config.go b/pkg/config/config.go index 957df36b1..b0c2c5f43 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -49,19 +49,13 @@ type LLMConfig struct { BaseURL string `json:"base_url" env:"CLAWDROID_LLM_BASE_URL"` } -type STTConfig struct { - APIKey string `json:"api_key" env:"CLAWDROID_STT_API_KEY"` -} - type Config struct { LLM LLMConfig `json:"llm"` - STT STTConfig `json:"stt"` Agents AgentsConfig `json:"agents"` Channels ChannelsConfig `json:"channels"` Gateway GatewayConfig `json:"gateway"` Tools ToolsConfig `json:"tools"` Heartbeat HeartbeatConfig `json:"heartbeat"` - Devices DevicesConfig `json:"devices"` RateLimits RateLimitsConfig `json:"rate_limits"` mu sync.RWMutex } @@ -182,11 +176,6 @@ type HeartbeatConfig struct { Interval int `json:"interval" env:"CLAWDROID_HEARTBEAT_INTERVAL"` // minutes, min 5 } -type DevicesConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_DEVICES_ENABLED"` - MonitorUSB bool `json:"monitor_usb" env:"CLAWDROID_DEVICES_MONITOR_USB"` -} - type RateLimitsConfig struct { MaxToolCallsPerMinute int `json:"max_tool_calls_per_minute" env:"CLAWDROID_RATE_LIMITS_MAX_TOOL_CALLS_PER_MINUTE"` // 0 = unlimited MaxRequestsPerMinute int `json:"max_requests_per_minute" env:"CLAWDROID_RATE_LIMITS_MAX_REQUESTS_PER_MINUTE"` // 0 = unlimited @@ -217,14 +206,6 @@ type ExecToolsConfig struct { Enabled bool `json:"enabled" env:"CLAWDROID_TOOLS_EXEC_ENABLED"` } -type I2CToolsConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_TOOLS_I2C_ENABLED"` -} - -type SPIToolsConfig struct { - Enabled bool `json:"enabled" env:"CLAWDROID_TOOLS_SPI_ENABLED"` -} - type MCPServerConfig struct { // Stdio transport Command string `json:"command,omitempty"` @@ -250,8 +231,6 @@ type MemoryToolsConfig struct { type ToolsConfig struct { Web WebToolsConfig `json:"web"` Exec ExecToolsConfig `json:"exec"` - I2C I2CToolsConfig `json:"i2c"` - SPI SPIToolsConfig `json:"spi"` Android AndroidToolsConfig `json:"android"` Memory MemoryToolsConfig `json:"memory"` MCP map[string]MCPServerConfig `json:"mcp,omitempty"` @@ -262,7 +241,6 @@ func DefaultConfig() *Config { LLM: LLMConfig{ Model: "zhipu/glm-4.7", }, - STT: STTConfig{}, Agents: AgentsConfig{ Defaults: AgentDefaults{ Workspace: "~/.clawdroid/workspace", @@ -355,12 +333,6 @@ func DefaultConfig() *Config { Exec: ExecToolsConfig{ Enabled: false, }, - I2C: I2CToolsConfig{ - Enabled: false, - }, - SPI: SPIToolsConfig{ - Enabled: false, - }, Android: AndroidToolsConfig{ Enabled: true, }, @@ -383,10 +355,6 @@ func DefaultConfig() *Config { Enabled: true, Interval: 30, // default 30 minutes }, - Devices: DevicesConfig{ - Enabled: false, - MonitorUSB: true, - }, RateLimits: RateLimitsConfig{ MaxToolCallsPerMinute: 60, MaxRequestsPerMinute: 30, diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 8e48d61e4..3b0adcc49 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -129,22 +129,6 @@ func TestDefaultConfig_ExecToolDisabled(t *testing.T) { } } -// TestDefaultConfig_I2CToolDisabled verifies I2C tool is disabled by default -func TestDefaultConfig_I2CToolDisabled(t *testing.T) { - cfg := DefaultConfig() - if cfg.Tools.I2C.Enabled { - t.Error("I2C tool should be disabled by default") - } -} - -// TestDefaultConfig_SPIToolDisabled verifies SPI tool is disabled by default -func TestDefaultConfig_SPIToolDisabled(t *testing.T) { - cfg := DefaultConfig() - if cfg.Tools.SPI.Enabled { - t.Error("SPI tool should be disabled by default") - } -} - // TestDefaultConfig_WebTools verifies web tools config func TestDefaultConfig_WebTools(t *testing.T) { cfg := DefaultConfig() diff --git a/pkg/devices/events/events.go b/pkg/devices/events/events.go deleted file mode 100644 index 01226179c..000000000 --- a/pkg/devices/events/events.go +++ /dev/null @@ -1,57 +0,0 @@ -package events - -import "context" - -type EventSource interface { - Kind() Kind - Start(ctx context.Context) (<-chan *DeviceEvent, error) - Stop() error -} - -type Action string - -const ( - ActionAdd Action = "add" - ActionRemove Action = "remove" - ActionChange Action = "change" -) - -type Kind string - -const ( - KindUSB Kind = "usb" - KindBluetooth Kind = "bluetooth" - KindPCI Kind = "pci" - KindGeneric Kind = "generic" -) - -type DeviceEvent struct { - Action Action - Kind Kind - DeviceID string // e.g. "1-2" for USB bus 1 dev 2 - Vendor string // Vendor name or ID - Product string // Product name or ID - Serial string // Serial number if available - Capabilities string // Human-readable capability description - Raw map[string]string // Raw properties for extensibility -} - -func (e *DeviceEvent) FormatMessage() string { - actionEmoji := "🔌" - actionText := "Connected" - if e.Action == ActionRemove { - actionEmoji = "🔌" - actionText = "Disconnected" - } - - msg := actionEmoji + " Device " + actionText + "\n\n" - msg += "Type: " + string(e.Kind) + "\n" - msg += "Device: " + e.Vendor + " " + e.Product + "\n" - if e.Capabilities != "" { - msg += "Capabilities: " + e.Capabilities + "\n" - } - if e.Serial != "" { - msg += "Serial: " + e.Serial + "\n" - } - return msg -} diff --git a/pkg/devices/service.go b/pkg/devices/service.go deleted file mode 100644 index 34553f49c..000000000 --- a/pkg/devices/service.go +++ /dev/null @@ -1,152 +0,0 @@ -package devices - -import ( - "context" - "strings" - "sync" - - "github.com/KarakuriAgent/clawdroid/pkg/bus" - "github.com/KarakuriAgent/clawdroid/pkg/constants" - "github.com/KarakuriAgent/clawdroid/pkg/devices/events" - "github.com/KarakuriAgent/clawdroid/pkg/devices/sources" - "github.com/KarakuriAgent/clawdroid/pkg/logger" - "github.com/KarakuriAgent/clawdroid/pkg/state" -) - -type Service struct { - bus *bus.MessageBus - state *state.Manager - sources []events.EventSource - enabled bool - ctx context.Context - cancel context.CancelFunc - mu sync.RWMutex -} - -type Config struct { - Enabled bool - MonitorUSB bool // When true, monitor USB hotplug (Linux only) - // Future: MonitorBluetooth, MonitorPCI, etc. -} - -func NewService(cfg Config, stateMgr *state.Manager) *Service { - s := &Service{ - state: stateMgr, - enabled: cfg.Enabled, - sources: make([]EventSource, 0), - } - - if cfg.Enabled && cfg.MonitorUSB { - s.sources = append(s.sources, sources.NewUSBMonitor()) - } - - return s -} - -func (s *Service) SetBus(msgBus *bus.MessageBus) { - s.mu.Lock() - defer s.mu.Unlock() - s.bus = msgBus -} - -func (s *Service) Start(ctx context.Context) error { - s.mu.Lock() - defer s.mu.Unlock() - - if !s.enabled || len(s.sources) == 0 { - logger.InfoC("devices", "Device event service disabled or no sources") - return nil - } - - s.ctx, s.cancel = context.WithCancel(ctx) - - for _, src := range s.sources { - eventCh, err := src.Start(s.ctx) - if err != nil { - logger.ErrorCF("devices", "Failed to start source", map[string]interface{}{ - "kind": src.Kind(), - "error": err.Error(), - }) - continue - } - go s.handleEvents(src.Kind(), eventCh) - logger.InfoCF("devices", "Device source started", map[string]interface{}{ - "kind": src.Kind(), - }) - } - - logger.InfoC("devices", "Device event service started") - return nil -} - -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.cancel != nil { - s.cancel() - s.cancel = nil - } - - for _, src := range s.sources { - src.Stop() - } - - logger.InfoC("devices", "Device event service stopped") -} - -func (s *Service) handleEvents(kind events.Kind, eventCh <-chan *events.DeviceEvent) { - for ev := range eventCh { - if ev == nil { - continue - } - s.sendNotification(ev) - } -} - -func (s *Service) sendNotification(ev *events.DeviceEvent) { - s.mu.RLock() - msgBus := s.bus - s.mu.RUnlock() - - if msgBus == nil { - return - } - - lastChannel := s.state.GetLastChannel() - if lastChannel == "" { - logger.DebugCF("devices", "No last channel, skipping notification", map[string]interface{}{ - "event": ev.FormatMessage(), - }) - return - } - - platform, userID := parseLastChannel(lastChannel) - if platform == "" || userID == "" || constants.IsInternalChannel(platform) { - return - } - - msg := ev.FormatMessage() - msgBus.PublishOutbound(bus.OutboundMessage{ - Channel: platform, - ChatID: userID, - Content: msg, - }) - - logger.InfoCF("devices", "Device notification sent", map[string]interface{}{ - "kind": ev.Kind, - "action": ev.Action, - "to": platform, - }) -} - -func parseLastChannel(lastChannel string) (platform, userID string) { - if lastChannel == "" { - return "", "" - } - parts := strings.SplitN(lastChannel, ":", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "" - } - return parts[0], parts[1] -} diff --git a/pkg/devices/source.go b/pkg/devices/source.go deleted file mode 100644 index a09986c92..000000000 --- a/pkg/devices/source.go +++ /dev/null @@ -1,5 +0,0 @@ -package devices - -import "github.com/KarakuriAgent/clawdroid/pkg/devices/events" - -type EventSource = events.EventSource diff --git a/pkg/devices/sources/usb_linux.go b/pkg/devices/sources/usb_linux.go deleted file mode 100644 index c80b9fa07..000000000 --- a/pkg/devices/sources/usb_linux.go +++ /dev/null @@ -1,198 +0,0 @@ -//go:build linux - -package sources - -import ( - "bufio" - "context" - "fmt" - "os/exec" - "strings" - "sync" - - "github.com/KarakuriAgent/clawdroid/pkg/devices/events" - "github.com/KarakuriAgent/clawdroid/pkg/logger" -) - -var usbClassToCapability = map[string]string{ - "00": "Interface Definition (by interface)", - "01": "Audio", - "02": "CDC Communication (Network Card/Modem)", - "03": "HID (Keyboard/Mouse/Gamepad)", - "05": "Physical Interface", - "06": "Image (Scanner/Camera)", - "07": "Printer", - "08": "Mass Storage (USB Flash Drive/Hard Disk)", - "09": "USB Hub", - "0a": "CDC Data", - "0b": "Smart Card", - "0e": "Video (Camera)", - "dc": "Diagnostic Device", - "e0": "Wireless Controller (Bluetooth)", - "ef": "Miscellaneous", - "fe": "Application Specific", - "ff": "Vendor Specific", -} - -type USBMonitor struct { - cmd *exec.Cmd - cancel context.CancelFunc - mu sync.Mutex -} - -func NewUSBMonitor() *USBMonitor { - return &USBMonitor{} -} - -func (m *USBMonitor) Kind() events.Kind { - return events.KindUSB -} - -func (m *USBMonitor) Start(ctx context.Context) (<-chan *events.DeviceEvent, error) { - m.mu.Lock() - defer m.mu.Unlock() - - // udevadm monitor outputs: UDEV/KERNEL [timestamp] action devpath (subsystem) - // Followed by KEY=value lines, empty line separates events - // Use -s/--subsystem-match (eudev) or --udev-subsystem-match (systemd udev) - cmd := exec.CommandContext(ctx, "udevadm", "monitor", "--property", "--subsystem-match=usb") - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("udevadm stdout pipe: %w", err) - } - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("udevadm start: %w (is udevadm installed?)", err) - } - - m.cmd = cmd - eventCh := make(chan *events.DeviceEvent, 16) - - go func() { - defer close(eventCh) - scanner := bufio.NewScanner(stdout) - var props map[string]string - var action string - isUdev := false // Only UDEV events have complete info (ID_VENDOR, ID_MODEL); KERNEL events come first with less info - - for scanner.Scan() { - line := scanner.Text() - if line == "" { - // End of event block - only process UDEV events (skip KERNEL to avoid duplicate/incomplete notifications) - if isUdev && props != nil && (action == "add" || action == "remove") { - if ev := parseUSBEvent(action, props); ev != nil { - select { - case eventCh <- ev: - case <-ctx.Done(): - return - } - } - } - props = nil - action = "" - isUdev = false - continue - } - - idx := strings.Index(line, "=") - // First line of block: "UDEV [ts] action devpath" or "KERNEL[ts] action devpath" - no KEY=value - if idx <= 0 { - isUdev = strings.HasPrefix(strings.TrimSpace(line), "UDEV") - continue - } - - // Parse KEY=value - key := line[:idx] - val := line[idx+1:] - if props == nil { - props = make(map[string]string) - } - props[key] = val - - if key == "ACTION" { - action = val - } - } - - if err := scanner.Err(); err != nil { - logger.ErrorCF("devices", "udevadm scan error", map[string]interface{}{"error": err.Error()}) - } - cmd.Wait() - }() - - return eventCh, nil -} - -func (m *USBMonitor) Stop() error { - m.mu.Lock() - defer m.mu.Unlock() - if m.cmd != nil && m.cmd.Process != nil { - m.cmd.Process.Kill() - m.cmd = nil - } - return nil -} - -func parseUSBEvent(action string, props map[string]string) *events.DeviceEvent { - // Only care about add/remove for physical devices (not interfaces) - subsystem := props["SUBSYSTEM"] - if subsystem != "usb" { - return nil - } - // Skip interface events - we want device-level only to avoid duplicates - devType := props["DEVTYPE"] - if devType == "usb_interface" { - return nil - } - // Prefer usb_device, but accept if DEVTYPE not set (varies by udev version) - if devType != "" && devType != "usb_device" { - return nil - } - - ev := &events.DeviceEvent{ - Raw: props, - } - switch action { - case "add": - ev.Action = events.ActionAdd - case "remove": - ev.Action = events.ActionRemove - default: - return nil - } - ev.Kind = events.KindUSB - - ev.Vendor = props["ID_VENDOR"] - if ev.Vendor == "" { - ev.Vendor = props["ID_VENDOR_ID"] - } - if ev.Vendor == "" { - ev.Vendor = "Unknown Vendor" - } - - ev.Product = props["ID_MODEL"] - if ev.Product == "" { - ev.Product = props["ID_MODEL_ID"] - } - if ev.Product == "" { - ev.Product = "Unknown Device" - } - - ev.Serial = props["ID_SERIAL_SHORT"] - ev.DeviceID = props["DEVPATH"] - if bus := props["BUSNUM"]; bus != "" { - if dev := props["DEVNUM"]; dev != "" { - ev.DeviceID = bus + ":" + dev - } - } - - // Map USB class to capability - if class := props["ID_USB_CLASS"]; class != "" { - ev.Capabilities = usbClassToCapability[strings.ToLower(class)] - } - if ev.Capabilities == "" { - ev.Capabilities = "USB Device" - } - - return ev -} diff --git a/pkg/devices/sources/usb_stub.go b/pkg/devices/sources/usb_stub.go deleted file mode 100644 index 31e4464a0..000000000 --- a/pkg/devices/sources/usb_stub.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build !linux - -package sources - -import ( - "context" - - "github.com/KarakuriAgent/clawdroid/pkg/devices/events" -) - -type USBMonitor struct{} - -func NewUSBMonitor() *USBMonitor { - return &USBMonitor{} -} - -func (m *USBMonitor) Kind() events.Kind { - return events.KindUSB -} - -func (m *USBMonitor) Start(ctx context.Context) (<-chan *events.DeviceEvent, error) { - ch := make(chan *events.DeviceEvent) - close(ch) // Immediately close, no events - return ch, nil -} - -func (m *USBMonitor) Stop() error { - return nil -} diff --git a/pkg/tools/i2c.go b/pkg/tools/i2c.go deleted file mode 100644 index abca5ec1e..000000000 --- a/pkg/tools/i2c.go +++ /dev/null @@ -1,147 +0,0 @@ -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 -} diff --git a/pkg/tools/i2c_linux.go b/pkg/tools/i2c_linux.go deleted file mode 100644 index 294f7ecbc..000000000 --- a/pkg/tools/i2c_linux.go +++ /dev/null @@ -1,282 +0,0 @@ -package tools - -import ( - "encoding/json" - "fmt" - "syscall" - "unsafe" -) - -// I2C ioctl constants from Linux kernel headers (, ) -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)) -} diff --git a/pkg/tools/i2c_other.go b/pkg/tools/i2c_other.go deleted file mode 100644 index d1d581348..000000000 --- a/pkg/tools/i2c_other.go +++ /dev/null @@ -1,18 +0,0 @@ -//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") -} diff --git a/pkg/tools/spi.go b/pkg/tools/spi.go deleted file mode 100644 index 4805d6a35..000000000 --- a/pkg/tools/spi.go +++ /dev/null @@ -1,156 +0,0 @@ -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, "" -} diff --git a/pkg/tools/spi_linux.go b/pkg/tools/spi_linux.go deleted file mode 100644 index 12b696007..000000000 --- a/pkg/tools/spi_linux.go +++ /dev/null @@ -1,196 +0,0 @@ -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)) -} diff --git a/pkg/tools/spi_other.go b/pkg/tools/spi_other.go deleted file mode 100644 index 6dfc86fd1..000000000 --- a/pkg/tools/spi_other.go +++ /dev/null @@ -1,13 +0,0 @@ -//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") -} diff --git a/workspace/skills/hardware/SKILL.md b/workspace/skills/hardware/SKILL.md deleted file mode 100644 index e89d1b6e7..000000000 --- a/workspace/skills/hardware/SKILL.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -name: hardware -description: Read and control I2C and SPI peripherals on Sipeed boards (LicheeRV Nano, MaixCAM, NanoKVM). -homepage: https://wiki.sipeed.com/hardware/en/lichee/RV_Nano/1_intro.html -metadata: {"nanobot":{"emoji":"🔧","requires":{"tools":["i2c","spi"]}}} ---- - -# Hardware (I2C / SPI) - -Use the `i2c` and `spi` tools to interact with sensors, displays, and other peripherals connected to the board. - -## Quick Start - -``` -# 1. Find available buses -i2c detect - -# 2. Scan for connected devices -i2c scan (bus: "1") - -# 3. Read from a sensor (e.g. AHT20 temperature/humidity) -i2c read (bus: "1", address: 0x38, register: 0xAC, length: 6) - -# 4. SPI devices -spi list -spi read (device: "2.0", length: 4) -``` - -## Before You Start — Pinmux Setup - -Most I2C/SPI pins are shared with WiFi on Sipeed boards. You must configure pinmux before use. - -See `references/board-pinout.md` for board-specific commands. - -**Common steps:** -1. Stop WiFi if using shared pins: `/etc/init.d/S30wifi stop` -2. Load i2c-dev module: `modprobe i2c-dev` -3. Configure pinmux with `devmem` (board-specific) -4. Verify with `i2c detect` and `i2c scan` - -## Safety - -- **Write operations** require `confirm: true` — always confirm with the user first -- I2C addresses are validated to 7-bit range (0x03-0x77) -- SPI modes are validated (0-3 only) -- Maximum per-transaction: 256 bytes (I2C), 4096 bytes (SPI) - -## Common Devices - -See `references/common-devices.md` for register maps and usage of popular sensors: -AHT20, BME280, SSD1306 OLED, MPU6050 IMU, DS3231 RTC, INA219 power monitor, PCA9685 PWM, and more. - -## Troubleshooting - -| Problem | Solution | -|---------|----------| -| No I2C buses found | `modprobe i2c-dev` and check device tree | -| Permission denied | Run as root or add user to `i2c` group | -| No devices on scan | Check wiring, pull-up resistors (4.7k typical), and pinmux | -| Bus number changed | I2C adapter numbers can shift between boots; use `i2c detect` to find current assignment | -| WiFi stopped working | I2C-1/SPI-2 share pins with WiFi SDIO; can't use both simultaneously | -| `devmem` not found | Download separately or use `busybox devmem` | -| SPI transfer returns all zeros | Check MISO wiring and device power | -| SPI transfer returns all 0xFF | Device not responding; check CS pin and clock polarity (mode) | diff --git a/workspace/skills/hardware/references/board-pinout.md b/workspace/skills/hardware/references/board-pinout.md deleted file mode 100644 index 4ef688e56..000000000 --- a/workspace/skills/hardware/references/board-pinout.md +++ /dev/null @@ -1,131 +0,0 @@ -# Board Pinout & Pinmux Reference - -## LicheeRV Nano (SG2002) - -### I2C Buses - -| Bus | Pins | Notes | -|-----|------|-------| -| I2C-1 | P18 (SCL), P21 (SDA) | **Shared with WiFi SDIO** — must stop WiFi first | -| I2C-3 | Available on header | Check device tree for pin assignment | -| I2C-5 | Software (BitBang) | Slower but no pin conflicts | - -### SPI Buses - -| Bus | Pins | Notes | -|-----|------|-------| -| SPI-2 | P18 (CS), P21 (MISO), P22 (MOSI), P23 (SCK) | **Shared with WiFi** — must stop WiFi first | -| SPI-4 | Software (BitBang) | Slower but no pin conflicts | - -### Setup Steps for I2C-1 - -```bash -# 1. Stop WiFi (shares pins with I2C-1) -/etc/init.d/S30wifi stop - -# 2. Configure pinmux for I2C-1 -devmem 0x030010D0 b 0x2 # P18 → I2C1_SCL -devmem 0x030010DC b 0x2 # P21 → I2C1_SDA - -# 3. Load i2c-dev module -modprobe i2c-dev - -# 4. Verify -ls /dev/i2c-* -``` - -### Setup Steps for SPI-2 - -```bash -# 1. Stop WiFi (shares pins with SPI-2) -/etc/init.d/S30wifi stop - -# 2. Configure pinmux for SPI-2 -devmem 0x030010D0 b 0x1 # P18 → SPI2_CS -devmem 0x030010DC b 0x1 # P21 → SPI2_MISO -devmem 0x030010E0 b 0x1 # P22 → SPI2_MOSI -devmem 0x030010E4 b 0x1 # P23 → SPI2_SCK - -# 3. Verify -ls /dev/spidev* -``` - -### Max Tested SPI Speed -- SPI-2 hardware: tested up to **93 MHz** -- `spidev_test` is pre-installed on the official image for loopback testing - ---- - -## MaixCAM - -### I2C Buses - -| Bus | Pins | Notes | -|-----|------|-------| -| I2C-1 | Overlaps with WiFi | Not recommended | -| I2C-3 | Overlaps with WiFi | Not recommended | -| I2C-5 | A15 (SCL), A27 (SDA) | **Recommended** — software I2C, no conflicts | - -### Setup Steps for I2C-5 - -```bash -# Configure pins using pinmap utility -# (MaixCAM uses a pinmap tool instead of devmem) -# Refer to: https://wiki.sipeed.com/hardware/en/maixcam/gpio.html - -# Load i2c-dev -modprobe i2c-dev - -# Verify -ls /dev/i2c-* -``` - ---- - -## MaixCAM2 - -### I2C Buses - -| Bus | Pins | Notes | -|-----|------|-------| -| I2C-6 | A1 (SCL), A0 (SDA) | Available on header | -| I2C-7 | Available | Check device tree | - -### Setup Steps - -```bash -# Configure pinmap for I2C-6 -# A1 → I2C6_SCL, A0 → I2C6_SDA -# Refer to MaixCAM2 documentation for pinmap commands - -modprobe i2c-dev -ls /dev/i2c-* -``` - ---- - -## NanoKVM - -Uses the same SG2002 SoC as LicheeRV Nano. GPIO and I2C access follows the same pinmux procedure. Refer to the LicheeRV Nano section above. - -Check NanoKVM-specific pin headers for available I2C/SPI lines: -- https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html - ---- - -## Common Issues - -### devmem not found -The `devmem` utility may not be in the default image. Options: -- Use `busybox devmem` if busybox is installed -- Download devmem from the Sipeed package repository -- Cross-compile from source (single C file) - -### Dynamic bus numbering -I2C adapter numbers can change between boots depending on driver load order. Always use `i2c detect` to find current bus assignments rather than hardcoding bus numbers. - -### Permissions -`/dev/i2c-*` and `/dev/spidev*` typically require root access. Options: -- Run clawdroid as root -- Add user to `i2c` and `spi` groups -- Create udev rules: `SUBSYSTEM=="i2c-dev", MODE="0666"` diff --git a/workspace/skills/hardware/references/common-devices.md b/workspace/skills/hardware/references/common-devices.md deleted file mode 100644 index 715e8ab7f..000000000 --- a/workspace/skills/hardware/references/common-devices.md +++ /dev/null @@ -1,78 +0,0 @@ -# Common I2C/SPI Device Reference - -## I2C Devices - -### AHT20 — Temperature & Humidity -- **Address:** 0x38 -- **Init:** Write `[0xBE, 0x08, 0x00]` then wait 10ms -- **Measure:** Write `[0xAC, 0x33, 0x00]`, wait 80ms, read 6 bytes -- **Parse:** Status=byte[0], Humidity=(byte[1]<<12|byte[2]<<4|byte[3]>>4)/2^20*100, Temp=(byte[3]&0x0F<<16|byte[4]<<8|byte[5])/2^20*200-50 -- **Notes:** No register addressing — write command bytes directly (omit `register` param) - -### BME280 / BMP280 — Temperature, Humidity, Pressure -- **Address:** 0x76 or 0x77 (SDO pin selects) -- **Chip ID register:** 0xD0 → BMP280=0x58, BME280=0x60 -- **Data registers:** 0xF7-0xFE (pressure, temperature, humidity) -- **Config:** Write 0xF2 (humidity oversampling), 0xF4 (temp/press oversampling + mode), 0xF5 (standby, filter) -- **Forced measurement:** Write `[0x25]` to register 0xF4, wait 40ms, read 8 bytes from 0xF7 -- **Calibration:** Read 26 bytes from 0x88 and 7 bytes from 0xE1 for compensation formulas -- **Also available via SPI** (mode 0 or 3) - -### SSD1306 — 128x64 OLED Display -- **Address:** 0x3C (or 0x3D if SA0 high) -- **Command prefix:** 0x00 (write to register 0x00) -- **Data prefix:** 0x40 (write to register 0x40) -- **Init sequence:** `[0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF]` -- **Display on:** 0xAF, **Display off:** 0xAE -- **Also available via SPI** (faster, recommended for animations) - -### MPU6050 — 6-axis Accelerometer + Gyroscope -- **Address:** 0x68 (or 0x69 if AD0 high) -- **WHO_AM_I:** Register 0x75 → should return 0x68 -- **Wake up:** Write `[0x00]` to register 0x6B (clear sleep bit) -- **Read accel:** 6 bytes from register 0x3B (XH,XL,YH,YL,ZH,ZL) — signed 16-bit, default ±2g -- **Read gyro:** 6 bytes from register 0x43 — signed 16-bit, default ±250°/s -- **Read temp:** 2 bytes from register 0x41 — Temp°C = value/340 + 36.53 - -### DS3231 — Real-Time Clock -- **Address:** 0x68 -- **Read time:** 7 bytes from register 0x00 (seconds, minutes, hours, day, date, month, year) — BCD encoded -- **Set time:** Write 7 BCD bytes to register 0x00 -- **Temperature:** 2 bytes from register 0x11 (signed, 0.25°C resolution) -- **Status:** Register 0x0F — bit 2 = busy, bit 0 = alarm 1 flag - -### INA219 — Current & Power Monitor -- **Address:** 0x40-0x4F (A0,A1 pin selectable) -- **Config:** Register 0x00 — set voltage range, gain, ADC resolution -- **Shunt voltage:** Register 0x01 (signed 16-bit, LSB=10µV) -- **Bus voltage:** Register 0x02 (bits 15:3, LSB=4mV) -- **Power:** Register 0x03 (after calibration) -- **Current:** Register 0x04 (after calibration) -- **Calibration:** Register 0x05 — set based on shunt resistor value - -### PCA9685 — 16-Channel PWM / Servo Controller -- **Address:** 0x40-0x7F (A0-A5 selectable, default 0x40) -- **Mode 1:** Register 0x00 — bit 4=sleep, bit 5=auto-increment -- **Set PWM freq:** Sleep → write prescale to 0xFE → wake. Prescale = round(25MHz / (4096 × freq)) - 1 -- **Channel N on/off:** Registers 0x06+4*N to 0x09+4*N (ON_L, ON_H, OFF_L, OFF_H) -- **Servo 0°-180°:** ON=0, OFF=150-600 (at 50Hz). Typical: 0°=150, 90°=375, 180°=600 - -### AT24C256 — 256Kbit EEPROM -- **Address:** 0x50-0x57 (A0-A2 selectable) -- **Read:** Write 2-byte address (high, low), then read N bytes -- **Write:** Write 2-byte address + up to 64 bytes (page write), wait 5ms for write cycle -- **Page size:** 64 bytes. Writes that cross page boundary wrap around. - -## SPI Devices - -### MCP3008 — 8-Channel 10-bit ADC -- **Interface:** SPI mode 0, max 3.6 MHz @ 5V -- **Read channel N:** Send `[0x01, (0x80 | N<<4), 0x00]`, result in last 10 bits of bytes 1-2 -- **Formula:** value = ((byte[1] & 0x03) << 8) | byte[2] -- **Voltage:** value × Vref / 1024 - -### W25Q128 — 128Mbit SPI Flash -- **Interface:** SPI mode 0 or 3, up to 104 MHz -- **Read ID:** Send `[0x9F, 0, 0, 0]` → manufacturer + device ID -- **Read data:** Send `[0x03, addr_high, addr_mid, addr_low]` + N zero bytes -- **Status:** Send `[0x05, 0]` → bit 0 = BUSY