Merge pull request #158 from easyzoom/feat/device-hotplug-notifications
feat: add device hotplug event notifications (USB on Linux)
This commit is contained in:
@@ -25,11 +25,13 @@ import (
|
|||||||
"github.com/sipeed/picoclaw/pkg/channels"
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/cron"
|
"github.com/sipeed/picoclaw/pkg/cron"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/devices"
|
||||||
"github.com/sipeed/picoclaw/pkg/heartbeat"
|
"github.com/sipeed/picoclaw/pkg/heartbeat"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
"github.com/sipeed/picoclaw/pkg/migrate"
|
"github.com/sipeed/picoclaw/pkg/migrate"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers"
|
"github.com/sipeed/picoclaw/pkg/providers"
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/state"
|
||||||
"github.com/sipeed/picoclaw/pkg/tools"
|
"github.com/sipeed/picoclaw/pkg/tools"
|
||||||
"github.com/sipeed/picoclaw/pkg/voice"
|
"github.com/sipeed/picoclaw/pkg/voice"
|
||||||
)
|
)
|
||||||
@@ -751,6 +753,18 @@ func gatewayCmd() {
|
|||||||
}
|
}
|
||||||
fmt.Println("✓ Heartbeat service started")
|
fmt.Println("✓ Heartbeat service started")
|
||||||
|
|
||||||
|
stateManager := state.NewManager(cfg.WorkspacePath())
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -763,6 +777,7 @@ func gatewayCmd() {
|
|||||||
|
|
||||||
fmt.Println("\nShutting down...")
|
fmt.Println("\nShutting down...")
|
||||||
cancel()
|
cancel()
|
||||||
|
deviceService.Stop()
|
||||||
heartbeatService.Stop()
|
heartbeatService.Stop()
|
||||||
cronService.Stop()
|
cronService.Stop()
|
||||||
agentLoop.Stop()
|
agentLoop.Stop()
|
||||||
|
|||||||
@@ -113,6 +113,10 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"interval": 30
|
"interval": 30
|
||||||
},
|
},
|
||||||
|
"devices": {
|
||||||
|
"enabled": false,
|
||||||
|
"monitor_usb": true
|
||||||
|
},
|
||||||
"gateway": {
|
"gateway": {
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 18790
|
"port": 18790
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type Config struct {
|
|||||||
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"`
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +151,11 @@ type HeartbeatConfig struct {
|
|||||||
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
|
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DevicesConfig struct {
|
||||||
|
Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
|
||||||
|
MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProvidersConfig struct {
|
type ProvidersConfig struct {
|
||||||
Anthropic ProviderConfig `json:"anthropic"`
|
Anthropic ProviderConfig `json:"anthropic"`
|
||||||
OpenAI ProviderConfig `json:"openai"`
|
OpenAI ProviderConfig `json:"openai"`
|
||||||
@@ -299,6 +305,10 @@ func DefaultConfig() *Config {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Interval: 30, // default 30 minutes
|
Interval: 30, // default 30 minutes
|
||||||
},
|
},
|
||||||
|
Devices: DevicesConfig{
|
||||||
|
Enabled: false,
|
||||||
|
MonitorUSB: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
pkg/devices/events/events.go
Normal file
57
pkg/devices/events/events.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
152
pkg/devices/service.go
Normal file
152
pkg/devices/service.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package devices
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/constants"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/devices/events"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/devices/sources"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
"github.com/sipeed/picoclaw/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]
|
||||||
|
}
|
||||||
5
pkg/devices/source.go
Normal file
5
pkg/devices/source.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package devices
|
||||||
|
|
||||||
|
import "github.com/sipeed/picoclaw/pkg/devices/events"
|
||||||
|
|
||||||
|
type EventSource = events.EventSource
|
||||||
198
pkg/devices/sources/usb_linux.go
Normal file
198
pkg/devices/sources/usb_linux.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package sources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/devices/events"
|
||||||
|
"github.com/sipeed/picoclaw/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
|
||||||
|
}
|
||||||
29
pkg/devices/sources/usb_stub.go
Normal file
29
pkg/devices/sources/usb_stub.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package sources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user