diff --git a/bus.go b/bus.go index c0a1b3c..5003a92 100644 --- a/bus.go +++ b/bus.go @@ -1,14 +1,18 @@ package lucifer3 import ( - "log" + "fmt" "sync" ) type ServiceKey struct{} type Event interface { - EventName() string + EventDescription() string +} + +type Command interface { + CommandDescription() string } type EventBus struct { @@ -18,7 +22,7 @@ type EventBus struct { } // JoinCallback joins the event bus for a moment. -func (b *EventBus) JoinCallback(cb func(event Event, sender ServiceID) bool) { +func (b *EventBus) JoinCallback(cb func(event Event) bool) { b.Join(newCallbackService(cb)) } @@ -30,7 +34,7 @@ func (b *EventBus) Join(service Service) { b.mu.Lock() listener := &serviceListener{ bus: b, - queue: make([]queuedEvent, 0, 16), + queue: make([]serviceMessage, 0, 16), service: service, } @@ -40,7 +44,17 @@ func (b *EventBus) Join(service Service) { b.mu.Unlock() } -func (b *EventBus) Send(event Event, sender ServiceID, recipient *ServiceID) { +func (b *EventBus) RunCommand(command Command) { + fmt.Println("[COMMAND]", command.CommandDescription()) + b.send(serviceMessage{command: command}) +} + +func (b *EventBus) RunEvent(event Event) { + fmt.Println("[EVENT]", event.EventDescription()) + b.send(serviceMessage{event: event}) +} + +func (b *EventBus) send(message serviceMessage) { b.mu.Lock() defer b.mu.Unlock() @@ -50,15 +64,9 @@ func (b *EventBus) Send(event Event, sender ServiceID, recipient *ServiceID) { deleteList = append(deleteList, i-len(deleteList)) continue } - if recipient != nil && *recipient != listener.service.ServiceID() { - continue - } listener.mu.Lock() - listener.queue = append(listener.queue, queuedEvent{ - event: event, - sender: sender, - }) + listener.queue = append(listener.queue, message) listener.mu.Unlock() } @@ -66,12 +74,6 @@ func (b *EventBus) Send(event Event, sender ServiceID, recipient *ServiceID) { b.listeners = append(b.listeners[:i], b.listeners[i+1:]...) } - if recipient != nil { - log.Printf("%s (-> %s): %s", &sender, recipient, event.EventName()) - } else { - log.Printf("%s: %s", &sender, event.EventName()) - } - if b.signal != nil { close(b.signal) b.signal = nil @@ -92,12 +94,18 @@ func (b *EventBus) signalCh() <-chan struct{} { type serviceListener struct { mu sync.Mutex bus *EventBus - queue []queuedEvent + queue []serviceMessage service Service } func (l *serviceListener) run(signal <-chan struct{}) { - queue := make([]queuedEvent, 0, 16) + // Detect command support + var activeService ActiveService + if as, ok := l.service.(ActiveService); ok { + activeService = as + } + + queue := make([]serviceMessage, 0, 16) for { // Listen for the signal, but stop here if the service has marked @@ -126,13 +134,19 @@ func (l *serviceListener) run(signal <-chan struct{}) { return } - l.service.Handle(l.bus, message.event, message.sender) + if message.event != nil { + l.service.HandleEvent(l.bus, message.event) + } + + if message.command != nil && activeService != nil { + activeService.HandleCommand(l.bus, message.command) + } } queue = queue[:0] } } -type queuedEvent struct { - event Event - sender ServiceID +type serviceMessage struct { + event Event + command Command } diff --git a/cmd/bustest/main.go b/cmd/bustest/main.go index b7caa58..2770b9c 100644 --- a/cmd/bustest/main.go +++ b/cmd/bustest/main.go @@ -2,6 +2,7 @@ package main import ( lucifer3 "git.aiterp.net/lucifer3/server" + "git.aiterp.net/lucifer3/server/commands" "git.aiterp.net/lucifer3/server/device" "git.aiterp.net/lucifer3/server/events" "git.aiterp.net/lucifer3/server/internal/color" @@ -13,65 +14,65 @@ import ( func main() { bus := lucifer3.EventBus{} - bus.JoinCallback(func(event lucifer3.Event, sender lucifer3.ServiceID) bool { + bus.JoinCallback(func(event lucifer3.Event) bool { switch event := event.(type) { - case events.HubConnected: - log.Println("Callback got connect event") - case events.DeviceAssignment: - log.Println("Callback can see", len(event.IDs), "devices are being assigned") - case events.DeviceRegister: - log.Println("Callback should not see that", event.InternalID, "has got a new ID") - case events.DeviceDesiredStateChange: - log.Println("Callback saw", event.ID, "got a new state.") + case events.Connected: + log.Println("Callback got connect event for", event.Prefix) } return true }) - c := color.RGB{Red: 1, Green: 0.7, Blue: 0.25}.ToXY() + bus.RunEvent(events.Connected{Prefix: "nanoleaf:10.80.1.11"}) - bus.Send(events.HubConnected{}, lucifer3.ServiceID{Kind: lucifer3.SKDeviceHub, ID: 1}, nil) - bus.Send(events.DeviceAssignment{ - IDs: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, - Version: 1, - State: &device.State{ - Power: gentools.Ptr(true), - Temperature: nil, - Intensity: nil, - Color: &color.Color{XY: &c}, - }, - }, lucifer3.ServiceID{Kind: lucifer3.SKSingleton, Name: "DeviceManager"}, nil) - bus.Send(events.DeviceAssignment{ - IDs: []int64{44, 45, 46, 47, 48, 49, 50, 51, 52, 53}, - Version: 2, - Effect: gentools.Ptr[int64](1044), - }, lucifer3.ServiceID{Kind: lucifer3.SKSingleton, Name: "DeviceManager"}, nil) + time.Sleep(time.Second / 2) + + for _, id := range []string{"e28c", "67db", "f744", "d057", "73c1"} { + bus.RunEvent(events.HardwareState{ + DeviceID: "nanoleaf:10.80.1.11:" + id, + SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity, + ColorFlags: device.CFlagRGB, + State: device.State{}, + }) + } + + bus.RunCommand(commands.ReplaceScene{ + IDs: []string{"nanoleaf:10.80.1.11:e28c", "nanoleaf:10.80.1.11:67db", "nanoleaf:10.80.1.11:f744", "nanoleaf:10.80.1.11:d057", "nanoleaf:10.80.1.11:73c1"}, + SceneID: 7, + }) + for _, id := range []string{"nanoleaf:10.80.1.11:e28c", "nanoleaf:10.80.1.11:67db", "nanoleaf:10.80.1.11:f744", "nanoleaf:10.80.1.11:d057", "nanoleaf:10.80.1.11:73c1"} { + bus.RunCommand(commands.SetState{ + ID: id, + State: device.State{ + Power: gentools.Ptr(true), + Intensity: gentools.Ptr(0.75), + Color: gentools.Ptr(color.MustParse("xy:0.3209,0.1542")), + }, + }) + } time.Sleep(time.Second / 2) - bus.Send(events.DeviceHardwareStateChange{ - InternalID: "c68b310c-7fd6-47a4-ae11-d60d701769da", - DeviceFlags: device.SFlagPower | device.SFlagIntensity, - ColorFlags: 0, + bus.RunEvent(events.HardwareState{ + DeviceID: "nanoleaf:10.80.1.11:40e5", + SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity, + ColorFlags: device.CFlagRGB, + State: device.State{}, + }) + bus.RunCommand(commands.ReplaceScene{ + IDs: []string{"nanoleaf:10.80.1.11:40e5", "nanoleaf:10.80.1.11:e28c", "nanoleaf:10.80.1.11:67db", "nanoleaf:10.80.1.11:f744", "nanoleaf:10.80.1.11:d057", "nanoleaf:10.80.1.11:73c1"}, + SceneID: 7, + }) + bus.RunCommand(commands.SetState{ + ID: "nanoleaf:10.80.1.11:40e5", State: device.State{ Power: gentools.Ptr(true), Intensity: gentools.Ptr(0.75), + Color: gentools.Ptr(color.MustParse("xy:0.3209,0.1542")), }, - }, lucifer3.ServiceID{Kind: lucifer3.SKDeviceHub, ID: 1}, nil) + }) time.Sleep(time.Second / 4) - bus.Send(events.DeviceRegister{ - InternalID: "c68b310c-7fd6-47a4-ae11-d60d701769da", - ID: 10, - }, lucifer3.ServiceID{Kind: lucifer3.SKSingleton, Name: "DeviceManager"}, &lucifer3.ServiceID{Kind: lucifer3.SKDeviceHub, ID: 1}) - bus.Send(events.DeviceDesiredStateChange{ - ID: 10, - NewState: device.State{ - Power: gentools.Ptr(true), - Intensity: gentools.Ptr(0.75), - }, - }, lucifer3.ServiceID{Kind: lucifer3.SKSingleton, Name: "DeviceManager"}, nil) - time.Sleep(time.Second * 4) } diff --git a/commands/assign.go b/commands/assign.go new file mode 100644 index 0000000..326d11b --- /dev/null +++ b/commands/assign.go @@ -0,0 +1,16 @@ +package commands + +import ( + "fmt" + lucifer3 "git.aiterp.net/lucifer3/server" + "git.aiterp.net/lucifer3/server/internal/formattools" +) + +type Assign struct { + IDs []string `json:"ids"` + Effect lucifer3.Effect `json:"effect"` +} + +func (c Assign) CommandDescription() string { + return fmt.Sprintf("Assign(%v, %s)", formattools.CompactIDList(c.IDs), c.Effect.EffectDescription()) +} diff --git a/commands/scene.go b/commands/scene.go new file mode 100644 index 0000000..66bdc4f --- /dev/null +++ b/commands/scene.go @@ -0,0 +1,23 @@ +package commands + +import ( + "fmt" + "git.aiterp.net/lucifer3/server/internal/formattools" +) + +type ReplaceScene struct { + IDs []string `json:"ids"` + SceneID int64 `json:"sceneId"` +} + +func (c ReplaceScene) CommandDescription() string { + return fmt.Sprintf("ReplaceScene(%v, %d)", formattools.CompactIDList(c.IDs), c.SceneID) +} + +type ClearScene struct { + IDs []string `json:"ids"` +} + +func (c ClearScene) CommandDescription() string { + return fmt.Sprintf("ClearScene(%v)", formattools.CompactIDList(c.IDs)) +} diff --git a/commands/state.go b/commands/state.go new file mode 100644 index 0000000..1c304f7 --- /dev/null +++ b/commands/state.go @@ -0,0 +1,15 @@ +package commands + +import ( + "fmt" + "git.aiterp.net/lucifer3/server/device" +) + +type SetState struct { + ID string + State device.State +} + +func (c SetState) CommandDescription() string { + return fmt.Sprintf("SetState(%s, %s)", c.ID, c.State) +} diff --git a/device/flags.go b/device/flags.go index 3e61ba5..f726d42 100644 --- a/device/flags.go +++ b/device/flags.go @@ -1,17 +1,12 @@ package device -import "math/bits" +import ( + "git.aiterp.net/lucifer3/server/internal/gentools" + "math/bits" +) type SupportFlags uint32 -func (f SupportFlags) HasAny(d SupportFlags) bool { - return (f & d) != 0 -} - -func (f SupportFlags) HasAll(d SupportFlags) bool { - return bits.OnesCount32(uint32(f&d)) == bits.OnesCount32(uint32(d)) -} - const ( SFlagPower SupportFlags = 1 << iota SFlagIntensity @@ -23,6 +18,20 @@ const ( SFlagSensorPresence ) +var supportFlags = []SupportFlags{SFlagPower, SFlagIntensity, SFlagColor, SFlagTemperature, SFlagSensorButtons, SFlagSensorTemperature, SFlagSensorLightLevel, SFlagSensorPresence} + +func (f SupportFlags) String() string { + return gentools.FlagString(f, supportFlags, []string{"P", "I", "C", "T", "SB", "ST", "SLL", "SP"}) +} + +func (f SupportFlags) HasAny(d SupportFlags) bool { + return (f & d) != 0 +} + +func (f SupportFlags) HasAll(d SupportFlags) bool { + return bits.OnesCount32(uint32(f&d)) == bits.OnesCount32(uint32(d)) +} + // ColorFlag is primarily to detect warm-white lights, as XY/RGB/HS/HSK can convert without trouble. type ColorFlag uint32 @@ -34,6 +43,12 @@ const ( CFlagKelvin ) +var colorFlags = []ColorFlag{CFlagXY, CFlagRGB, CFlagHS, CFlagHSK, CFlagKelvin} + +func (f ColorFlag) String() string { + return gentools.FlagString(f, colorFlags, []string{"XY", "RGB", "HS", "HSK", "K"}) +} + func (f ColorFlag) IsColor() bool { return f.HasAny(CFlagXY | CFlagRGB | CFlagHS | CFlagHSK) } diff --git a/effects/manual.go b/effects/manual.go new file mode 100644 index 0000000..8261b4b --- /dev/null +++ b/effects/manual.go @@ -0,0 +1,32 @@ +package effects + +import ( + "fmt" + "git.aiterp.net/lucifer3/server/device" + "git.aiterp.net/lucifer3/server/internal/color" + "time" +) + +type Manual struct { + Power *bool `json:"power,omitempty"` + Color *color.Color `json:"color,omitempty"` + Intensity *float64 `json:"intensity,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` +} + +func (e *Manual) EffectDescription() string { + return fmt.Sprintf("Manual%s", e.State(0, 0, 0).String()) +} + +func (e *Manual) Frequency() time.Duration { + return 0 +} + +func (e *Manual) State(int, int, int) device.State { + return device.State{ + Power: e.Power, + Temperature: e.Temperature, + Intensity: e.Intensity, + Color: e.Color, + } +} diff --git a/events/connection.go b/events/connection.go new file mode 100644 index 0000000..4f5d4e2 --- /dev/null +++ b/events/connection.go @@ -0,0 +1,20 @@ +package events + +import "fmt" + +type Connected struct { + Prefix string `json:"prefix"` +} + +func (e Connected) EventDescription() string { + return fmt.Sprintf("Connect(prefix:%s)", e.Prefix) +} + +type Disconnected struct { + Prefix string `json:"prefix"` + Reason string `json:"reason"` +} + +func (e Disconnected) EventDescription() string { + return fmt.Sprintf("Disconnected(prefix:%s, reason:%s)", e.Prefix, e.Reason) +} diff --git a/events/device.go b/events/device.go index c100e0e..0ec305c 100644 --- a/events/device.go +++ b/events/device.go @@ -3,86 +3,25 @@ package events import ( "fmt" "git.aiterp.net/lucifer3/server/device" - "git.aiterp.net/lucifer3/server/internal/gentools" - "strings" ) -type DeviceHardwareStateChange struct { - ID int64 `json:"id,omitempty"` - InternalID string `json:"internalId"` - DeviceFlags device.SupportFlags `json:"deviceFlags"` - ColorFlags device.ColorFlag `json:"colorFlags"` - State device.State `json:"state"` +type HardwareState struct { + DeviceID string `json:"internalId"` + SupportFlags device.SupportFlags `json:"deviceFlags"` + ColorFlags device.ColorFlag `json:"colorFlags"` + State device.State `json:"state"` } -func (d DeviceHardwareStateChange) EventName() string { - idStr := "" - if d.ID != 0 { - idStr = fmt.Sprintf("id:%d, ", d.ID) - } - - return fmt.Sprintf("DeviceHardwareStateChange(%siid:%s, dflags:%d, cflags:%d, state:%s)", - idStr, d.InternalID, d.DeviceFlags, d.ColorFlags, d.State, +func (d HardwareState) EventDescription() string { + return fmt.Sprintf("HardwareState(id:%s, sflags:%s, cflags:%s, state:%s)", + d.DeviceID, d.SupportFlags, d.ColorFlags, d.State, ) } -// DeviceRegister must be sent directly to the device hub that took it. -type DeviceRegister struct { - ID int64 `json:"id"` - InternalID string `json:"internalId"` -} - -func (d DeviceRegister) EventName() string { - return fmt.Sprintf("DeviceRegister(id:%d, iid:%s)", d.ID, d.InternalID) -} - -type DeviceDesiredStateChange struct { - ID int64 `json:"id"` - NewState device.State `json:"newState"` +type Unreachable struct { + DeviceID string `json:"deviceId"` } -func (d DeviceDesiredStateChange) EventName() string { - return fmt.Sprintf("DeviceDesiredStateChange(id:%d, state:%s)", - d.ID, d.NewState, - ) -} - -type DeviceAssignment struct { - IDs []int64 `json:"ids"` - Version int64 `json:"version"` // DeviceManager sets the version. - State *device.State `json:"state"` // DeviceManager sets the state in a follow-up event. Others still need to see it to kick the device. - Effect *int64 `json:"effect"` // An effect will pick this up. A scene may defer to own effects - Scene *int64 `json:"scene"` // A scene will take it and handle it. Might move it out a level so scenes are just filters that create assignments -} - -func (d DeviceAssignment) EventName() string { - s := "(!!no change!!)" - switch { - case d.State != nil: - s = fmt.Sprintf("state:%s", *d.State) - case d.Effect != nil: - s = fmt.Sprintf("effect:%d", *d.Effect) - case d.Scene != nil: - s = fmt.Sprintf("scene:%d", *d.Scene) - } - - return fmt.Sprintf("DeviceAssignment(ids:%s, version:%d, %s)", - strings.Join(gentools.FmtSprintArray(d.IDs), ","), - d.Version, - s, - ) -} - -// DeviceRestore attempts to restore devices to a previous version. It may not be -// successful. -type DeviceRestore struct { - IDs []int64 `json:"ids"` - Version int64 `json:"version"` -} - -func (d DeviceRestore) EventName() string { - return fmt.Sprintf("DeviceRestore(ids:%s, version:%d)", - strings.Join(gentools.FmtSprintArray(d.IDs), ","), - d.Version, - ) +func (d Unreachable) EventDescription() string { + return fmt.Sprintf("Unreachable(id:%s)", d.DeviceID) } diff --git a/events/hub.go b/events/hub.go deleted file mode 100644 index 1d56e19..0000000 --- a/events/hub.go +++ /dev/null @@ -1,17 +0,0 @@ -package events - -import "fmt" - -type HubConnected struct{} - -func (c HubConnected) EventName() string { - return "HubConnected" -} - -type HubDisconnected struct { - Reason string -} - -func (d HubDisconnected) EventName() string { - return fmt.Sprintf("HubDisconnected(reason:%s)", d.Reason) -} diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..61936dd --- /dev/null +++ b/interface.go @@ -0,0 +1,12 @@ +package lucifer3 + +import ( + "git.aiterp.net/lucifer3/server/device" + "time" +) + +type Effect interface { + State(index, round, random int) device.State + Frequency() time.Duration + EffectDescription() string +} diff --git a/internal/color/color.go b/internal/color/color.go index 0fabaa9..21038f1 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -206,6 +206,15 @@ func (col *Color) colorful() colorful.Color { } } +func MustParse(raw string) Color { + col, err := Parse(raw) + if err != nil { + panic(err) + } + + return col +} + func Parse(raw string) (col Color, err error) { if raw == "" { return diff --git a/internal/formattools/compactidlist.go b/internal/formattools/compactidlist.go new file mode 100644 index 0000000..0e54313 --- /dev/null +++ b/internal/formattools/compactidlist.go @@ -0,0 +1,51 @@ +package formattools + +import ( + "fmt" + "sort" + "strings" +) + +func CompactIDList(ids []string) []string { + sort.Strings(ids) + + currentGroup := "" + currentIDs := make([]string, 0, len(ids)) + groups := make([]string, 0, 2) + for _, id := range ids { + if id == "" { + continue + } + + split := strings.SplitN(id, ":", 3) + if len(split) < 3 { + continue + } + + group := id[:len(split[0])+len(split[1])+1] + + if group != currentGroup { + if len(currentIDs) > 0 { + if len(currentIDs) == 1 { + groups = append(groups, fmt.Sprintf("%s:%s", currentGroup, currentIDs[0])) + } else { + groups = append(groups, fmt.Sprintf("%s:{%s}", currentGroup, strings.Join(currentIDs, ","))) + } + currentIDs = currentIDs[:0] + } + currentGroup = group + } + + currentIDs = append(currentIDs, split[2]) + } + + if len(currentIDs) > 0 { + if len(currentIDs) == 1 { + groups = append(groups, fmt.Sprintf("%s:%s", currentGroup, currentIDs[0])) + } else { + groups = append(groups, fmt.Sprintf("%s:{%s}", currentGroup, strings.Join(currentIDs, ","))) + } + } + + return groups +} diff --git a/internal/gentools/commalist.go b/internal/gentools/commalist.go index 6b93466..9a37731 100644 --- a/internal/gentools/commalist.go +++ b/internal/gentools/commalist.go @@ -1,6 +1,9 @@ package gentools -import "fmt" +import ( + "fmt" + "strings" +) func FmtSprintArray[T any](arr []T) []string { res := make([]string, len(arr)) @@ -10,3 +13,7 @@ func FmtSprintArray[T any](arr []T) []string { return res } + +func FmtSprintArrayJoin[T any](arr []T, sep string) string { + return strings.Join(FmtSprintArray(arr), sep) +} diff --git a/internal/gentools/flagstr.go b/internal/gentools/flagstr.go new file mode 100644 index 0000000..8914075 --- /dev/null +++ b/internal/gentools/flagstr.go @@ -0,0 +1,18 @@ +package gentools + +import "strings" + +func FlagString[T ~uint32](value T, flags []T, names []string) string { + if value == 0 { + return "(none)" + } + + parts := make([]string, 0, len(flags)) + for i, flag := range flags { + if value&flag == flag { + parts = append(parts, names[i]) + } + } + + return strings.Join(parts, "|") +} diff --git a/service.go b/service.go index bdc3ee3..84cf17b 100644 --- a/service.go +++ b/service.go @@ -1,41 +1,20 @@ package lucifer3 import ( - "fmt" "sync/atomic" ) type Service interface { Active() bool - ServiceID() ServiceID - Handle(bus *EventBus, event Event, sender ServiceID) + HandleEvent(bus *EventBus, event Event) } -type ServiceID struct { - Kind ServiceKind - ID int64 - Name string -} - -func (s *ServiceID) String() string { - if s == nil { - return "BROADCAST" - } - if s.Kind == SKNotService { - return "EXTERNAL" - } - - if s.Name != "" { - return s.Name - } else { - return fmt.Sprintf("%s[%d]", s.Kind.String(), s.ID) - } - +type ActiveService interface { + HandleCommand(bus *EventBus, command Command) } type ServiceCommon struct { - serviceID ServiceID - inactive uint32 + inactive uint32 } func (s *ServiceCommon) Active() bool { @@ -46,60 +25,20 @@ func (s *ServiceCommon) markInactive() { atomic.StoreUint32(&s.inactive, 1) } -func (s *ServiceCommon) ServiceID() ServiceID { - return s.serviceID -} - -type ServiceKind int - -func (s ServiceKind) String() string { - switch s { - case SKSingleton: - return "Singleton" - case SKStorage: - return "Storage" - case SKDeviceHub: - return "DeviceHub" - case SKEffect: - return "Effect" - case SKCallback: - return "Callback" - case SKNotService: - return "NotService" - default: - return "???" - } -} - -const ( - SKNotService ServiceKind = iota - SKSingleton - SKStorage - SKDeviceHub - SKEffect - SKCallback -) - type callbackService struct { ServiceCommon - cb func(event Event, sender ServiceID) bool + cb func(event Event) bool } -func (c *callbackService) Handle(_ *EventBus, event Event, sender ServiceID) { - if !c.cb(event, sender) { +func (c *callbackService) HandleEvent(_ *EventBus, event Event) { + if !c.cb(event) { c.markInactive() } } -var nextCallback int64 = 0 - -func newCallbackService(cb func(event Event, sender ServiceID) bool) Service { +func newCallbackService(cb func(event Event) bool) Service { return &callbackService{ ServiceCommon: ServiceCommon{ - serviceID: ServiceID{ - Kind: SKCallback, - ID: atomic.AddInt64(&nextCallback, 1), - }, inactive: 0, }, cb: cb,