Gisle Aune
2 years ago
commit
e752b1bafd
17 changed files with 1169 additions and 0 deletions
-
2.gitignore
-
138bus.go
-
77cmd/bustest/main.go
-
59device/flags.go
-
55device/state.go
-
88events/device.go
-
17events/hub.go
-
5go.mod
-
2go.sum
-
368internal/color/color.go
-
6internal/color/errors.go
-
17internal/color/hs.go
-
29internal/color/rgb.go
-
182internal/color/xy.go
-
12internal/gentools/commalist.go
-
5internal/gentools/ptr.go
-
107service.go
@ -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 |
|||
} |
@ -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, |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue