-
Notifications
You must be signed in to change notification settings - Fork 1
/
streamdeck.go
217 lines (190 loc) · 6.99 KB
/
streamdeck.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//
// Copyright (c) 2023 Matthew Penner
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package streamdeck
import (
"context"
"image"
"sync"
"sync/atomic"
)
// StreamDeck represents an Elgato Stream Deck.
type StreamDeck struct {
// device is a wrapper of the underlying USB HID Device.
device *Device
// brightness is the Stream Deck's target brightness. brightness is not
// always guaranteed to be the Stream Deck's current brightness, like if
// the Stream Deck is sleeping for example.
brightness atomic.Uint32
// isSleeping determines if the Stream Deck is sleeping. Sleep mode turns
// off the display on the Stream Deck by setting the brightness to 0 and
// disables the button press handler. If a button is pressed while the
// Stream Deck is in sleep mode, the screen will be reset to the brightness
// it was at before sleep mode was activated, and the button press will NOT
// be propagated. Once the Stream Deck is no longer sleeping mode, button
// presses will continue functioning.
isSleeping atomic.Bool
// cancel is used to cancel the button press and callback goroutines.
cancel context.CancelFunc
// ch is the internal channel used to receive button press events.
ch chan int
// pressHandlerMx is a mutex used to protect the pressHandler field.
pressHandlerMx sync.Mutex
// pressHandler is the callback that is called whenever a button is pressed.
pressHandler func(context.Context, int) error
}
// New opens a connection to a Stream Deck and provides a user-friendly wrapper
// that makes interacting with the Stream Deck easier and more convenient.
func New(ctx context.Context) (*StreamDeck, error) {
device, err := Open(ctx)
if err != nil {
return nil, err
}
if device == nil {
return nil, err
}
return NewFromDevice(ctx, device)
}
// NewFromDevice creates a new Stream Deck from an existing Device, most users
// should use the New function instead.
//
// This function can be useful if you have a specific USB device you want to use
// like if you want to connect to multiple Stream Decks or use a specific device
// that is not auto-detected correctly.
func NewFromDevice(ctx context.Context, device *Device) (*StreamDeck, error) {
ctx, cancel := context.WithCancel(ctx)
s := &StreamDeck{
device: device,
cancel: cancel,
ch: make(chan int),
}
// TODO: is this always wanted?
s.brightness.Store(uint32(BrightnessFull))
go s.device.buttonPressListener(ctx, s.ch)
go s.buttonCallbackListener(ctx)
return s, nil
}
// Close stops the event listeners and closes the underlying connection to the
// Stream Deck device.
func (s *StreamDeck) Close(ctx context.Context) error {
s.cancel()
return s.device.Close(ctx)
}
// Device returns the underlying Stream Deck device.
func (s *StreamDeck) Device() *Device {
return s.device
}
// Brightness returns the target brightness of the Stream Deck. This will not
// return 0 if the Stream Deck is sleeping. To check if the Stream Deck is
// sleeping use StreamDeck#IsSleeping().
func (s *StreamDeck) Brightness() uint8 {
return uint8(s.brightness.Load())
}
// SetBrightness sets the brightness of the Stream Deck.
func (s *StreamDeck) SetBrightness(ctx context.Context, brightness uint8) error {
if brightness < BrightnessMin {
brightness = BrightnessMin
}
if brightness > BrightnessFull {
brightness = BrightnessFull
}
// Only update the Stream Deck's actual brightness if it isn't sleeping.
if !s.IsSleeping() {
if err := s.setBrightness(ctx, brightness); err != nil {
return err
}
}
// Always persist the new target brightness.
s.brightness.Store(uint32(brightness))
return nil
}
// setBrightness sets the brightness of the Stream Deck.
func (s *StreamDeck) setBrightness(ctx context.Context, brightness uint8) error {
if err := s.device.SetBrightness(ctx, brightness); err != nil {
return err
}
return nil
}
// IsSleeping returns true if the Stream Deck is currently sleeping.
func (s *StreamDeck) IsSleeping() bool {
return s.isSleeping.Load()
}
// SetSleeping sets whether the Stream Deck is sleeping or not.
func (s *StreamDeck) SetSleeping(ctx context.Context, sleeping bool) error {
newBrightness := s.Brightness()
if sleeping {
newBrightness = BrightnessMin
}
if err := s.setBrightness(ctx, newBrightness); err != nil {
return err
}
// Update the isSleeping state only after successfully changing the
// Stream Deck's brightness.
s.isSleeping.Store(sleeping)
return nil
}
// ToggleSleep toggles the sleep state for the Stream Deck.
func (s *StreamDeck) ToggleSleep(ctx context.Context) (bool, error) {
if err := s.SetSleeping(ctx, !s.IsSleeping()); err != nil {
return false, err
}
return s.IsSleeping(), nil
}
// SetHandler sets the button press handler used by the end-user to handle press
// events.
func (s *StreamDeck) SetHandler(fn func(context.Context, int) error) {
s.pressHandlerMx.Lock()
defer s.pressHandlerMx.Unlock()
s.pressHandler = fn
}
// ProcessImage processes an image to be used with the Stream Deck.
func (s *StreamDeck) ProcessImage(img image.Image) ([]byte, error) {
return s.device.EncodeImage(img)
}
// buttonCallbackListener listens for events to be sent over the StreamDeck#ch
// channel and calls StreamDeck#pressHandler with the data.
func (s *StreamDeck) buttonCallbackListener(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case index := <-s.ch:
s.pressHandlerMx.Lock()
pressHandler := s.pressHandler
s.pressHandlerMx.Unlock()
// Disable sleep whenever a button is pressed, another button press
// is required to trigger the underlying press handler.
if s.IsSleeping() {
// TODO: clients may use a inactivity timeout to toggle sleep,
// we may want to send an event when sleep is disabled or
// handle the inactivity timeout in this library natively.
// TODO: we should probably do something about this error.
_ = s.SetSleeping(ctx, false)
continue
}
if pressHandler == nil {
continue
}
// TODO: we should probably do something about this error.
_ = pressHandler(ctx, index)
}
}
}