Update main.py
This commit is contained in:
498
main.py
498
main.py
@@ -6,54 +6,95 @@ import requests
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
# Get the user's Documents folder (cross-platform)
|
||||
# -------------------------
|
||||
# Paths & defaults
|
||||
# -------------------------
|
||||
documents_folder = os.path.join(os.path.expanduser("~"), "Documents")
|
||||
|
||||
# Create your app folder inside Documents
|
||||
app_folder = os.path.join(documents_folder, "RocketLaunchCountdown")
|
||||
os.makedirs(app_folder, exist_ok=True)
|
||||
|
||||
# Define file paths
|
||||
COUNTDOWN_HTML = os.path.join(app_folder, "countdown.html")
|
||||
GONOGO_HTML = os.path.join(app_folder, "gonogo.html")
|
||||
SHEET_LINK = "https://docs.google.com/spreadsheets/d/1UPJTW8vH2mgEzispjg_Y_zSqYTFaLoxuoZnqleVlSZ0/export?format=csv&gid=855477916"
|
||||
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
|
||||
|
||||
# Default CSV link you provided (kept for CSV fetch fallback)
|
||||
DEFAULT_CSV_LINK = "https://docs.google.com/spreadsheets/d/1UPJTW8vH2mgEzispjg_Y_zSqYTFaLoxuoZnqleVlSZ0/export?format=csv&gid=855477916"
|
||||
|
||||
session = requests.Session()
|
||||
appVersion = "0.2.1"
|
||||
appVersion = "0.3.0"
|
||||
|
||||
# -------------------------
|
||||
# Fetch Go/No-Go Data
|
||||
# Settings helpers
|
||||
# -------------------------
|
||||
def fetch_gonogo():
|
||||
"""Fetch Go/No-Go parameters from L2, L3, L4 (rows 2,3,4; col 12)"""
|
||||
def load_settings():
|
||||
if not os.path.exists(SETTINGS_FILE):
|
||||
# default settings
|
||||
s = {"mode": "sheet", "sheet_url": ""}
|
||||
save_settings(s)
|
||||
return s
|
||||
try:
|
||||
resp = session.get(SHEET_LINK, timeout=2) # timeout for faster failure if network is slow
|
||||
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {"mode": "sheet", "sheet_url": ""}
|
||||
|
||||
def save_settings(settings):
|
||||
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
|
||||
# -------------------------
|
||||
# Fetch (CSV) Go/No-Go (fallback/prefill)
|
||||
# -------------------------
|
||||
def fetch_gonogo_csv(csv_link=DEFAULT_CSV_LINK, timeout=3):
|
||||
"""
|
||||
Fetch Go/No-Go parameters from CSV export (rows 2-4, col 12).
|
||||
Returns [Range, Weather, Vehicle] (strings).
|
||||
"""
|
||||
try:
|
||||
resp = session.get(csv_link, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
reader = csv.reader(io.StringIO(resp.text))
|
||||
data = list(reader)
|
||||
gonogo = []
|
||||
# rows index 1,2,3 correspond to spreadsheet rows 2,3,4
|
||||
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
|
||||
value = data[i][11] if len(data) > i and len(data[i]) > 11 else "N/A"
|
||||
gonogo.append(value.strip().upper())
|
||||
return gonogo
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to fetch Go/No-Go: {e}")
|
||||
return ["ERROR"] * 3
|
||||
|
||||
print(f"[ERROR] Failed to fetch Go/No-Go CSV: {e}")
|
||||
return ["N/A", "N/A", "N/A"]
|
||||
|
||||
# -------------------------
|
||||
# Helper for color
|
||||
# Utility
|
||||
# -------------------------
|
||||
def get_status_color(status):
|
||||
"""Return color name for a Go/No-Go status string."""
|
||||
try:
|
||||
return "green" if str(status).strip().upper() == "GO" else "red"
|
||||
except Exception:
|
||||
return "white"
|
||||
status = (status or "").strip().upper()
|
||||
if status == "GO": return "green"
|
||||
if status == "NOGO" or status == "NOGO" or status == "NOGO": return "red"
|
||||
return "white"
|
||||
|
||||
def ensure_iframe_url(url):
|
||||
"""
|
||||
Convert many common Google sheet URLs to an embeddable pubhtml URL.
|
||||
If user pasted a 'publish to web' link already, return as-is.
|
||||
If they pasted an /edit? URL, attempt to convert to preview/pub versions.
|
||||
"""
|
||||
if not url:
|
||||
return ""
|
||||
if "pubhtml" in url:
|
||||
return url
|
||||
# If it's the spreadsheet "edit" URL, try to convert to /preview (works in many cases)
|
||||
if "/edit" in url:
|
||||
return url.split("/edit")[0] + "/preview"
|
||||
# If it's the direct docs/d/<id>/ URL without pubhtml, attempt the embed pattern
|
||||
# This won't always preserve sheet/tab selection; best is to instruct users to Publish to web.
|
||||
return url
|
||||
|
||||
# -------------------------
|
||||
# Write Countdown HTML
|
||||
# HTML writers
|
||||
# -------------------------
|
||||
def write_countdown_html(mission_name, timer_text):
|
||||
html = f"""<!DOCTYPE html>
|
||||
@@ -74,9 +115,7 @@ body {{
|
||||
#mission {{ font-size: 4vw; margin-bottom: 0; }}
|
||||
#timer {{ font-size: 8vw; margin-bottom: 40px; }}
|
||||
</style>
|
||||
<script>
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
</script>
|
||||
<script>setTimeout(()=>location.reload(),1000);</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mission">{mission_name}</div>
|
||||
@@ -86,12 +125,12 @@ setTimeout(() => location.reload(), 1000);
|
||||
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"]
|
||||
def write_gonogo_html_from_values(values):
|
||||
"""
|
||||
values: [Range, Weather, Vehicle] strings like "GO" or "NO-GO"
|
||||
"""
|
||||
# ensure three items:
|
||||
vals = (values + ["N/A","N/A","N/A"])[:3]
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -122,14 +161,53 @@ body {{
|
||||
.nogo {{ color: #f00; }}
|
||||
</style>
|
||||
<script>
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
setTimeout(()=>location.reload(),5000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="gonogo">
|
||||
<div class="status-box {'go' if gonogo_values[0].lower()=='go' else 'nogo'}">Range: {gonogo_values[0]}</div>
|
||||
<div class="status-box {'go' if gonogo_values[2].lower()=='go' else 'nogo'}">Vehicle: {gonogo_values[2]}</div>
|
||||
<div class="status-box {'go' if gonogo_values[1].lower()=='go' else 'nogo'}">Weather: {gonogo_values[1]}</div>
|
||||
<div class="status-box {'go' if vals[0].strip().upper()=='GO' else 'nogo'}">Range: {vals[0]}</div>
|
||||
<div class="status-box {'go' if vals[2].strip().upper()=='GO' else 'nogo'}">Vehicle: {vals[2]}</div>
|
||||
<div class="status-box {'go' if vals[1].strip().upper()=='GO' else 'nogo'}">Weather: {vals[1]}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
with open(GONOGO_HTML, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
|
||||
def write_gonogo_html_iframe(sheet_embed_url):
|
||||
"""
|
||||
Writes a gonogo.html that embeds the published Google Sheet via iframe.
|
||||
Prefer users to publish-to-web and paste the pubhtml URL.
|
||||
"""
|
||||
# safe empty handling
|
||||
if not sheet_embed_url:
|
||||
content = "<div style='color:orange'>No sheet URL set in settings</div>"
|
||||
else:
|
||||
content = f'<iframe src="{sheet_embed_url}" width="100%" height="600" style="border:none;"></iframe>'
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-family: Consolas, monospace;
|
||||
}}
|
||||
.container {{
|
||||
width: 95%;
|
||||
margin: 10px auto;
|
||||
}}
|
||||
</style>
|
||||
<script>
|
||||
setTimeout(()=>location.reload(),3000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{content}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
@@ -142,12 +220,12 @@ setTimeout(() => location.reload(), 5000);
|
||||
class CountdownApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("RocketLaunchCountdown" + " " + appVersion)
|
||||
self.root.title(f"RocketLaunchCountdown {appVersion}")
|
||||
self.root.config(bg="black")
|
||||
self.root.attributes("-topmost", True)
|
||||
self.root.geometry("800x575")
|
||||
self.root.geometry("900x700")
|
||||
|
||||
# State
|
||||
# state
|
||||
self.running = False
|
||||
self.on_hold = False
|
||||
self.scrubbed = False
|
||||
@@ -156,130 +234,194 @@ class CountdownApp:
|
||||
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()
|
||||
|
||||
# Title
|
||||
# manual gonogo statuses (default N/A)
|
||||
self.manual_range = "N/A"
|
||||
self.manual_weather = "N/A"
|
||||
self.manual_vehicle = "N/A"
|
||||
|
||||
# load settings
|
||||
self.settings = load_settings()
|
||||
# if sheet mode and sheet_url empty, try to fallback to CSV link to prefill GUI
|
||||
self.csv_link = DEFAULT_CSV_LINK
|
||||
|
||||
# initial gonogo_html: if settings.mode == sheet -> iframe embed, else fill from CSV/fallback or manual
|
||||
if self.settings.get("mode", "sheet") == "sheet":
|
||||
embed_url = ensure_iframe_url(self.settings.get("sheet_url", ""))
|
||||
write_gonogo_html_iframe(embed_url)
|
||||
else:
|
||||
# manual: populate from csv fetch as initial values or keep N/A
|
||||
vals = fetch_gonogo_csv(self.csv_link)
|
||||
# csv returns [Range, Weather, Vehicle] index 0,1,2 -> but our layout expects [Range, Weather, Vehicle]
|
||||
write_gonogo_html_from_values(vals)
|
||||
self.manual_range, self.manual_weather, self.manual_vehicle = vals[0], vals[1], vals[2]
|
||||
|
||||
write_countdown_html("Placeholder Mission", "T-00:00:00")
|
||||
|
||||
# GUI layout
|
||||
self.titletext = tk.Label(root, text="RocketLaunchCountdown", font=("Consolas", 24), fg="white", bg="black")
|
||||
self.titletext.pack(pady=(10, 0))
|
||||
|
||||
# Display
|
||||
self.text = tk.Label(root, text="T-00:00:00", font=("Consolas", 80, "bold"), fg="white", bg="black")
|
||||
self.text.pack(pady=(0, 5))
|
||||
|
||||
# Mission name input
|
||||
# mission 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 = tk.Entry(frame_top, width=30, font=("Arial", 16))
|
||||
self.mission_entry.insert(0, self.mission_name)
|
||||
self.mission_entry.pack(side="left")
|
||||
self.mission_entry.pack(side="left", padx=5)
|
||||
|
||||
# Mode toggle
|
||||
# Mode toggle (duration/clock)
|
||||
frame_mode = tk.Frame(root, bg="black")
|
||||
frame_mode.pack(pady=5)
|
||||
|
||||
self.mode_var = tk.StringVar(value="duration")
|
||||
|
||||
self.radio_duration = tk.Radiobutton(
|
||||
frame_mode,
|
||||
text="Duration",
|
||||
variable=self.mode_var,
|
||||
value="duration",
|
||||
fg="white",
|
||||
bg="black",
|
||||
selectcolor="black", # makes the dot visible
|
||||
command=self.update_inputs
|
||||
)
|
||||
self.radio_duration = tk.Radiobutton(frame_mode, text="Duration", variable=self.mode_var, value="duration",
|
||||
fg="white", bg="black", selectcolor="black", command=self.update_inputs)
|
||||
self.radio_duration.pack(side="left", padx=5)
|
||||
|
||||
self.radio_clock = tk.Radiobutton(
|
||||
frame_mode,
|
||||
text="Clock Time",
|
||||
variable=self.mode_var,
|
||||
value="clock",
|
||||
fg="white",
|
||||
bg="black",
|
||||
selectcolor="black", # makes the dot visible
|
||||
command=self.update_inputs
|
||||
)
|
||||
self.radio_clock = tk.Radiobutton(frame_mode, text="Clock Time", variable=self.mode_var, value="clock",
|
||||
fg="white", bg="black", selectcolor="black", command=self.update_inputs)
|
||||
self.radio_clock.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 = tk.Entry(frame_duration, width=3, font=("Arial", 16))
|
||||
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 = tk.Entry(frame_duration, width=3, font=("Arial", 16))
|
||||
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 = tk.Entry(frame_duration, width=3, font=("Arial", 16))
|
||||
self.seconds_entry.insert(0, "0")
|
||||
self.seconds_entry.pack(side="left", padx=2)
|
||||
|
||||
# Clock time input
|
||||
# Clock 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 = tk.Entry(frame_clock, width=7, font=("Arial", 16))
|
||||
self.clock_entry.insert(0, "14:00")
|
||||
self.clock_entry.pack(side="left", padx=5)
|
||||
|
||||
# Control buttons
|
||||
# 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.resume_btn.grid_remove()
|
||||
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)
|
||||
|
||||
# Settings + manual GO/NO-GO controls
|
||||
ctrl_frame = tk.Frame(root, bg="black")
|
||||
ctrl_frame.pack(pady=10)
|
||||
|
||||
self.settings_btn = tk.Button(ctrl_frame, text="⚙ Settings", command=self.open_settings, font=("Arial", 12))
|
||||
self.settings_btn.pack(side="left", padx=6)
|
||||
|
||||
# Manual GO/NO-GO toggles (visible only in manual mode)
|
||||
self.manual_frame = tk.Frame(ctrl_frame, bg="black")
|
||||
self.manual_frame.pack(side="left", padx=10)
|
||||
tk.Label(self.manual_frame, text="Manual:", fg="white", bg="black").pack(side="left", padx=(0,6))
|
||||
self.range_btn = tk.Button(self.manual_frame, text="Range: N/A", command=self.toggle_range, font=("Arial", 12))
|
||||
self.range_btn.pack(side="left", padx=4)
|
||||
self.weather_btn = tk.Button(self.manual_frame, text="Weather: N/A", command=self.toggle_weather, font=("Arial", 12))
|
||||
self.weather_btn.pack(side="left", padx=4)
|
||||
self.vehicle_btn = tk.Button(self.manual_frame, text="Vehicle: N/A", command=self.toggle_vehicle, font=("Arial", 12))
|
||||
self.vehicle_btn.pack(side="left", padx=4)
|
||||
|
||||
# Go/No-Go display labels in main GUI (mirror)
|
||||
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 = tk.Label(frame_gn, text="RANGE: N/A", font=("Consolas", 18), 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 = tk.Label(frame_gn, text="WEATHER: N/A", font=("Consolas", 18), 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 = tk.Label(frame_gn, text="VEHICLE: N/A", font=("Consolas", 18), fg="white", bg="black")
|
||||
self.vehicle_label.pack()
|
||||
|
||||
# Footer
|
||||
# footer
|
||||
footer_frame = tk.Frame(root, bg="black")
|
||||
footer_frame.pack(side="bottom", pady=0, fill="x")
|
||||
|
||||
self.footer_label = tk.Label(
|
||||
footer_frame,
|
||||
text="Made by HamsterSpaceNerd3000", # or whatever you want
|
||||
font=("Consolas", 12),
|
||||
fg="black",
|
||||
bg="white"
|
||||
)
|
||||
self.footer_label = tk.Label(footer_frame, text=f"Made by HamsterSpaceNerd3000 — v{appVersion}",
|
||||
font=("Consolas", 12), fg="black", bg="white")
|
||||
self.footer_label.pack(fill="x")
|
||||
|
||||
|
||||
# initialize visibility + values
|
||||
self.update_inputs()
|
||||
self.apply_settings_to_ui()
|
||||
self.update_clock()
|
||||
|
||||
# ----------------------------
|
||||
# Update input visibility based on mode
|
||||
# Settings UI and behavior
|
||||
# ----------------------------
|
||||
def apply_settings_to_ui(self):
|
||||
# show/hide manual controls based on settings.mode
|
||||
mode = self.settings.get("mode", "sheet")
|
||||
if mode == "manual":
|
||||
self.manual_frame.pack(side="left", padx=10)
|
||||
else:
|
||||
# sheet mode -> hide manual controls
|
||||
self.manual_frame.pack_forget()
|
||||
|
||||
def open_settings(self):
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Settings")
|
||||
win.config(bg="black")
|
||||
win.geometry("700x180")
|
||||
tk.Label(win, text="Data Source Mode:", bg="black", fg="white").pack(anchor="w", padx=10, pady=(10,0))
|
||||
mode_var = tk.StringVar(value=self.settings.get("mode", "sheet"))
|
||||
|
||||
rb1 = tk.Radiobutton(win, text="Google Sheet (embed published sheet)", variable=mode_var, value="sheet",
|
||||
bg="black", fg="white", selectcolor="black")
|
||||
rb1.pack(anchor="w", padx=20)
|
||||
rb2 = tk.Radiobutton(win, text="Manual GO/NOGO (use GUI buttons)", variable=mode_var, value="manual",
|
||||
bg="black", fg="white", selectcolor="black")
|
||||
rb2.pack(anchor="w", padx=20)
|
||||
|
||||
tk.Label(win, text="Google Sheet embed URL (Publish to web → copy link):", bg="black", fg="white").pack(anchor="w", padx=10, pady=(10,0))
|
||||
sheet_entry = tk.Entry(win, width=100)
|
||||
sheet_entry.insert(0, self.settings.get("sheet_url", ""))
|
||||
sheet_entry.pack(padx=10, pady=(0,10))
|
||||
|
||||
def save_settings_cmd():
|
||||
new_mode = mode_var.get()
|
||||
new_url = sheet_entry.get().strip()
|
||||
self.settings["mode"] = new_mode
|
||||
self.settings["sheet_url"] = new_url
|
||||
save_settings(self.settings)
|
||||
# regenerate gonogo HTML according to mode
|
||||
if new_mode == "sheet":
|
||||
embed = ensure_iframe_url(new_url)
|
||||
write_gonogo_html_iframe(embed)
|
||||
# prefill labels from CSV fetch if possible
|
||||
vals = fetch_gonogo_csv(self.csv_link)
|
||||
self.range_label.config(text=f"RANGE: {vals[0]}", fg=get_status_color(vals[0]))
|
||||
self.weather_label.config(text=f"WEATHER: {vals[1]}", fg=get_status_color(vals[1]))
|
||||
self.vehicle_label.config(text=f"VEHICLE: {vals[2]}", fg=get_status_color(vals[2]))
|
||||
else:
|
||||
# manual: write current manual values
|
||||
write_gonogo_html_from_values([self.manual_range, self.manual_weather, self.manual_vehicle])
|
||||
self.apply_settings_to_ui()
|
||||
win.destroy()
|
||||
|
||||
btn_frame = tk.Frame(win, bg="black")
|
||||
btn_frame.pack(fill="x", pady=(0,10))
|
||||
tk.Button(btn_frame, text="Save", command=save_settings_cmd, width=12).pack(side="right", padx=10)
|
||||
|
||||
# ----------------------------
|
||||
# Inputs
|
||||
# ----------------------------
|
||||
def update_inputs(self):
|
||||
if self.mode_var.get() == "duration":
|
||||
@@ -293,6 +435,33 @@ class CountdownApp:
|
||||
self.seconds_entry.config(state="disabled")
|
||||
self.clock_entry.config(state="normal")
|
||||
|
||||
# ----------------------------
|
||||
# Manual toggle callbacks
|
||||
# ----------------------------
|
||||
def toggle_range(self):
|
||||
if self.settings.get("mode", "sheet") != "manual":
|
||||
return
|
||||
self.manual_range = "GO" if self.manual_range.strip().upper() != "GO" else "NOGO"
|
||||
self.range_btn.config(text=f"Range: {self.manual_range}")
|
||||
self.range_label.config(text=f"RANGE: {self.manual_range}", fg=get_status_color(self.manual_range))
|
||||
write_gonogo_html_from_values([self.manual_range, self.manual_weather, self.manual_vehicle])
|
||||
|
||||
def toggle_weather(self):
|
||||
if self.settings.get("mode", "sheet") != "manual":
|
||||
return
|
||||
self.manual_weather = "GO" if self.manual_weather.strip().upper() != "GO" else "NOGO"
|
||||
self.weather_btn.config(text=f"Weather: {self.manual_weather}")
|
||||
self.weather_label.config(text=f"WEATHER: {self.manual_weather}", fg=get_status_color(self.manual_weather))
|
||||
write_gonogo_html_from_values([self.manual_range, self.manual_weather, self.manual_vehicle])
|
||||
|
||||
def toggle_vehicle(self):
|
||||
if self.settings.get("mode", "sheet") != "manual":
|
||||
return
|
||||
self.manual_vehicle = "GO" if self.manual_vehicle.strip().upper() != "GO" else "NOGO"
|
||||
self.vehicle_btn.config(text=f"Vehicle: {self.manual_vehicle}")
|
||||
self.vehicle_label.config(text=f"VEHICLE: {self.manual_vehicle}", fg=get_status_color(self.manual_vehicle))
|
||||
write_gonogo_html_from_values([self.manual_range, self.manual_weather, self.manual_vehicle])
|
||||
|
||||
# ----------------------------
|
||||
# Control logic
|
||||
# ----------------------------
|
||||
@@ -303,7 +472,6 @@ class CountdownApp:
|
||||
self.scrubbed = False
|
||||
self.counting_up = False
|
||||
self.show_hold_button()
|
||||
|
||||
try:
|
||||
if self.mode_var.get() == "duration":
|
||||
h = int(self.hours_entry.get())
|
||||
@@ -323,7 +491,6 @@ class CountdownApp:
|
||||
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
|
||||
|
||||
@@ -364,7 +531,7 @@ class CountdownApp:
|
||||
self.show_hold_button()
|
||||
|
||||
# ----------------------------
|
||||
# Clock updating
|
||||
# Format/clock loop
|
||||
# ----------------------------
|
||||
def format_time(self, seconds, prefix="T-"):
|
||||
h = int(seconds // 3600)
|
||||
@@ -375,116 +542,115 @@ class CountdownApp:
|
||||
def update_clock(self):
|
||||
now_time = time.time()
|
||||
|
||||
# Update timer
|
||||
# Timer logic
|
||||
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-")
|
||||
if getattr(self, 'paused', False):
|
||||
try:
|
||||
secs = int(self.remaining_time)
|
||||
except Exception:
|
||||
secs = 0
|
||||
timer_text = self.format_time(secs, "T-")
|
||||
else:
|
||||
timer_text = "T-00:00:00"
|
||||
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}", fg=get_status_color(self.range_status))
|
||||
self.weather_label.config(text=f"WEATHER: {self.weather}", fg=get_status_color(self.weather))
|
||||
self.vehicle_label.config(text=f"VEHICLE: {self.vehicle}", fg=get_status_color(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)
|
||||
# Update gonogo block depending on settings.mode
|
||||
mode = self.settings.get("mode", "sheet")
|
||||
if mode == "sheet":
|
||||
# ensure embed is current
|
||||
embed = ensure_iframe_url(self.settings.get("sheet_url", ""))
|
||||
write_gonogo_html_iframe(embed)
|
||||
# optionally update mirrored labels by fetching CSV (best-effort)
|
||||
vals = fetch_gonogo_csv(self.csv_link)
|
||||
self.range_label.config(text=f"RANGE: {vals[0]}", fg=get_status_color(vals[0]))
|
||||
self.weather_label.config(text=f"WEATHER: {vals[1]}", fg=get_status_color(vals[1]))
|
||||
self.vehicle_label.config(text=f"VEHICLE: {vals[2]}", fg=get_status_color(vals[2]))
|
||||
else:
|
||||
# manual mode: write gonogo based on manual toggles and update labels
|
||||
vals = [self.manual_range, self.manual_weather, self.manual_vehicle]
|
||||
write_gonogo_html_from_values(vals)
|
||||
self.range_label.config(text=f"RANGE: {vals[0]}", fg=get_status_color(vals[0]))
|
||||
self.weather_label.config(text=f"WEATHER: {vals[1]}", fg=get_status_color(vals[1]))
|
||||
self.vehicle_label.config(text=f"VEHICLE: {vals[2]}", fg=get_status_color(vals[2]))
|
||||
|
||||
# schedule next tick
|
||||
self.root.after(500, self.update_clock) # 2× per second
|
||||
|
||||
# -------------------------
|
||||
# Main: splash then launch
|
||||
# -------------------------
|
||||
if __name__ == "__main__":
|
||||
# Show a small splash/loading GUI while we fetch initial data and write HTML files.
|
||||
def show_splash_and_start():
|
||||
splash = tk.Tk()
|
||||
splash.title("RocketLaunchCountdown — Initialaization")
|
||||
splash.title(f"RocketLaunchCountdown — Initialization {appVersion}")
|
||||
splash.config(bg="black")
|
||||
splash.geometry("400x175")
|
||||
splash.geometry("500x200")
|
||||
splash.attributes("-topmost", True)
|
||||
|
||||
title = tk.Label(splash, text="RocketLaunchCountdown", fg="white", bg="black", font=("Arial", 20, "bold"))
|
||||
title.pack(pady=(10,0))
|
||||
|
||||
lbl = tk.Label(splash, text="Loading resources...", fg="white", bg="black", font=("Arial", 14))
|
||||
lbl.pack(pady=(0,5))
|
||||
|
||||
info = tk.Label(splash, text="Fetching Go/No-Go and preparing HTML files.", fg="#ccc", bg="black", font=("Arial", 10))
|
||||
info.pack()
|
||||
|
||||
cont_btn = tk.Button(splash, text="Continue", state="disabled", width=12)
|
||||
tk.Label(splash, text="RocketLaunchCountdown", fg="white", bg="black", font=("Arial", 20, "bold")).pack(pady=(12,4))
|
||||
info = tk.Label(splash, text="Loading resources...", fg="#ccc", bg="black", font=("Arial", 12))
|
||||
info.pack(pady=6)
|
||||
cont_btn = tk.Button(splash, text="Continue", state="disabled", width=16)
|
||||
cont_btn.pack(pady=8)
|
||||
|
||||
# Footer
|
||||
footer_frame = tk.Frame(splash, bg="black")
|
||||
footer_frame.pack(side="bottom", pady=0, fill="x")
|
||||
|
||||
footer_label = tk.Label(
|
||||
footer_frame,
|
||||
text="Made by HamsterSpaceNerd3000", # or whatever you want
|
||||
font=("Consolas", 12),
|
||||
fg="black",
|
||||
bg="white"
|
||||
)
|
||||
footer_label.pack(fill="x")
|
||||
|
||||
# Shared flag to indicate initialization complete
|
||||
init_state = { 'done': False, 'error': None }
|
||||
init_state = {'done': False, 'error': None}
|
||||
|
||||
def init_worker():
|
||||
try:
|
||||
# perform the same initial writes you had before
|
||||
gonogo = fetch_gonogo()
|
||||
# pre-create files
|
||||
s = load_settings()
|
||||
# create initial countdown & gonogo files
|
||||
write_countdown_html("Placeholder Mission", "T-00:00:00")
|
||||
write_gonogo_html(gonogo)
|
||||
if s.get("mode", "sheet") == "sheet":
|
||||
write_gonogo_html_iframe(ensure_iframe_url(s.get("sheet_url", "")))
|
||||
else:
|
||||
vals = fetch_gonogo_csv(DEFAULT_CSV_LINK)
|
||||
write_gonogo_html_from_values(vals)
|
||||
init_state['done'] = True
|
||||
except Exception as e:
|
||||
init_state['error'] = str(e)
|
||||
init_state['done'] = True
|
||||
|
||||
# Start background initialization
|
||||
threading.Thread(target=init_worker, daemon=True).start()
|
||||
|
||||
def check_init():
|
||||
if init_state['done']:
|
||||
if init_state['error']:
|
||||
info.config(text=f"Initialization error: {init_state['error']}")
|
||||
cont_btn.config(state="normal")
|
||||
else:
|
||||
info.config(text="Ready. You may open browser sources now, then click Continue.")
|
||||
cont_btn.config(state="normal")
|
||||
splash.after(5000, on_continue)
|
||||
return
|
||||
splash.after(200, check_init)
|
||||
|
||||
def on_continue():
|
||||
splash.destroy()
|
||||
# now create the real main window
|
||||
root = tk.Tk()
|
||||
app = CountdownApp(root)
|
||||
root.mainloop()
|
||||
|
||||
cont_btn.config(command=on_continue)
|
||||
# begin polling
|
||||
splash.after(100, check_init)
|
||||
splash.after(200, check_init)
|
||||
splash.mainloop()
|
||||
|
||||
show_splash_and_start()
|
||||
|
||||
Reference in New Issue
Block a user