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"
+ for concern in concerns_list:
+ concerns_html += f" - {concern}
\n"
+ 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