Merge pull request #2 from HamsterSpaceNerd3000/dev

Dev
This commit is contained in:
HamsterSpaceNerd3000
2025-10-16 21:21:07 -04:00
committed by GitHub
10 changed files with 8328 additions and 8240 deletions

View File

@@ -1,375 +1,143 @@
import tkinter as tk
import time
from datetime import datetime
import requests
import csv
import io
import threading
import time
import json
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()
SETTINGS_FILE = "settings.json"
# -------------------------
# 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"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{
margin: 0;
background-color: black;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
font-family: Consolas, monospace;
}}
#mission {{ font-size: 4vw; margin-bottom: 20px; }}
#timer {{ font-size: 8vw; margin-bottom: 40px; }}
</style>
<script>
setTimeout(() => location.reload(), 1000);
</script>
</head>
<body>
<div id="mission">{mission_name}</div>
<div id="timer">{timer_text}</div>
</body>
</html>"""
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"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{
margin: 0;
background-color: black;
color: white;
font-family: Consolas, monospace;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}}
#gonogo {{
display: flex;
gap: 40px;
}}
.status-box {{
border: 2px solid white;
padding: 20px 40px;
font-size: 2.5vw;
text-align: center;
background-color: #111;
}}
.go {{ color: #0f0; }}
.nogo {{ color: #f00; }}
</style>
<script>
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>
</body>
</html>"""
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)
self.root.title("Launch Control - GO/NOGO")
# 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")
self.rb_duration = tk.Radiobutton(frame_mode, text="Duration", variable=self.mode_var, value="duration",
fg="white", bg="black", indicatoron=0, width=12,
command=self.update_inputs)
self.rb_duration.pack(side="left", padx=5)
self.rb_clock = tk.Radiobutton(frame_mode, text="Clock Time", variable=self.mode_var, value="clock",
fg="black", bg="white", indicatoron=0, width=12,
command=self.update_inputs)
self.rb_clock.pack(side="left", padx=5)
def update_mode_buttons(*args):
val = self.mode_var.get()
if val == 'duration':
self.rb_duration.config(bg='black', fg='white', relief='sunken')
self.rb_clock.config(bg='white', fg='black', relief='raised')
else:
self.rb_duration.config(bg='white', fg='black', relief='raised')
self.rb_clock.config(bg='black', fg='white', relief='sunken')
self.mode_var.trace_add('write', update_mode_buttons)
update_mode_buttons()
# 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.go_nogo_labels = {}
self.sheet_data = {}
self.last_data = {}
self.running = True
self.on_hold = False
self.scrubbed = False
self.counting_up = False
self.show_hold_button()
# Load settings
self.settings = self.load_settings()
tk.Label(root, text="GO/NOGO STATUS", font=("Arial", 16, "bold")).pack(pady=10)
# Create display area
self.frame = tk.Frame(root)
self.frame.pack(pady=10)
# Buttons
tk.Button(root, text="Add Spreadsheet", command=self.add_spreadsheet_window).pack(pady=5)
tk.Button(root, text="Stop", command=self.stop).pack(pady=5)
self.start_update_thread()
def load_settings(self):
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()
with open(SETTINGS_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
return {"spreadsheets": []}
def save_settings(self):
with open(SETTINGS_FILE, "w") as f:
json.dump(self.settings, f, indent=4)
def add_spreadsheet_window(self):
win = tk.Toplevel(self.root)
win.title("Add Spreadsheet")
tk.Label(win, text="Name:").grid(row=0, column=0)
name_entry = tk.Entry(win)
name_entry.grid(row=0, column=1)
tk.Label(win, text="Link (CSV export or share link):").grid(row=1, column=0)
link_entry = tk.Entry(win, width=60)
link_entry.grid(row=1, column=1)
tk.Label(win, text="Range cell (e.g., L2):").grid(row=2, column=0)
range_entry = tk.Entry(win)
range_entry.grid(row=2, column=1)
def save_sheet():
name = name_entry.get().strip()
link = link_entry.get().strip()
cell = range_entry.get().strip().upper()
if name and link and cell:
self.settings["spreadsheets"].append({
"name": name,
"link": link,
"cell": cell
})
self.save_settings()
self.add_go_nogo_label(name)
win.destroy()
tk.Button(win, text="Save", command=save_sheet).grid(row=3, column=0, columnspan=2, pady=10)
def add_go_nogo_label(self, name):
if name not in self.go_nogo_labels:
label = tk.Label(self.frame, text=f"{name}: ---", font=("Arial", 14), width=25)
label.pack(pady=2)
self.go_nogo_labels[name] = label
def update_labels(self):
for sheet in self.settings["spreadsheets"]:
name = sheet["name"]
link = sheet["link"]
cell = sheet["cell"]
# Convert normal sheet link to CSV export link if needed
if "/edit" in link and "export" not in link:
link = link.split("/edit")[0] + "/gviz/tq?tqx=out:csv"
try:
r = requests.get(link, timeout=5)
if r.status_code == 200:
content = r.text
if name not in self.last_data or self.last_data[name] != content:
self.last_data[name] = content
# Just read raw content and extract cell text if possible
value = self.extract_cell_value(content, cell)
self.update_label_color(name, value)
except Exception as e:
print(f"Error updating {name}: {e}")
def extract_cell_value(self, csv_data, cell):
# Simple CSV parser to get cell data like L2
try:
rows = [r.split(",") for r in csv_data.splitlines() if r.strip()]
col = ord(cell[0]) - 65
row = int(cell[1:]) - 1
return rows[row][col].strip().upper()
except Exception:
self.text.config(text="Invalid time")
write_countdown_html(self.mission_name, "Invalid time")
return "ERR"
def update_label_color(self, name, value):
label = self.go_nogo_labels.get(name)
if not label:
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"
if "GO" in value:
label.config(text=f"{name}: GO", bg="green", fg="white")
elif "NO" in value:
label.config(text=f"{name}: NO GO", bg="red", fg="white")
else:
timer_text = self.text.cget("text")
label.config(text=f"{name}: ---", bg="gray", fg="black")
self.text.config(text=timer_text)
write_countdown_html(self.mission_name, timer_text)
def start_update_thread(self):
threading.Thread(target=self.update_loop, daemon=True).start()
# 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
def update_loop(self):
while self.running:
self.update_labels()
time.sleep(0.1)
self.root.after(200, self.update_clock)
def stop(self):
self.running = False
self.root.destroy()
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()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -25,7 +25,7 @@ missing module named multiprocessing.BufferTooShort - imported by multiprocessin
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional), posixpath (optional)
missing module named resource - imported by posix (top-level)
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)

