diff --git a/config.json b/config.json new file mode 100644 index 0000000..83fe386 --- /dev/null +++ b/config.json @@ -0,0 +1,10 @@ +{ + "ltc_device": "/dev/audio", + "offset": { + "hours": 0, + "minutes": 0, + "seconds": 0, + "milliseconds": 0 + }, + "frame_rate": 25 +} diff --git a/hermione.py b/hermione.py deleted file mode 100644 index 5d3a007..0000000 --- a/hermione.py +++ /dev/null @@ -1,109 +0,0 @@ -ο»Ώimport json -import subprocess -import time -import threading -import os -from datetime import datetime - -CONFIG_FILE = "hermione_config.json" - -# Load config or create default -def load_config(): - if not os.path.exists(CONFIG_FILE): - default_config = { - "framerate": 25, - "start_mode": "system", - "manual_time": "12:00:00", - "duration_seconds": 3600, - "ltc_gen_path": "ltc-gen.exe", - "autostart_timeout": 5 - } - with open(CONFIG_FILE, "w") as f: - json.dump(default_config, f, indent=4) - return default_config - else: - with open(CONFIG_FILE, "r") as f: - return json.load(f) - -# Save updated config -def save_config(config): - with open(CONFIG_FILE, "w") as f: - json.dump(config, f, indent=4) - -# Prompt with timeout -def prompt_with_timeout(prompt, timeout): - print(prompt, end='', flush=True) - input_data = [] - - def get_input(): - try: - input_data.append(input()) - except EOFError: - pass - - thread = threading.Thread(target=get_input) - thread.daemon = True - thread.start() - thread.join(timeout) - return input_data[0] if input_data else "" - -# Get timecode based on config -def get_start_time(config): - if config["start_mode"] == "system": - now = datetime.now() - return now.strftime("%H:%M:%S") - else: - return config["manual_time"] - -# Run ltc-gen -def run_ltc_gen(config): - start_time = get_start_time(config) - framerate = str(config["framerate"]) - duration = str(config["duration_seconds"]) - ltc_gen_path = config["ltc_gen_path"] - - cmd = [ - ltc_gen_path, - "-f", framerate, - "-l", duration, - "-t", start_time - ] - - print(f"\n🎬 Running Hermione with:") - print(f" Start Time: {start_time}") - print(f" Framerate: {framerate} fps") - print(f" Duration: {duration} seconds") - print(f" Executable: {ltc_gen_path}\n") - - try: - subprocess.run(cmd) - except FileNotFoundError: - print(f"❌ Error: {ltc_gen_path} not found!") - except Exception as e: - print(f"❌ Failed to run Hermione: {e}") - -# Main logic -def main(): - config = load_config() - user_input = prompt_with_timeout( - "\nPress [Enter] to run with config or type 'm' to modify (auto-starts in 5s): ", - config.get("autostart_timeout", 5) - ) - - if user_input.lower() == 'm': - try: - config["framerate"] = int(input("Enter framerate (e.g. 25): ")) - config["start_mode"] = input("Start from system time or manual? (system/manual): ").strip().lower() - if config["start_mode"] == "manual": - config["manual_time"] = input("Enter manual start time (HH:MM:SS): ") - config["duration_seconds"] = int(input("Enter duration in seconds: ")) - config["ltc_gen_path"] = input("Enter path to ltc-gen.exe (or leave blank for default): ") or config["ltc_gen_path"] - save_config(config) - except Exception as e: - print(f"⚠️ Error updating config: {e}") - return - - run_ltc_gen(config) - -if __name__ == "__main__": - main() diff --git a/hermione_config.json b/hermione_config.json deleted file mode 100644 index cd64a0a..0000000 --- a/hermione_config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "framerate": 25, - "start_mode": "system", - "manual_time": "12:00:00", - "duration_seconds": 3600, - "ltc_gen_path": "ltc-gen.exe", - "autostart_timeout": 5 -} diff --git a/timeturner.py b/timeturner.py new file mode 100644 index 0000000..2d34ae2 --- /dev/null +++ b/timeturner.py @@ -0,0 +1,116 @@ +ο»Ώ#!/usr/bin/env python3 + +""" +timeturner.py +NTP Timeturner β€” LTC-to-NTP time server for Raspberry Pi +Now with a colourful LTC status light 🌈🟒 +""" + +import os +import sys +import time +import logging +import json +import curses +from datetime import datetime, timedelta + +CONFIG_PATH = "config.json" + +DEFAULT_CONFIG = { + "ltc_device": "/dev/audio", + "offset": { + "hours": 0, + "minutes": 0, + "seconds": 0, + "milliseconds": 0 + }, + "frame_rate": 25 +} + +TIMETURNER_SPINNER = ['β§—', 'β§–', 'β§—', 'β§–', 'πŸ•°'] +HEARTBEAT_PULSE = ['●', 'β—‹'] + +def load_config(path=CONFIG_PATH): + if not os.path.exists(path): + logging.warning("Config file not found, using defaults.") + return DEFAULT_CONFIG + with open(path, "r") as f: + return json.load(f) + +def read_ltc_time(): + return datetime.utcnow() + +def apply_offset(base_time, offset): + delta = timedelta( + hours=offset.get("hours", 0), + minutes=offset.get("minutes", 0), + seconds=offset.get("seconds", 0), + milliseconds=offset.get("milliseconds", 0) + ) + return base_time + delta + +def set_system_time(new_time): + formatted = new_time.strftime('%Y-%m-%d %H:%M:%S') + logging.info(f"Setting system time to: {formatted}") + # os.system(f"sudo timedatectl set-time \"{formatted}\"") + +def draw_dashboard(stdscr, ltc_time, adjusted_time, config, frame): + stdscr.clear() + + # Setup strings + offset = config["offset"] + offset_str = f"{offset['hours']:02}:{offset['minutes']:02}:{offset['seconds']:02}.{offset['milliseconds']:03}" + spinner = TIMETURNER_SPINNER[frame % len(TIMETURNER_SPINNER)] + heartbeat = HEARTBEAT_PULSE[frame % len(HEARTBEAT_PULSE)] + + # Hardcoded LTC status + ltc_status = "LOCKED" + ltc_colour = curses.color_pair(2) # Green + + # Draw + stdscr.addstr(0, 0, f"β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”") + stdscr.addstr(1, 0, f"β”‚ {spinner} NTP Timeturner {spinner} β”‚") + stdscr.addstr(2, 0, f"β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€") + stdscr.addstr(3, 0, f"β”‚ LTC Time: {ltc_time.strftime('%H:%M:%S:%f')[:-3]} β”‚") + stdscr.addstr(4, 0, f"β”‚ Offset Applied: +{offset_str:<17}β”‚") + stdscr.addstr(5, 0, f"β”‚ System Time: {adjusted_time.strftime('%H:%M:%S')} β”‚") + stdscr.addstr(6, 0, f"β”‚ Frame Rate: {config['frame_rate']} fps β”‚") + stdscr.addstr(7, 0, f"β”‚ LTC Status: ") + stdscr.addstr("● ", ltc_colour) + stdscr.addstr(f"{ltc_status:<14}β”‚") + stdscr.addstr(8, 0, f"β”‚ NTP Broadcast: PENDING β”‚") + stdscr.addstr(9, 0, f"β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€") + stdscr.addstr(10, 0, f"β”‚ System Status: {heartbeat} β”‚") + stdscr.addstr(11, 0, f"β”‚ [Ctrl+C to exit] β”‚") + stdscr.addstr(12, 0, f"β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜") + + stdscr.refresh() + +def start_timeturner(stdscr): + curses.curs_set(0) + curses.start_color() + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # Not used yet + curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # LOCKED + curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # UNSTABLE + + stdscr.nodelay(True) + stdscr.timeout(1000) + + config = load_config() + frame = 0 + + while True: + try: + ltc_time = read_ltc_time() + adjusted_time = apply_offset(ltc_time, config["offset"]) + set_system_time(adjusted_time) + draw_dashboard(stdscr, ltc_time, adjusted_time, config, frame) + frame += 1 + except KeyboardInterrupt: + break + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, + format="[%(asctime)s] %(levelname)s: %(message)s") + logging.info("✨ Timeturner console mode started.") + curses.wrapper(start_timeturner)