Browse Source

scenemap and effect enforcers.

beelzebub
Gisle Aune 2 years ago
parent
commit
fd898050bf
  1. 3
      bus.go
  2. 43
      cmd/bustest/main.go
  3. 5
      commands/assign.go
  4. 4
      device/interfaces.go
  5. 6
      effects/manual.go
  6. 4
      interface.go
  7. 51
      internal/color/color.go
  8. 160
      services/effectenforcer.go
  9. 67
      services/scenemap.go

3
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()
}

43
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)
}

5
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())
}

4
device/Resolver.go → device/interfaces.go

@ -3,3 +3,7 @@ package device
type Resolver interface {
Resolve(pattern string) []Pointer
}
type SceneMap interface {
SceneID(id string) *int64
}

6
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,

4
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

51
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) {

160
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
}

67
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()
}
}
}
Loading…
Cancel
Save