Add UDP control protocol and IDS camera scripts
- Added UDP_CONTROL_PROTOCOL.md documenting the UDP control interface - Added launch-ids.py for IDS camera control - Added test_exposure_control.py for testing exposure settings - Added udp_backup.reg for UDP configuration backup - Added visualize_line_realtime.py for real-time visualization - Updated .gitignore and ROLLINGSUM_GUIDE.md - Removed ini/200fps-2456x4pix-cw.ini configuration file
This commit is contained in:
341
scripts/launch-ids.py
Normal file
341
scripts/launch-ids.py
Normal file
@@ -0,0 +1,341 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = "==3.13"
|
||||
# dependencies = []
|
||||
# ///
|
||||
#
|
||||
# IDS uEye Camera Control Script with UDP Exposure Control
|
||||
#
|
||||
# This script streams video from an IDS uEye camera via UDP and provides
|
||||
# a UDP control interface for dynamically adjusting exposure and framerate.
|
||||
#
|
||||
# Setup:
|
||||
# Run with: . .\scripts\setup_gstreamer_env.ps1 && uv run .\scripts\launch-ids.py
|
||||
#
|
||||
# Features:
|
||||
# - Video streaming on UDP port 5000 (127.0.0.1)
|
||||
# - Control interface on UDP port 5001 (0.0.0.0)
|
||||
# - Dynamic exposure control (0.001-1.0 seconds)
|
||||
# - Dynamic framerate control (1-500 fps)
|
||||
#
|
||||
# Control Commands:
|
||||
# SET_EXPOSURE <value> - Set exposure in seconds (e.g., 0.016)
|
||||
# GET_EXPOSURE - Get current exposure value
|
||||
# SET_FRAMERATE <value> - Set framerate in Hz (e.g., 30)
|
||||
# GET_FRAMERATE - Get current framerate
|
||||
# STATUS - Get pipeline status and current settings
|
||||
#
|
||||
# Example Usage:
|
||||
# echo "SET_EXPOSURE 0.010" | nc -u 127.0.0.1 5001
|
||||
# echo "GET_EXPOSURE" | nc -u 127.0.0.1 5001
|
||||
#
|
||||
# Testing:
|
||||
# Run test client: uv run .\scripts\test_exposure_control.py
|
||||
#
|
||||
# Documentation:
|
||||
# See scripts/UDP_CONTROL_PROTOCOL.md for full protocol details
|
||||
#
|
||||
# Add GStreamer Python packages
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import threading
|
||||
|
||||
# Check for required environment variable
|
||||
gst_root = os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64")
|
||||
if not gst_root:
|
||||
print("ERROR: GSTREAMER_1_0_ROOT_MSVC_X86_64 environment variable is not set")
|
||||
print("Expected: C:\\bin\\gstreamer\\1.0\\msvc_x86_64\\")
|
||||
print("Please run: . .\\scripts\\setup_gstreamer_env.ps1")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Remove trailing backslash if present
|
||||
gst_root = gst_root.rstrip("\\")
|
||||
|
||||
gst_site_packages = os.path.join(gst_root, "lib", "site-packages")
|
||||
sys.path.insert(0, gst_site_packages)
|
||||
|
||||
# Add GI typelibs
|
||||
os.environ["GI_TYPELIB_PATH"] = os.path.join(gst_root, "lib", "girepository-1.0")
|
||||
|
||||
# Add GStreamer DLL bin directory
|
||||
os.environ["PATH"] = os.path.join(gst_root, "bin") + ";" + os.environ["PATH"]
|
||||
|
||||
import gi
|
||||
gi.require_version("Gst", "1.0")
|
||||
from gi.repository import Gst
|
||||
|
||||
class ControlServer:
|
||||
"""UDP server for controlling camera parameters during runtime"""
|
||||
|
||||
def __init__(self, src, pipeline=None, port=5001):
|
||||
self.src = src
|
||||
self.pipeline = pipeline
|
||||
self.port = port
|
||||
self.running = False
|
||||
self.sock = None
|
||||
self.thread = None
|
||||
|
||||
def start(self):
|
||||
"""Start the control server in a separate thread"""
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self.run, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the control server"""
|
||||
self.running = False
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
||||
if self.thread:
|
||||
self.thread.join(timeout=2.0)
|
||||
|
||||
def run(self):
|
||||
"""Main server loop"""
|
||||
try:
|
||||
# Create UDP socket
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.sock.settimeout(0.5) # Non-blocking with timeout
|
||||
|
||||
try:
|
||||
self.sock.bind(("0.0.0.0", self.port))
|
||||
except OSError as e:
|
||||
print(f"ERROR: Could not bind control server to port {self.port}: {e}")
|
||||
print("Control server disabled. Video streaming will continue.")
|
||||
return
|
||||
|
||||
print(f"Control server listening on UDP port {self.port}")
|
||||
print(" Commands: SET_EXPOSURE <val>, GET_EXPOSURE, SET_FRAMERATE <val>, GET_FRAMERATE, STATUS")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# Receive command
|
||||
data, addr = self.sock.recvfrom(1024)
|
||||
command = data.decode('utf-8', errors='ignore').strip()
|
||||
|
||||
if command:
|
||||
# Process command
|
||||
response = self.process_command(command, addr)
|
||||
|
||||
# Send response
|
||||
self.send_response(response, addr)
|
||||
|
||||
except socket.timeout:
|
||||
# Normal timeout, continue loop
|
||||
continue
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
print(f"Control server error: {e}")
|
||||
|
||||
finally:
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def process_command(self, command, addr):
|
||||
"""Process incoming command and return response"""
|
||||
try:
|
||||
parts = command.strip().upper().split()
|
||||
if not parts:
|
||||
return "ERROR INVALID_SYNTAX: Empty command"
|
||||
|
||||
cmd = parts[0]
|
||||
|
||||
if cmd == "SET_EXPOSURE":
|
||||
return self.handle_set_exposure(parts)
|
||||
elif cmd == "GET_EXPOSURE":
|
||||
return self.handle_get_exposure()
|
||||
elif cmd == "SET_FRAMERATE":
|
||||
return self.handle_set_framerate(parts)
|
||||
elif cmd == "GET_FRAMERATE":
|
||||
return self.handle_get_framerate()
|
||||
elif cmd == "STATUS":
|
||||
return self.handle_status()
|
||||
else:
|
||||
return f"ERROR INVALID_COMMAND: Unknown command '{cmd}'"
|
||||
|
||||
except Exception as e:
|
||||
return f"ERROR PROCESSING: {str(e)}"
|
||||
|
||||
def handle_set_exposure(self, parts):
|
||||
"""Handle SET_EXPOSURE command"""
|
||||
if len(parts) != 2:
|
||||
return "ERROR INVALID_SYNTAX: Usage: SET_EXPOSURE <value>"
|
||||
|
||||
try:
|
||||
value = float(parts[1])
|
||||
if value < 0.001 or value > 1.0:
|
||||
return "ERROR OUT_OF_RANGE: Exposure must be 0.001-1.0 seconds"
|
||||
|
||||
self.src.set_property("exposure", value)
|
||||
# Verify the value was set
|
||||
actual = self.src.get_property("exposure")
|
||||
return f"OK {actual}"
|
||||
|
||||
except ValueError:
|
||||
return "ERROR INVALID_SYNTAX: Exposure must be a number"
|
||||
except Exception as e:
|
||||
return f"ERROR: {str(e)}"
|
||||
|
||||
def handle_get_exposure(self):
|
||||
"""Handle GET_EXPOSURE command"""
|
||||
try:
|
||||
value = self.src.get_property("exposure")
|
||||
return f"OK {value}"
|
||||
except Exception as e:
|
||||
return f"ERROR: {str(e)}"
|
||||
|
||||
def handle_set_framerate(self, parts):
|
||||
"""Handle SET_FRAMERATE command"""
|
||||
if len(parts) != 2:
|
||||
return "ERROR INVALID_SYNTAX: Usage: SET_FRAMERATE <value>"
|
||||
|
||||
try:
|
||||
value = float(parts[1])
|
||||
if value < 1.0 or value > 500.0:
|
||||
return "ERROR OUT_OF_RANGE: Framerate must be 1.0-500.0 Hz"
|
||||
|
||||
self.src.set_property("framerate", value)
|
||||
actual = self.src.get_property("framerate")
|
||||
return f"OK {actual}"
|
||||
|
||||
except ValueError:
|
||||
return "ERROR INVALID_SYNTAX: Framerate must be a number"
|
||||
except Exception as e:
|
||||
return f"ERROR: {str(e)}"
|
||||
|
||||
def handle_get_framerate(self):
|
||||
"""Handle GET_FRAMERATE command"""
|
||||
try:
|
||||
value = self.src.get_property("framerate")
|
||||
return f"OK {value}"
|
||||
except Exception as e:
|
||||
return f"ERROR: {str(e)}"
|
||||
|
||||
def handle_status(self):
|
||||
"""Handle STATUS command"""
|
||||
try:
|
||||
exposure = self.src.get_property("exposure")
|
||||
framerate = self.src.get_property("framerate")
|
||||
|
||||
# Get pipeline state
|
||||
state = "UNKNOWN"
|
||||
if self.pipeline:
|
||||
_, current_state, _ = self.pipeline.get_state(0)
|
||||
state = current_state.value_nick.upper()
|
||||
|
||||
return f"OK exposure={exposure} framerate={framerate} state={state}"
|
||||
except Exception as e:
|
||||
return f"ERROR: {str(e)}"
|
||||
|
||||
def send_response(self, response, addr):
|
||||
"""Send response back to client"""
|
||||
try:
|
||||
self.sock.sendto((response + '\n').encode(), addr)
|
||||
except Exception as e:
|
||||
print(f"Failed to send response: {e}")
|
||||
|
||||
|
||||
Gst.init(None)
|
||||
|
||||
pipeline = Gst.Pipeline()
|
||||
|
||||
src = Gst.ElementFactory.make("idsueyesrc", "src")
|
||||
src.set_property("config-file", "ini/100fps-10exp-2456x4pix-500top-cw-extragain.ini")
|
||||
|
||||
# Exposure in seconds (e.g., 0.016)
|
||||
src.set_property("exposure", 0.016)
|
||||
|
||||
# Frame rate
|
||||
src.set_property("framerate", 22)
|
||||
|
||||
# Video crop to remove bottom 3 pixels
|
||||
videocrop = Gst.ElementFactory.make("videocrop", "crop")
|
||||
videocrop.set_property("bottom", 3)
|
||||
|
||||
# Queue for buffering
|
||||
queue = Gst.ElementFactory.make("queue", "queue")
|
||||
|
||||
# UDP sink to send the raw data
|
||||
udpsink = Gst.ElementFactory.make("udpsink", "sink")
|
||||
udpsink.set_property("host", "127.0.0.1")
|
||||
udpsink.set_property("port", 5000)
|
||||
|
||||
# Add elements to pipeline
|
||||
pipeline.add(src)
|
||||
pipeline.add(videocrop)
|
||||
pipeline.add(queue)
|
||||
pipeline.add(udpsink)
|
||||
|
||||
# Link elements: src -> videocrop -> queue -> udpsink
|
||||
if not src.link(videocrop):
|
||||
print("ERROR: Failed to link src to videocrop")
|
||||
exit(1)
|
||||
if not videocrop.link(queue):
|
||||
print("ERROR: Failed to link videocrop to queue")
|
||||
exit(1)
|
||||
if not queue.link(udpsink):
|
||||
print("ERROR: Failed to link queue to udpsink")
|
||||
exit(1)
|
||||
|
||||
print("Pipeline created successfully")
|
||||
print(f"Video stream: UDP port 5000 (host: 127.0.0.1)")
|
||||
print("Pipeline: idsueyesrc -> videocrop (bottom=3) -> queue -> udpsink")
|
||||
print()
|
||||
|
||||
# Create and start control server
|
||||
control_server = ControlServer(src, pipeline, port=5001)
|
||||
control_server.start()
|
||||
|
||||
# Start the pipeline
|
||||
ret = pipeline.set_state(Gst.State.PLAYING)
|
||||
if ret == Gst.StateChangeReturn.FAILURE:
|
||||
print("ERROR: Unable to set the pipeline to the playing state")
|
||||
exit(1)
|
||||
|
||||
print()
|
||||
print("Pipeline is PLAYING...")
|
||||
print("Press Ctrl+C to stop")
|
||||
|
||||
# Wait until error or EOS
|
||||
bus = pipeline.get_bus()
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Use timeout to allow Ctrl+C to be caught quickly
|
||||
msg = bus.timed_pop_filtered(
|
||||
100 * Gst.MSECOND, # 100ms timeout
|
||||
Gst.MessageType.ERROR | Gst.MessageType.EOS | Gst.MessageType.STATE_CHANGED
|
||||
)
|
||||
|
||||
if msg:
|
||||
t = msg.type
|
||||
if t == Gst.MessageType.ERROR:
|
||||
err, debug = msg.parse_error()
|
||||
print(f"ERROR: {err.message}")
|
||||
print(f"Debug info: {debug}")
|
||||
break
|
||||
elif t == Gst.MessageType.EOS:
|
||||
print("End-Of-Stream reached")
|
||||
break
|
||||
elif t == Gst.MessageType.STATE_CHANGED:
|
||||
if msg.src == pipeline:
|
||||
old_state, new_state, pending_state = msg.parse_state_changed()
|
||||
print(f"Pipeline state changed from {old_state.value_nick} to {new_state.value_nick}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user")
|
||||
|
||||
# Cleanup
|
||||
print("Stopping control server...")
|
||||
control_server.stop()
|
||||
print("Stopping pipeline...")
|
||||
pipeline.set_state(Gst.State.NULL)
|
||||
print("Pipeline stopped")
|
||||
Reference in New Issue
Block a user