View File

@@ -154,6 +154,7 @@ imports:
&#8226; <a href="#genericpath">genericpath</a>
&#8226; <a href="#heapq">heapq</a>
&#8226; <a href="#io">io</a>
&#8226; <a href="#json">json</a>
&#8226; <a href="#keyword">keyword</a>
&#8226; <a href="#linecache">linecache</a>
&#8226; <a href="#locale">locale</a>
@@ -9422,7 +9423,6 @@ imports:
&#8226; <a href="#importlib">importlib</a>
&#8226; <a href="#importlib._bootstrap">importlib._bootstrap</a>
&#8226; <a href="#importlib._bootstrap_external">importlib._bootstrap_external</a>
&#8226; <a href="#importlib.machinery">importlib.machinery</a>
&#8226; <a href="#sys">sys</a>
&#8226; <a href="#warnings">warnings</a>
@@ -9557,8 +9557,7 @@ imports:
</div>
<div class="import">
imported by:
<a href="#importlib">importlib</a>
&#8226; <a href="#importlib.abc">importlib.abc</a>
<a href="#importlib.abc">importlib.abc</a>
&#8226; <a href="#inspect">inspect</a>
&#8226; <a href="#pkgutil">pkgutil</a>
&#8226; <a href="#py_compile">py_compile</a>
@@ -10141,6 +10140,7 @@ imported by:
&#8226; <a href="#json.decoder">json.decoder</a>
&#8226; <a href="#json.encoder">json.encoder</a>
&#8226; <a href="#json.scanner">json.scanner</a>
&#8226; <a href="#main.py">main.py</a>
&#8226; <a href="#requests.compat">requests.compat</a>
&#8226; <a href="#urllib3._request_methods">urllib3._request_methods</a>
&#8226; <a href="#urllib3.contrib.emscripten.fetch">urllib3.contrib.emscripten.fetch</a>

