Skip to content

Commit f911fc6

Browse files
authored
Merge pull request #76 from sourcebots/kch-support
Add support for KCH leds
2 parents 62baac0 + 2f36840 commit f911fc6

File tree

3 files changed

+92
-32
lines changed

3 files changed

+92
-32
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ jobs:
1010
MYPY: venv/bin/mypy
1111
steps:
1212
- uses: actions/checkout@v3
13-
- name: Set up Python 3.7
13+
- name: Set up Python 3.8
1414
uses: actions/setup-python@v3
1515
with:
16-
python-version: 3.7
16+
python-version: 3.8
1717
- name: Set up virtualenv
1818
run: python -m venv venv
1919
- name: Install dependencies

runusb/__main__.py

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
# This will be populated if we have the config file
4949
# url format: mqtt[s]://[<username>[:<password>]@]<host>[:<port>]/<topic_root>
5050
MQTT_URL = None
51+
MQTT_TOPIC_ROOT = ''
52+
MQTT_CLIENT = None
5153
MQTT_CONFIG_FILE = '/etc/sbot/mqtt.conf'
5254

5355

@@ -86,37 +88,84 @@ class Mountpoint(NamedTuple):
8688
)
8789

8890

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+
8999
class LEDController():
90100
@unique
91101
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
95109

96110
def __init__(self) -> None:
97111
if IS_PI:
98112
LOGGER.debug("Configuring LED controller")
113+
self._register_exit()
99114
atexit.register(GPIO.cleanup) # type: ignore[attr-defined]
100115
GPIO.setmode(GPIO.BCM)
101116
GPIO.setup([led.value for led in self.LEDs], GPIO.OUT, initial=GPIO.LOW)
102117

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:
104148
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)
108150

109-
def yellow(self) -> None:
151+
def set_wifi(self, value: bool) -> None:
110152
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)
114154

115-
def green(self) -> None:
155+
def set_status(self, value: LedStatus) -> None:
116156
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+
)
120169

121170

122171
LED_CONTROLLER = LEDController()
@@ -198,7 +247,9 @@ def close(self) -> None:
198247
class RobotUSBHandler(USBHandler):
199248
def __init__(self, mountpoint_path: str) -> None:
200249
self._setup_logging(mountpoint_path)
201-
LED_CONTROLLER.yellow()
250+
LED_CONTROLLER.set_code(True)
251+
LED_CONTROLLER.set_status(LedStatus.Running)
252+
202253
env = dict(os.environ)
203254
env["SBOT_METADATA_PATH"] = MOUNTPOINT_DIR
204255
if MQTT_URL is not None:
@@ -223,15 +274,19 @@ def __init__(self, mountpoint_path: str) -> None:
223274
target=self._log_output, args=(self.process.stdout,))
224275
self.log_thread.start()
225276

226-
def close(self) -> None:
277+
def cleanup(self) -> None:
227278
self._send_signal(signal.SIGTERM)
228279
try:
229280
# Wait for the process to exit
230281
self.process.communicate(timeout=5)
231282
except subprocess.TimeoutExpired:
232283
# The process did not exit after 5 seconds, so kill it.
233284
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)
235290
USERCODE_LOGGER.removeHandler(self.handler)
236291

237292
def _send_signal(self, sig: int) -> None:
@@ -245,8 +300,10 @@ def _watch_process(self) -> None:
245300
self.process.wait()
246301
if self.process.returncode != 0:
247302
USERCODE_LOGGER.warning(f"Process exited with code {self.process.returncode}")
303+
LED_CONTROLLER.set_status(LedStatus.Crashed)
248304
else:
249305
USERCODE_LOGGER.info("Your code finished successfully.")
306+
LED_CONTROLLER.set_status(LedStatus.Finished)
250307

251308
process_lifetime = time.time() - self.process_start_time
252309

@@ -256,7 +313,7 @@ def _watch_process(self) -> None:
256313
time.sleep(1 - process_lifetime)
257314

258315
# Start clean-up
259-
self.close()
316+
self.cleanup()
260317

261318
def _setup_logging(self, log_dir: str) -> None:
262319
self._rotate_old_logs(log_dir)
@@ -284,12 +341,6 @@ def _log_output(self, pipe: IO[str]) -> None:
284341
USERCODE_LOGGER.log(USERCODE_LEVEL, line.rstrip('\n'))
285342
LOGGER.info('Process output finished')
286343

287-
def _set_leds(self) -> None:
288-
if self.process.returncode == 0:
289-
LED_CONTROLLER.green()
290-
else:
291-
LED_CONTROLLER.red()
292-
293344
def _rotate_old_logs(self, log_dir: str) -> None:
294345
"""
295346
Add a suffix to the old log file, if it exists.
@@ -309,10 +360,12 @@ def _rotate_old_logs(self, log_dir: str) -> None:
309360

310361
class MetadataUSBHandler(USBHandler):
311362
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)
313366

314367
def close(self) -> None:
315-
pass # Nothing to do.
368+
LED_CONTROLLER.set_comp(False)
316369

317370

318371
class AutorunProcessRegistry(object):
@@ -414,7 +467,7 @@ def read_mqtt_config_file() -> MQTTVariables | None:
414467

415468

416469
def setup_usercode_logging() -> None:
417-
global REL_TIME_FILTER
470+
global REL_TIME_FILTER, MQTT_CLIENT, MQTT_TOPIC_ROOT
418471
REL_TIME_FILTER = RelativeTimeFilter()
419472
USERCODE_LOGGER.addFilter(REL_TIME_FILTER)
420473
USERCODE_LOGGER.setLevel(logging.DEBUG)
@@ -432,7 +485,11 @@ def setup_usercode_logging() -> None:
432485
username=mqtt_config.username,
433486
password=mqtt_config.password,
434487
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),
435490
)
491+
MQTT_CLIENT = handler.mqtt
492+
MQTT_TOPIC_ROOT = mqtt_config.topic_prefix
436493

437494
handler.setLevel(logging.INFO)
438495
handler.setFormatter(TieredFormatter(
@@ -452,6 +509,9 @@ def main():
452509

453510
registry = AutorunProcessRegistry()
454511

512+
LED_CONTROLLER.mark_start()
513+
LED_CONTROLLER.set_status(LedStatus.NoUSB)
514+
455515
# Initial pass (in case an autorun FS is already mounted)
456516
registry.update_filesystems(fstab_reader.read())
457517

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ url = https://github.com/sourcebots/runusb
1111
python_requires = >=3.8
1212
packages = find:
1313
install_requires =
14-
logger-extras==0.4.0
14+
logger-extras==0.4.1
1515
rpi.GPIO==0.7.1
1616

1717
[options.extras_require]
18-
mqtt = logger-extras[mqtt]==0.4.0
18+
mqtt = logger-extras[mqtt]==0.4.1
1919

2020
[options.entry_points]
2121
console_scripts =

0 commit comments

Comments
 (0)