V0.2.1
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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!")
|
||||
@@ -0,0 +1,4 @@
|
||||
[5c7b2022-def0-4334-8705-aad8101699f0]
|
||||
name = Camera 1
|
||||
ip = 192.168.0.223
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user