diff --git a/timeturner.py b/timeturner.py index 76b63b8..403f3aa 100644 --- a/timeturner.py +++ b/timeturner.py @@ -1,34 +1,48 @@ #!/usr/bin/env python3 -import curses +import os import subprocess -import shutil +import curses import time +import shutil import select +import signal -def start_ltc_stream(): - # Launch arecord piped into ltcdump - arecord = subprocess.Popen( - ["arecord", "-f", "S16_LE", "-c", "1", "-r", "48000", "-D", "hw:2,0"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL - ) - ltcdump = subprocess.Popen( - ["ltcdump", "-f", "-"], - stdin=arecord.stdout, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - bufsize=1 - ) - arecord.stdout.close() # Let ltcdump consume the pipe - return arecord, ltcdump +FIFO_PATH = "/tmp/ltcpipe" + +def setup_fifo(): + if os.path.exists(FIFO_PATH): + os.remove(FIFO_PATH) + os.mkfifo(FIFO_PATH) + +def start_arecord(): + return subprocess.Popen([ + "arecord", "-f", "S16_LE", "-c", "1", "-r", "48000", "-D", "hw:2,0", FIFO_PATH + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +def start_ltcdump(): + return subprocess.Popen([ + "ltcdump", "-f", FIFO_PATH + ], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1) + +def clean_up_processes(*procs): + for proc in procs: + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=1) + except subprocess.TimeoutExpired: + proc.kill() + if os.path.exists(FIFO_PATH): + os.remove(FIFO_PATH) def main(stdscr): curses.curs_set(0) stdscr.nodelay(True) - arecord_proc, ltcdump_proc = start_ltc_stream() + setup_fifo() + arecord_proc = start_arecord() + ltcdump_proc = start_ltcdump() latest_tc = "⌛ Waiting for LTC..." last_update = time.time() @@ -39,35 +53,32 @@ def main(stdscr): rlist, _, _ = select.select([ltcdump_proc.stdout], [], [], 0) if rlist: line = ltcdump_proc.stdout.readline() - if line: - line = line.strip() - if line and line[0].isdigit(): - latest_tc = line - last_update = time.time() + if line and line[0].isdigit(): + latest_tc = line.strip() + last_update = time.time() - # Timeout / error detection + # If stream stalls or breaks if time.time() - last_update > 1: if ltcdump_proc.poll() is not None or arecord_proc.poll() is not None: - latest_tc = "💥 Decoder crashed or stream stopped" + latest_tc = "💥 Stream stopped or decoder crashed" else: - latest_tc = "⚠️ No LTC signal" + latest_tc = "⚠️ No LTC signal detected" - # Draw the curses UI + # Draw interface stdscr.erase() stdscr.addstr(1, 2, "🌀 NTP Timeturner Status") - stdscr.addstr(3, 4, "Streaming LTC from hw:2,0...") + stdscr.addstr(3, 4, f"Reading LTC from hw:2,0 via FIFO...") stdscr.addstr(5, 6, f"🕰️ LTC Timecode: {latest_tc}") stdscr.refresh() - time.sleep(0.04) # ~25 FPS + time.sleep(0.04) # 25Hz refresh except KeyboardInterrupt: - stdscr.addstr(8, 6, "🔚 Shutting down...") + stdscr.addstr(8, 6, "🔚 Exiting gracefully...") stdscr.refresh() time.sleep(1) finally: - arecord_proc.terminate() - ltcdump_proc.terminate() + clean_up_processes(arecord_proc, ltcdump_proc) if __name__ == "__main__": if not shutil.which("ltcdump") or not shutil.which("arecord"):