Browse Source

more tinkering.

beelzebub
Gisle Aune 2 years ago
parent
commit
15a4f3289f
  1. 62
      bus.go
  2. 87
      cmd/bustest/main.go
  3. 16
      commands/assign.go
  4. 23
      commands/scene.go
  5. 15
      commands/state.go
  6. 33
      device/flags.go
  7. 32
      effects/manual.go
  8. 20
      events/connection.go
  9. 85
      events/device.go
  10. 17
      events/hub.go
  11. 12
      interface.go
  12. 9
      internal/color/color.go
  13. 51
      internal/formattools/compactidlist.go
  14. 9
      internal/gentools/commalist.go
  15. 18
      internal/gentools/flagstr.go
  16. 77
      service.go

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

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

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

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

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

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

32
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,
}
}

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

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

17
events/hub.go

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

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

9
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

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

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

18
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, "|")
}

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

Loading…
Cancel
Save