diff --git a/bus.go b/bus.go index d1a4fa3..ccc1201 100644 --- a/bus.go +++ b/bus.go @@ -2,7 +2,9 @@ package lucifer3 import ( "fmt" + "strings" "sync" + "sync/atomic" ) type ServiceKey struct{} @@ -28,6 +30,7 @@ type EventBus struct { listeners []*serviceListener privilegedList []ActiveService signal chan struct{} + setStates int32 } // JoinCallback joins the event bus for a moment. @@ -62,7 +65,20 @@ func (b *EventBus) JoinPrivileged(service ActiveService) { } func (b *EventBus) RunCommand(command Command) { - fmt.Println("[COMMAND]", command.CommandDescription()) + if cd := command.CommandDescription(); !strings.HasPrefix(cd, "SetState") { + if setStates := atomic.LoadInt32(&b.setStates); setStates > 0 { + fmt.Println("[INFO]", setStates, "SetState commands hidden.") + atomic.AddInt32(&b.setStates, -setStates) + } + + fmt.Println("[COMMAND]", cd) + } else { + if atomic.AddInt32(&b.setStates, 1) >= 1000 { + fmt.Println("[INFO] 100 SetState commands hidden.") + atomic.AddInt32(&b.setStates, -1000) + } + } + b.send(serviceMessage{command: command}) } diff --git a/cmd/bustest/main.go b/cmd/bustest/main.go index 400e8f8..ebd7ca8 100644 --- a/cmd/bustest/main.go +++ b/cmd/bustest/main.go @@ -27,24 +27,37 @@ func main() { switch event.(type) { case events.DevicesReady: bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:2d0c", Alias: "lucifer:name:Hex 5"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:542f", Alias: "lucifer:name:Hex 4"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:e760", Alias: "lucifer:name:Hex 3"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:207a", Alias: "lucifer:name:Hex 2"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:df9a", Alias: "lucifer:name:Hex 1"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:cdd5", Alias: "lucifer:name:Hex 6"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:4597", Alias: "lucifer:name:Hex 7"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:82cb", Alias: "lucifer:name:Hex 8"}) - bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:09fd", Alias: "lucifer:name:Hex 9"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:542f", Alias: "lucifer:name:Hex 6"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:e760", Alias: "lucifer:name:Hex 7"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:207a", Alias: "lucifer:name:Hex 8"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:df9a", Alias: "lucifer:name:Hex 9"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:cdd5", Alias: "lucifer:name:Hex 4"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:4597", Alias: "lucifer:name:Hex 3"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:82cb", Alias: "lucifer:name:Hex 2"}) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:09fd", Alias: "lucifer:name:Hex 1"}) bus.RunCommand(commands.Assign{ Match: "nanoleaf:10.80.1.14:*", - Effect: effects.Gradient{ + Effect: effects.Pattern{ States: []device.State{ {Power: p(true), Intensity: p(0.3), Color: p(color.MustParse("xy:0.22,0.18"))}, + {Power: p(true), Intensity: p(0.4), Color: p(color.MustParse("xy:0.22,0.18"))}, + {Power: p(true), Intensity: p(0.5), Color: p(color.MustParse("xy:0.22,0.18"))}, + }, + AnimationMS: 200, + }, + }) + + time.Sleep(time.Second * 3) + bus.RunCommand(commands.AddAlias{Match: "nanoleaf:10.80.1.14:*", Alias: "lucifer:tag:Magic Lamps"}) + bus.RunCommand(commands.Assign{ + Match: "lucifer:tag:Magic Lamps", + Effect: effects.Gradient{ + States: []device.State{ {Power: p(true), Intensity: p(0.5), Color: p(color.MustParse("xy:0.22,0.18"))}, + {Power: p(true), Intensity: p(0.8), Color: p(color.MustParse("xy:0.22,0.18"))}, }, Interpolate: true, - AnimationMS: 1000, - Reverse: false, + AnimationMS: 500, }, }) @@ -56,7 +69,7 @@ func main() { bus.RunCommand(commands.ConnectDevice{ ID: "nanoleaf:10.80.1.14", - APIKey: "", + APIKey: "QRj5xcQxAQMQsjK4gvaprdhOwr1sCIcj", }) time.Sleep(time.Hour) diff --git a/commands/state.go b/commands/state.go index 487499c..bc5e0f8 100644 --- a/commands/state.go +++ b/commands/state.go @@ -22,3 +22,9 @@ func (c SetState) Matches(driver string) (sub string, ok bool) { func (c SetState) CommandDescription() string { return fmt.Sprintf("SetState(%s, %s)", c.ID, c.State) } + +type SetStateBatch map[string]device.State + +func (c SetStateBatch) CommandDescription() string { + return fmt.Sprintf("SetStateBatch(%d devices)", len(c)) +} diff --git a/device/state.go b/device/state.go index 5269b2b..4f6b029 100644 --- a/device/state.go +++ b/device/state.go @@ -47,7 +47,7 @@ func (s State) Interpolate(s2 State, f float64) State { } if s.Intensity != nil && s2.Intensity != nil { - newState.Intensity = gentools.Ptr((*s.Intensity * f) + (*s2.Intensity * (1.0 - f))) + newState.Intensity = gentools.Ptr((*s2.Intensity * f) + (*s.Intensity * (1.0 - f))) } else if s.Intensity != nil { newState.Intensity = gentools.ShallowCopy(s.Intensity) } else if s2.Intensity != nil { @@ -55,7 +55,7 @@ func (s State) Interpolate(s2 State, f float64) State { } if s.Temperature != nil && s2.Temperature != nil { - newState.Temperature = gentools.Ptr((*s.Temperature * f) + (*s2.Temperature * (1.0 - f))) + newState.Temperature = gentools.Ptr((*s2.Temperature * f) + (*s.Temperature * (1.0 - f))) } else if s.Temperature != nil { newState.Temperature = gentools.ShallowCopy(s.Temperature) } else if s2.Temperature != nil { diff --git a/effects/gradient.go b/effects/gradient.go index 6d7d930..40c8a50 100644 --- a/effects/gradient.go +++ b/effects/gradient.go @@ -3,7 +3,6 @@ package effects import ( "fmt" "git.aiterp.net/lucifer3/server/device" - "math" "time" ) @@ -14,41 +13,17 @@ type Gradient struct { Interpolate bool `json:"interpolate,omitempty"` } -func (e Gradient) State(index, length, round, _ int) device.State { +func (e Gradient) State(index, length, round int) device.State { if len(e.States) == 0 { return device.State{} } if e.Reverse { - index = length - (index + 1) + round = -round } + walkedIndex := ((length + index) - round) % length - walkedIndex := (index + round) % length - indexFactor := math.Min(float64(walkedIndex)/float64(length-1), 1) - stateIncrement := 1.0 / float64(len(e.States)-1) - - for i := range e.States { - a := float64(i) * stateIncrement - if indexFactor >= a { - si := e.States[i] - sj := e.States[(i+1)%len(e.States)] - f := (indexFactor - a) / stateIncrement - - if f < 0 || f > 1 { - panic(f) - } - - if e.Interpolate { - return si.Interpolate(sj, f) - } else if f < 0.5 { - return si - } else { - return sj - } - } - } - - return e.States[len(e.States)-1] + return gradientState(e.States, e.Interpolate, walkedIndex, length) } func (e Gradient) Frequency() time.Duration { diff --git a/effects/manual.go b/effects/manual.go index c57a97e..45e09a8 100644 --- a/effects/manual.go +++ b/effects/manual.go @@ -15,14 +15,14 @@ type Manual struct { } func (e Manual) EffectDescription() string { - return fmt.Sprintf("Manual%s", e.State(0, 0, 0, 0).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, int) device.State { +func (e Manual) State(int, int, int) device.State { return device.State{ Power: e.Power, Temperature: e.Temperature, diff --git a/effects/pattern.go b/effects/pattern.go index 4bf25ca..0c3f6c9 100644 --- a/effects/pattern.go +++ b/effects/pattern.go @@ -11,7 +11,7 @@ type Pattern struct { AnimationMS int64 `json:"animationMs,omitempty"` } -func (e Pattern) State(index, _, round, _ int) device.State { +func (e Pattern) State(index, _, round int) device.State { if len(e.States) == 0 { return device.State{} } diff --git a/effects/random.go b/effects/random.go index c187881..d5e88e6 100644 --- a/effects/random.go +++ b/effects/random.go @@ -13,36 +13,12 @@ type Random struct { AnimationMS int64 `json:"animationMs,omitempty"` } -func (e Random) State(_, _, _, _ int) device.State { +func (e Random) State(_, _, _ int) device.State { if len(e.States) == 0 { return device.State{} } - indexFactor := rand.Float64() - stateIncrement := 1.0 / float64(len(e.States)-1) - - for i := range e.States { - a := float64(i) * stateIncrement - if indexFactor >= a { - si := e.States[i] - sj := e.States[(i+1)%len(e.States)] - f := (indexFactor - a) / stateIncrement - - if f < 0 || f > 1 { - panic(f) - } - - if e.Interpolate { - return si.Interpolate(sj, f) - } else if f < 0.5 { - return si - } else { - return sj - } - } - } - - return e.States[len(e.States)-1] + return gradientStateFactor(e.States, e.Interpolate, rand.Float64()) } func (e Random) Frequency() time.Duration { diff --git a/effects/utils.go b/effects/utils.go index 1bf2a52..580c4ce 100644 --- a/effects/utils.go +++ b/effects/utils.go @@ -1,7 +1,9 @@ package effects import ( + "fmt" "git.aiterp.net/lucifer3/server/device" + "math" "strings" ) @@ -21,3 +23,41 @@ func statesDescription(states []device.State) string { return sb.String() } + +func gradientState(states []device.State, interpolate bool, index, length int) device.State { + indexFactor := math.Min(float64(index)/float64(length-1), 1) + + return gradientStateFactor(states, interpolate, indexFactor) +} + +func gradientStateFactor(states []device.State, interpolate bool, factor float64) device.State { + var stateIncrement float64 + if interpolate { + stateIncrement = 1.0 / float64(len(states)-1) + } else { + stateIncrement = 1.0 / float64(len(states)) + } + + for i := range states { + a := float64(i) * stateIncrement + b := float64(i+1) * stateIncrement + + if factor >= a && factor < b { + si := states[i] + if !interpolate || i+1 == len(states) { + return si + } + sj := states[i+1] + + f := (factor - a) / stateIncrement + + if f < 0 || f > 1 { + panic(fmt.Sprintf("assert(0 <= f <= 1) f = %.2f (if = %.2f, a = %.2f, si = %.2f)", f, factor, a, stateIncrement)) + } + + return si.Interpolate(sj, f) + } + } + + return states[len(states)-1] +} diff --git a/interface.go b/interface.go index 6745b82..152aac8 100644 --- a/interface.go +++ b/interface.go @@ -10,8 +10,7 @@ type Effect interface { // index: The position of this ID; // len: The total length of the effect's IDs; // 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, len, round, random int) device.State + State(index, len, round int) device.State Frequency() time.Duration EffectDescription() string } diff --git a/services/effectenforcer.go b/services/effectenforcer.go index 4cf1903..69a9ac6 100644 --- a/services/effectenforcer.go +++ b/services/effectenforcer.go @@ -4,7 +4,6 @@ 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" @@ -58,7 +57,7 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3. } s.list = append(s.list, newRun) - // Remove the ids from any old run. + // Switch over the indices. for _, id := range allowedIDs { if oldRun := s.index[id]; oldRun != nil { oldRun.remove(id) @@ -75,11 +74,12 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3. func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) { deleteList := make([]int, 0, 8) - commandQueue := make([]commands.SetState, 0, 32) + batch := make(commands.SetStateBatch, 64) 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 @@ -88,17 +88,13 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) { continue } - r := rand.Int() for j, id := range run.ids { if id == "" { continue } - state := run.effect.State(j, len(run.ids), run.round, r) - commandQueue = append(commandQueue, commands.SetState{ - ID: id, - State: state, - }) + state := run.effect.State(j, len(run.ids), run.round) + batch[id] = state } if freq := run.effect.Frequency(); freq > 0 { @@ -123,12 +119,9 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) { } s.mu.Unlock() - if len(commandQueue) > 0 { - for _, command := range commandQueue { - bus.RunCommand(command) - } - - commandQueue = commandQueue[:0] + if len(batch) > 0 { + bus.RunCommand(batch) + batch = make(commands.SetStateBatch, 64) } } } diff --git a/services/nanoleaf/client.go b/services/nanoleaf/client.go index 3b843c7..02fa467 100644 --- a/services/nanoleaf/client.go +++ b/services/nanoleaf/client.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" 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" @@ -168,45 +169,28 @@ func (b *bridge) URL(resource ...string) string { } func (b *bridge) Update(id string, change device.State) { - b.mu.Lock() - defer b.mu.Unlock() - transitionTime := time.Now().Add(time.Millisecond * 255) + b.mu.Lock() for _, panel := range b.panels { if panel.FullID == id { - if change.Intensity != nil { - panel.Intensity = *change.Intensity - } - - if change.Color != nil { - rgbColor, ok := change.Color.ToRGB() - if !ok { - newColor := [4]byte{255, 255, 255, 255} - if newColor != panel.ColorRGBA { - panel.update(newColor, transitionTime) - } - - continue - } - - rgb := rgbColor.RGB.AtIntensity(panel.Intensity) - red := byte(rgb.Red * 255.0001) - green := byte(rgb.Green * 255.0001) - blue := byte(rgb.Blue * 255.0001) - newColor := [4]byte{red, green, blue, 255} - if newColor != panel.ColorRGBA { - panel.update(newColor, time.Now().Add(time.Millisecond*220)) - } - } + panel.apply(change, transitionTime) + break + } + } + b.mu.Unlock() +} - if change.Power != nil { - panel.On = *change.Power - } +func (b *bridge) UpdateBatch(batch commands.SetStateBatch) { + transitionTime := time.Now().Add(time.Millisecond * 255) - break + b.mu.Lock() + for _, panel := range b.panels { + if change, ok := batch[panel.FullID]; ok { + panel.apply(change, transitionTime) } } + b.mu.Unlock() } func (b *bridge) Run(ctx context.Context, bus *lucifer3.EventBus) error { diff --git a/services/nanoleaf/data.go b/services/nanoleaf/data.go index 3c04e9e..f903065 100644 --- a/services/nanoleaf/data.go +++ b/services/nanoleaf/data.go @@ -2,6 +2,7 @@ package nanoleaf import ( "encoding/binary" + "git.aiterp.net/lucifer3/server/device" "time" ) @@ -208,6 +209,42 @@ func (p *panel) update(colorRGBA [4]byte, transitionAt time.Time) { p.TransitionAt = transitionAt } +func (p *panel) apply(change device.State, transitionTime time.Time) { + if change.Power != nil { + p.On = *change.Power + } + + if change.Intensity != nil { + p.Intensity = *change.Intensity + } + + if change.Color != nil { + if !p.On { + newColor := [4]byte{0, 0, 0, 0} + if newColor != p.ColorRGBA { + p.update(newColor, transitionTime) + } + } + + rgbColor, ok := change.Color.ToRGB() + if !ok { + newColor := [4]byte{255, 255, 255, 255} + if newColor != p.ColorRGBA { + p.update(newColor, transitionTime) + } + } + + rgb := rgbColor.RGB.AtIntensity(p.Intensity) + red := byte(rgb.Red * 255.0001) + green := byte(rgb.Green * 255.0001) + blue := byte(rgb.Blue * 255.0001) + newColor := [4]byte{red, green, blue, 255} + if newColor != p.ColorRGBA { + p.update(newColor, transitionTime) + } + } +} + type panelUpdate []byte func (u *panelUpdate) Add(message [8]byte) { diff --git a/services/nanoleaf/service.go b/services/nanoleaf/service.go index 12cad71..128ab17 100644 --- a/services/nanoleaf/service.go +++ b/services/nanoleaf/service.go @@ -35,6 +35,11 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command s.bridges[sub].Update(command.ID, command.State) } + case commands.SetStateBatch: + for _, b := range s.bridges { + b.UpdateBatch(command) + } + case commands.SearchDevices: if sub, ok := command.Matches("nanoleaf"); ok { if s.bridges[sub] != nil {