0.7.0
This commit is contained in:
BIN
dist/exe/0.7.0/main.exe
vendored
Normal file
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
BIN
dist/installers/0.7.0/RLCInstaller.exe
vendored
Normal file
Binary file not shown.
275
main.py
275
main.py
@@ -30,6 +30,8 @@ session = requests.Session()
|
|||||||
appVersion = "0.7.0"
|
appVersion = "0.7.0"
|
||||||
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
|
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
|
||||||
|
|
||||||
|
concerns_list = []
|
||||||
|
|
||||||
# Default settings
|
# Default settings
|
||||||
DEFAULT_SETTINGS = {
|
DEFAULT_SETTINGS = {
|
||||||
"mode": "spreadsheet",
|
"mode": "spreadsheet",
|
||||||
@@ -37,8 +39,9 @@ DEFAULT_SETTINGS = {
|
|||||||
"range_row": 2,
|
"range_row": 2,
|
||||||
"weather_row": 3,
|
"weather_row": 3,
|
||||||
"vehicle_row": 4,
|
"vehicle_row": 4,
|
||||||
|
"major_concerns_row": 9,
|
||||||
"column": 12,
|
"column": 12,
|
||||||
"hide_mission_name": False
|
"hide_mission_name": True
|
||||||
}
|
}
|
||||||
# default timezone: 'local' uses system local tz, otherwise an IANA name or 'UTC'
|
# default timezone: 'local' uses system local tz, otherwise an IANA name or 'UTC'
|
||||||
DEFAULT_SETTINGS.setdefault("timezone", "local")
|
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
|
# Auto-hold times: list of seconds before T at which timer should automatically enter hold
|
||||||
DEFAULT_SETTINGS.setdefault("auto_hold_times", [])
|
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.
|
# A small list of common timezone choices.
|
||||||
TIMEZONE_CHOICES = [
|
TIMEZONE_CHOICES = [
|
||||||
"local",
|
"local",
|
||||||
@@ -110,7 +118,12 @@ def load_settings():
|
|||||||
try:
|
try:
|
||||||
if os.path.exists(SETTINGS_FILE):
|
if os.path.exists(SETTINGS_FILE):
|
||||||
with open(SETTINGS_FILE, "r", encoding="utf-8") as fh:
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# ensure default saved
|
# ensure default saved
|
||||||
@@ -182,9 +195,6 @@ def fetch_gonogo():
|
|||||||
return ["ERROR", "ERROR", "ERROR"]
|
return ["ERROR", "ERROR", "ERROR"]
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Helper for color
|
|
||||||
# -------------------------
|
|
||||||
def get_status_color(status):
|
def get_status_color(status):
|
||||||
"""
|
"""
|
||||||
Accepts status which may be:
|
Accepts status which may be:
|
||||||
@@ -259,9 +269,142 @@ def format_status_display(status):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return str(status or "")
|
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):
|
def write_countdown_html(mission_name, timer_text):
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
# Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility
|
# Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility
|
||||||
@@ -396,7 +539,11 @@ class CountdownApp:
|
|||||||
self.mission_name = "Placeholder Mission"
|
self.mission_name = "Placeholder Mission"
|
||||||
# fetch_gonogo() returns [Range, Weather, Vehicle] to match gonogo.html writer
|
# fetch_gonogo() returns [Range, Weather, Vehicle] to match gonogo.html writer
|
||||||
self.gonogo_values = fetch_gonogo()
|
self.gonogo_values = fetch_gonogo()
|
||||||
|
self.major_concerns = fetch_major_concerns()
|
||||||
|
write_major_concerns_html(self.major_concerns)
|
||||||
self.last_gonogo_update = time.time()
|
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
|
# track which auto-holds we've already triggered for the current run
|
||||||
self._auto_hold_triggered = set()
|
self._auto_hold_triggered = set()
|
||||||
# count mode
|
# count mode
|
||||||
@@ -753,6 +900,11 @@ class CountdownApp:
|
|||||||
)
|
)
|
||||||
self.vehicle_label.pack()
|
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
|
||||||
footer_frame = tk.Frame(root, bg="black")
|
footer_frame = tk.Frame(root, bg="black")
|
||||||
footer_frame.pack(side="bottom", pady=0, fill="x")
|
footer_frame.pack(side="bottom", pady=0, fill="x")
|
||||||
@@ -1063,6 +1215,37 @@ class CountdownApp:
|
|||||||
except Exception:
|
except Exception:
|
||||||
vehicle_cell.insert(0, f"L5")
|
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
|
# Manual buttons config
|
||||||
frame_buttons_cfg = tk.LabelFrame(
|
frame_buttons_cfg = tk.LabelFrame(
|
||||||
win, text="Manual Go/No-Go (Buttons mode)", fg=win_text, bg=win_bg
|
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(),
|
"range_cell": range_cell.get().strip().upper(),
|
||||||
"weather_cell": weather_cell.get().strip().upper(),
|
"weather_cell": weather_cell.get().strip().upper(),
|
||||||
"vehicle_cell": vehicle_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
|
# persist manual values if present
|
||||||
"manual_range": getattr(fetch_gonogo, "manual_range", None),
|
"manual_range": getattr(fetch_gonogo, "manual_range", None),
|
||||||
"manual_weather": getattr(fetch_gonogo, "manual_weather", 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"
|
new_val = "NO-GO" if cur_val == "GO" else "GO"
|
||||||
self.set_manual(which, new_val)
|
self.set_manual(which, new_val)
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Control logic
|
|
||||||
# ----------------------------
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.mission_name = self.mission_entry.get().strip() or "Placeholder Mission"
|
self.mission_name = self.mission_entry.get().strip() or "Placeholder Mission"
|
||||||
self.running = True
|
self.running = True
|
||||||
self.on_hold = False
|
self.on_hold = False
|
||||||
self.scrubbed = False
|
self.scrubbed = False
|
||||||
self.counting_up = False
|
|
||||||
self.show_hold_button()
|
self.show_hold_button()
|
||||||
|
self._auto_hold_triggered = set()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.mode_var.get() == "duration":
|
if self.mode_var.get() == "duration":
|
||||||
@@ -1986,41 +2169,28 @@ class CountdownApp:
|
|||||||
total_seconds = h * 3600 + m * 60 + s
|
total_seconds = h * 3600 + m * 60 + s
|
||||||
else:
|
else:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
# read separate HH, MM, SS boxes
|
|
||||||
h = int(self.clock_hours_entry.get() or 0)
|
h = int(self.clock_hours_entry.get() or 0)
|
||||||
m = int(self.clock_minutes_entry.get() or 0)
|
m = int(self.clock_minutes_entry.get() or 0)
|
||||||
s = int(self.clock_seconds_entry.get() or 0)
|
s = int(self.clock_seconds_entry.get() or 0)
|
||||||
# determine timezone from settings
|
|
||||||
ssettings = load_settings()
|
ssettings = load_settings()
|
||||||
tzname = ssettings.get(
|
tzname = ssettings.get("timezone", DEFAULT_SETTINGS.get("timezone", "local"))
|
||||||
"timezone", DEFAULT_SETTINGS.get("timezone", "local")
|
|
||||||
)
|
|
||||||
if ZoneInfo is None or tzname in (None, "", "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:
|
if target_today <= now:
|
||||||
target_today = target_today + timedelta(days=1)
|
target_today = target_today + timedelta(days=1)
|
||||||
total_seconds = (target_today - now).total_seconds()
|
total_seconds = (target_today - now).total_seconds()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
tz = ZoneInfo(tzname)
|
tz = ZoneInfo(tzname)
|
||||||
# construct aware "now" in that timezone and create the target time
|
|
||||||
now_tz = datetime.now(tz)
|
now_tz = datetime.now(tz)
|
||||||
target = now_tz.replace(
|
target = now_tz.replace(hour=h, minute=m, second=s, microsecond=0)
|
||||||
hour=h, minute=m, second=s, microsecond=0
|
|
||||||
)
|
|
||||||
# if target already passed in that tz, roll to next day
|
|
||||||
if target <= now_tz:
|
if target <= now_tz:
|
||||||
target = target + timedelta(days=1)
|
target = target + timedelta(days=1)
|
||||||
# compute total seconds using aware-datetime subtraction to avoid epoch mixing
|
|
||||||
total_seconds = (target - now_tz).total_seconds()
|
total_seconds = (target - now_tz).total_seconds()
|
||||||
except Exception:
|
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:
|
if target_today <= now:
|
||||||
target_today = target_today + timedelta(days=1)
|
target_today = target_today + timedelta(days=1)
|
||||||
total_seconds = (target_today - now).total_seconds()
|
total_seconds = (target_today - now).total_seconds()
|
||||||
@@ -2029,8 +2199,23 @@ class CountdownApp:
|
|||||||
write_countdown_html(self.mission_name, "Invalid time")
|
write_countdown_html(self.mission_name, "Invalid time")
|
||||||
return
|
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
|
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):
|
def hold(self):
|
||||||
if self.running and not self.on_hold and not self.scrubbed:
|
if self.running and not self.on_hold and not self.scrubbed:
|
||||||
@@ -2163,6 +2348,32 @@ class CountdownApp:
|
|||||||
write_gonogo_html(self.gonogo_values)
|
write_gonogo_html(self.gonogo_values)
|
||||||
self.last_gonogo_update = now_time
|
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)
|
self.root.after(200, self.update_clock)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
30
readme.md
30
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.
|
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:
|
Features Added:
|
||||||
Fixed Browser Source Cache Issue (More Info Below as OBS changes need to be made)
|
Added ability to change clock once in T+.\
|
||||||
Optimizations
|
Added major concerns display.\
|
||||||
|
|
||||||
|
|
||||||
OBS INSTRUCTIONS
|
#OBS INSTRUCTIONS\
|
||||||
For setting up the HTML sources to use in OBS, or similiar softwares. Follow the below steps:
|
For setting up the HTML sources to use in OBS, or similiar softwares. Follow the below steps:\
|
||||||
1. Create a browser source
|
1. Create a browser source\
|
||||||
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"
|
5. Install the extension "xObsBrowserAutoRefresh"\
|
||||||
- Download from https://obsproject.com/forum/resources/xobsbrowserautorefresh-timed-automatic-browser-source-refreshing.1677
|
- Download from https://obsproject.com/forum/resources/xobsbrowserautorefresh-timed-automatic-browser-source-refreshing.1677 \
|
||||||
- Set auto refresh times to whatever you desire.
|
- Set auto refresh times to whatever you desire.\
|
||||||
- NOTE: The faster the refresh time the smaller the chance is of the cache crashing
|
- 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!
|
Install is simple, download the installer, run it, and wham bam dans the man, you got a countdown manager!
|
||||||
Reference in New Issue
Block a user