This commit is contained in:
William Henderson
2026-03-30 14:40:57 +01:00
commit 4b74957427
13 changed files with 680 additions and 0 deletions
Binary file not shown.
@@ -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": ""
}
]
}
]
}
]
}
@@ -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": ""
}
]
}
]
}
]
}
+2
View File
@@ -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
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+5
View File
@@ -0,0 +1,5 @@
<Solution>
<Project Path="ZCAM Manager/ZCAM Manager.pyproj" Type="888888a0-9f3d-457c-b088-3a5042f75d52">
<Build Project="false" />
</Project>
</Solution>
@@ -0,0 +1,40 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>73a03639-ce0c-4b0b-bb86-8d2f8bba2e3e</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>launcher.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<Name>ZCAM Manager</Name>
<RootNamespace>ZCAM Manager</RootNamespace>
<InterpreterId>Global|PythonCore|3.14</InterpreterId>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<ItemGroup>
<Compile Include="launcher.py" />
<Compile Include="ZCAM_Manager.py" />
</ItemGroup>
<ItemGroup>
<InterpreterReference Include="Global|PythonCore|3.14" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
<!-- Uncomment the CoreCompile target to enable the Build command in
Visual Studio and specify your pre- and post-build commands in
the BeforeBuild and AfterBuild targets below. -->
<!--<Target Name="CoreCompile" />-->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
</Project>
+496
View File
@@ -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 = """
<style>
#MainMenu {visibility: hidden;}
header {visibility: hidden;}
footer {visibility: hidden;}
/* Reduce whitespace at the top of the page */
.block-container {
padding-top: 2rem;
padding-bottom: 1rem;
}
</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!")
+4
View File
@@ -0,0 +1,4 @@
[5c7b2022-def0-4334-8705-aad8101699f0]
name = Camera 1
ip = 192.168.0.223
+17
View File
@@ -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())