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