forked from omriharel/deej
-
Notifications
You must be signed in to change notification settings - Fork 1
/
deej.go
208 lines (160 loc) · 5.26 KB
/
deej.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
// Package deej provides a machine-side client that pairs with an Arduino
// chip to form a tactile, physical volume control system/
package deej
import (
"errors"
"fmt"
"os"
"go.uber.org/zap"
"github.com/omriharel/deej/util"
)
const (
// when this is set to anything, deej won't use a tray icon
envNoTray = "DEEJ_NO_TRAY_ICON"
)
// Deej is the main entity managing access to all sub-components
type Deej struct {
logger *zap.SugaredLogger
notifier Notifier
config *CanonicalConfig
serial *SerialIO
sessions *sessionMap
stopChannel chan bool
version string
verbose bool
}
// NewDeej creates a Deej instance
func NewDeej(logger *zap.SugaredLogger, verbose bool) (*Deej, error) {
logger = logger.Named("deej")
notifier, err := NewToastNotifier(logger)
if err != nil {
logger.Errorw("Failed to create ToastNotifier", "error", err)
return nil, fmt.Errorf("create new ToastNotifier: %w", err)
}
config, err := NewConfig(logger, notifier)
if err != nil {
logger.Errorw("Failed to create Config", "error", err)
return nil, fmt.Errorf("create new Config: %w", err)
}
d := &Deej{
logger: logger,
notifier: notifier,
config: config,
stopChannel: make(chan bool),
verbose: verbose,
}
serial, err := NewSerialIO(d, logger)
if err != nil {
logger.Errorw("Failed to create SerialIO", "error", err)
return nil, fmt.Errorf("create new SerialIO: %w", err)
}
d.serial = serial
sessionFinder, err := newSessionFinder(logger)
if err != nil {
logger.Errorw("Failed to create SessionFinder", "error", err)
return nil, fmt.Errorf("create new SessionFinder: %w", err)
}
sessions, err := newSessionMap(d, logger, sessionFinder)
if err != nil {
logger.Errorw("Failed to create sessionMap", "error", err)
return nil, fmt.Errorf("create new sessionMap: %w", err)
}
d.sessions = sessions
logger.Debug("Created deej instance")
return d, nil
}
// Initialize sets up components and starts to run in the background
func (d *Deej) Initialize() error {
d.logger.Debug("Initializing")
// load the config for the first time
if err := d.config.Load(); err != nil {
d.logger.Errorw("Failed to load config during initialization", "error", err)
return fmt.Errorf("load config during init: %w", err)
}
// initialize the session map
if err := d.sessions.initialize(); err != nil {
d.logger.Errorw("Failed to initialize session map", "error", err)
return fmt.Errorf("init session map: %w", err)
}
// decide whether to run with/without tray
if _, noTraySet := os.LookupEnv(envNoTray); noTraySet {
d.logger.Debugw("Running without tray icon", "reason", "envvar set")
// run in main thread while waiting on ctrl+C
d.setupInterruptHandler()
d.run()
} else {
d.setupInterruptHandler()
d.initializeTray(d.run)
}
return nil
}
// SetVersion causes deej to add a version string to its tray menu if called before Initialize
func (d *Deej) SetVersion(version string) {
d.version = version
}
// Verbose returns a boolean indicating whether deej is running in verbose mode
func (d *Deej) Verbose() bool {
return d.verbose
}
func (d *Deej) setupInterruptHandler() {
interruptChannel := util.SetupCloseHandler()
go func() {
signal := <-interruptChannel
d.logger.Debugw("Interrupted", "signal", signal)
d.signalStop()
}()
}
func (d *Deej) run() {
d.logger.Info("Run loop starting")
// watch the config file for changes
go d.config.WatchConfigFileChanges()
// connect to the arduino for the first time
go func() {
if err := d.serial.Start(); err != nil {
d.logger.Warnw("Failed to start first-time serial connection", "error", err)
// If the port is busy, that's because something else is connected - notify and quit
if errors.Is(err, os.ErrPermission) {
d.logger.Warnw("Serial port seems busy, notifying user and closing",
"comPort", d.config.ConnectionInfo.COMPort)
d.notifier.Notify(fmt.Sprintf("Can't connect to %s!", d.config.ConnectionInfo.COMPort),
"This serial port is busy, make sure to close any serial monitor or other deej instance.")
d.signalStop()
// also notify if the COM port they gave isn't found, maybe their config is wrong
} else if errors.Is(err, os.ErrNotExist) {
d.logger.Warnw("Provided COM port seems wrong, notifying user and closing",
"comPort", d.config.ConnectionInfo.COMPort)
d.notifier.Notify(fmt.Sprintf("Can't connect to %s!", d.config.ConnectionInfo.COMPort),
"This serial port doesn't exist, check your configuration and make sure it's set correctly.")
d.signalStop()
}
}
}()
// wait until stopped (gracefully)
<-d.stopChannel
d.logger.Debug("Stop channel signaled, terminating")
if err := d.stop(); err != nil {
d.logger.Warnw("Failed to stop deej", "error", err)
os.Exit(1)
} else {
// exit with 0
os.Exit(0)
}
}
func (d *Deej) signalStop() {
d.logger.Debug("Signalling stop channel")
d.stopChannel <- true
}
func (d *Deej) stop() error {
d.logger.Info("Stopping")
d.config.StopWatchingConfigFile()
d.serial.Stop()
// release the session map
if err := d.sessions.release(); err != nil {
d.logger.Errorw("Failed to release session map", "error", err)
return fmt.Errorf("release session map: %w", err)
}
d.stopTray()
// attempt to sync on exit - this won't necessarily work but can't harm
d.logger.Sync()
return nil
}