Browse Source

initial commit (some event stuffs)

beelzebub
Gisle Aune 2 years ago
commit
e752b1bafd
  1. 2
      .gitignore
  2. 138
      bus.go
  3. 77
      cmd/bustest/main.go
  4. 59
      device/flags.go
  5. 55
      device/state.go
  6. 88
      events/device.go
  7. 17
      events/hub.go
  8. 5
      go.mod
  9. 2
      go.sum
  10. 368
      internal/color/color.go
  11. 6
      internal/color/errors.go
  12. 17
      internal/color/hs.go
  13. 29
      internal/color/rgb.go
  14. 182
      internal/color/xy.go
  15. 12
      internal/gentools/commalist.go
  16. 5
      internal/gentools/ptr.go
  17. 107
      service.go

2
.gitignore

@ -0,0 +1,2 @@
.idea
.vscode

138
bus.go

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

77
cmd/bustest/main.go

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

59
device/flags.go

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

55
device/state.go

@ -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"`
}

88
events/device.go

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

17
events/hub.go

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

5
go.mod

@ -0,0 +1,5 @@
module git.aiterp.net/lucifer3/server
go 1.19
require github.com/lucasb-eyer/go-colorful v1.2.0

2
go.sum

@ -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=

368
internal/color/color.go

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

6
internal/color/errors.go

@ -0,0 +1,6 @@
package color
import "errors"
var ErrBadColorInput = errors.New("bad color input")
var ErrUnknownColorFormat = errors.New("bad color format")

17
internal/color/hs.go

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

29
internal/color/rgb.go

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

182
internal/color/xy.go

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

12
internal/gentools/commalist.go

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

5
internal/gentools/ptr.go

@ -0,0 +1,5 @@
package gentools
func Ptr[T any](t T) *T {
return &t
}

107
service.go

@ -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,
}
}
Loading…
Cancel
Save