diff --git a/countdown.html b/countdown.html index cd6f0f8..c4176c7 100644 --- a/countdown.html +++ b/countdown.html @@ -1,46 +1,27 @@ - - - - - - - -
Mission
-
T-00:00:00
- - - - - \ No newline at end of file + + + + + + + +
Starship Flight 12
+
T-00:00:00
+ + \ No newline at end of file diff --git a/experiment.py b/experiment.py new file mode 100644 index 0000000..f5317e9 --- /dev/null +++ b/experiment.py @@ -0,0 +1,359 @@ +import tkinter as tk +import time +from datetime import datetime +import requests +import csv +import io + +COUNTDOWN_HTML = "countdown.html" +GONOGO_HTML = "gonogo.html" +SHEET_LINK = "https://docs.google.com/spreadsheets/d/1UPJTW8vH2mgEzispjg_Y_zSqYTFaLoxuoZnqleVlSZ0/export?format=csv&gid=855477916" +session = requests.Session() + +# ------------------------- +# Fetch Go/No-Go Data +# ------------------------- +def fetch_gonogo(): + """Fetch Go/No-Go parameters from L2, L3, L4 (rows 2,3,4; col 12)""" + try: + resp = session.get(SHEET_LINK, timeout=2) # timeout for faster failure if network is slow + resp.raise_for_status() + reader = csv.reader(io.StringIO(resp.text)) + data = list(reader) + gonogo = [] + for i in [1, 2, 3]: + value = data[i][11] if len(data[i]) > 11 else "N/A" + gonogo.append(value.strip().upper()) # <-- always uppercase + return gonogo + except Exception as e: + print(f"[ERROR] Failed to fetch Go/No-Go: {e}") + return ["ERROR"] * 3 + +# ------------------------- +# Write Countdown HTML +# ------------------------- +def write_countdown_html(mission_name, timer_text): + html = f""" + + + + + + + +
{mission_name}
+
{timer_text}
+ +""" + with open(COUNTDOWN_HTML, "w", encoding="utf-8") as f: + f.write(html) + +# ------------------------- +# Write Go/No-Go HTML +# ------------------------- +def write_gonogo_html(gonogo_values=None): + if gonogo_values is None: + gonogo_values = ["N/A", "N/A", "N/A"] + html = f""" + + + + + + + +
+
Range: {gonogo_values[0]}
+
Vehicle: {gonogo_values[2]}
+
Weather: {gonogo_values[1]}
+
+ +""" + with open(GONOGO_HTML, "w", encoding="utf-8") as f: + f.write(html) + +# ------------------------- +# Countdown App +# ------------------------- +class CountdownApp: + def __init__(self, root): + self.root = root + self.root.title("RocketLaunchCountdown") + self.root.config(bg="black") + self.root.attributes("-topmost", True) + + # State + self.running = False + self.on_hold = False + self.scrubbed = False + self.counting_up = False + self.target_time = None + self.hold_start_time = None + self.remaining_time = 0 + self.mission_name = "Placeholder Mission" + # fetch_gonogo() returns [Range, Weather, Vehicle] to match gonogo.html writer + self.gonogo_values = fetch_gonogo() + self.last_gonogo_update = time.time() + + # Display + self.text = tk.Label(root, text="T-00:00:00", font=("Consolas", 80, "bold"), fg="white", bg="black") + self.text.pack(padx=50, pady=20) + + # Mission name input + frame_top = tk.Frame(root, bg="black") + frame_top.pack(pady=5) + tk.Label(frame_top, text="Mission Name:", fg="white", bg="black").pack(side="left") + self.mission_entry = tk.Entry(frame_top, width=20, font=("Arial", 18)) + self.mission_entry.insert(0, self.mission_name) + self.mission_entry.pack(side="left") + + # Mode toggle + frame_mode = tk.Frame(root, bg="black") + frame_mode.pack(pady=5) + self.mode_var = tk.StringVar(value="duration") + tk.Radiobutton(frame_mode, text="Duration", variable=self.mode_var, value="duration", + fg="white", bg="black", command=self.update_inputs).pack(side="left", padx=5) + tk.Radiobutton(frame_mode, text="Clock Time", variable=self.mode_var, value="clock", + fg="white", bg="black", command=self.update_inputs).pack(side="left", padx=5) + + # Duration inputs + frame_duration = tk.Frame(root, bg="black") + frame_duration.pack(pady=5) + tk.Label(frame_duration, text="H", fg="white", bg="black").pack(side="left") + self.hours_entry = tk.Entry(frame_duration, width=3, font=("Arial", 18)) + self.hours_entry.insert(0, "0") + self.hours_entry.pack(side="left", padx=2) + tk.Label(frame_duration, text="M", fg="white", bg="black").pack(side="left") + self.minutes_entry = tk.Entry(frame_duration, width=3, font=("Arial", 18)) + self.minutes_entry.insert(0, "5") + self.minutes_entry.pack(side="left", padx=2) + tk.Label(frame_duration, text="S", fg="white", bg="black").pack(side="left") + self.seconds_entry = tk.Entry(frame_duration, width=3, font=("Arial", 18)) + self.seconds_entry.insert(0, "0") + self.seconds_entry.pack(side="left", padx=2) + + # Clock time input + frame_clock = tk.Frame(root, bg="black") + frame_clock.pack(pady=5) + tk.Label(frame_clock, text="HH:MM", fg="white", bg="black").pack(side="left") + self.clock_entry = tk.Entry(frame_clock, width=7, font=("Arial", 18)) + self.clock_entry.insert(0, "14:00") + self.clock_entry.pack(side="left", padx=5) + + # Control buttons + frame_buttons = tk.Frame(root, bg="black") + frame_buttons.pack(pady=10) + + self.start_btn = tk.Button(frame_buttons, text="▶ Start", command=self.start, font=("Arial", 14)) + self.start_btn.grid(row=0, column=0, padx=5) + + # Hold and resume share the same position + self.hold_btn = tk.Button(frame_buttons, text="⏸ Hold", command=self.hold, font=("Arial", 14)) + self.hold_btn.grid(row=0, column=1, padx=5) + + self.resume_btn = tk.Button(frame_buttons, text="⏵ Resume", command=self.resume, font=("Arial", 14)) + self.resume_btn.grid(row=0, column=1, padx=5) + self.resume_btn.grid_remove() # hidden at start + + self.scrub_btn = tk.Button(frame_buttons, text="🚫 Scrub", command=self.scrub, font=("Arial", 14), fg="red") + self.scrub_btn.grid(row=0, column=2, padx=5) + + self.reset_btn = tk.Button(frame_buttons, text="⟳ Reset", command=self.reset, font=("Arial", 14)) + self.reset_btn.grid(row=0, column=3, padx=5) + + frame_gn = tk.Frame(root, bg="black") + frame_gn.pack(pady=10) + # Labels displayed: Range, Weather, Vehicle — match write_gonogo_html ordering + self.range_label = tk.Label(frame_gn, text="RANGE: N/A", font=("Consolas", 20), fg="white", bg="black") + self.range_label.pack() + self.weather_label = tk.Label(frame_gn, text="WEATHER: N/A", font=("Consolas", 20), fg="white", bg="black") + self.weather_label.pack() + self.vehicle_label = tk.Label(frame_gn, text="VEHICLE: N/A", font=("Consolas", 20), fg="white", bg="black") + self.vehicle_label.pack() + + + self.update_inputs() + self.update_clock() + + # ---------------------------- + # Update input visibility based on mode + # ---------------------------- + def update_inputs(self): + if self.mode_var.get() == "duration": + self.hours_entry.config(state="normal") + self.minutes_entry.config(state="normal") + self.seconds_entry.config(state="normal") + self.clock_entry.config(state="disabled") + else: + self.hours_entry.config(state="disabled") + self.minutes_entry.config(state="disabled") + self.seconds_entry.config(state="disabled") + self.clock_entry.config(state="normal") + + # ---------------------------- + # Control logic + # ---------------------------- + def start(self): + self.mission_name = self.mission_entry.get().strip() or "Placeholder Mission" + self.running = True + self.on_hold = False + self.scrubbed = False + self.counting_up = False + self.show_hold_button() + + try: + if self.mode_var.get() == "duration": + h = int(self.hours_entry.get()) + m = int(self.minutes_entry.get()) + s = int(self.seconds_entry.get()) + total_seconds = h * 3600 + m * 60 + s + else: + now = datetime.now() + parts = [int(p) for p in self.clock_entry.get().split(":")] + h, m = parts[0], parts[1] + s = parts[2] if len(parts) == 3 else 0 + target_today = now.replace(hour=h, minute=m, second=s, microsecond=0) + if target_today < now: + target_today = target_today.replace(day=now.day + 1) + total_seconds = (target_today - now).total_seconds() + except Exception: + self.text.config(text="Invalid time") + write_countdown_html(self.mission_name, "Invalid time") + return + + self.target_time = time.time() + total_seconds + self.remaining_time = total_seconds + + def hold(self): + if self.running and not self.on_hold and not self.scrubbed: + self.on_hold = True + self.hold_start_time = time.time() + self.remaining_time = max(0, self.target_time - self.hold_start_time) + self.show_resume_button() + + def resume(self): + if self.running and self.on_hold and not self.scrubbed: + self.on_hold = False + self.target_time = time.time() + self.remaining_time + self.show_hold_button() + + def show_hold_button(self): + self.resume_btn.grid_remove() + self.hold_btn.grid() + + def show_resume_button(self): + self.hold_btn.grid_remove() + self.resume_btn.grid() + + def scrub(self): + self.scrubbed = True + self.running = False + write_countdown_html(self.mission_name, "SCRUB") + self.text.config(text="SCRUB") + + def reset(self): + self.running = False + self.on_hold = False + self.scrubbed = False + self.counting_up = False + self.text.config(text="T-00:00:00") + write_countdown_html(self.mission_name, "T-00:00:00") + self.show_hold_button() + + # ---------------------------- + # Clock updating + # ---------------------------- + def format_time(self, seconds, prefix="T-"): + h = int(seconds // 3600) + m = int((seconds % 3600) // 60) + s = int(seconds % 60) + return f"{prefix}{h:02}:{m:02}:{s:02}" + + def update_clock(self): + now_time = time.time() + + # Update timer + if self.running and not self.scrubbed: + if self.on_hold: + elapsed = int(now_time - self.hold_start_time) + timer_text = self.format_time(elapsed, "H+") + elif self.target_time: + diff = int(self.target_time - now_time) + if diff <= 0 and not self.counting_up: + self.counting_up = True + self.target_time = now_time + diff = 0 + if self.counting_up: + elapsed = int(now_time - self.target_time) + timer_text = self.format_time(elapsed, "T+") + else: + timer_text = self.format_time(diff, "T-") + else: + timer_text = "T-00:00:00" + else: + timer_text = self.text.cget("text") + + self.text.config(text=timer_text) + write_countdown_html(self.mission_name, timer_text) + + # Update Go/No-Go every 10 seconds + if now_time - self.last_gonogo_update > 0.1: + # fetch_gonogo returns [Range, Weather, Vehicle] + self.range_status, self.weather, self.vehicle = fetch_gonogo() + self.range_label.config(text=f"RANGE: {self.range_status}") + self.weather_label.config(text=f"WEATHER: {self.weather}") + self.vehicle_label.config(text=f"VEHICLE: {self.vehicle}") + self.gonogo_values = [self.range_status, self.weather, self.vehicle] + write_gonogo_html(self.gonogo_values) + self.last_gonogo_update = now_time + + self.root.after(200, self.update_clock) + + +if __name__ == "__main__": + root = tk.Tk() + app = CountdownApp(root) + write_countdown_html("Placeholder Mission", "T-00:00:00") + write_gonogo_html(fetch_gonogo()) + root.mainloop() diff --git a/gonogo.html b/gonogo.html new file mode 100644 index 0000000..3a9e1ee --- /dev/null +++ b/gonogo.html @@ -0,0 +1,41 @@ + + + + + + + + +
+
Range: GO
+
Vehicle: GO
+
Weather: GO
+
+ + \ No newline at end of file diff --git a/main.py b/main.py index a54f9ba..15aa0e5 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,38 @@ import tkinter as tk import time from datetime import datetime +import requests +import csv +import io -HTML_FILE = "countdown.html" +COUNTDOWN_HTML = "countdown.html" +GONOGO_HTML = "gonogo.html" +SHEET_LINK = "https://docs.google.com/spreadsheets/d/1UPJTW8vH2mgEzispjg_Y_zSqYTFaLoxuoZnqleVlSZ0/export?format=csv&gid=855477916" +session = requests.Session() -def write_html(mission_name, timer_text): +# ------------------------- +# Fetch Go/No-Go Data +# ------------------------- +def fetch_gonogo(): + """Fetch Go/No-Go parameters from L2, L3, L4 (rows 2,3,4; col 12)""" + try: + resp = session.get(SHEET_LINK, timeout=2) # timeout for faster failure if network is slow + resp.raise_for_status() + reader = csv.reader(io.StringIO(resp.text)) + data = list(reader) + gonogo = [] + for i in [1, 2, 3]: + value = data[i][11] if len(data[i]) > 11 else "N/A" + gonogo.append(value.strip().upper()) # <-- always uppercase + return gonogo + except Exception as e: + print(f"[ERROR] Failed to fetch Go/No-Go: {e}") + return ["ERROR"] * 3 + +# ------------------------- +# Write Countdown HTML +# ------------------------- +def write_countdown_html(mission_name, timer_text): html = f""" @@ -17,25 +45,11 @@ body {{ flex-direction: column; justify-content: center; align-items: center; -}} -#mission {{ color: white; font-family: Consolas, monospace; - font-size: 4vw; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 90vw; - text-align: center; -}} -#timer {{ - color: white; - font-family: Consolas, monospace; - font-size: 8vw; - line-height: 1; - white-space: nowrap; - text-align: center; }} +#mission {{ font-size: 4vw; margin-bottom: 20px; }} +#timer {{ font-size: 8vw; margin-bottom: 40px; }} + + +
+
Range: {gonogo_values[0]}
+
Vehicle: {gonogo_values[2]}
+
Weather: {gonogo_values[1]}
+
+ +""" + with open(GONOGO_HTML, "w", encoding="utf-8") as f: + f.write(html) +# ------------------------- +# Countdown App +# ------------------------- class CountdownApp: def __init__(self, root): self.root = root @@ -66,15 +132,11 @@ class CountdownApp: self.hold_start_time = None self.remaining_time = 0 self.mission_name = "Mission" + self.gonogo_values = fetch_gonogo() + self.last_gonogo_update = time.time() # Display - self.text = tk.Label( - root, - text="T-00:00:00", - font=("Consolas", 80, "bold"), - fg="white", - bg="black" - ) + self.text = tk.Label(root, text="T-00:00:00", font=("Consolas", 80, "bold"), fg="white", bg="black") self.text.pack(padx=50, pady=20) # Mission name input @@ -139,7 +201,6 @@ class CountdownApp: self.reset_btn = tk.Button(frame_buttons, text="⟳ Reset", command=self.reset, font=("Arial", 14)) self.reset_btn.grid(row=0, column=3, padx=5) - self.update_inputs() self.update_clock() @@ -186,7 +247,7 @@ class CountdownApp: total_seconds = (target_today - now).total_seconds() except Exception: self.text.config(text="Invalid time") - write_html(self.mission_name, "Invalid time") + write_countdown_html(self.mission_name, "Invalid time") return self.target_time = time.time() + total_seconds @@ -213,11 +274,10 @@ class CountdownApp: self.hold_btn.grid_remove() self.resume_btn.grid() - def scrub(self): self.scrubbed = True self.running = False - write_html(self.mission_name, "SCRUB") + write_countdown_html(self.mission_name, "SCRUB") self.text.config(text="SCRUB") def reset(self): @@ -226,7 +286,7 @@ class CountdownApp: self.scrubbed = False self.counting_up = False self.text.config(text="T-00:00:00") - write_html(self.mission_name, "T-00:00:00") + write_countdown_html(self.mission_name, "T-00:00:00") self.show_hold_button() # ---------------------------- @@ -239,19 +299,21 @@ class CountdownApp: return f"{prefix}{h:02}:{m:02}:{s:02}" def update_clock(self): + now_time = time.time() + + # Update timer if self.running and not self.scrubbed: - now = time.time() if self.on_hold: - elapsed = int(now - self.hold_start_time) + elapsed = int(now_time - self.hold_start_time) timer_text = self.format_time(elapsed, "H+") elif self.target_time: - diff = int(self.target_time - now) + diff = int(self.target_time - now_time) if diff <= 0 and not self.counting_up: self.counting_up = True - self.target_time = now + self.target_time = now_time diff = 0 if self.counting_up: - elapsed = int(now - self.target_time) + elapsed = int(now_time - self.target_time) timer_text = self.format_time(elapsed, "T+") else: timer_text = self.format_time(diff, "T-") @@ -261,12 +323,20 @@ class CountdownApp: timer_text = self.text.cget("text") self.text.config(text=timer_text) - write_html(self.mission_name, timer_text) + write_countdown_html(self.mission_name, timer_text) + + # Update Go/No-Go every 10 seconds + if now_time - self.last_gonogo_update > 0.5: + self.gonogo_values = fetch_gonogo() + write_gonogo_html(self.gonogo_values) + self.last_gonogo_update = now_time + self.root.after(200, self.update_clock) if __name__ == "__main__": root = tk.Tk() app = CountdownApp(root) - write_html("Mission", "T-00:00:00") + write_countdown_html("Mission", "T-00:00:00") + write_gonogo_html(fetch_gonogo()) root.mainloop()