From fd898050bfeb23f2edca4ec78c5a24f7df575106 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Thu, 18 Aug 2022 22:46:38 +0200 Subject: [PATCH] scenemap and effect enforcers. --- bus.go | 3 +- cmd/bustest/main.go | 43 ++++--- commands/assign.go | 5 +- device/{Resolver.go => interfaces.go} | 4 + effects/manual.go | 6 +- interface.go | 4 + internal/color/color.go | 51 ++++++++ services/effectenforcer.go | 160 ++++++++++++++++++++++++++ services/scenemap.go | 67 +++++++++++ 9 files changed, 319 insertions(+), 24 deletions(-) rename device/{Resolver.go => interfaces.go} (58%) create mode 100644 services/effectenforcer.go create mode 100644 services/scenemap.go diff --git a/bus.go b/bus.go index b7ee0de..7d05dcf 100644 --- a/bus.go +++ b/bus.go @@ -39,15 +39,14 @@ func (b *EventBus) Join(service Service) { // so that they're processed without having to wait for a new event. signal := b.signalCh() - b.mu.Lock() listener := &serviceListener{ bus: b, queue: make([]serviceMessage, 0, 16), service: service, } - go listener.run(signal) + b.mu.Lock() b.listeners = append(b.listeners, listener) b.mu.Unlock() } diff --git a/cmd/bustest/main.go b/cmd/bustest/main.go index 36ecb63..c4d3759 100644 --- a/cmd/bustest/main.go +++ b/cmd/bustest/main.go @@ -5,7 +5,10 @@ import ( lucifer3 "git.aiterp.net/lucifer3/server" "git.aiterp.net/lucifer3/server/commands" "git.aiterp.net/lucifer3/server/device" + "git.aiterp.net/lucifer3/server/effects" "git.aiterp.net/lucifer3/server/events" + "git.aiterp.net/lucifer3/server/internal/color" + "git.aiterp.net/lucifer3/server/internal/gentools" "git.aiterp.net/lucifer3/server/services" "log" "time" @@ -15,11 +18,15 @@ func main() { bus := lucifer3.EventBus{} resolver := services.NewResolver() + sceneMap := services.NewSceneMap(resolver) + bus.Join(resolver) + bus.Join(sceneMap) + bus.Join(services.NewEffectEnforcer(resolver, sceneMap)) - bus.RunEvent(events.Connected{Prefix: "nanoleaf:10.80.1.11"}) + time.Sleep(time.Millisecond) - time.Sleep(time.Second / 2) + bus.RunEvent(events.Connected{Prefix: "nanoleaf:10.80.1.11"}) numbers := []int{5, 2, 3, 1, 4} for i, id := range []string{"e28c", "67db", "f744", "d057", "73c1"} { @@ -32,19 +39,21 @@ func main() { }) } + time.Sleep(time.Millisecond) + bus.RunCommand(commands.ReplaceScene{ Match: "lucifer:name:Hex*", SceneID: 7, }) - time.Sleep(time.Second / 8) + time.Sleep(time.Millisecond) bus.RunCommand(commands.AddAlias{ Match: "nanoleaf:10.80.1.{11,7,16,5}:*", Alias: "lucifer:tag:Magic Lamps", }) - time.Sleep(time.Second / 8) + time.Sleep(time.Millisecond) bus.RunEvent(events.HardwareState{ ID: "nanoleaf:10.80.1.11:40e5", @@ -54,31 +63,33 @@ func main() { State: device.State{}, }) - time.Sleep(time.Second / 8) - - bus.RunCommand(commands.ReplaceScene{ - Match: "lucifer:name:Hex*", - SceneID: 7, - }) + time.Sleep(time.Millisecond) bus.RunEvent(events.ExternalEvent{ Kind: "weather", Values: map[string]string{ - "city": "Ørland", - "region": "Trøndelag", - "country": "Norway", + "location": "Brekstad", "temperature_celsius": "21.00", "precipitation_mm": "3.21", }, }) - time.Sleep(time.Second / 8) + time.Sleep(time.Millisecond) + + bus.RunCommand(commands.Assign{ + Match: "**:Hexagon *", + Effect: effects.Manual{ + Power: gentools.Ptr(true), + Color: gentools.Ptr(color.MustParse("rgb:#ffcc11")), + Intensity: gentools.Ptr(1.0), + }, + }) + + time.Sleep(time.Millisecond * 100) log.Println("Search \"**:Hexagon {1,5,6}\"") for _, dev := range resolver.Resolve("lucifer:name:Hexagon {1,5,6}") { log.Println("- ID:", dev.ID) log.Println(" Aliases:", dev.Aliases) } - - time.Sleep(time.Second / 4) } diff --git a/commands/assign.go b/commands/assign.go index 326d11b..7bb3459 100644 --- a/commands/assign.go +++ b/commands/assign.go @@ -3,14 +3,13 @@ package commands import ( "fmt" lucifer3 "git.aiterp.net/lucifer3/server" - "git.aiterp.net/lucifer3/server/internal/formattools" ) type Assign struct { - IDs []string `json:"ids"` + Match string `json:"match"` Effect lucifer3.Effect `json:"effect"` } func (c Assign) CommandDescription() string { - return fmt.Sprintf("Assign(%v, %s)", formattools.CompactIDList(c.IDs), c.Effect.EffectDescription()) + return fmt.Sprintf("Assign(%s, %s)", c.Match, c.Effect.EffectDescription()) } diff --git a/device/Resolver.go b/device/interfaces.go similarity index 58% rename from device/Resolver.go rename to device/interfaces.go index ec40bfe..83c0eaa 100644 --- a/device/Resolver.go +++ b/device/interfaces.go @@ -3,3 +3,7 @@ package device type Resolver interface { Resolve(pattern string) []Pointer } + +type SceneMap interface { + SceneID(id string) *int64 +} diff --git a/effects/manual.go b/effects/manual.go index 8261b4b..45e09a8 100644 --- a/effects/manual.go +++ b/effects/manual.go @@ -14,15 +14,15 @@ type Manual struct { Temperature *float64 `json:"temperature,omitempty"` } -func (e *Manual) EffectDescription() string { +func (e Manual) EffectDescription() string { return fmt.Sprintf("Manual%s", e.State(0, 0, 0).String()) } -func (e *Manual) Frequency() time.Duration { +func (e Manual) Frequency() time.Duration { return 0 } -func (e *Manual) State(int, int, int) device.State { +func (e Manual) State(int, int, int) device.State { return device.State{ Power: e.Power, Temperature: e.Temperature, diff --git a/interface.go b/interface.go index 61936dd..d0a30ce 100644 --- a/interface.go +++ b/interface.go @@ -6,6 +6,10 @@ import ( ) type Effect interface { + // State uses three values: + // index: The position of this ID; + // round: Increases by one for each run of the effect if Frequency > 0; + // random: A random positive int in the full 31-bit range. 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 21038f1..119896f 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -3,6 +3,7 @@ package color import ( "fmt" "github.com/lucasb-eyer/go-colorful" + "math" "strconv" "strings" ) @@ -38,6 +39,56 @@ func (col *Color) SetXY(xy XY) { *col = Color{XY: &xy} } +func (col *Color) AlmostEquals(other Color) bool { + if (col.K != nil) != (other.K != nil) { + return false + } + + if col.HS != nil && other.HS != nil { + if math.Abs(col.HS.Hue-other.HS.Hue) > 0.01 { + return false + } + if math.Abs(col.HS.Sat-other.HS.Sat) > 0.01 { + return false + } + + if col.K != nil && *col.K != *other.K { + return false + } + + return true + } + + if col.K != nil { + return *col.K == *other.K + } + + if col.RGB != nil && other.RGB != nil { + if math.Abs(col.RGB.Red-other.RGB.Red) > 0.01 { + return false + } + if math.Abs(col.RGB.Blue-other.RGB.Blue) > 0.01 { + return false + } + if math.Abs(col.RGB.Green-other.RGB.Green) > 0.01 { + return false + } + + return true + } + + xy1, _ := col.ToXY() + xy2, _ := col.ToXY() + if math.Abs(xy1.XY.X-xy2.XY.X) > 0.001 { + return false + } + if math.Abs(xy1.XY.Y-xy2.XY.Y) > 0.001 { + return false + } + + return true +} + // ToRGB tries to copy the color to an RGB color. If it's already RGB, it will be plainly copied, but HS // will be returned. If ok is false, then no copying has occurred and cv2 will be blank. func (col *Color) ToRGB() (col2 Color, ok bool) { diff --git a/services/effectenforcer.go b/services/effectenforcer.go new file mode 100644 index 0000000..56e416f --- /dev/null +++ b/services/effectenforcer.go @@ -0,0 +1,160 @@ +package services + +import ( + lucifer3 "git.aiterp.net/lucifer3/server" + "git.aiterp.net/lucifer3/server/commands" + "git.aiterp.net/lucifer3/server/device" + "math/rand" + "sync" + "sync/atomic" + "time" +) + +func NewEffectEnforcer(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService { + s := &effectEnforcer{ + resolver: resolver, + sceneMap: sceneMap, + list: make([]*effectEnforcerRun, 0, 16), + index: make(map[string]*effectEnforcerRun, 8), + } + + return s +} + +type effectEnforcer struct { + mu sync.Mutex + resolver device.Resolver + sceneMap device.SceneMap + + started uint32 + + list []*effectEnforcerRun + index map[string]*effectEnforcerRun +} + +func (s *effectEnforcer) Active() bool { + return true +} + +func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {} + +func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) { + switch command := command.(type) { + case commands.Assign: + pointers := s.resolver.Resolve(command.Match) + allowedIDs := make([]string, 0, len(pointers)) + for _, ptr := range pointers { + if s.sceneMap.SceneID(ptr.ID) == nil { + allowedIDs = append(allowedIDs, ptr.ID) + } + } + + s.mu.Lock() + // Create a new run + newRun := &effectEnforcerRun{ + due: time.Now(), + ids: allowedIDs, + effect: command.Effect, + } + s.list = append(s.list, newRun) + + // Remove the ids from any old run. + for _, id := range allowedIDs { + if oldRun := s.index[id]; oldRun != nil { + oldRun.remove(id) + } + s.index[id] = newRun + } + s.mu.Unlock() + + if atomic.CompareAndSwapUint32(&s.started, 0, 1) { + go s.runLoop(bus) + } + } +} + +func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) { + deleteList := make([]int, 0, 8) + commandQueue := make([]commands.SetState, 0, 32) + + for now := range time.NewTicker(time.Millisecond * 100).C { + s.mu.Lock() + for i, run := range s.list { + if run.dead { + deleteList = append(deleteList, i-len(deleteList)) + continue + } + if run.due.IsZero() || now.Before(run.due) { + continue + } + + r := rand.Int() + for j, id := range run.ids { + if id == "" { + continue + } + + state := run.effect.State(j, run.round, r) + commandQueue = append(commandQueue, commands.SetState{ + ID: id, + State: state, + }) + } + + if freq := run.effect.Frequency(); freq > 0 { + if freq < 100*time.Millisecond { + run.due = now + } else { + run.due = run.due.Add(freq) + } + + run.round += 1 + } else { + run.due = time.Time{} + } + } + + if len(deleteList) > 0 { + for _, i := range deleteList { + deleteList = append(deleteList[:i], deleteList[i+1:]...) + } + + deleteList = deleteList[:0] + } + s.mu.Unlock() + + if len(commandQueue) > 0 { + for _, command := range commandQueue { + bus.RunCommand(command) + } + + commandQueue = commandQueue[:0] + } + } +} + +type effectEnforcerRun struct { + due time.Time + ids []string + effect lucifer3.Effect + dead bool + round int +} + +// remove takes out an id from the effect, and returns whether the effect is empty. +func (r *effectEnforcerRun) remove(id string) { + anyLeft := false + for i, id2 := range r.ids { + if id2 == id { + r.ids[i] = "" + } else if id2 != "" { + anyLeft = true + } + } + + if !anyLeft { + r.dead = true + } + + return +} diff --git a/services/scenemap.go b/services/scenemap.go new file mode 100644 index 0000000..e35432c --- /dev/null +++ b/services/scenemap.go @@ -0,0 +1,67 @@ +package services + +import ( + lucifer3 "git.aiterp.net/lucifer3/server" + "git.aiterp.net/lucifer3/server/commands" + "git.aiterp.net/lucifer3/server/device" + "log" + "sync" +) + +func NewSceneMap(resolver device.Resolver) *SceneMap { + return &SceneMap{ + resolver: resolver, + sceneMap: make(map[string]int64, 64), + } +} + +type SceneMap struct { + resolver device.Resolver + + mu sync.Mutex + sceneMap map[string]int64 +} + +func (s *SceneMap) SceneID(id string) *int64 { + s.mu.Lock() + defer s.mu.Unlock() + + sceneID, ok := s.sceneMap[id] + if !ok { + return nil + } + + return &sceneID +} + +func (s *SceneMap) Active() bool { + return true +} + +func (s *SceneMap) HandleEvent(*lucifer3.EventBus, lucifer3.Event) {} + +func (s *SceneMap) HandleCommand(_ *lucifer3.EventBus, command lucifer3.Command) { + switch command := command.(type) { + case commands.ReplaceScene: + matched := s.resolver.Resolve(command.Match) + + if len(matched) > 0 { + s.mu.Lock() + for _, ptr := range matched { + s.sceneMap[ptr.ID] = command.SceneID + } + s.mu.Unlock() + } + case commands.ClearScene: + matched := s.resolver.Resolve(command.Match) + + if len(matched) > 0 { + s.mu.Lock() + for _, ptr := range matched { + log.Println("Got") + delete(s.sceneMap, ptr.ID) + } + s.mu.Unlock() + } + } +}