1 Commits

Author SHA1 Message Date
William Henderson 27b4711f70 V0.3.1 2026-03-31 11:13:03 +01:00
6 changed files with 79 additions and 9 deletions
Binary file not shown.
@@ -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": ""
@@ -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": ""
+1 -1
View File
@@ -1,5 +1,5 @@
<Solution>
<Project Path="ZCAM Manager/ZCAM Manager.pyproj" Type="888888a0-9f3d-457c-b088-3a5042f75d52">
<Project Path="ZCAM Manager/ZCAM Manager.pyproj" Type="888888a0-9f3d-457c-b088-3a5042f75d52" Id="73a03639-ce0c-4b0b-bb86-8d2f8bba2e3e">
<Build Project="false" />
</Project>
</Solution>
+75 -5
View File
@@ -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")