diff --git a/dist/exe/0.7.0/main.exe b/dist/exe/0.7.0/main.exe new file mode 100644 index 0000000..04cf46e Binary files /dev/null and b/dist/exe/0.7.0/main.exe differ diff --git a/dist/installers/0.7.0/RLCInstaller.exe b/dist/installers/0.7.0/RLCInstaller.exe new file mode 100644 index 0000000..7727029 Binary files /dev/null and b/dist/installers/0.7.0/RLCInstaller.exe differ diff --git a/main.py b/main.py index 8568b27..12421ef 100644 --- a/main.py +++ b/main.py @@ -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 += "\n" + else: + concerns_html = "

No major concerns

" + + html = f""" + + + + + + + +

Major Concerns

+{concerns_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 - self.remaining_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) diff --git a/readme.md b/readme.md index 2c4b800..fab0225 100644 --- a/readme.md +++ b/readme.md @@ -1,22 +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. - - NOTE: The faster the refresh time the smaller the chance is of the cache crashing +#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! \ No newline at end of file