BIN
dist/exe/0.3.0/main.exe vendored Normal file

Binary file not shown.

BIN
dist/installers/0.3.0/RLCInstaller.exe vendored Normal file

Binary file not shown.

344
main.py
View File

@@ -6,6 +6,7 @@ import requests
import csv
import io
import os
import json
# Get the user's Documents folder (cross-platform)
@@ -20,26 +21,79 @@ 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"
session = requests.Session()
appVersion = "0.2.1"
appVersion = "0.3.0"
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
# Default settings
DEFAULT_SETTINGS = {
"mode": "spreadsheet", # or 'buttons'
"sheet_link": SHEET_LINK,
# rows are 1-based as shown in spreadsheet; default 2,3,4 -> indices 1,2,3
"range_row": 2,
"weather_row": 3,
"vehicle_row": 4,
"column": 12 # 1-based column (default column 12 -> index 11)
}
def load_settings():
try:
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, 'r', encoding='utf-8') as fh:
return json.load(fh)
except Exception:
pass
# ensure default saved
save_settings(DEFAULT_SETTINGS)
return DEFAULT_SETTINGS.copy()
def save_settings(s):
try:
with open(SETTINGS_FILE, 'w', encoding='utf-8') as fh:
json.dump(s, fh, indent=2)
except Exception:
pass
# -------------------------
# Fetch Go/No-Go Data
# -------------------------
def fetch_gonogo():
"""Fetch Go/No-Go parameters from L2, L3, L4 (rows 2,3,4; col 12)"""
"""Fetch Go/No-Go parameters either from configured spreadsheet or return manual button values."""
settings = load_settings()
mode = settings.get('mode', 'spreadsheet')
# If manual mode, read values from a runtime stash (set by the GUI buttons)
if mode == 'buttons':
# stored values will be on the app class; fallback to N/A
try:
return [getattr(fetch_gonogo, 'manual_range', 'N/A'),
getattr(fetch_gonogo, 'manual_weather', 'N/A'),
getattr(fetch_gonogo, 'manual_vehicle', 'N/A')]
except Exception:
return ['N/A', 'N/A', 'N/A']
# spreadsheet mode
link = settings.get('sheet_link', SHEET_LINK)
col = max(1, int(settings.get('column', 12))) - 1
rows = [int(settings.get('range_row', 2)) - 1,
int(settings.get('weather_row', 3)) - 1,
int(settings.get('vehicle_row', 4)) - 1]
try:
resp = session.get(SHEET_LINK, timeout=2) # timeout for faster failure if network is slow
resp = session.get(link, timeout=3)
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
for r in rows:
val = 'N/A'
if 0 <= r < len(data) and len(data[r]) > col:
val = data[r][col]
gonogo.append(val.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 from sheet: {e}")
return ["ERROR", "ERROR", "ERROR"]
# -------------------------
@@ -142,10 +196,10 @@ 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("800x615")
# State
self.running = False
@@ -250,6 +304,28 @@ 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)
# Settings button moved next to control buttons (match size/style)
settings_btn = tk.Button(frame_buttons, text="Settings", command=self.show_settings_window, font=("Arial", 14), width=10)
settings_btn.grid(row=0, column=4, padx=6)
# Note: gonogo mode switching remains in Settings; manual buttons appear when mode == 'buttons'
# Manual Go/No-Go buttons will go next to control buttons
self.manual_frame = tk.Frame(root, bg="black")
self.manual_frame.pack(pady=6)
# Buttons now toggle current state between GO and NOGO
self.range_toggle_btn = tk.Button(self.manual_frame, text="Range: Toggle", width=12,
command=lambda: self._toggle_manual('range'))
self.weather_toggle_btn = tk.Button(self.manual_frame, text="Weather: Toggle", width=12,
command=lambda: self._toggle_manual('weather'))
self.vehicle_toggle_btn = tk.Button(self.manual_frame, text="Vehicle: Toggle", width=12,
command=lambda: self._toggle_manual('vehicle'))
# Placeholders; visibility will be controlled by settings
self.range_toggle_btn.grid(row=0, column=0, padx=4, pady=2)
self.weather_toggle_btn.grid(row=0, column=1, padx=4, pady=2)
self.vehicle_toggle_btn.grid(row=0, column=2, padx=4, pady=2)
frame_gn = tk.Frame(root, bg="black")
frame_gn.pack(pady=10)
@@ -273,11 +349,200 @@ class CountdownApp:
bg="white"
)
self.footer_label.pack(fill="x")
self.update_inputs()
# set initial manual button visibility from settings
self.update_manual_visibility()
self.update_clock()
# ----------------------------
# Settings window
# ----------------------------
def show_settings_window(self):
settings = load_settings()
win = tk.Toplevel(self.root)
win.config(bg="black")
win.title("Settings")
win.geometry("560x200")
win.transient(self.root)
# Mode selection
frame_mode = tk.Frame(win)
frame_mode.config(bg="black")
frame_mode.pack(fill='x', pady=8, padx=8)
tk.Label(frame_mode, text="Mode:", fg="white", bg="black").pack(side='left')
mode_var = tk.StringVar(value=settings.get('mode', 'spreadsheet'))
tk.Radiobutton(frame_mode, text='Spreadsheet', variable=mode_var, value='spreadsheet', fg="white", bg="black", selectcolor="black").pack(side='left', padx=8)
tk.Radiobutton(frame_mode, text='Buttons (manual)', variable=mode_var, value='buttons', fg="white", bg="black", selectcolor="black").pack(side='left', padx=8)
# Spreadsheet config
frame_sheet = tk.LabelFrame(win, text='Spreadsheet configuration', fg='white', bg='black')
frame_sheet.config(bg="black")
frame_sheet.pack(fill='x', padx=8, pady=6)
tk.Label(frame_sheet, text='Sheet link (CSV export):', fg='white', bg='black').pack(anchor='w')
sheet_entry = tk.Entry(frame_sheet, width=80, fg='white', bg='#222', insertbackground='white')
sheet_entry.pack(fill='x', padx=6, pady=4)
sheet_entry.insert(0, settings.get('sheet_link', SHEET_LINK))
# Accept cells in 'L3' format for each parameter
cell_frame = tk.Frame(frame_sheet)
cell_frame.config(bg="black")
cell_frame.pack(fill='x', padx=6, pady=2)
tk.Label(cell_frame, text='Range cell (e.g. L3):', fg='white', bg='black').grid(row=0, column=0)
range_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white')
range_cell.grid(row=0, column=1, padx=4)
# show as L3 if present, otherwise build from numeric settings
try:
if 'range_cell' in settings:
range_cell.insert(0, settings.get('range_cell'))
else:
# convert numeric row/column to cell like L3
col = settings.get('column', DEFAULT_SETTINGS['column'])
row = settings.get('range_row', DEFAULT_SETTINGS['range_row'])
# column number to letters
def col_to_letters(n):
s = ''
while n > 0:
n, r = divmod(n - 1, 26)
s = chr(ord('A') + r) + s
return s
range_cell.insert(0, f"{col_to_letters(col)}{row}")
except Exception:
range_cell.insert(0, f"L3")
tk.Label(cell_frame, text='Weather cell (e.g. L4):', fg='white', bg='black').grid(row=0, column=2)
weather_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white')
weather_cell.grid(row=0, column=3, padx=4)
try:
if 'weather_cell' in settings:
weather_cell.insert(0, settings.get('weather_cell'))
else:
col = settings.get('column', DEFAULT_SETTINGS['column'])
row = settings.get('weather_row', DEFAULT_SETTINGS['weather_row'])
def col_to_letters(n):
s = ''
while n > 0:
n, r = divmod(n - 1, 26)
s = chr(ord('A') + r) + s
return s
weather_cell.insert(0, f"{col_to_letters(col)}{row}")
except Exception:
weather_cell.insert(0, f"L4")
tk.Label(cell_frame, text='Vehicle cell (e.g. L5):', fg='white', bg='black').grid(row=0, column=4)
vehicle_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white')
vehicle_cell.grid(row=0, column=5, padx=4)
try:
if 'vehicle_cell' in settings:
vehicle_cell.insert(0, settings.get('vehicle_cell'))
else:
col = settings.get('column', DEFAULT_SETTINGS['column'])
row = settings.get('vehicle_row', DEFAULT_SETTINGS['vehicle_row'])
def col_to_letters(n):
s = ''
while n > 0:
n, r = divmod(n - 1, 26)
s = chr(ord('A') + r) + s
return s
vehicle_cell.insert(0, f"{col_to_letters(col)}{row}")
except Exception:
vehicle_cell.insert(0, f"L5")
# Manual buttons config
frame_buttons_cfg = tk.LabelFrame(win, text='Manual Go/No-Go (Buttons mode)', fg='white', bg='black')
frame_buttons_cfg.config(bg='black')
frame_buttons_cfg.pack(fill='x', padx=8, pady=6)
def set_manual(val_type, val):
# store on fetch_gonogo func for now
if val_type == 'range':
fetch_gonogo.manual_range = val
elif val_type == 'weather':
fetch_gonogo.manual_weather = val
elif val_type == 'vehicle':
fetch_gonogo.manual_vehicle = val
# helper to set manual and update UI from main app
def set_manual_and_update(val_type, val):
set_manual(val_type, val)
# update labels and write html
self.gonogo_values = fetch_gonogo()
# update GUI labels immediately
self.range_label.config(text=f"RANGE: {self.gonogo_values[0]}", fg=get_status_color(self.gonogo_values[0]))
self.weather_label.config(text=f"WEATHER: {self.gonogo_values[1]}", fg=get_status_color(self.gonogo_values[1]))
self.vehicle_label.config(text=f"VEHICLE: {self.gonogo_values[2]}", fg=get_status_color(self.gonogo_values[2]))
write_gonogo_html(self.gonogo_values)
# Save/Cancel
def cell_to_rc(cell_str):
s = (cell_str or '').strip().upper()
if not s:
return None, None
# split letters and digits
letters = ''
digits = ''
for ch in s:
if ch.isalpha():
letters += ch
elif ch.isdigit():
digits += ch
if not digits:
return None, None
# convert letters to number
col = 0
for ch in letters:
col = col * 26 + (ord(ch) - ord('A') + 1)
row = int(digits)
return row, col
def on_save():
# parse cells
r_row, r_col = cell_to_rc(range_cell.get())
w_row, w_col = cell_to_rc(weather_cell.get())
v_row, v_col = cell_to_rc(vehicle_cell.get())
# fallbacks
if r_row is None:
r_row = DEFAULT_SETTINGS['range_row']
if w_row is None:
w_row = DEFAULT_SETTINGS['weather_row']
if v_row is None:
v_row = DEFAULT_SETTINGS['vehicle_row']
# determine column to use (prefer range column, else weather, else vehicle, else default)
col_val = r_col or w_col or v_col or DEFAULT_SETTINGS['column']
new_settings = {
'mode': mode_var.get(),
'sheet_link': sheet_entry.get().strip() or SHEET_LINK,
'range_row': int(r_row),
'weather_row': int(w_row),
'vehicle_row': int(v_row),
'column': int(col_val),
# persist the textual cells for convenience
'range_cell': range_cell.get().strip().upper(),
'weather_cell': weather_cell.get().strip().upper(),
'vehicle_cell': vehicle_cell.get().strip().upper(),
# persist manual values if present
'manual_range': getattr(fetch_gonogo, 'manual_range', None),
'manual_weather': getattr(fetch_gonogo, 'manual_weather', None),
'manual_vehicle': getattr(fetch_gonogo, 'manual_vehicle', None)
}
save_settings(new_settings)
# update immediately
self.gonogo_values = fetch_gonogo()
write_gonogo_html(self.gonogo_values)
# update manual visibility in main UI
self.update_manual_visibility()
win.destroy()
def on_cancel():
win.destroy()
btn_frame = tk.Frame(win)
btn_frame = tk.Frame(win, bg='black')
btn_frame.pack(fill='x', pady=8)
tk.Button(btn_frame, text='Save', command=on_save, fg='white', bg='#333', activebackground='#444').pack(side='right', padx=8)
tk.Button(btn_frame, text='Cancel', command=on_cancel, fg='white', bg='#333', activebackground='#444').pack(side='right')
# ----------------------------
# Update input visibility based on mode
# ----------------------------
@@ -293,6 +558,61 @@ class CountdownApp:
self.seconds_entry.config(state="disabled")
self.clock_entry.config(state="normal")
# ----------------------------
# Manual controls & helpers
# ----------------------------
def set_manual(self, which, val):
# normalize
v = (val or '').strip().upper()
if which == 'range':
fetch_gonogo.manual_range = v
elif which == 'weather':
fetch_gonogo.manual_weather = v
elif which == 'vehicle':
fetch_gonogo.manual_vehicle = v
# update GUI and HTML
self.gonogo_values = fetch_gonogo()
try:
self.range_label.config(text=f"RANGE: {self.gonogo_values[0]}", fg=get_status_color(self.gonogo_values[0]))
self.weather_label.config(text=f"WEATHER: {self.gonogo_values[1]}", fg=get_status_color(self.gonogo_values[1]))
self.vehicle_label.config(text=f"VEHICLE: {self.gonogo_values[2]}", fg=get_status_color(self.gonogo_values[2]))
except Exception:
pass
write_gonogo_html(self.gonogo_values)
# persist manual values immediately so they survive restarts
try:
s = load_settings()
s['manual_range'] = getattr(fetch_gonogo, 'manual_range', s.get('manual_range'))
s['manual_weather'] = getattr(fetch_gonogo, 'manual_weather', s.get('manual_weather'))
s['manual_vehicle'] = getattr(fetch_gonogo, 'manual_vehicle', s.get('manual_vehicle'))
save_settings(s)
except Exception:
pass
def update_manual_visibility(self):
s = load_settings()
mode = s.get('mode', 'spreadsheet')
visible = (mode == 'buttons')
# show or hide manual frame
if visible:
self.manual_frame.pack(pady=6)
else:
self.manual_frame.pack_forget()
def _toggle_manual(self, which):
# get current values (Range, Weather, Vehicle)
cur = fetch_gonogo()
# map which to index
idx_map = {'range': 0, 'weather': 1, 'vehicle': 2}
idx = idx_map.get(which, 0)
try:
cur_val = (cur[idx] or '').strip().upper()
except Exception:
cur_val = 'N/A'
# toggle: if GO -> NOGO, else -> GO
new_val = 'NOGO' if cur_val == 'GO' else 'GO'
self.set_manual(which, new_val)
# ----------------------------
# Control logic
# ----------------------------