245
main.py
245
main.py
@@ -27,7 +27,7 @@ COUNTDOWN_HTML = os.path.join(app_folder, "countdown.html")
|
|||||||
GONOGO_HTML = os.path.join(app_folder, "gonogo.html")
|
GONOGO_HTML = os.path.join(app_folder, "gonogo.html")
|
||||||
SHEET_LINK = ""
|
SHEET_LINK = ""
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
appVersion = "0.6.1"
|
appVersion = "0.7.0"
|
||||||
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
|
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
|
||||||
|
|
||||||
# Default settings
|
# Default settings
|
||||||
@@ -53,6 +53,8 @@ DEFAULT_SETTINGS.setdefault("gn_bg_color", "#111111")
|
|||||||
DEFAULT_SETTINGS.setdefault("gn_border_color", "#FFFFFF")
|
DEFAULT_SETTINGS.setdefault("gn_border_color", "#FFFFFF")
|
||||||
DEFAULT_SETTINGS.setdefault("gn_go_color", "#00FF00")
|
DEFAULT_SETTINGS.setdefault("gn_go_color", "#00FF00")
|
||||||
DEFAULT_SETTINGS.setdefault("gn_nogo_color", "#FF0000")
|
DEFAULT_SETTINGS.setdefault("gn_nogo_color", "#FF0000")
|
||||||
|
DEFAULT_SETTINGS.setdefault("gn_caution_color", "#FFA500") # orange
|
||||||
|
DEFAULT_SETTINGS.setdefault("html_gn_caution_color", "#FFA500")
|
||||||
DEFAULT_SETTINGS.setdefault("gn_font_px", 20)
|
DEFAULT_SETTINGS.setdefault("gn_font_px", 20)
|
||||||
DEFAULT_SETTINGS.setdefault("appearance_mode", "dark")
|
DEFAULT_SETTINGS.setdefault("appearance_mode", "dark")
|
||||||
|
|
||||||
@@ -133,16 +135,28 @@ def fetch_gonogo():
|
|||||||
mode = settings.get("mode", "spreadsheet")
|
mode = settings.get("mode", "spreadsheet")
|
||||||
# If manual mode, read values from a runtime stash (set by the GUI buttons)
|
# If manual mode, read values from a runtime stash (set by the GUI buttons)
|
||||||
if mode == "buttons":
|
if mode == "buttons":
|
||||||
# stored values will be on the app class; fallback to N/A
|
# stored values will be on the function object (set by the GUI dialog/buttons)
|
||||||
try:
|
try:
|
||||||
return [
|
range_val = getattr(fetch_gonogo, "manual_range", "N/A")
|
||||||
getattr(fetch_gonogo, "manual_range", "N/A"),
|
weather_val = getattr(fetch_gonogo, "manual_weather", "N/A")
|
||||||
getattr(fetch_gonogo, "manual_weather", "N/A"),
|
vehicle_val = getattr(fetch_gonogo, "manual_vehicle", "N/A")
|
||||||
getattr(fetch_gonogo, "manual_vehicle", "N/A"),
|
|
||||||
]
|
# If values are numeric strings or numbers, we keep them numeric (so formatting + coloring works)
|
||||||
|
# convert empty strings -> "N/A"
|
||||||
|
def normv(v):
|
||||||
|
if v is None:
|
||||||
|
return "N/A"
|
||||||
|
s = str(v).strip()
|
||||||
|
if s == "":
|
||||||
|
return "N/A"
|
||||||
|
return s
|
||||||
|
|
||||||
|
return [normv(range_val), normv(weather_val), normv(vehicle_val)]
|
||||||
except Exception:
|
except Exception:
|
||||||
return ["N/A", "N/A", "N/A"]
|
return ["N/A", "N/A", "N/A"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# spreadsheet mode
|
# spreadsheet mode
|
||||||
link = settings.get("sheet_link", SHEET_LINK)
|
link = settings.get("sheet_link", SHEET_LINK)
|
||||||
col = max(1, int(settings.get("column", 12))) - 1
|
col = max(1, int(settings.get("column", 12))) - 1
|
||||||
@@ -172,34 +186,79 @@ def fetch_gonogo():
|
|||||||
# Helper for color
|
# Helper for color
|
||||||
# -------------------------
|
# -------------------------
|
||||||
def get_status_color(status):
|
def get_status_color(status):
|
||||||
"""Return color name for a Go/No-Go status string."""
|
"""
|
||||||
|
Accepts status which may be:
|
||||||
|
- a percentage string/number (e.g. '82', '82.0', 82)
|
||||||
|
- 'GO', 'NO-GO', 'CAUTION' or variants
|
||||||
|
- anything else -> white
|
||||||
|
Returns CSS color names / hex strings.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
s = str(status or "").strip().upper()
|
if status is None:
|
||||||
# normalize to letters only so variants like 'NO GO', 'NO-GO', 'NOGO' match
|
return "white"
|
||||||
norm = re.sub(r"[^A-Z]", "", s)
|
s = str(status).strip()
|
||||||
|
# Try numeric percentage first
|
||||||
|
try:
|
||||||
|
pct = float(s.replace("%", ""))
|
||||||
|
|
||||||
|
# Bound it to 0..100
|
||||||
|
pct = max(0.0, min(100.0, pct))
|
||||||
|
if pct >= 75.0:
|
||||||
|
return "green"
|
||||||
|
if pct >= 50.0:
|
||||||
|
return "orange"
|
||||||
|
return "red"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Non-numeric: interpret standard words
|
||||||
|
norm = re.sub(r"[^A-Z]", "", s.upper())
|
||||||
if norm == "GO":
|
if norm == "GO":
|
||||||
return "green"
|
return "green"
|
||||||
if norm == "NOGO":
|
if norm == "NOGO" or norm == "NOGO" or norm == "NOGO":
|
||||||
return "red"
|
return "red"
|
||||||
# fallback: treat unknown/empty as white
|
if norm == "CAUTION" or norm == "CAUT":
|
||||||
|
return "orange"
|
||||||
return "white"
|
return "white"
|
||||||
except Exception:
|
except Exception:
|
||||||
return "white"
|
return "white"
|
||||||
|
|
||||||
|
|
||||||
def format_status_display(status):
|
def format_status_display(status):
|
||||||
|
"""
|
||||||
|
Formats the status for display:
|
||||||
|
- numeric -> 'NN%' (rounded as needed)
|
||||||
|
- 'NO-GO'/'GO' -> canonical formatting
|
||||||
|
- otherwise return original string
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
s = str(status or "").strip().upper()
|
if status is None:
|
||||||
norm = re.sub(r"[^A-Z]", "", s)
|
return "N/A"
|
||||||
|
s = str(status).strip()
|
||||||
|
# numeric
|
||||||
|
try:
|
||||||
|
pct = float(s)
|
||||||
|
# bound and show as integer if whole, else one decimal
|
||||||
|
pct = max(0.0, min(100.0, pct))
|
||||||
|
if abs(pct - round(pct)) < 0.001:
|
||||||
|
return f"{int(round(pct))}%"
|
||||||
|
return f"{pct:.1f}%"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# canonical text forms
|
||||||
|
norm = re.sub(r"[^A-Z]", "", s.upper())
|
||||||
if norm == "GO":
|
if norm == "GO":
|
||||||
return "GO"
|
return "GO"
|
||||||
if norm == "NOGO":
|
if norm == "NOGO":
|
||||||
return "NO-GO"
|
return "NO-GO"
|
||||||
|
if norm == "CAUTION":
|
||||||
|
return "CAUTION"
|
||||||
|
# fallback to raw
|
||||||
return s
|
return s
|
||||||
except Exception:
|
except Exception:
|
||||||
return str(status or "")
|
return str(status or "")
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Write Countdown HTML
|
# Write Countdown HTML
|
||||||
# -------------------------
|
# -------------------------
|
||||||
@@ -222,6 +281,7 @@ def write_countdown_html(mission_name, timer_text):
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="refresh" content="1">
|
||||||
<style>
|
<style>
|
||||||
body {{
|
body {{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -236,11 +296,6 @@ body {{
|
|||||||
#mission {{ font-size: {mission_px}px; margin-bottom: 0; }}
|
#mission {{ font-size: {mission_px}px; margin-bottom: 0; }}
|
||||||
#timer {{ font-size: {timer_px}px; margin-bottom: 40px; }}
|
#timer {{ font-size: {timer_px}px; margin-bottom: 40px; }}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
|
||||||
setTimeout(() => {{
|
|
||||||
location.href = location.href.split('?')[0] + '?t=' + Date.now();
|
|
||||||
}}, 1000);
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{mission_div_hidden}
|
{mission_div_hidden}
|
||||||
@@ -251,30 +306,28 @@ body {{
|
|||||||
f.write(html)
|
f.write(html)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Write Go/No-Go HTML
|
|
||||||
# -------------------------
|
|
||||||
def write_gonogo_html(gonogo_values=None):
|
def write_gonogo_html(gonogo_values=None):
|
||||||
if gonogo_values is None:
|
if gonogo_values is None:
|
||||||
gonogo_values = ["N/A", "N/A", "N/A"]
|
gonogo_values = ["N/A", "N/A", "N/A"]
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
# Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility
|
|
||||||
bg = s.get("html_bg_color", s.get("bg_color", "#000000"))
|
bg = s.get("html_bg_color", s.get("bg_color", "#000000"))
|
||||||
text = s.get("html_text_color", s.get("text_color", "#FFFFFF"))
|
text = s.get("html_text_color", s.get("text_color", "#FFFFFF"))
|
||||||
font = s.get("html_font_family", s.get("font_family", "Consolas, monospace"))
|
font = s.get("html_font_family", s.get("font_family", "Consolas, monospace"))
|
||||||
gn_bg = s.get("html_gn_bg_color", s.get("gn_bg_color", "#111111"))
|
gn_bg = s.get("html_gn_bg_color", s.get("gn_bg_color", "#111111"))
|
||||||
gn_border = s.get("html_gn_border_color", s.get("gn_border_color", "#FFFFFF"))
|
gn_border = s.get("html_gn_border_color", s.get("gn_border_color", "#FFFFFF"))
|
||||||
gn_go = s.get("html_gn_go_color", s.get("gn_go_color", "#00FF00"))
|
|
||||||
gn_nogo = s.get("html_gn_nogo_color", s.get("gn_nogo_color", "#FF0000"))
|
|
||||||
gn_px = int(s.get("html_gn_font_px", s.get("gn_font_px", 28)))
|
gn_px = int(s.get("html_gn_font_px", s.get("gn_font_px", 28)))
|
||||||
# normalize and format display values so variants like 'NO GO' become 'NO-GO'
|
|
||||||
disp0 = format_status_display(gonogo_values[0])
|
|
||||||
disp1 = format_status_display(gonogo_values[1])
|
|
||||||
disp2 = format_status_display(gonogo_values[2])
|
|
||||||
n0 = re.sub(r"[^A-Z]", "", (str(gonogo_values[0] or "")).strip().upper())
|
|
||||||
n1 = re.sub(r"[^A-Z]", "", (str(gonogo_values[1] or "")).strip().upper())
|
|
||||||
n2 = re.sub(r"[^A-Z]", "", (str(gonogo_values[2] or "")).strip().upper())
|
|
||||||
|
|
||||||
|
# Prepare display text and color for each item
|
||||||
|
disp = [format_status_display(v) for v in gonogo_values]
|
||||||
|
|
||||||
|
def pick_css_color(v):
|
||||||
|
return get_status_color(v)
|
||||||
|
|
||||||
|
c0 = pick_css_color(gonogo_values[0])
|
||||||
|
c1 = pick_css_color(gonogo_values[1])
|
||||||
|
c2 = pick_css_color(gonogo_values[2])
|
||||||
|
|
||||||
|
# Inline style approach per box to ensure color picks up numeric/word cases
|
||||||
html = f"""<!DOCTYPE html>
|
html = f"""<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -301,8 +354,6 @@ body {{
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: {gn_bg};
|
background-color: {gn_bg};
|
||||||
}}
|
}}
|
||||||
.go {{ color: {gn_go}; }}
|
|
||||||
.nogo {{ color: {gn_nogo}; }}
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() => {{
|
setTimeout(() => {{
|
||||||
@@ -312,9 +363,9 @@ body {{
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="gonogo">
|
<div id="gonogo">
|
||||||
<div class="status-box {'go' if n0=='GO' else 'nogo'}">Range: {disp0}</div>
|
<div class="status-box" style="color: {c0};">Range: {disp[0]}</div>
|
||||||
<div class="status-box {'go' if n2=='GO' else 'nogo'}">Vehicle: {disp2}</div>
|
<div class="status-box" style="color: {c2};">Vehicle: {disp[2]}</div>
|
||||||
<div class="status-box {'go' if n1=='GO' else 'nogo'}">Weather: {disp1}</div>
|
<div class="status-box" style="color: {c1};">Weather: {disp[1]}</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
@@ -322,6 +373,7 @@ body {{
|
|||||||
f.write(html)
|
f.write(html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Countdown App
|
# Countdown App
|
||||||
# -------------------------
|
# -------------------------
|
||||||
@@ -670,6 +722,16 @@ class CountdownApp:
|
|||||||
command=lambda: self._toggle_manual("vehicle"),
|
command=lambda: self._toggle_manual("vehicle"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# new button to open manual percentage dialog
|
||||||
|
self.manual_percent_btn = tk.Button(
|
||||||
|
self.manual_frame,
|
||||||
|
text="Manual Percentages...",
|
||||||
|
width=18,
|
||||||
|
command=self.open_manual_percent_dialog,
|
||||||
|
)
|
||||||
|
self.manual_percent_btn.grid(row=1, column=0, columnspan=3, pady=(6,0))
|
||||||
|
|
||||||
|
|
||||||
# Placeholders; visibility will be controlled by settings
|
# Placeholders; visibility will be controlled by settings
|
||||||
self.range_toggle_btn.grid(row=0, column=0, padx=4, pady=2)
|
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.weather_toggle_btn.grid(row=0, column=1, padx=4, pady=2)
|
||||||
@@ -713,6 +775,101 @@ class CountdownApp:
|
|||||||
pass
|
pass
|
||||||
self.update_clock()
|
self.update_clock()
|
||||||
|
|
||||||
|
def open_manual_percent_dialog(self):
|
||||||
|
"""Open dialog to set manual percentages for Range, Weather, Vehicle.
|
||||||
|
Stores values on fetch_gonogo function so fetch_gonogo() reads them."""
|
||||||
|
dlg = tk.Toplevel(self.root)
|
||||||
|
dlg.transient(self.root)
|
||||||
|
dlg.title("Manual Percentages (0-100)")
|
||||||
|
dlg.geometry("360x180")
|
||||||
|
ssettings = load_settings()
|
||||||
|
mode_local = ssettings.get("appearance_mode", "dark")
|
||||||
|
if mode_local == "dark":
|
||||||
|
dlg_bg = "#000000"
|
||||||
|
dlg_fg = "#FFFFFF"
|
||||||
|
entry_bg = "#222222"
|
||||||
|
btn_bg = "#FFFFFF"
|
||||||
|
btn_fg = "#000000"
|
||||||
|
else:
|
||||||
|
dlg_bg = "#FFFFFF"
|
||||||
|
dlg_fg = "#000000"
|
||||||
|
entry_bg = "#b4b4b4"
|
||||||
|
btn_bg = "#000000"
|
||||||
|
btn_fg = "#FFFFFF"
|
||||||
|
dlg.config(bg=dlg_bg)
|
||||||
|
|
||||||
|
tk.Label(dlg, text="Weather (%)", fg=dlg_fg, bg=dlg_bg).pack(pady=(8, 0))
|
||||||
|
w_entry = tk.Entry(dlg, bg=entry_bg, fg=dlg_fg, insertbackground=dlg_fg)
|
||||||
|
w_entry.pack()
|
||||||
|
|
||||||
|
# populate with existing manual values (if any)
|
||||||
|
try:
|
||||||
|
w = getattr(fetch_gonogo, "manual_weather", "")
|
||||||
|
w_entry.insert(0, str(w))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_save():
|
||||||
|
def clean_val(x):
|
||||||
|
try:
|
||||||
|
if x is None:
|
||||||
|
return ""
|
||||||
|
s = str(x).strip()
|
||||||
|
if s == "":
|
||||||
|
return ""
|
||||||
|
# try to parse numeric; if numeric, store as number-ish string
|
||||||
|
f = float(s)
|
||||||
|
# clamp 0..100
|
||||||
|
f = max(0.0, min(100.0, f))
|
||||||
|
# store as int if whole
|
||||||
|
if abs(f - round(f)) < 0.001:
|
||||||
|
return str(int(round(f)))
|
||||||
|
return f"{f:.1f}"
|
||||||
|
except Exception:
|
||||||
|
# store raw (allow GO/NOGO legacy)
|
||||||
|
return s
|
||||||
|
|
||||||
|
try:
|
||||||
|
fetch_gonogo.manual_weather = clean_val(w_entry.get())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# update local labels immediately
|
||||||
|
self.gonogo_values = fetch_gonogo()
|
||||||
|
self._refresh_gonogo_labels()
|
||||||
|
dlg.destroy()
|
||||||
|
|
||||||
|
btnf = tk.Frame(dlg, bg=dlg_bg)
|
||||||
|
btnf.pack(fill="x", pady=8)
|
||||||
|
tk.Button(btnf, text="Save", command=do_save, width=10, bg=btn_bg, fg=btn_fg).pack(
|
||||||
|
side="right", padx=8
|
||||||
|
)
|
||||||
|
tk.Button(btnf, text="Cancel", command=dlg.destroy, width=10, bg=btn_bg, fg=btn_fg).pack(
|
||||||
|
side="right"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_gonogo_labels(self):
|
||||||
|
# reads current gonogo_values and updates the three label widgets with color
|
||||||
|
try:
|
||||||
|
vals = self.gonogo_values
|
||||||
|
# in case gonogo_values is stale, fetch again
|
||||||
|
if not vals or len(vals) < 3:
|
||||||
|
vals = fetch_gonogo()
|
||||||
|
self.gonogo_values = vals
|
||||||
|
# format and apply colors
|
||||||
|
r_disp = format_status_display(vals[0])
|
||||||
|
w_disp = format_status_display(vals[1])
|
||||||
|
v_disp = format_status_display(vals[2])
|
||||||
|
self.range_label.config(text=f"RANGE: {r_disp}", fg=get_status_color(vals[0]))
|
||||||
|
self.weather_label.config(text=f"WEATHER: {w_disp}", fg=get_status_color(vals[1]))
|
||||||
|
self.vehicle_label.config(text=f"VEHICLE: {v_disp}", fg=get_status_color(vals[2]))
|
||||||
|
# also write out the html so external display updates
|
||||||
|
write_gonogo_html(vals)
|
||||||
|
self.last_gonogo_update = time.time()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Settings window
|
# Settings window
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
@@ -1323,14 +1480,8 @@ class CountdownApp:
|
|||||||
self.weather_label.config(
|
self.weather_label.config(
|
||||||
text=f"WEATHER: {display_weather}", bg=bg, font=(font_family, gn_px)
|
text=f"WEATHER: {display_weather}", bg=bg, font=(font_family, gn_px)
|
||||||
)
|
)
|
||||||
wv = (weather_val or "").strip().upper()
|
color = get_status_color(weather_val)
|
||||||
wnorm = re.sub(r"[^A-Z]", "", wv)
|
self.weather_label.config(fg=color)
|
||||||
if wnorm == "GO":
|
|
||||||
self.weather_label.config(fg=gn_go)
|
|
||||||
elif wnorm == "NOGO":
|
|
||||||
self.weather_label.config(fg=gn_nogo)
|
|
||||||
else:
|
|
||||||
self.weather_label.config(fg=text)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ For setting up the HTML sources to use in OBS, or similiar softwares. Follow the
|
|||||||
2. Select "Local file"
|
2. Select "Local file"
|
||||||
3. Select html file
|
3. Select html file
|
||||||
4. Check both "Shutdown source when not visible" and "Refresh browser when scene becomes active"
|
4. Check both "Shutdown source when not visible" and "Refresh browser when scene becomes active"
|
||||||
|
5. Install the extension "xObsBrowserAutoRefresh"
|
||||||
|
- Download from https://obsproject.com/forum/resources/xobsbrowserautorefresh-timed-automatic-browser-source-refreshing.1677
|
||||||
|
- Set auto refresh times to whatever you desire.
|
||||||
|
|
||||||
INSTALL INSTRUCTIONS
|
INSTALL INSTRUCTIONS
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user