48
48
# This will be populated if we have the config file
49
49
# url format: mqtt[s]://[<username>[:<password>]@]<host>[:<port>]/<topic_root>
50
50
MQTT_URL = None
51
+ MQTT_TOPIC_ROOT = ''
52
+ MQTT_CLIENT = None
51
53
MQTT_CONFIG_FILE = '/etc/sbot/mqtt.conf'
52
54
53
55
@@ -86,37 +88,84 @@ class Mountpoint(NamedTuple):
86
88
)
87
89
88
90
91
+ class LedStatus (Enum ):
92
+ NoUSB = (False , False , False ) # Off
93
+ Running = (False , False , True ) # Blue
94
+ Killed = (True , False , True ) # Magenta
95
+ Finished = (False , True , False ) # Green
96
+ Crashed = (True , False , False ) # Red
97
+
98
+
89
99
class LEDController ():
90
100
@unique
91
101
class LEDs (IntEnum ):
92
- RED = 2
93
- YELLOW = 3
94
- GREEN = 4
102
+ BOOT_100 = 13
103
+ CODE = 11
104
+ COMP = 16
105
+ WIFI = 8
106
+ STATUS_RED = 26
107
+ STATUS_GREEN = 20
108
+ STATUS_BLUE = 21
95
109
96
110
def __init__ (self ) -> None :
97
111
if IS_PI :
98
112
LOGGER .debug ("Configuring LED controller" )
113
+ self ._register_exit ()
99
114
atexit .register (GPIO .cleanup ) # type: ignore[attr-defined]
100
115
GPIO .setmode (GPIO .BCM )
101
116
GPIO .setup ([led .value for led in self .LEDs ], GPIO .OUT , initial = GPIO .LOW )
102
117
103
- def red (self ) -> None :
118
+ def _register_exit (self ) -> None :
119
+ """
120
+ Ensure `atexit` triggers on `SIGTERM`.
121
+
122
+ > The functions registered via [`atexit`] are not called when the program is
123
+ killed by a signal not handled by Python
124
+ """
125
+
126
+ if signal .getsignal (signal .SIGTERM ) != signal .SIG_DFL :
127
+ # If a signal handler is already present for SIGTERM,
128
+ # this is sufficient for `atexit` to trigger, so do nothing.
129
+ return
130
+
131
+ def handle_signal (handled_signum : int , frame ) -> None :
132
+ """Semi-default signal handler for SIGTERM, enough for atexit."""
133
+ USERCODE_LOGGER .error (signal .strsignal (handled_signum ))
134
+ exit (128 + handled_signum ) # 143 for SIGTERM
135
+
136
+ # Add the null-ish signal handler
137
+ signal .signal (signal .SIGTERM , handle_signal )
138
+
139
+ def mark_start (self ) -> None :
140
+ if IS_PI :
141
+ GPIO .output (self .LEDs .BOOT_100 , GPIO .HIGH )
142
+
143
+ def set_comp (self , value : bool ) -> None :
144
+ if IS_PI :
145
+ GPIO .output (self .LEDs .COMP , GPIO .HIGH if value else GPIO .LOW )
146
+
147
+ def set_code (self , value : bool ) -> None :
104
148
if IS_PI :
105
- GPIO .output (self .LEDs .RED , GPIO .HIGH )
106
- GPIO .output (self .LEDs .YELLOW , GPIO .LOW )
107
- GPIO .output (self .LEDs .GREEN , GPIO .LOW )
149
+ GPIO .output (self .LEDs .CODE , GPIO .HIGH if value else GPIO .LOW )
108
150
109
- def yellow (self ) -> None :
151
+ def set_wifi (self , value : bool ) -> None :
110
152
if IS_PI :
111
- GPIO .output (self .LEDs .RED , GPIO .LOW )
112
- GPIO .output (self .LEDs .YELLOW , GPIO .HIGH )
113
- GPIO .output (self .LEDs .GREEN , GPIO .LOW )
153
+ GPIO .output (self .LEDs .WIFI , GPIO .HIGH if value else GPIO .LOW )
114
154
115
- def green (self ) -> None :
155
+ def set_status (self , value : LedStatus ) -> None :
116
156
if IS_PI :
117
- GPIO .output (self .LEDs .RED , GPIO .LOW )
118
- GPIO .output (self .LEDs .YELLOW , GPIO .LOW )
119
- GPIO .output (self .LEDs .GREEN , GPIO .HIGH )
157
+ GPIO .output (self .LEDs .STATUS_RED , GPIO .HIGH if value .value [0 ] else GPIO .LOW )
158
+ GPIO .output (self .LEDs .STATUS_GREEN , GPIO .HIGH if value .value [1 ] else GPIO .LOW )
159
+ GPIO .output (self .LEDs .STATUS_BLUE , GPIO .HIGH if value .value [2 ] else GPIO .LOW )
160
+
161
+ # Also send the status over MQTT
162
+ if MQTT_CLIENT is not None :
163
+ MQTT_CLIENT .publish (
164
+ f'{ MQTT_TOPIC_ROOT } /state' ,
165
+ json .dumps ({"state" : value .name }),
166
+ qos = 1 ,
167
+ retain = True ,
168
+ )
120
169
121
170
122
171
LED_CONTROLLER = LEDController ()
@@ -198,7 +247,9 @@ def close(self) -> None:
198
247
class RobotUSBHandler (USBHandler ):
199
248
def __init__ (self , mountpoint_path : str ) -> None :
200
249
self ._setup_logging (mountpoint_path )
201
- LED_CONTROLLER .yellow ()
250
+ LED_CONTROLLER .set_code (True )
251
+ LED_CONTROLLER .set_status (LedStatus .Running )
252
+
202
253
env = dict (os .environ )
203
254
env ["SBOT_METADATA_PATH" ] = MOUNTPOINT_DIR
204
255
if MQTT_URL is not None :
@@ -223,15 +274,19 @@ def __init__(self, mountpoint_path: str) -> None:
223
274
target = self ._log_output , args = (self .process .stdout ,))
224
275
self .log_thread .start ()
225
276
226
- def close (self ) -> None :
277
+ def cleanup (self ) -> None :
227
278
self ._send_signal (signal .SIGTERM )
228
279
try :
229
280
# Wait for the process to exit
230
281
self .process .communicate (timeout = 5 )
231
282
except subprocess .TimeoutExpired :
232
283
# The process did not exit after 5 seconds, so kill it.
233
284
self ._send_signal (signal .SIGKILL )
234
- self ._set_leds ()
285
+
286
+ def close (self ) -> None :
287
+ self .cleanup ()
288
+ LED_CONTROLLER .set_status (LedStatus .NoUSB )
289
+ LED_CONTROLLER .set_code (False )
235
290
USERCODE_LOGGER .removeHandler (self .handler )
236
291
237
292
def _send_signal (self , sig : int ) -> None :
@@ -245,8 +300,10 @@ def _watch_process(self) -> None:
245
300
self .process .wait ()
246
301
if self .process .returncode != 0 :
247
302
USERCODE_LOGGER .warning (f"Process exited with code { self .process .returncode } " )
303
+ LED_CONTROLLER .set_status (LedStatus .Crashed )
248
304
else :
249
305
USERCODE_LOGGER .info ("Your code finished successfully." )
306
+ LED_CONTROLLER .set_status (LedStatus .Finished )
250
307
251
308
process_lifetime = time .time () - self .process_start_time
252
309
@@ -256,7 +313,7 @@ def _watch_process(self) -> None:
256
313
time .sleep (1 - process_lifetime )
257
314
258
315
# Start clean-up
259
- self .close ()
316
+ self .cleanup ()
260
317
261
318
def _setup_logging (self , log_dir : str ) -> None :
262
319
self ._rotate_old_logs (log_dir )
@@ -284,12 +341,6 @@ def _log_output(self, pipe: IO[str]) -> None:
284
341
USERCODE_LOGGER .log (USERCODE_LEVEL , line .rstrip ('\n ' ))
285
342
LOGGER .info ('Process output finished' )
286
343
287
- def _set_leds (self ) -> None :
288
- if self .process .returncode == 0 :
289
- LED_CONTROLLER .green ()
290
- else :
291
- LED_CONTROLLER .red ()
292
-
293
344
def _rotate_old_logs (self , log_dir : str ) -> None :
294
345
"""
295
346
Add a suffix to the old log file, if it exists.
@@ -309,10 +360,12 @@ def _rotate_old_logs(self, log_dir: str) -> None:
309
360
310
361
class MetadataUSBHandler (USBHandler ):
311
362
def __init__ (self , mountpoint_path : str ) -> None :
312
- pass # Nothing to do.
363
+ # NOTE the comp LED just represents the presence of a comp mode USB
364
+ # not whether comp mode is enabled
365
+ LED_CONTROLLER .set_comp (True )
313
366
314
367
def close (self ) -> None :
315
- pass # Nothing to do.
368
+ LED_CONTROLLER . set_comp ( False )
316
369
317
370
318
371
class AutorunProcessRegistry (object ):
@@ -414,7 +467,7 @@ def read_mqtt_config_file() -> MQTTVariables | None:
414
467
415
468
416
469
def setup_usercode_logging () -> None :
417
- global REL_TIME_FILTER
470
+ global REL_TIME_FILTER , MQTT_CLIENT , MQTT_TOPIC_ROOT
418
471
REL_TIME_FILTER = RelativeTimeFilter ()
419
472
USERCODE_LOGGER .addFilter (REL_TIME_FILTER )
420
473
USERCODE_LOGGER .setLevel (logging .DEBUG )
@@ -432,7 +485,11 @@ def setup_usercode_logging() -> None:
432
485
username = mqtt_config .username ,
433
486
password = mqtt_config .password ,
434
487
connected_topic = f"{ mqtt_config .topic_prefix } /connected" ,
488
+ connected_callback = lambda : LED_CONTROLLER .set_wifi (True ),
489
+ disconnected_callback = lambda : LED_CONTROLLER .set_wifi (False ),
435
490
)
491
+ MQTT_CLIENT = handler .mqtt
492
+ MQTT_TOPIC_ROOT = mqtt_config .topic_prefix
436
493
437
494
handler .setLevel (logging .INFO )
438
495
handler .setFormatter (TieredFormatter (
@@ -452,6 +509,9 @@ def main():
452
509
453
510
registry = AutorunProcessRegistry ()
454
511
512
+ LED_CONTROLLER .mark_start ()
513
+ LED_CONTROLLER .set_status (LedStatus .NoUSB )
514
+
455
515
# Initial pass (in case an autorun FS is already mounted)
456
516
registry .update_filesystems (fstab_reader .read ())
457
517
0 commit comments