commit 4b74957427fc626ac5e0d7731d66e312ddf61161 Author: William Henderson Date: Mon Mar 30 14:40:57 2026 +0100 V0.2.1 diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/b9f64ded-4bae-4abe-ae22-d51d243d08f1.vsidx b/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/b9f64ded-4bae-4abe-ae22-d51d243d08f1.vsidx new file mode 100644 index 0000000..4c0eb23 Binary files /dev/null and b/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/b9f64ded-4bae-4abe-ae22-d51d243d08f1.vsidx differ diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/fbc00939-47cb-4600-9b3c-24cca1a1fb51.vsidx b/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/fbc00939-47cb-4600-9b3c-24cca1a1fb51.vsidx new file mode 100644 index 0000000..7e1534e Binary files /dev/null and b/ZCAM Manager/.vs/ZCAM Manager.slnx/FileContentIndex/fbc00939-47cb-4600-9b3c-24cca1a1fb51.vsidx differ diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/.suo b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/.suo new file mode 100644 index 0000000..29b0a00 Binary files /dev/null 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 new file mode 100644 index 0000000..3e890a7 --- /dev/null +++ b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.backup.json @@ -0,0 +1,58 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\", + "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{73A03639-CE0C-4B0B-BB86-8D2F8BBA2E3E}|ZCAM Manager\\ZCAM Manager.pyproj|C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\ZCAM_Manager.py||{8B382828-6202-11D1-8870-0000F87579D2}", + "RelativeMoniker": "D:0:0:{73A03639-CE0C-4B0B-BB86-8D2F8BBA2E3E}|ZCAM Manager\\ZCAM Manager.pyproj|solutionrelative:ZCAM Manager\\ZCAM_Manager.py||{8B382828-6202-11D1-8870-0000F87579D2}" + }, + { + "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\cameras.ini||{3B902123-F8A7-4915-9F01-361F908088D0}", + "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:ZCAM Manager\\cameras.ini||{3B902123-F8A7-4915-9F01-361F908088D0}" + } + ], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 209, + "SelectedChildIndex": 2, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:0:0:{e506b91c-c606-466a-90a9-123d1d1e12b3}" + }, + { + "$type": "Document", + "DocumentIndex": 1, + "Title": "cameras.ini", + "DocumentMoniker": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\cameras.ini", + "RelativeDocumentMoniker": "ZCAM Manager\\cameras.ini", + "ToolTip": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\cameras.ini", + "RelativeToolTip": "ZCAM Manager\\cameras.ini", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.002768|", + "WhenOpened": "2026-03-30T10:41:56.131Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "ZCAM_Manager.py", + "DocumentMoniker": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\ZCAM_Manager.py", + "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==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.002457|", + "WhenOpened": "2026-03-30T10:08:49.086Z", + "EditorCaption": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.json b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.json new file mode 100644 index 0000000..dad1a88 --- /dev/null +++ b/ZCAM Manager/.vs/ZCAM Manager.slnx/v18/DocumentLayout.json @@ -0,0 +1,58 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\", + "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{73A03639-CE0C-4B0B-BB86-8D2F8BBA2E3E}|ZCAM Manager\\ZCAM Manager.pyproj|C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\ZCAM_Manager.py||{8B382828-6202-11D1-8870-0000F87579D2}", + "RelativeMoniker": "D:0:0:{73A03639-CE0C-4B0B-BB86-8D2F8BBA2E3E}|ZCAM Manager\\ZCAM Manager.pyproj|solutionrelative:ZCAM Manager\\ZCAM_Manager.py||{8B382828-6202-11D1-8870-0000F87579D2}" + }, + { + "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\cameras.ini||{3B902123-F8A7-4915-9F01-361F908088D0}", + "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:ZCAM Manager\\cameras.ini||{3B902123-F8A7-4915-9F01-361F908088D0}" + } + ], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 209, + "SelectedChildIndex": 2, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:0:0:{e506b91c-c606-466a-90a9-123d1d1e12b3}" + }, + { + "$type": "Document", + "DocumentIndex": 1, + "Title": "cameras.ini", + "DocumentMoniker": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\cameras.ini", + "RelativeDocumentMoniker": "ZCAM Manager\\cameras.ini", + "ToolTip": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\cameras.ini", + "RelativeToolTip": "ZCAM Manager\\cameras.ini", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.002768|", + "WhenOpened": "2026-03-30T10:41:56.131Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "ZCAM_Manager.py", + "DocumentMoniker": "C:\\Users\\william\\Documents\\Git\\ZCAM-Manager\\ZCAM Manager\\ZCAM Manager\\ZCAM_Manager.py", + "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==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.002457|", + "WhenOpened": "2026-03-30T10:08:49.086Z", + "EditorCaption": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/ZCAM Manager/Builds/Build.bat b/ZCAM Manager/Builds/Build.bat new file mode 100644 index 0000000..a3e8324 --- /dev/null +++ b/ZCAM Manager/Builds/Build.bat @@ -0,0 +1,2 @@ +cd C:\Users\william\Desktop\ZCAM Manager +pyinstaller --noconfirm --onefile --icon="ZCAM.ico" --add-data "ZCAM_Manager.py;." --copy-metadata streamlit --collect-all streamlit launcher.py \ No newline at end of file diff --git a/ZCAM Manager/Builds/ZCAM Manager V0.2.1.zip b/ZCAM Manager/Builds/ZCAM Manager V0.2.1.zip new file mode 100644 index 0000000..4614b73 Binary files /dev/null and b/ZCAM Manager/Builds/ZCAM Manager V0.2.1.zip differ diff --git a/ZCAM Manager/Builds/ZCAM.ico b/ZCAM Manager/Builds/ZCAM.ico new file mode 100644 index 0000000..d73602e Binary files /dev/null and b/ZCAM Manager/Builds/ZCAM.ico differ diff --git a/ZCAM Manager/ZCAM Manager.slnx b/ZCAM Manager/ZCAM Manager.slnx new file mode 100644 index 0000000..ed24e91 --- /dev/null +++ b/ZCAM Manager/ZCAM Manager.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/ZCAM Manager/ZCAM Manager/ZCAM Manager.pyproj b/ZCAM Manager/ZCAM Manager/ZCAM Manager.pyproj new file mode 100644 index 0000000..636d1be --- /dev/null +++ b/ZCAM Manager/ZCAM Manager/ZCAM Manager.pyproj @@ -0,0 +1,40 @@ + + + Debug + 2.0 + 73a03639-ce0c-4b0b-bb86-8d2f8bba2e3e + . + launcher.py + + + . + . + ZCAM Manager + ZCAM Manager + Global|PythonCore|3.14 + + + true + false + + + true + false + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ZCAM Manager/ZCAM Manager/ZCAM_Manager.py b/ZCAM Manager/ZCAM Manager/ZCAM_Manager.py new file mode 100644 index 0000000..2bcbe03 --- /dev/null +++ b/ZCAM Manager/ZCAM Manager/ZCAM_Manager.py @@ -0,0 +1,496 @@ +import streamlit as st +import time +import configparser +import os +import sys +import uuid +import requests +import pandas as pd +from datetime import datetime +from concurrent.futures import ThreadPoolExecutor +from streamlit.runtime.state.session_state import STREAMLIT_INTERNAL_KEY_PREFIX + +# --- File Setup --- +CONFIG_FILE = "cameras.ini" + +def load_cameras(): + config = configparser.ConfigParser() + if os.path.exists(CONFIG_FILE): + config.read(CONFIG_FILE) + cameras = [] + for section in config.sections(): + cameras.append({ + "id": section, + "name": config[section].get("name", ""), + "ip": config[section].get("ip", "") + }) + return cameras + +def save_cameras(cameras): + config = configparser.ConfigParser() + for cam in cameras: + config[cam["id"]] = {"name": cam["name"], "ip": cam["ip"]} + with open(CONFIG_FILE, "w") as configfile: + config.write(configfile) + +# --- Configuration --- +st.set_page_config(page_title="ZCAM Manager", page_icon="🎥", layout="wide") + +# Hide Streamlit UI elements and reduce top padding +hide_st_style = """ + + """ +st.markdown(hide_st_style, unsafe_allow_html=True) + +# --- State Management --- +if 'cameras' not in st.session_state: + st.session_state.cameras = load_cameras() +if 'offset_history' not in st.session_state: + st.session_state.offset_history = pd.DataFrame() + +# State for Camera Settings check +if 'settings_check_results' not in st.session_state: + st.session_state.settings_check_results = {} +if 'settings_need_fix' not in st.session_state: + st.session_state.settings_need_fix = False + +# State for Sync Master +if 'master_cam_id' not in st.session_state: + st.session_state.master_cam_id = None + +# --- 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} + try: + # 1. General Status (Fast) + status_resp = requests.get(f"http://{cam['ip']}/camera_status", timeout=1.0) + if status_resp.status_code == 200: + results.update(status_resp.json()) + results["online"] = True + + # 2. Offset Poll + time_url = f"http://{cam['ip']}/ctrl/EzLink?action=get&key=time_info" + time_resp = requests.get(time_url, timeout=1.0) + if time_resp.status_code == 200: + raw_offset = time_resp.json().get("offset") + if raw_offset is not None: + results["offset"] = int(raw_offset / 1000) + except Exception: + pass + return results + +def sync_camera_to_master(ref_ip, slave_ip): + """Handles the 3-step synchronization process.""" + timeout_sec = 3.0 + try: + # 1. Force stop recording on Slave + stop_url = f"http://{slave_ip}/ctrl/rec?action=force_stop" + requests.get(stop_url, timeout=timeout_sec) + + # 2. Get Reference Camera ts + url_get = f"http://{ref_ip}/ctrl/EzLink?action=get&key=rejoin" + response = requests.get(url_get, timeout=timeout_sec) + + if response.status_code != 200: + return False, f"Error getting reference data: HTTP {response.status_code}" + + data = response.json() + seq_id = data.get("seq") + ts = data.get("ts") + code = data.get("code", -1) + + if ts is None or seq_id is None or code != 0: + return False, "Error getting valid ts/seq from Master reference." + + # 3. Push rejoin info to Slave Camera + url_set = f"http://{slave_ip}/ctrl/EzLink?action=set&key=rejoin&value={ts}&seq={seq_id}" + set_response = requests.get(url_set, timeout=timeout_sec) + + if set_response.status_code == 200: + return True, "Rejoin command sent successfully." + else: + return False, f"Error sending rejoin command: HTTP {set_response.status_code}" + + except requests.exceptions.RequestException as e: + return False, f"Connection Error: {e}" + except ValueError: + return False, "Error: Invalid JSON response." + +# --- Navigation Tabs --- +APP_VERSION = "0.2.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"]) + +# --- Page: Dashboard (Fragment) --- +@st.fragment(run_every=5) +def render_dashboard_status(): + st.subheader("Quick Status") + + if not st.session_state.cameras: + st.info("No cameras configured.") + return + + with ThreadPoolExecutor() as executor: + stats_list = list(executor.map(fetch_camera_data, st.session_state.cameras)) + + current_row = {"Timestamp": datetime.now().strftime("%H:%M:%S")} + + num_cols = min(len(stats_list), 6) + if num_cols == 0: + return + + cols = st.columns(num_cols) + + for i, data in enumerate(stats_list): + with cols[i % len(cols)]: + with st.container(border=True): + st.write(f"**{data['name']}**") + + if data["online"]: + st.success("Online") + + if data.get("isp") == "OK": + st.success("ISP: OK") + else: + st.error(f"ISP: {data.get('isp')}") + + sync = data.get("sync_link") + if sync == "OK": + st.success("Sync: OK") + elif sync == "NG": + st.error("Sync: No Sync") + else: + st.error(f"Sync: {sync}") + + lost = data.get("stream0_lost_frame", 0) + if lost == 0: + st.success("Dropped Frames: 0") + else: + st.error(f"Dropped Frames: {lost}") + + if data["offset"] is not None: + st.metric("PTP Offset", f"{data['offset']} μs") + current_row[data['name']] = data["offset"] + else: + st.error("Offline") + + # --- Line Graph --- + st.divider() + st.subheader("Clock Offset History (Polled every 5s)") + + if len(current_row) > 1: + new_entry = pd.DataFrame([current_row]) + st.session_state.offset_history = pd.concat([st.session_state.offset_history, new_entry], ignore_index=True).tail(500) + + if not st.session_state.offset_history.empty: + chart_data = st.session_state.offset_history.set_index("Timestamp") + st.line_chart(chart_data) + +# --- Apply Layouts to Tabs --- + +with tab_dash: + render_dashboard_status() + +# --- Apply Layouts to Tabs --- + +with tab_sync: + st.write("Synchronize camera clocks and rejoin frames to a Master reference.") + + if not st.session_state.cameras: + st.info("No cameras configured.") + else: + # 1. Master Selection + st.subheader("Reference Control") + + # Default to the first camera if no master is set yet + if st.session_state.master_cam_id is None: + st.session_state.master_cam_id = st.session_state.cameras[0]['id'] + + # Create a dictionary for the selectbox format_func + camera_dict = {cam['id']: f"{cam['name']} ({cam['ip']})" for cam in st.session_state.cameras} + + selected_master_id = st.selectbox( + "Select Master Camera", + options=list(camera_dict.keys()), + format_func=lambda x: camera_dict[x], + index=list(camera_dict.keys()).index(st.session_state.master_cam_id) if st.session_state.master_cam_id in camera_dict else 0 + ) + st.session_state.master_cam_id = selected_master_id + + # Get the IP of the chosen master + master_ip = next(cam['ip'] for cam in st.session_state.cameras if cam['id'] == selected_master_id) + + st.write("") + st.subheader("Sync Status") + + # 2. Render Camera List with Sync Buttons + for cam in st.session_state.cameras: + with st.container(border=True): + col1, col2 = st.columns([3, 1]) + + with col1: + st.write(f"**{cam['name']}**") + st.caption(cam['ip']) + + with col2: + if cam['id'] == selected_master_id: + st.info("MASTER REFERENCE") + else: + if st.button("Sync to Master", key=f"sync_btn_{cam['id']}"): + with st.spinner(f"Syncing {cam['name']}..."): + success, message = sync_camera_to_master(ref_ip=master_ip, slave_ip=cam['ip']) + + if success: + st.success(message) + else: + st.error(message) + +with tab_cam: + st.write("Check and configure settings across all cameras.") + + # --- Target Configurations --- + DESIRED_SYSTEM_SETTINGS = { + "sdi": "Off", + "led": "Off", + } + + DESIRED_WIFI_SETTINGS = { + "wifi_on": 0, + } + + DESIRED_STREAM0_SETTINGS = { + "gop_n": 1, + } + + DESIRED_IMAGE_SETTINGS = { + "noise_reduction": "Medium", + "sharpness": "None", + } + + total_checks = len(DESIRED_SYSTEM_SETTINGS) + len(DESIRED_WIFI_SETTINGS) + len(DESIRED_STREAM0_SETTINGS) + len(DESIRED_IMAGE_SETTINGS) + st.write(f"**Target Configuration:** Enforcing {total_checks} settings.") + + if st.button("Get camera settings", type="primary"): + st.session_state.settings_check_results = {} + st.session_state.settings_need_fix = False + + if not st.session_state.cameras: + st.warning("No cameras configured.") + else: + with st.spinner("Fetching settings from cameras..."): + for cam in st.session_state.cameras: + cam_id = cam['id'] + try: + # 1. Fetch System Settings + sys_url = f"http://{cam['ip']}/ctrl/getbatch?catalog=system" + resp_sys = requests.get(sys_url, timeout=3.0) + + # 2. Fetch WiFi Settings + wifi_url = f"http://{cam['ip']}/ctrl/wifi_ctrl?action=query" + resp_wifi = requests.get(wifi_url, timeout=3.0) + + # 3. Fetch Stream0 Settings + stream_url = f"http://{cam['ip']}/ctrl/stream_setting?index=stream0&action=query" + resp_stream = requests.get(stream_url, timeout=3.0) + + # 4. Fetch Image Settings + image_url = f"http://{cam['ip']}/ctrl/getbatch?catalog=image" + resp_image = requests.get(image_url, timeout=3.0) + + if resp_sys.status_code == 200 and resp_wifi.status_code == 200 and resp_stream.status_code == 200 and resp_image.status_code == 200: + sys_data = resp_sys.json() + wifi_data = resp_wifi.json() + stream_data = resp_stream.json() + image_data = resp_image.json() + camera_errors = {} + + # Check System Mismatches + for item in sys_data.get("cfgs", []): + key = item.get("key") + if key in DESIRED_SYSTEM_SETTINGS: + current_val = item.get("value") + expected_val = DESIRED_SYSTEM_SETTINGS[key] + if current_val != expected_val: + camera_errors[key] = {"current": current_val, "expected": expected_val, "type": "system"} + + # Check WiFi Mismatches + for key, expected_val in DESIRED_WIFI_SETTINGS.items(): + current_val = wifi_data.get(key) + if current_val != expected_val: + camera_errors[key] = {"current": current_val, "expected": expected_val, "type": "wifi"} + + # Check stream Mismatches + for key, expected_val in DESIRED_STREAM0_SETTINGS.items(): + current_val = stream_data.get(key) + if current_val != expected_val: + camera_errors[key] = {"current": current_val, "expected": expected_val, "type": "stream"} + + # Check image Mismatches + for item in image_data.get("cfgs", []): + key = item.get("key") + if key in DESIRED_IMAGE_SETTINGS: + current_val = item.get("value") + expected_val = DESIRED_IMAGE_SETTINGS[key] + if current_val != expected_val: + camera_errors[key] = {"current": current_val, "expected": expected_val, "type": "system"} + + st.session_state.settings_check_results[cam_id] = { + "name": cam['name'], + "ip": cam['ip'], + "errors": camera_errors, + "status": "Checked" + } + + if camera_errors: + st.session_state.settings_need_fix = True + + else: + st.session_state.settings_check_results[cam_id] = { + "name": cam['name'], "ip": cam['ip'], "errors": {}, + "status": f"HTTP Error (Sys: {resp_sys.status_code}, WiFi: {resp_wifi.status_code}, Stream: {resp_stream.status_code}, Image: {resp_image.status_code})" + } + except requests.exceptions.RequestException: + st.session_state.settings_check_results[cam_id] = { + "name": cam['name'], "ip": cam['ip'], "errors": {}, "status": "Offline / Unreachable" + } + + # Display results + if st.session_state.settings_check_results: + st.subheader("Results") + + has_errors = False + + for cam_id, info in st.session_state.settings_check_results.items(): + if info["status"] != "Checked": + st.warning(f"{info['name']} ({info['ip']}): {info['status']}") + has_errors = True + elif info["errors"]: + has_errors = True + + error_messages = [] + for key, vals in info["errors"].items(): + error_messages.append(f"- **{key}** is '{vals['current']}' (should be '{vals['expected']}')") + + st.error(f"**{info['name']} ({info['ip']}) Requires Fixes:**\n" + "\n".join(error_messages)) + + if not has_errors: + st.success("All cameras match the target configuration.") + + # Show dynamic fix button if any errors exist + if st.session_state.settings_need_fix: + st.write("") + if st.button("Fix All Incorrect Settings", type="primary"): + with st.spinner("Sending fix commands..."): + for cam_id, info in st.session_state.settings_check_results.items(): + if info["errors"]: + try: + # Separate the fixes by their required endpoint + sys_fixes = {k: v["expected"] for k, v in info["errors"].items() if v["type"] == "system"} + wifi_fixes = {k: v["expected"] for k, v in info["errors"].items() if v["type"] == "wifi"} + stream_fixes = {k: v["expected"] for k, v in info["errors"].items() if v["type"] == "stream"} + image_fixes = {k: v["expected"] for k, v in info["errors"].items() if v["type"] == "image"} + + # Send System Fixes + if sys_fixes: + query = "&".join([f"{k}={v}" for k, v in sys_fixes.items()]) + requests.get(f"http://{info['ip']}/ctrl/set?{query}", timeout=2.0) + + # Send WiFi Fixes + if wifi_fixes: + query = "&".join([f"{k}={v}" for k, v in wifi_fixes.items()]) + requests.get(f"http://{info['ip']}/ctrl/set?wifi=Off", timeout=2.0) + + # Send Stream Fixes + if stream_fixes: + query = "&".join([f"{k}={v}" for k, v in stream_fixes.items()]) + requests.get(f"http://{info['ip']}/ctrl/stream_setting?index=stream0&gop_n=1", timeout=2.0) + + # Send Image Fixes + if image_fixes: + query = "&".join([f"{k}={v}" for k, v in image_fixes.items()]) + requests.get(f"http://{info['ip']}/ctrl/set?noise_reduction=Medium", timeout=2.0) + requests.get(f"http://{info['ip']}/ctrl/set?sharpness=None", timeout=2.0) + + except requests.exceptions.RequestException: + pass + + # Clear results to force re-check + st.session_state.settings_check_results = {} + st.session_state.settings_need_fix = False + + st.success("Fix commands sent. Please click 'Get camera settings' to verify.") + time.sleep(1.5) + st.rerun() + + +with tab_app: + st.write("Configure cameras.") + + st.subheader("Camera Management") + + default_name = f"Camera {len(st.session_state.cameras) + 1}" + with st.form("add_camera_form", clear_on_submit=True): + st.write("Add a New Camera") + col1, col2 = st.columns(2) + with col1: + new_name = st.text_input("Camera Name", value=default_name) + with col2: + new_ip = st.text_input("IP Address", placeholder="192.168.1.x") + + if st.form_submit_button("Add Camera"): + if new_ip: + new_id = str(uuid.uuid4()) + st.session_state.cameras.append({ + "id": new_id, + "name": new_name, + "ip": new_ip + }) + save_cameras(st.session_state.cameras) + st.toast(f"Added {new_name}") + st.rerun() + else: + st.error("IP Address is required to add a camera.") + + st.write("") + + if st.session_state.cameras: + st.write("Edit Existing Cameras") + for i, cam in enumerate(st.session_state.cameras): + with st.expander(f"{cam['name']} ({cam['ip']})"): + edit_name = st.text_input("Name", value=cam['name'], key=f"name_{cam['id']}") + edit_ip = st.text_input("IP Address", value=cam['ip'], key=f"ip_{cam['id']}") + + col_save, col_del = st.columns([1, 1]) + with col_save: + if st.button("Save Changes", key=f"save_{cam['id']}"): + st.session_state.cameras[i]['name'] = edit_name + st.session_state.cameras[i]['ip'] = edit_ip + save_cameras(st.session_state.cameras) + st.toast(f"Updated {edit_name}!") + st.rerun() + with col_del: + if st.button("Delete Camera", key=f"del_{cam['id']}", type="primary"): + st.session_state.cameras.pop(i) + save_cameras(st.session_state.cameras) + st.toast("Camera deleted.") + st.rerun() + else: + st.info("No cameras added yet.") + + + + st.write("") + if st.button("Save App Settings", type="secondary"): + st.toast("App settings updated!") diff --git a/ZCAM Manager/ZCAM Manager/cameras.ini b/ZCAM Manager/ZCAM Manager/cameras.ini new file mode 100644 index 0000000..ae86d3c --- /dev/null +++ b/ZCAM Manager/ZCAM Manager/cameras.ini @@ -0,0 +1,4 @@ +[5c7b2022-def0-4334-8705-aad8101699f0] +name = Camera 1 +ip = 192.168.0.223 + diff --git a/ZCAM Manager/ZCAM Manager/launcher.py b/ZCAM Manager/ZCAM Manager/launcher.py new file mode 100644 index 0000000..94d1494 --- /dev/null +++ b/ZCAM Manager/ZCAM Manager/launcher.py @@ -0,0 +1,17 @@ +import os +import sys +import streamlit.web.cli as stcli + +if __name__ == "__main__": + # PyInstaller extracts files to a temporary folder called _MEIPASS + if getattr(sys, 'frozen', False): + base_dir = sys._MEIPASS + else: + base_dir = os.path.dirname(os.path.abspath(__file__)) + + script_path = os.path.join(base_dir, "ZCAM_Manager.py") + + # Simulate the terminal command 'streamlit run zcam_toolkit.py' + sys.argv = ["streamlit", "run", script_path, "--global.developmentMode=false"] + + sys.exit(stcli.main())