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