refactor: remove I2C/SPI tools, devices package, and hardware skills
Linux board向けのI2C/SPIツール、udevadm USBホットプラグ監視の devicesパッケージ、hardwareスキルをAndroid Termux専用化のため削除。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
01353f6b00
commit
393c5d7810
21 changed files with 0 additions and 1875 deletions
|
|
@ -27,15 +27,12 @@ import (
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/channels"
|
"github.com/KarakuriAgent/clawdroid/pkg/channels"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/config"
|
"github.com/KarakuriAgent/clawdroid/pkg/config"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/cron"
|
"github.com/KarakuriAgent/clawdroid/pkg/cron"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/devices"
|
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/health"
|
"github.com/KarakuriAgent/clawdroid/pkg/health"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/heartbeat"
|
"github.com/KarakuriAgent/clawdroid/pkg/heartbeat"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/logger"
|
"github.com/KarakuriAgent/clawdroid/pkg/logger"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/providers"
|
"github.com/KarakuriAgent/clawdroid/pkg/providers"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/skills"
|
"github.com/KarakuriAgent/clawdroid/pkg/skills"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/state"
|
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/tools"
|
"github.com/KarakuriAgent/clawdroid/pkg/tools"
|
||||||
"github.com/KarakuriAgent/clawdroid/pkg/voice"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate cp -r ../../workspace .
|
//go:generate cp -r ../../workspace .
|
||||||
|
|
@ -518,33 +515,6 @@ func gatewayCmd() {
|
||||||
// Inject channel manager into agent loop for command handling
|
// Inject channel manager into agent loop for command handling
|
||||||
agentLoop.SetChannelManager(channelManager)
|
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()
|
enabledChannels := channelManager.GetEnabledChannels()
|
||||||
if len(enabledChannels) > 0 {
|
if len(enabledChannels) > 0 {
|
||||||
fmt.Printf("✓ Channels enabled: %s\n", enabledChannels)
|
fmt.Printf("✓ Channels enabled: %s\n", enabledChannels)
|
||||||
|
|
@ -568,18 +538,6 @@ func gatewayCmd() {
|
||||||
}
|
}
|
||||||
fmt.Println("✓ Heartbeat service started")
|
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 {
|
if err := channelManager.StartAll(ctx); err != nil {
|
||||||
fmt.Printf("Error starting channels: %v\n", err)
|
fmt.Printf("Error starting channels: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
@ -601,7 +559,6 @@ func gatewayCmd() {
|
||||||
fmt.Println("\nShutting down...")
|
fmt.Println("\nShutting down...")
|
||||||
cancel()
|
cancel()
|
||||||
healthServer.Stop(context.Background())
|
healthServer.Stop(context.Background())
|
||||||
deviceService.Stop()
|
|
||||||
heartbeatService.Stop()
|
heartbeatService.Stop()
|
||||||
cronService.Stop()
|
cronService.Stop()
|
||||||
agentLoop.Stop()
|
agentLoop.Stop()
|
||||||
|
|
@ -649,9 +606,6 @@ func statusCmd() {
|
||||||
if cfg.LLM.BaseURL != "" {
|
if cfg.LLM.BaseURL != "" {
|
||||||
fmt.Printf("Base URL: %s\n", cfg.LLM.BaseURL)
|
fmt.Printf("Base URL: %s\n", cfg.LLM.BaseURL)
|
||||||
}
|
}
|
||||||
if cfg.STT.APIKey != "" {
|
|
||||||
fmt.Println("STT API Key: ✓")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@
|
||||||
"api_key": "",
|
"api_key": "",
|
||||||
"base_url": ""
|
"base_url": ""
|
||||||
},
|
},
|
||||||
"stt": {
|
|
||||||
"api_key": ""
|
|
||||||
},
|
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"workspace": "~/.clawdroid/workspace",
|
"workspace": "~/.clawdroid/workspace",
|
||||||
|
|
@ -96,12 +93,6 @@
|
||||||
"exec": {
|
"exec": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"i2c": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"spi": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"android": {
|
"android": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
|
@ -140,10 +131,6 @@
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"interval": 30
|
"interval": 30
|
||||||
},
|
},
|
||||||
"devices": {
|
|
||||||
"enabled": false,
|
|
||||||
"monitor_usb": true
|
|
||||||
},
|
|
||||||
"gateway": {
|
"gateway": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 18790
|
"port": 18790
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,6 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
||||||
}
|
}
|
||||||
registry.Register(tools.NewWebFetchTool(50000))
|
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
|
// Android device control tool
|
||||||
sendCallbackWithType := func(channel, chatID, content, msgType string) error {
|
sendCallbackWithType := func(channel, chatID, content, msgType string) error {
|
||||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||||
|
|
|
||||||
|
|
@ -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
|
// TestAgentLoop_Stop verifies Stop() sets running to false
|
||||||
func TestAgentLoop_Stop(t *testing.T) {
|
func TestAgentLoop_Stop(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,6 @@ func statusLabel(toolName string, args map[string]interface{}) string {
|
||||||
return "アシスタント終了中..."
|
return "アシスタント終了中..."
|
||||||
case "mcp":
|
case "mcp":
|
||||||
return mcpStatusLabel(args)
|
return mcpStatusLabel(args)
|
||||||
case "i2c":
|
|
||||||
return i2cStatusLabel(args)
|
|
||||||
case "spi":
|
|
||||||
return spiStatusLabel(args)
|
|
||||||
default:
|
default:
|
||||||
return "処理中..."
|
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.
|
// strArg extracts a string argument from a tool arguments map.
|
||||||
func strArg(args map[string]interface{}, key string) string {
|
func strArg(args map[string]interface{}, key string) string {
|
||||||
if v, ok := args[key]; ok {
|
if v, ok := args[key]; ok {
|
||||||
|
|
@ -233,19 +186,6 @@ func strArg(args map[string]interface{}, key string) string {
|
||||||
return ""
|
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.
|
// truncLabel truncates a string to maxRunes runes, appending "..." if truncated.
|
||||||
func truncLabel(s string, maxRunes int) string {
|
func truncLabel(s string, maxRunes int) string {
|
||||||
if utf8.RuneCountInString(s) <= maxRunes {
|
if utf8.RuneCountInString(s) <= maxRunes {
|
||||||
|
|
|
||||||
|
|
@ -49,19 +49,13 @@ type LLMConfig struct {
|
||||||
BaseURL string `json:"base_url" env:"CLAWDROID_LLM_BASE_URL"`
|
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 {
|
type Config struct {
|
||||||
LLM LLMConfig `json:"llm"`
|
LLM LLMConfig `json:"llm"`
|
||||||
STT STTConfig `json:"stt"`
|
|
||||||
Agents AgentsConfig `json:"agents"`
|
Agents AgentsConfig `json:"agents"`
|
||||||
Channels ChannelsConfig `json:"channels"`
|
Channels ChannelsConfig `json:"channels"`
|
||||||
Gateway GatewayConfig `json:"gateway"`
|
Gateway GatewayConfig `json:"gateway"`
|
||||||
Tools ToolsConfig `json:"tools"`
|
Tools ToolsConfig `json:"tools"`
|
||||||
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||||
Devices DevicesConfig `json:"devices"`
|
|
||||||
RateLimits RateLimitsConfig `json:"rate_limits"`
|
RateLimits RateLimitsConfig `json:"rate_limits"`
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
@ -182,11 +176,6 @@ type HeartbeatConfig struct {
|
||||||
Interval int `json:"interval" env:"CLAWDROID_HEARTBEAT_INTERVAL"` // minutes, min 5
|
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 {
|
type RateLimitsConfig struct {
|
||||||
MaxToolCallsPerMinute int `json:"max_tool_calls_per_minute" env:"CLAWDROID_RATE_LIMITS_MAX_TOOL_CALLS_PER_MINUTE"` // 0 = unlimited
|
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
|
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"`
|
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 {
|
type MCPServerConfig struct {
|
||||||
// Stdio transport
|
// Stdio transport
|
||||||
Command string `json:"command,omitempty"`
|
Command string `json:"command,omitempty"`
|
||||||
|
|
@ -250,8 +231,6 @@ type MemoryToolsConfig struct {
|
||||||
type ToolsConfig struct {
|
type ToolsConfig struct {
|
||||||
Web WebToolsConfig `json:"web"`
|
Web WebToolsConfig `json:"web"`
|
||||||
Exec ExecToolsConfig `json:"exec"`
|
Exec ExecToolsConfig `json:"exec"`
|
||||||
I2C I2CToolsConfig `json:"i2c"`
|
|
||||||
SPI SPIToolsConfig `json:"spi"`
|
|
||||||
Android AndroidToolsConfig `json:"android"`
|
Android AndroidToolsConfig `json:"android"`
|
||||||
Memory MemoryToolsConfig `json:"memory"`
|
Memory MemoryToolsConfig `json:"memory"`
|
||||||
MCP map[string]MCPServerConfig `json:"mcp,omitempty"`
|
MCP map[string]MCPServerConfig `json:"mcp,omitempty"`
|
||||||
|
|
@ -262,7 +241,6 @@ func DefaultConfig() *Config {
|
||||||
LLM: LLMConfig{
|
LLM: LLMConfig{
|
||||||
Model: "zhipu/glm-4.7",
|
Model: "zhipu/glm-4.7",
|
||||||
},
|
},
|
||||||
STT: STTConfig{},
|
|
||||||
Agents: AgentsConfig{
|
Agents: AgentsConfig{
|
||||||
Defaults: AgentDefaults{
|
Defaults: AgentDefaults{
|
||||||
Workspace: "~/.clawdroid/workspace",
|
Workspace: "~/.clawdroid/workspace",
|
||||||
|
|
@ -355,12 +333,6 @@ func DefaultConfig() *Config {
|
||||||
Exec: ExecToolsConfig{
|
Exec: ExecToolsConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
},
|
},
|
||||||
I2C: I2CToolsConfig{
|
|
||||||
Enabled: false,
|
|
||||||
},
|
|
||||||
SPI: SPIToolsConfig{
|
|
||||||
Enabled: false,
|
|
||||||
},
|
|
||||||
Android: AndroidToolsConfig{
|
Android: AndroidToolsConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
},
|
},
|
||||||
|
|
@ -383,10 +355,6 @@ func DefaultConfig() *Config {
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Interval: 30, // default 30 minutes
|
Interval: 30, // default 30 minutes
|
||||||
},
|
},
|
||||||
Devices: DevicesConfig{
|
|
||||||
Enabled: false,
|
|
||||||
MonitorUSB: true,
|
|
||||||
},
|
|
||||||
RateLimits: RateLimitsConfig{
|
RateLimits: RateLimitsConfig{
|
||||||
MaxToolCallsPerMinute: 60,
|
MaxToolCallsPerMinute: 60,
|
||||||
MaxRequestsPerMinute: 30,
|
MaxRequestsPerMinute: 30,
|
||||||
|
|
|
||||||
|
|
@ -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
|
// TestDefaultConfig_WebTools verifies web tools config
|
||||||
func TestDefaultConfig_WebTools(t *testing.T) {
|
func TestDefaultConfig_WebTools(t *testing.T) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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]
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package devices
|
|
||||||
|
|
||||||
import "github.com/KarakuriAgent/clawdroid/pkg/devices/events"
|
|
||||||
|
|
||||||
type EventSource = events.EventSource
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
147
pkg/tools/i2c.go
147
pkg/tools/i2c.go
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
156
pkg/tools/spi.go
156
pkg/tools/spi.go
|
|
@ -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, ""
|
|
||||||
}
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
@ -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) |
|
|
||||||
|
|
@ -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"`
|
|
||||||
|
|
@ -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
|
|
||||||
Loading…
Add table
Reference in a new issue