153 lines
3.1 KiB
Go
153 lines
3.1 KiB
Go
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]
|
|
}
|