diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/fbc00939-47cb-4600-9b3c-24cca1a1fb51.vsidx b/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/17b22148-b31f-4e1b-822b-a0289f4e27d6.vsidx similarity index 52% rename from ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/fbc00939-47cb-4600-9b3c-24cca1a1fb51.vsidx rename to ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/17b22148-b31f-4e1b-822b-a0289f4e27d6.vsidx index 7e1534e..b03ec0f 100644 Binary files a/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/fbc00939-47cb-4600-9b3c-24cca1a1fb51.vsidx and b/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/17b22148-b31f-4e1b-822b-a0289f4e27d6.vsidx differ diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/.suo b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/.suo index 29b0a00..a05296c 100644 Binary files a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/.suo and b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/.suo differ diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.backup.json b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.backup.json index 3e890a7..1f0cfdb 100644 --- a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.backup.json +++ b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.backup.json @@ -45,7 +45,7 @@ "RelativeDocumentMoniker": "ZCAM Manager\\ZCAM_Manager.py", "ToolTip": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\ZCAM_Manager.py", "RelativeToolTip": "ZCAM Manager\\ZCAM_Manager.py", - "ViewState": "AgIAAAwAAAAAAAAAAAAAADQAAAAyAAAAAAAAAA==", + "ViewState": "AgIAABcAAAAAAAAAAAAgwDQAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.002457|", "WhenOpened": "2026-03-30T10:08:49.086Z", "EditorCaption": "" diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.json b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.json index dad1a88..b95c775 100644 --- a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.json +++ b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.json @@ -45,7 +45,7 @@ "RelativeDocumentMoniker": "ZCAM Manager\\ZCAM_Manager.py", "ToolTip": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\ZCAM_Manager.py", "RelativeToolTip": "ZCAM Manager\\ZCAM_Manager.py", - "ViewState": "AgIAAPwAAAAAAAAAAAAgwBMBAAAAAAAAAAAAAA==", + "ViewState": "AgIAAJAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAA==", "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.002457|", "WhenOpened": "2026-03-30T10:08:49.086Z", "EditorCaption": "" diff --git a/ZCAM Manager/ZCAM Manager.slnx b/ZCAM Manager/ZCAM Manager.slnx index ed24e91..a460ae5 100644 --- a/ZCAM Manager/ZCAM Manager.slnx +++ b/ZCAM Manager/ZCAM Manager.slnx @@ -1,5 +1,5 @@ - + diff --git a/ZCAM Manager/ZCAM Manager/ZCAM_Manager.py b/ZCAM Manager/ZCAM Manager/ZCAM_Manager.py index 2bcbe03..24f31fb 100644 --- a/ZCAM Manager/ZCAM Manager/ZCAM_Manager.py +++ b/ZCAM Manager/ZCAM Manager/ZCAM_Manager.py @@ -68,10 +68,20 @@ if 'settings_need_fix' not in st.session_state: if 'master_cam_id' not in st.session_state: st.session_state.master_cam_id = None +# State for Max Exposure checking +if 'max_exp_last_check' not in st.session_state: + st.session_state.max_exp_last_check = {} +if 'max_exp_status' not in st.session_state: + st.session_state.max_exp_status = {} +if 'max_exp_was_enabled' not in st.session_state: + st.session_state.max_exp_was_enabled = False + # --- Helper Function for Parallel Requests --- -def fetch_camera_data(cam): - """Hits both endpoints every time it is called.""" - results = {"name": cam['name'], "ip": cam['ip'], "online": False, "offset": None} +def fetch_camera_data(args): + """Hits both endpoints every time it is called. Piggybacks the 2-min exposure check if active.""" + cam, check_max_exp, last_check, current_status = args + results = {"id": cam["id"], "name": cam['name'], "ip": cam['ip'], "online": False, "offset": None} + try: # 1. General Status (Fast) status_resp = requests.get(f"http://{cam['ip']}/camera_status", timeout=1.0) @@ -86,8 +96,37 @@ def fetch_camera_data(cam): raw_offset = time_resp.json().get("offset") if raw_offset is not None: results["offset"] = int(raw_offset / 1000) + + # 3. Max Exposure Check (Every 120s if enabled) + if check_max_exp: + now = time.time() + if now - last_check >= 120 or current_status is None: + try: + exp_url = f"http://{cam['ip']}/ctrl/get?k=max_exp_shutter_time" + exp_resp = requests.get(exp_url, timeout=1.0) + + if exp_resp.status_code == 200: + val = exp_resp.json().get("value") + if val != "1/1600": + # Force correct value + set_url = f"http://{cam['ip']}/ctrl/set?max_exp_shutter_time=1%2F1600" + requests.get(set_url, timeout=1.0) + results["max_exp_status"] = "Set to 1/1600" + else: + results["max_exp_status"] = "OK (1/1600)" + else: + results["max_exp_status"] = "Not streaming" + except Exception: + results["max_exp_status"] = "Not streaming" + + results["max_exp_timestamp"] = now + else: + results["max_exp_status"] = current_status + results["max_exp_timestamp"] = last_check + except Exception: pass + return results def sync_camera_to_master(ref_ip, slave_ip): @@ -128,7 +167,7 @@ def sync_camera_to_master(ref_ip, slave_ip): return False, "Error: Invalid JSON response." # --- Navigation Tabs --- -APP_VERSION = "0.2.1" +APP_VERSION = "0.3.1" st.title("ZCAM Manager") st.caption(f"Version {APP_VERSION}") tab_dash, tab_cam, tab_sync, tab_app = st.tabs(["Dashboard", "Camera Settings", "Camera Sync", "App Settings"]) @@ -138,12 +177,31 @@ tab_dash, tab_cam, tab_sync, tab_app = st.tabs(["Dashboard", "Camera Settings", def render_dashboard_status(): st.subheader("Quick Status") + col_stat, col_exp = st.columns([3, 1]) + with col_exp: + # Max Exposure Feature Checkbox + max_exp_enabled = st.checkbox("Max Exposure (Set 1/1600)", help="Checks and fixes max shutter speed every 2 mins.") + + # Detect if it was just turned on to trigger an immediate check + if max_exp_enabled and not st.session_state.max_exp_was_enabled: + for cam in st.session_state.cameras: + st.session_state.max_exp_last_check[cam['id']] = 0 + + st.session_state.max_exp_was_enabled = max_exp_enabled + if not st.session_state.cameras: st.info("No cameras configured.") return + # Prepare parallel arguments + args_list = [] + for cam in st.session_state.cameras: + last_check = st.session_state.max_exp_last_check.get(cam['id'], 0) + curr_status = st.session_state.max_exp_status.get(cam['id'], None) + args_list.append((cam, max_exp_enabled, last_check, curr_status)) + with ThreadPoolExecutor() as executor: - stats_list = list(executor.map(fetch_camera_data, st.session_state.cameras)) + stats_list = list(executor.map(fetch_camera_data, args_list)) current_row = {"Timestamp": datetime.now().strftime("%H:%M:%S")} @@ -183,6 +241,18 @@ def render_dashboard_status(): if data["offset"] is not None: st.metric("PTP Offset", f"{data['offset']} μs") current_row[data['name']] = data["offset"] + + # Render Max Exposure Status + if max_exp_enabled and "max_exp_timestamp" in data: + # Save state for the next fragment loop run + st.session_state.max_exp_last_check[data['id']] = data["max_exp_timestamp"] + st.session_state.max_exp_status[data['id']] = data.get("max_exp_status") + + m_status = data.get("max_exp_status") + if m_status == "Not streaming": + st.warning(f"Max Exp: {m_status}") + else: + st.success(f"Max Exp: {m_status}") else: st.error("Offline") @@ -493,4 +563,4 @@ with tab_app: st.write("") if st.button("Save App Settings", type="secondary"): - st.toast("App settings updated!") + st.toast("App settings updated!") \ No newline at end of file