scenemap and effect enforcers.
This commit is contained in:
@@ -39,15 +39,14 @@ func (b *EventBus) Join(service Service) {
|
|||||||
// so that they're processed without having to wait for a new event.
|
// so that they're processed without having to wait for a new event.
|
||||||
signal := b.signalCh()
|
signal := b.signalCh()
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
listener := &serviceListener{
|
listener := &serviceListener{
|
||||||
bus: b,
|
bus: b,
|
||||||
queue: make([]serviceMessage, 0, 16),
|
queue: make([]serviceMessage, 0, 16),
|
||||||
service: service,
|
service: service,
|
||||||
}
|
}
|
||||||
|
|
||||||
go listener.run(signal)
|
go listener.run(signal)
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
b.listeners = append(b.listeners, listener)
|
b.listeners = append(b.listeners, listener)
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-16
@@ -5,7 +5,10 @@ import (
|
|||||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||||
"git.aiterp.net/lucifer3/server/commands"
|
"git.aiterp.net/lucifer3/server/commands"
|
||||||
"git.aiterp.net/lucifer3/server/device"
|
"git.aiterp.net/lucifer3/server/device"
|
||||||
|
"git.aiterp.net/lucifer3/server/effects"
|
||||||
"git.aiterp.net/lucifer3/server/events"
|
"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"
|
"git.aiterp.net/lucifer3/server/services"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
@@ -15,12 +18,16 @@ func main() {
|
|||||||
bus := lucifer3.EventBus{}
|
bus := lucifer3.EventBus{}
|
||||||
|
|
||||||
resolver := services.NewResolver()
|
resolver := services.NewResolver()
|
||||||
|
sceneMap := services.NewSceneMap(resolver)
|
||||||
|
|
||||||
bus.Join(resolver)
|
bus.Join(resolver)
|
||||||
|
bus.Join(sceneMap)
|
||||||
|
bus.Join(services.NewEffectEnforcer(resolver, sceneMap))
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
|
||||||
bus.RunEvent(events.Connected{Prefix: "nanoleaf:10.80.1.11"})
|
bus.RunEvent(events.Connected{Prefix: "nanoleaf:10.80.1.11"})
|
||||||
|
|
||||||
time.Sleep(time.Second / 2)
|
|
||||||
|
|
||||||
numbers := []int{5, 2, 3, 1, 4}
|
numbers := []int{5, 2, 3, 1, 4}
|
||||||
for i, id := range []string{"e28c", "67db", "f744", "d057", "73c1"} {
|
for i, id := range []string{"e28c", "67db", "f744", "d057", "73c1"} {
|
||||||
bus.RunEvent(events.HardwareState{
|
bus.RunEvent(events.HardwareState{
|
||||||
@@ -32,19 +39,21 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
|
||||||
bus.RunCommand(commands.ReplaceScene{
|
bus.RunCommand(commands.ReplaceScene{
|
||||||
Match: "lucifer:name:Hex*",
|
Match: "lucifer:name:Hex*",
|
||||||
SceneID: 7,
|
SceneID: 7,
|
||||||
})
|
})
|
||||||
|
|
||||||
time.Sleep(time.Second / 8)
|
time.Sleep(time.Millisecond)
|
||||||
|
|
||||||
bus.RunCommand(commands.AddAlias{
|
bus.RunCommand(commands.AddAlias{
|
||||||
Match: "nanoleaf:10.80.1.{11,7,16,5}:*",
|
Match: "nanoleaf:10.80.1.{11,7,16,5}:*",
|
||||||
Alias: "lucifer:tag:Magic Lamps",
|
Alias: "lucifer:tag:Magic Lamps",
|
||||||
})
|
})
|
||||||
|
|
||||||
time.Sleep(time.Second / 8)
|
time.Sleep(time.Millisecond)
|
||||||
|
|
||||||
bus.RunEvent(events.HardwareState{
|
bus.RunEvent(events.HardwareState{
|
||||||
ID: "nanoleaf:10.80.1.11:40e5",
|
ID: "nanoleaf:10.80.1.11:40e5",
|
||||||
@@ -54,31 +63,33 @@ func main() {
|
|||||||
State: device.State{},
|
State: device.State{},
|
||||||
})
|
})
|
||||||
|
|
||||||
time.Sleep(time.Second / 8)
|
time.Sleep(time.Millisecond)
|
||||||
|
|
||||||
bus.RunCommand(commands.ReplaceScene{
|
|
||||||
Match: "lucifer:name:Hex*",
|
|
||||||
SceneID: 7,
|
|
||||||
})
|
|
||||||
|
|
||||||
bus.RunEvent(events.ExternalEvent{
|
bus.RunEvent(events.ExternalEvent{
|
||||||
Kind: "weather",
|
Kind: "weather",
|
||||||
Values: map[string]string{
|
Values: map[string]string{
|
||||||
"city": "Ørland",
|
"location": "Brekstad",
|
||||||
"region": "Trøndelag",
|
|
||||||
"country": "Norway",
|
|
||||||
"temperature_celsius": "21.00",
|
"temperature_celsius": "21.00",
|
||||||
"precipitation_mm": "3.21",
|
"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}\"")
|
log.Println("Search \"**:Hexagon {1,5,6}\"")
|
||||||
for _, dev := range resolver.Resolve("lucifer:name:Hexagon {1,5,6}") {
|
for _, dev := range resolver.Resolve("lucifer:name:Hexagon {1,5,6}") {
|
||||||
log.Println("- ID:", dev.ID)
|
log.Println("- ID:", dev.ID)
|
||||||
log.Println(" Aliases:", dev.Aliases)
|
log.Println(" Aliases:", dev.Aliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Second / 4)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -3,14 +3,13 @@ package commands
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||||
"git.aiterp.net/lucifer3/server/internal/formattools"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Assign struct {
|
type Assign struct {
|
||||||
IDs []string `json:"ids"`
|
Match string `json:"match"`
|
||||||
Effect lucifer3.Effect `json:"effect"`
|
Effect lucifer3.Effect `json:"effect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Assign) CommandDescription() string {
|
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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ package device
|
|||||||
type Resolver interface {
|
type Resolver interface {
|
||||||
Resolve(pattern string) []Pointer
|
Resolve(pattern string) []Pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SceneMap interface {
|
||||||
|
SceneID(id string) *int64
|
||||||
|
}
|
||||||
+3
-3
@@ -14,15 +14,15 @@ type Manual struct {
|
|||||||
Temperature *float64 `json:"temperature,omitempty"`
|
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())
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Manual) State(int, int, int) device.State {
|
func (e Manual) State(int, int, int) device.State {
|
||||||
return device.State{
|
return device.State{
|
||||||
Power: e.Power,
|
Power: e.Power,
|
||||||
Temperature: e.Temperature,
|
Temperature: e.Temperature,
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Effect interface {
|
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
|
State(index, round, random int) device.State
|
||||||
Frequency() time.Duration
|
Frequency() time.Duration
|
||||||
EffectDescription() string
|
EffectDescription() string
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package color
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/lucasb-eyer/go-colorful"
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -38,6 +39,56 @@ func (col *Color) SetXY(xy XY) {
|
|||||||
*col = Color{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
|
// 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.
|
// 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) {
|
func (col *Color) ToRGB() (col2 Color, ok bool) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user