initial commit (some event stuffs)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
.vscode
|
||||
@@ -0,0 +1,138 @@
|
||||
package lucifer3
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ServiceKey struct{}
|
||||
|
||||
type Event interface {
|
||||
EventName() string
|
||||
}
|
||||
|
||||
type EventBus struct {
|
||||
mu sync.Mutex
|
||||
listeners []*serviceListener
|
||||
signal chan struct{}
|
||||
}
|
||||
|
||||
// JoinCallback joins the event bus for a moment.
|
||||
func (b *EventBus) JoinCallback(cb func(event Event, sender ServiceID) bool) {
|
||||
b.Join(newCallbackService(cb))
|
||||
}
|
||||
|
||||
func (b *EventBus) Join(service Service) {
|
||||
// Take the signal here so that it will receive the events sent by the calling function
|
||||
// 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([]queuedEvent, 0, 16),
|
||||
service: service,
|
||||
}
|
||||
|
||||
go listener.run(signal)
|
||||
|
||||
b.listeners = append(b.listeners, listener)
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *EventBus) Send(event Event, sender ServiceID, recipient *ServiceID) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
deleteList := make([]int, 0, 0)
|
||||
for i, listener := range b.listeners {
|
||||
if !listener.service.Active() {
|
||||
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.mu.Unlock()
|
||||
}
|
||||
|
||||
for i := range deleteList {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (b *EventBus) signalCh() <-chan struct{} {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.signal == nil {
|
||||
b.signal = make(chan struct{})
|
||||
}
|
||||
|
||||
return b.signal
|
||||
}
|
||||
|
||||
type serviceListener struct {
|
||||
mu sync.Mutex
|
||||
bus *EventBus
|
||||
queue []queuedEvent
|
||||
service Service
|
||||
}
|
||||
|
||||
func (l *serviceListener) run(signal <-chan struct{}) {
|
||||
queue := make([]queuedEvent, 0, 16)
|
||||
|
||||
for {
|
||||
// Listen for the signal, but stop here if the service has marked
|
||||
// itself as inactive.
|
||||
<-signal
|
||||
if !l.service.Active() {
|
||||
return
|
||||
}
|
||||
|
||||
// Take a new signal before copying the queue to avoid delay
|
||||
// in case a message is about to be enqueued right now!
|
||||
signal = l.bus.signalCh()
|
||||
|
||||
// Take a copy of the queue.
|
||||
l.mu.Lock()
|
||||
for _, message := range l.queue {
|
||||
queue = append(queue, message)
|
||||
}
|
||||
l.queue = l.queue[:0]
|
||||
l.mu.Unlock()
|
||||
|
||||
// Handle the messages in the queue, but stop mid-queue if the previous message
|
||||
// or external circumstances marked the service inactive.
|
||||
for _, message := range queue {
|
||||
if !l.service.Active() {
|
||||
return
|
||||
}
|
||||
|
||||
l.service.Handle(l.bus, message.event, message.sender)
|
||||
}
|
||||
queue = queue[:0]
|
||||
}
|
||||
}
|
||||
|
||||
type queuedEvent struct {
|
||||
event Event
|
||||
sender ServiceID
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bus := lucifer3.EventBus{}
|
||||
|
||||
bus.JoinCallback(func(event lucifer3.Event, sender lucifer3.ServiceID) 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.")
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
c := color.RGB{Red: 1, Green: 0.7, Blue: 0.25}.ToXY()
|
||||
|
||||
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)
|
||||
|
||||
bus.Send(events.DeviceHardwareStateChange{
|
||||
InternalID: "c68b310c-7fd6-47a4-ae11-d60d701769da",
|
||||
DeviceFlags: device.SFlagPower | device.SFlagIntensity,
|
||||
ColorFlags: 0,
|
||||
State: device.State{
|
||||
Power: gentools.Ptr(true),
|
||||
Intensity: gentools.Ptr(0.75),
|
||||
},
|
||||
}, 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)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package device
|
||||
|
||||
import "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
|
||||
SFlagColor
|
||||
SFlagTemperature
|
||||
SFlagSensorButtons
|
||||
SFlagSensorTemperature
|
||||
SFlagSensorLightLevel
|
||||
SFlagSensorPresence
|
||||
)
|
||||
|
||||
// ColorFlag is primarily to detect warm-white lights, as XY/RGB/HS/HSK can convert without trouble.
|
||||
type ColorFlag uint32
|
||||
|
||||
const (
|
||||
CFlagXY ColorFlag = 1 << iota
|
||||
CFlagRGB
|
||||
CFlagHS
|
||||
CFlagHSK
|
||||
CFlagKelvin
|
||||
)
|
||||
|
||||
func (f ColorFlag) IsColor() bool {
|
||||
return f.HasAny(CFlagXY | CFlagRGB | CFlagHS | CFlagHSK)
|
||||
}
|
||||
|
||||
func (f ColorFlag) IsWarmWhite() bool {
|
||||
return f&CFlagKelvin == CFlagKelvin
|
||||
}
|
||||
|
||||
func (f ColorFlag) IsColorOnly() bool {
|
||||
return f != 0 && f&CFlagKelvin == 0
|
||||
}
|
||||
|
||||
func (f ColorFlag) IsWarmWhiteOnly() bool {
|
||||
return f == CFlagKelvin
|
||||
}
|
||||
|
||||
func (f ColorFlag) HasAny(d ColorFlag) bool {
|
||||
return (f & d) != 0
|
||||
}
|
||||
|
||||
func (f ColorFlag) HasAll(d ColorFlag) bool {
|
||||
return bits.OnesCount32(uint32(f&d)) == bits.OnesCount32(uint32(d))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Power *bool `json:"power"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
Intensity *float64 `json:"intensity"`
|
||||
Color *color.Color `json:"color"`
|
||||
|
||||
SensedTemperature *float64 `json:"sensedTemperature"`
|
||||
SensedLightLevel *float64 `json:"sensedLightLevel"`
|
||||
SensedPresence *PresenceState `json:"sensedPresence"`
|
||||
SensedButton *int `json:"sensedButton"`
|
||||
}
|
||||
|
||||
func (d State) String() string {
|
||||
parts := make([]string, 0, 4)
|
||||
if d.Power != nil {
|
||||
parts = append(parts, fmt.Sprintf("power:%t", *d.Power))
|
||||
}
|
||||
if d.Temperature != nil {
|
||||
parts = append(parts, fmt.Sprintf("temperature:%f", *d.Temperature))
|
||||
}
|
||||
if d.Intensity != nil {
|
||||
parts = append(parts, fmt.Sprintf("intensity:%.2f", *d.Intensity))
|
||||
}
|
||||
if d.Color != nil {
|
||||
parts = append(parts, fmt.Sprintf("color:%s", d.Color.String()))
|
||||
}
|
||||
|
||||
if d.SensedTemperature != nil {
|
||||
parts = append(parts, fmt.Sprintf("sensedTemperature:%.2f", *d.SensedTemperature))
|
||||
}
|
||||
if d.SensedLightLevel != nil {
|
||||
parts = append(parts, fmt.Sprintf("sensedLightLevel:%.1f", *d.SensedLightLevel))
|
||||
}
|
||||
if d.SensedPresence != nil {
|
||||
parts = append(parts, fmt.Sprintf("sensedPresense:(%t,%.1f)", d.SensedPresence.Present, d.SensedPresence.AbsenceSeconds))
|
||||
}
|
||||
if d.SensedButton != nil {
|
||||
parts = append(parts, fmt.Sprintf("sensedButton:%d", *d.SensedButton))
|
||||
}
|
||||
|
||||
return fmt.Sprint("(", strings.Join(parts, ", "), ")")
|
||||
}
|
||||
|
||||
type PresenceState struct {
|
||||
Present bool `json:"present"`
|
||||
AbsenceSeconds float64 `json:"absenceSeconds,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module git.aiterp.net/lucifer3/server
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
@@ -0,0 +1,368 @@
|
||||
package color
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Color struct {
|
||||
RGB *RGB `json:"rgb,omitempty"`
|
||||
HS *HueSat `json:"hs,omitempty"`
|
||||
K *int `json:"k,omitempty"`
|
||||
XY *XY `json:"xy,omitempty"`
|
||||
}
|
||||
|
||||
func (col *Color) IsHueSat() bool {
|
||||
return col.HS != nil
|
||||
}
|
||||
|
||||
func (col *Color) IsHueSatKelvin() bool {
|
||||
return col.HS != nil && col.K != nil
|
||||
}
|
||||
|
||||
func (col *Color) IsKelvin() bool {
|
||||
return col.K != nil
|
||||
}
|
||||
|
||||
func (col *Color) IsEmpty() bool {
|
||||
return *col == Color{}
|
||||
}
|
||||
|
||||
func (col *Color) SetK(k int) {
|
||||
*col = Color{K: &k}
|
||||
}
|
||||
|
||||
func (col *Color) SetXY(xy XY) {
|
||||
*col = Color{XY: &xy}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if col.RGB != nil {
|
||||
rgb := *col.RGB
|
||||
col2 = Color{RGB: &rgb}
|
||||
ok = true
|
||||
} else if col.HS != nil {
|
||||
rgb := col.HS.ToRGB()
|
||||
col2 = Color{RGB: &rgb}
|
||||
ok = true
|
||||
} else if col.XY != nil {
|
||||
rgb := col.XY.ToRGB()
|
||||
col2 = Color{RGB: &rgb}
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (col *Color) ToHS() (col2 Color, ok bool) {
|
||||
if col.HS != nil {
|
||||
hs := *col.HS
|
||||
col2 = Color{HS: &hs}
|
||||
ok = true
|
||||
} else if col.RGB != nil {
|
||||
hs := col.RGB.ToHS()
|
||||
col2 = Color{HS: &hs}
|
||||
ok = true
|
||||
} else if col.XY != nil {
|
||||
hs := col.XY.ToHS()
|
||||
col2 = Color{HS: &hs}
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (col *Color) ToHSK() (col2 Color, ok bool) {
|
||||
k := 4000
|
||||
|
||||
if col.HS != nil {
|
||||
hs := *col.HS
|
||||
col2 = Color{HS: &hs}
|
||||
|
||||
if col.K != nil {
|
||||
k = *col.K
|
||||
}
|
||||
col2.K = &k
|
||||
|
||||
ok = true
|
||||
} else if col.RGB != nil {
|
||||
hs := col.RGB.ToHS()
|
||||
col2 = Color{HS: &hs}
|
||||
col2.K = &k
|
||||
ok = true
|
||||
} else if col.XY != nil {
|
||||
hs := col.XY.ToHS()
|
||||
col2 = Color{HS: &hs}
|
||||
col2.K = &k
|
||||
ok = true
|
||||
} else if col.K != nil {
|
||||
k = *col.K
|
||||
col2.HS = &HueSat{Hue: 0, Sat: 0}
|
||||
col2.K = &k
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ToXY tries to copy the color to an XY color.
|
||||
func (col *Color) ToXY() (col2 Color, ok bool) {
|
||||
if col.XY != nil {
|
||||
xy := *col.XY
|
||||
col2 = Color{XY: &xy}
|
||||
ok = true
|
||||
} else if col.HS != nil {
|
||||
xy := col.HS.ToXY()
|
||||
col2 = Color{XY: &xy}
|
||||
ok = true
|
||||
} else if col.RGB != nil {
|
||||
xy := col.RGB.ToXY()
|
||||
col2 = Color{XY: &xy}
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (col *Color) Interpolate(other Color, fac float64) Color {
|
||||
// Special case for kelvin values.
|
||||
if col.IsKelvin() && other.IsKelvin() {
|
||||
k1 := *col.K
|
||||
k2 := *col.K
|
||||
k3 := k1 + int(float64(k2-k1)*fac)
|
||||
return Color{K: &k3}
|
||||
}
|
||||
|
||||
// Get the colorful values.
|
||||
cvCF := col.colorful()
|
||||
otherCF := other.colorful()
|
||||
|
||||
// Blend and normalize
|
||||
blended := cvCF.BlendLuv(otherCF, fac)
|
||||
blendedHue, blendedSat, _ := blended.Hsv()
|
||||
blendedHs := HueSat{Hue: blendedHue, Sat: blendedSat}
|
||||
|
||||
// Convert to the first's type
|
||||
switch col.Kind() {
|
||||
case "rgb":
|
||||
rgb := blendedHs.ToRGB()
|
||||
return Color{RGB: &rgb}
|
||||
case "xy":
|
||||
xy := blendedHs.ToXY()
|
||||
return Color{XY: &xy}
|
||||
default:
|
||||
return Color{HS: &blendedHs}
|
||||
}
|
||||
}
|
||||
|
||||
func (col *Color) Kind() string {
|
||||
switch {
|
||||
case col.RGB != nil:
|
||||
return "rgb"
|
||||
case col.XY != nil:
|
||||
return "xy"
|
||||
case col.HS != nil && col.K != nil:
|
||||
return "hsk"
|
||||
case col.HS != nil:
|
||||
return "hs"
|
||||
case col.K != nil:
|
||||
return "k"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (col *Color) String() string {
|
||||
switch {
|
||||
case col.RGB != nil:
|
||||
return fmt.Sprintf("rgb:%.3f,%.3f,%.3f", col.RGB.Red, col.RGB.Green, col.RGB.Blue)
|
||||
case col.XY != nil:
|
||||
return fmt.Sprintf("xy:%.4f,%.4f", col.XY.X, col.XY.Y)
|
||||
case col.HS != nil && col.K != nil:
|
||||
return fmt.Sprintf("hsk:%.4f,%.3f,%d", col.HS.Hue, col.HS.Sat, *col.K)
|
||||
case col.HS != nil:
|
||||
return fmt.Sprintf("hs:%.4f,%.3f", col.HS.Hue, col.HS.Sat)
|
||||
case col.K != nil:
|
||||
return fmt.Sprintf("k:%d", *col.K)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (col *Color) colorful() colorful.Color {
|
||||
switch {
|
||||
case col.HS != nil:
|
||||
return colorful.Hsv(col.HS.Hue, col.HS.Sat, 1)
|
||||
case col.RGB != nil:
|
||||
return colorful.Color{R: col.RGB.Red, G: col.RGB.Green, B: col.RGB.Blue}
|
||||
case col.XY != nil:
|
||||
return colorful.Xyy(col.XY.X, col.XY.Y, 0.5)
|
||||
default:
|
||||
return colorful.Color{R: 255, B: 255, G: 255}
|
||||
}
|
||||
}
|
||||
|
||||
func Parse(raw string) (col Color, err error) {
|
||||
if raw == "" {
|
||||
return
|
||||
}
|
||||
|
||||
tokens := strings.SplitN(raw, ":", 2)
|
||||
if len(tokens) != 2 {
|
||||
err = ErrBadColorInput
|
||||
return
|
||||
}
|
||||
|
||||
switch tokens[0] {
|
||||
case "kelvin", "k":
|
||||
{
|
||||
parsedPart, err := strconv.Atoi(tokens[1])
|
||||
if err != nil {
|
||||
err = ErrBadColorInput
|
||||
break
|
||||
}
|
||||
|
||||
col.K = &parsedPart
|
||||
}
|
||||
|
||||
case "xy":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
x, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
y, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
err = ErrBadColorInput
|
||||
break
|
||||
}
|
||||
|
||||
col.XY = &XY{x, y}
|
||||
}
|
||||
|
||||
case "hs":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
err = ErrBadColorInput
|
||||
break
|
||||
}
|
||||
|
||||
col.HS = &HueSat{Hue: part1, Sat: part2}
|
||||
}
|
||||
|
||||
case "hsk":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 3 {
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
part3, err3 := strconv.Atoi(parts[2])
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
err = ErrBadColorInput
|
||||
break
|
||||
}
|
||||
|
||||
col.HS = &HueSat{Hue: part1, Sat: part2}
|
||||
col.K = &part3
|
||||
}
|
||||
|
||||
case "rgb":
|
||||
{
|
||||
if strings.HasPrefix(tokens[1], "#") {
|
||||
hex := tokens[1][1:]
|
||||
if !validHex(hex) {
|
||||
err = ErrBadColorInput
|
||||
break
|
||||
}
|
||||
|
||||
if len(hex) == 6 {
|
||||
col.RGB = &RGB{
|
||||
Red: float64(hex2num(hex[0:2])) / 255.0,
|
||||
Green: float64(hex2num(hex[2:4])) / 255.0,
|
||||
Blue: float64(hex2num(hex[4:6])) / 255.0,
|
||||
}
|
||||
} else if len(hex) == 3 {
|
||||
col.RGB = &RGB{
|
||||
Red: float64(hex2digit(hex[0])) / 15.0,
|
||||
Green: float64(hex2digit(hex[1])) / 15.0,
|
||||
Blue: float64(hex2digit(hex[2])) / 15.0,
|
||||
}
|
||||
} else {
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
} else {
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 3 {
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
part3, err3 := strconv.ParseFloat(parts[2], 64)
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
err = ErrBadColorInput
|
||||
break
|
||||
}
|
||||
|
||||
col.RGB = &RGB{Red: part1, Green: part2, Blue: part3}
|
||||
}
|
||||
|
||||
normalizedRGB := col.RGB.ToHS().ToRGB()
|
||||
col.RGB = &normalizedRGB
|
||||
}
|
||||
|
||||
default:
|
||||
err = ErrUnknownColorFormat
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func validHex(h string) bool {
|
||||
for _, ch := range h {
|
||||
if !((ch >= 'a' && ch <= 'f') || (ch >= '0' || ch <= '9')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func hex2num(s string) int {
|
||||
v := 0
|
||||
for _, h := range s {
|
||||
v *= 16
|
||||
v += hex2digit(byte(h))
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func hex2digit(h byte) int {
|
||||
if h >= 'a' && h <= 'f' {
|
||||
return 10 + int(h-'a')
|
||||
} else {
|
||||
return int(h - '0')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package color
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrBadColorInput = errors.New("bad color input")
|
||||
var ErrUnknownColorFormat = errors.New("bad color format")
|
||||
@@ -0,0 +1,17 @@
|
||||
package color
|
||||
|
||||
import "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
type HueSat struct {
|
||||
Hue float64 `json:"hue"`
|
||||
Sat float64 `json:"sat"`
|
||||
}
|
||||
|
||||
func (hs HueSat) ToXY() XY {
|
||||
return hs.ToRGB().ToXY()
|
||||
}
|
||||
|
||||
func (hs HueSat) ToRGB() RGB {
|
||||
c := colorful.Hsv(hs.Hue, hs.Sat, 1)
|
||||
return RGB{Red: c.R, Green: c.G, Blue: c.B}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package color
|
||||
|
||||
import "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
type RGB struct {
|
||||
Red float64 `json:"red"`
|
||||
Green float64 `json:"green"`
|
||||
Blue float64 `json:"blue"`
|
||||
}
|
||||
|
||||
func (rgb RGB) AtIntensity(intensity float64) RGB {
|
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
|
||||
hsv2 := colorful.Hsv(hue, sat, intensity)
|
||||
return RGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B}
|
||||
}
|
||||
|
||||
func (rgb RGB) ToHS() HueSat {
|
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
|
||||
return HueSat{Hue: hue, Sat: sat}
|
||||
}
|
||||
|
||||
func (rgb RGB) ToXY() XY {
|
||||
x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz()
|
||||
|
||||
return XY{
|
||||
X: x / (x + y + z),
|
||||
Y: y / (x + y + z),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package color
|
||||
|
||||
import (
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"math"
|
||||
)
|
||||
|
||||
const eps = 0.0001
|
||||
const epsSquare = eps * eps
|
||||
|
||||
type Gamut struct {
|
||||
Red XY `json:"red"`
|
||||
Green XY `json:"green"`
|
||||
Blue XY `json:"blue"`
|
||||
}
|
||||
|
||||
func (cg *Gamut) side(x1, y1, x2, y2, x, y float64) float64 {
|
||||
return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1)
|
||||
}
|
||||
|
||||
func (cg *Gamut) naiveContains(color XY) bool {
|
||||
x, y := color.X, color.Y
|
||||
x1, y1 := cg.Red.X, cg.Red.Y
|
||||
x2, y2 := cg.Green.X, cg.Green.Y
|
||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
||||
|
||||
checkSide1 := cg.side(x1, y1, x2, y2, x, y) < 0
|
||||
checkSide2 := cg.side(x2, y2, x3, y3, x, y) < 0
|
||||
checkSide3 := cg.side(x3, y3, x1, y1, x, y) < 0
|
||||
|
||||
return checkSide1 && checkSide2 && checkSide3
|
||||
}
|
||||
|
||||
func (cg *Gamut) getBounds() (xMin, xMax, yMin, yMax float64) {
|
||||
x1, y1 := cg.Red.X, cg.Red.Y
|
||||
x2, y2 := cg.Green.X, cg.Green.Y
|
||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
||||
|
||||
xMin = math.Min(x1, math.Min(x2, x3)) - eps
|
||||
xMax = math.Max(x1, math.Max(x2, x3)) + eps
|
||||
yMin = math.Min(y1, math.Min(y2, y3)) - eps
|
||||
yMax = math.Max(y1, math.Max(y2, y3)) + eps
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (cg *Gamut) isInBounds(color XY) bool {
|
||||
x, y := color.X, color.Y
|
||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
||||
|
||||
return !(x < xMin || xMax < x || y < yMin || yMax < y)
|
||||
}
|
||||
|
||||
func (cg *Gamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) float64 {
|
||||
sqLength1 := (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)
|
||||
dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1
|
||||
if dotProduct < 0 {
|
||||
return (x-x1)*(x-x1) + (y-y1)*(y-y1)
|
||||
} else if dotProduct <= 1 {
|
||||
sqLength2 := (x1-x)*(x1-x) + (y1-y)*(y1-y)
|
||||
return sqLength2 - dotProduct*dotProduct*sqLength1
|
||||
} else {
|
||||
return (x-x2)*(x-x2) + (y-y2)*(y-y2)
|
||||
}
|
||||
}
|
||||
|
||||
func (cg *Gamut) atTheEdge(color XY) bool {
|
||||
x, y := color.X, color.Y
|
||||
x1, y1 := cg.Red.X, cg.Red.Y
|
||||
x2, y2 := cg.Green.X, cg.Green.Y
|
||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
||||
|
||||
if cg.distanceSquarePointToSegment(x1, y1, x2, y2, x, y) <= epsSquare {
|
||||
return true
|
||||
}
|
||||
if cg.distanceSquarePointToSegment(x2, y2, x3, y3, x, y) <= epsSquare {
|
||||
return true
|
||||
}
|
||||
if cg.distanceSquarePointToSegment(x3, y3, x1, y1, x, y) <= epsSquare {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cg *Gamut) Contains(color XY) bool {
|
||||
if cg == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color))
|
||||
}
|
||||
|
||||
func (cg *Gamut) Conform(color XY) XY {
|
||||
if cg.Contains(color) {
|
||||
return color
|
||||
}
|
||||
|
||||
var best *XY
|
||||
|
||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
||||
|
||||
for x := xMin; x < xMax; x += 0.001 {
|
||||
for y := yMin; y < yMax; y += 0.001 {
|
||||
color2 := XY{X: x, Y: y}
|
||||
|
||||
if cg.Contains(color2) {
|
||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
||||
best = &color2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best == nil {
|
||||
centerX := (cg.Red.X + cg.Green.X + cg.Blue.X) / 3
|
||||
centerY := (cg.Red.Y + cg.Green.Y + cg.Blue.Y) / 3
|
||||
|
||||
stepX := (centerX - color.X) / 5000
|
||||
stepY := (centerY - color.Y) / 5000
|
||||
|
||||
for !cg.Contains(color) {
|
||||
color.X += stepX
|
||||
color.Y += stepY
|
||||
}
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
for x := best.X - 0.001; x < best.X+0.001; x += 0.0002 {
|
||||
for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0002 {
|
||||
color2 := XY{X: x, Y: y}
|
||||
|
||||
if cg.atTheEdge(color2) {
|
||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
||||
best = &color2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for x := best.X - 0.0001; x < best.X+0.0001; x += 0.00003 {
|
||||
for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00003 {
|
||||
color2 := XY{X: x, Y: y}
|
||||
|
||||
if cg.atTheEdge(color2) {
|
||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
||||
best = &color2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return *best
|
||||
}
|
||||
|
||||
type XY struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
}
|
||||
|
||||
func (xy XY) ToRGB() RGB {
|
||||
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
||||
c := colorful.Hsv(h, s, 1)
|
||||
return RGB{Red: c.R, Green: c.G, Blue: c.B}
|
||||
}
|
||||
|
||||
func (xy XY) ToHS() HueSat {
|
||||
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
||||
return HueSat{Hue: h, Sat: s}
|
||||
}
|
||||
|
||||
func (xy XY) DistanceTo(other XY) float64 {
|
||||
return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
|
||||
}
|
||||
|
||||
func (xy XY) Round() XY {
|
||||
return XY{
|
||||
X: math.Round(xy.X*10000) / 10000,
|
||||
Y: math.Round(xy.Y*10000) / 10000,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gentools
|
||||
|
||||
import "fmt"
|
||||
|
||||
func FmtSprintArray[T any](arr []T) []string {
|
||||
res := make([]string, len(arr))
|
||||
for i, e := range arr {
|
||||
res[i] = fmt.Sprint(e)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package gentools
|
||||
|
||||
func Ptr[T any](t T) *T {
|
||||
return &t
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package lucifer3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Active() bool
|
||||
ServiceID() ServiceID
|
||||
Handle(bus *EventBus, event Event, sender ServiceID)
|
||||
}
|
||||
|
||||
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 ServiceCommon struct {
|
||||
serviceID ServiceID
|
||||
inactive uint32
|
||||
}
|
||||
|
||||
func (s *ServiceCommon) Active() bool {
|
||||
return atomic.LoadUint32(&s.inactive) == 0
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *callbackService) Handle(_ *EventBus, event Event, sender ServiceID) {
|
||||
if !c.cb(event, sender) {
|
||||
c.markInactive()
|
||||
}
|
||||
}
|
||||
|
||||
var nextCallback int64 = 0
|
||||
|
||||
func newCallbackService(cb func(event Event, sender ServiceID) bool) Service {
|
||||
return &callbackService{
|
||||
ServiceCommon: ServiceCommon{
|
||||
serviceID: ServiceID{
|
||||
Kind: SKCallback,
|
||||
ID: atomic.AddInt64(&nextCallback, 1),
|
||||
},
|
||||
inactive: 0,
|
||||
},
|
||||
cb: cb,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user