Merge pull request #17 from HamsterSpaceNerd3000/dev

Dev
This commit is contained in:
HamsterSpaceNerd3000
2025-11-20 21:58:48 -05:00
committed by GitHub
4 changed files with 260 additions and 46 deletions

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

Binary file not shown.

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

Binary file not shown.

275
main.py
View File

@@ -30,6 +30,8 @@ session = requests.Session()
appVersion = "0.7.0"
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
concerns_list = []
# Default settings
DEFAULT_SETTINGS = {
"mode": "spreadsheet",
@@ -37,8 +39,9 @@ DEFAULT_SETTINGS = {
"range_row": 2,
"weather_row": 3,
"vehicle_row": 4,
"major_concerns_row": 9,
"column": 12,
"hide_mission_name": False
"hide_mission_name": True
}
# default timezone: 'local' uses system local tz, otherwise an IANA name or 'UTC'
DEFAULT_SETTINGS.setdefault("timezone", "local")
@@ -91,6 +94,11 @@ DEFAULT_SETTINGS.setdefault("html_gn_font_px", DEFAULT_SETTINGS.get("gn_font_px"
# Auto-hold times: list of seconds before T at which timer should automatically enter hold
DEFAULT_SETTINGS.setdefault("auto_hold_times", [])
DEFAULT_SETTINGS.setdefault("major_concerns_cell", "L9")
# How often (seconds) to refresh major concerns from the sheet
DEFAULT_SETTINGS.setdefault("concerns_update_interval", 2)
# A small list of common timezone choices.
TIMEZONE_CHOICES = [
"local",
@@ -110,7 +118,12 @@ def load_settings():
try:
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, "r", encoding="utf-8") as fh:
return json.load(fh)
loaded = json.load(fh)
# Merge loaded settings with defaults so missing keys get sane values
merged = DEFAULT_SETTINGS.copy()
if isinstance(loaded, dict):
merged.update(loaded)
return merged
except Exception:
pass
# ensure default saved
@@ -182,9 +195,6 @@ def fetch_gonogo():
return ["ERROR", "ERROR", "ERROR"]
# -------------------------
# Helper for color
# -------------------------
def get_status_color(status):
"""
Accepts status which may be:
@@ -259,9 +269,142 @@ def format_status_display(status):
except Exception:
return str(status or "")
# -------------------------
# Write Countdown HTML
# -------------------------
def fetch_major_concerns_from_sheet():
"""Fetch major concerns from the same CSV sheet used by fetch_gonogo().
Returns a list of non-empty strings. This mirrors the safe access
pattern in `fetch_gonogo()`:
- uses `session.get()` to fetch the CSV
- respects `major_concerns_cell` (A1-style) if configured
- splits multi-item cell values on newlines or semicolons
- falls back to `major_concerns_row` and returns non-empty cells from that row
"""
settings = load_settings()
link = settings.get("sheet_link", SHEET_LINK)
cell_ref = settings.get("major_concerns_cell", "").upper().strip()
def col_letters_to_index(letters):
result = 0
for c in letters:
result = result * 26 + (ord(c.upper()) - ord('A') + 1)
return result - 1
try:
resp = session.get(link, timeout=3)
resp.raise_for_status()
reader = csv.reader(io.StringIO(resp.text))
data = list(reader)
try:
print(f"[DEBUG] fetch_major_concerns_from_sheet: cell={cell_ref!r}, rows={len(data)}")
except Exception:
pass
concerns = []
if cell_ref:
# allow multiple cell refs separated by comma/semicolon/whitespace, e.g. "L9, L10"
tokens = [t.strip() for t in re.split(r"[;,\s]+", cell_ref) if t.strip()]
for tok in tokens:
m = re.match(r"^([A-Z]+)(\d+)$", tok)
if not m:
continue
col_letters, row_str = m.groups()
row_idx = int(row_str) - 1
col_idx = col_letters_to_index(col_letters)
if 0 <= row_idx < len(data):
row = data[row_idx]
if 0 <= col_idx < len(row):
val = row[col_idx].strip()
if val:
# split cell contents on newline, semicolon or comma into separate concerns
parts = re.split(r"[\r\n,;]+", val)
for p in parts:
p = p.strip()
if p:
concerns.append(p)
# fallback: use configured major_concerns_row
if not concerns:
row_idx = int(settings.get("major_concerns_row", DEFAULT_SETTINGS.get("major_concerns_row", 9))) - 1
if 0 <= row_idx < len(data):
concerns = [c.strip() for c in data[row_idx] if c.strip()]
return concerns if concerns else ["No concerns listed"]
except Exception as e:
print(f"[ERROR] Failed to fetch major concerns from sheet: {e}")
return ["ERROR fetching concerns"]
def fetch_major_concerns():
"""Compatibility wrapper for existing callers.
Calls `fetch_major_concerns_from_sheet()` so other parts of the code
that expect `fetch_major_concerns()` continue to work.
"""
try:
return fetch_major_concerns_from_sheet()
except Exception:
return ["ERROR fetching concerns"]
def write_major_concerns_html(concerns_list=None):
if concerns_list is None:
concerns_list = []
s = load_settings()
bg = s.get("html_bg_color", s.get("bg_color", "#000000"))
text = s.get("html_text_color", s.get("text_color", "#FFFFFF"))
font = s.get("html_font_family", s.get("font_family", "Consolas, monospace"))
font_size = s.get("html_mission_font_px", 32) # adjust size for concerns
# Build HTML for the concerns
concerns_html = ""
if concerns_list:
concerns_html += "<ul style='list-style-type: disc; padding-left: 40px; text-align: left;'>\n"
for concern in concerns_list:
concerns_html += f" <li>{concern}</li>\n"
concerns_html += "</ul>\n"
else:
concerns_html = "<p>No major concerns</p>"
html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="5">
<style>
body {{
margin: 0;
background-color: {bg};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: {text};
font-family: {font};
font-size: {font_size}px;
}}
ul {{
margin: 0;
}}
</style>
</head>
<body>
<h2>Major Concerns</h2>
{concerns_html}
</body>
</html>"""
concerns_file = os.path.join(app_folder, "major_concerns.html")
with open(concerns_file, "w", encoding="utf-8") as f:
f.write(html)
def write_countdown_html(mission_name, timer_text):
s = load_settings()
# Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility
@@ -396,7 +539,11 @@ class CountdownApp:
self.mission_name = "Placeholder Mission"
# fetch_gonogo() returns [Range, Weather, Vehicle] to match gonogo.html writer
self.gonogo_values = fetch_gonogo()
self.major_concerns = fetch_major_concerns()
write_major_concerns_html(self.major_concerns)
self.last_gonogo_update = time.time()
# track last time we refreshed major concerns
self.last_concerns_update = time.time()
# track which auto-holds we've already triggered for the current run
self._auto_hold_triggered = set()
# count mode
@@ -753,6 +900,11 @@ class CountdownApp:
)
self.vehicle_label.pack()
self.concerns_label = tk.Label(
frame_gn, text=F"Major Concerns: {self.major_concerns}" , font=("Consolas", 14), fg="white", bg="black"
)
self.concerns_label.pack()
# Footer
footer_frame = tk.Frame(root, bg="black")
footer_frame.pack(side="bottom", pady=0, fill="x")
@@ -1063,6 +1215,37 @@ class CountdownApp:
except Exception:
vehicle_cell.insert(0, f"L5")
tk.Label(
cell_frame, text="Major concerns cell (e.g. L6):", fg=win_text, bg=win_bg
).grid(row=1, column=0, sticky="w")
major_concerns_cell = tk.Entry(
cell_frame,
width=8,
fg=range_cell_fg,
bg=range_cell_bg,
insertbackground=range_cell_fg,
)
major_concerns_cell.grid(row=1, column=1, padx=4)
try:
if "major_concerns_cell" in settings:
major_concerns_cell.insert(0, settings.get("major_concerns_cell"))
else:
col = settings.get("column", DEFAULT_SETTINGS["column"])
row = settings.get("major_concerns_row", DEFAULT_SETTINGS["major_concerns_row"])
def col_to_letters(n):
s = ""
while n > 0:
n, r = divmod(n - 1, 26)
s = chr(ord("A") + r) + s
return s
major_concerns_cell.insert(0, f"{col_to_letters(col)}{row}")
except Exception:
# fall back to the canonical default setting (keeps UI/defaults consistent)
major_concerns_cell.insert(0, settings.get("major_concerns_cell", DEFAULT_SETTINGS.get("major_concerns_cell", "L9")))
# Manual buttons config
frame_buttons_cfg = tk.LabelFrame(
win, text="Manual Go/No-Go (Buttons mode)", fg=win_text, bg=win_bg
@@ -1172,6 +1355,8 @@ class CountdownApp:
"range_cell": range_cell.get().strip().upper(),
"weather_cell": weather_cell.get().strip().upper(),
"vehicle_cell": vehicle_cell.get().strip().upper(),
# persist major concerns cell if provided
"major_concerns_cell": major_concerns_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),
@@ -1967,16 +2152,14 @@ class CountdownApp:
new_val = "NO-GO" if cur_val == "GO" else "GO"
self.set_manual(which, new_val)
# ----------------------------
# 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()
self._auto_hold_triggered = set()
try:
if self.mode_var.get() == "duration":
@@ -1986,41 +2169,28 @@ class CountdownApp:
total_seconds = h * 3600 + m * 60 + s
else:
now = datetime.now()
# read separate HH, MM, SS boxes
h = int(self.clock_hours_entry.get() or 0)
m = int(self.clock_minutes_entry.get() or 0)
s = int(self.clock_seconds_entry.get() or 0)
# determine timezone from settings
ssettings = load_settings()
tzname = ssettings.get(
"timezone", DEFAULT_SETTINGS.get("timezone", "local")
)
tzname = ssettings.get("timezone", DEFAULT_SETTINGS.get("timezone", "local"))
if ZoneInfo is None or tzname in (None, "", "local"):
# naive local time handling (existing behavior) — use timedelta to roll day
target_today = now.replace(
hour=h, minute=m, second=s, microsecond=0
)
target_today = now.replace(hour=h, minute=m, second=s, microsecond=0)
if target_today <= now:
target_today = target_today + timedelta(days=1)
total_seconds = (target_today - now).total_seconds()
else:
try:
tz = ZoneInfo(tzname)
# construct aware "now" in that timezone and create the target time
now_tz = datetime.now(tz)
target = now_tz.replace(
hour=h, minute=m, second=s, microsecond=0
)
# if target already passed in that tz, roll to next day
target = now_tz.replace(hour=h, minute=m, second=s, microsecond=0)
if target <= now_tz:
target = target + timedelta(days=1)
# compute total seconds using aware-datetime subtraction to avoid epoch mixing
total_seconds = (target - now_tz).total_seconds()
except Exception:
# fallback to naive local behavior
target_today = now.replace(
hour=h, minute=m, second=s, microsecond=0
)
target_today = now.replace(hour=h, minute=m, second=s, microsecond=0)
if target_today <= now:
target_today = target_today + timedelta(days=1)
total_seconds = (target_today - now).total_seconds()
@@ -2029,8 +2199,23 @@ class CountdownApp:
write_countdown_html(self.mission_name, "Invalid time")
return
self.target_time = time.time() + total_seconds
now_time = time.time()
mode = self.count_mode.get()
if mode in ("T-", "L-"):
# countdown
self.target_time = now_time + total_seconds
self.counting_up = False
self.remaining_time = total_seconds
elif mode in ("T+", "L+"):
# count up
self.target_time = now_time - total_seconds
self.counting_up = True
self.remaining_time = total_seconds
# Immediately update display
#self.update_clock()
def hold(self):
if self.running and not self.on_hold and not self.scrubbed:
@@ -2163,6 +2348,32 @@ class CountdownApp:
write_gonogo_html(self.gonogo_values)
self.last_gonogo_update = now_time
# Update Major Concerns on a configurable interval
try:
s = load_settings()
interval = float(s.get("concerns_update_interval", DEFAULT_SETTINGS.get("concerns_update_interval", 10)))
except Exception:
interval = DEFAULT_SETTINGS.get("concerns_update_interval", 10)
if now_time - getattr(self, "last_concerns_update", 0) > interval:
try:
concerns = fetch_major_concerns()
# update runtime state and HTML
self.major_concerns = concerns
write_major_concerns_html(concerns)
# update GUI label concisely
try:
display = ", ".join(concerns) if isinstance(concerns, (list, tuple)) else str(concerns)
# shorten if very long
if len(display) > 200:
display = display[:197] + "..."
self.concerns_label.config(text=f"Major Concerns: {display}")
except Exception:
pass
except Exception as e:
print(f"[DEBUG] failed to refresh major concerns: {e}")
self.last_concerns_update = now_time
self.root.after(200, self.update_clock)

View File

@@ -1,21 +1,24 @@
RocketLaunchCountdown is a python and HTML system to operate a launch countdown and go/nogo indicator. The go/nogo indicator is operated either by a spreadsheet, using a sharelink inserted in the settings windows. Or there are buttons in the app that can manage it as well. Which mode is used is dictated by a simple radio button in the settings window.
Latest release: [RocketLaunchCountdown 0.6.1](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/v0.6.1-alpha)
#Latest release: [RocketLaunchCountdown 0.7.0](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/v0.7.0-alpha)
Features Added:
Fixed Browser Source Cache Issue (More Info Below as OBS changes need to be made)
Optimizations
Added ability to change clock once in T+.\
Added major concerns display.\
OBS INSTRUCTIONS
For setting up the HTML sources to use in OBS, or similiar softwares. Follow the below steps:
1. Create a browser source
2. Select "Local file"
3. Select html file
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.
#OBS INSTRUCTIONS\
For setting up the HTML sources to use in OBS, or similiar softwares. Follow the below steps:\
1. Create a browser source\
2. Select "Local file"\
3. Select html file\
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.\
- NOTE: The faster the refresh time the smaller the chance is of the cache crashing\
[!CAUTION]
This plugin does not currently have a version for Mac.
INSTALL INSTRUCTIONS
#INSTALL INSTRUCTIONS\
Install is simple, download the installer, run it, and wham bam dans the man, you got a countdown manager!