diff --git a/.gitignore b/.gitignore index ff2d9eb..48bdc28 100644 --- a/.gitignore +++ b/.gitignore @@ -5,23 +5,6 @@ Thumbs.db #ignore build folder [Bb]uild*/ -#Ignore files build by Visual Studio -*.obj -*.exe -*.pdb -*.user -*.aps -*.pch -*.vspscc -*_i.c -*_p.c -*.ncb -*.suo -*.tlb -*.tlh -*.bak -*.cache -*.ilk *.log .vscode [Bb]in @@ -39,5 +22,6 @@ ipch/ *.mkv *.raw *.dot +*.avi gst_plugs/ results/ \ No newline at end of file diff --git a/ROLLINGSUM_GUIDE.md b/ROLLINGSUM_GUIDE.md index d576b40..0f29f1b 100644 --- a/ROLLINGSUM_GUIDE.md +++ b/ROLLINGSUM_GUIDE.md @@ -100,7 +100,7 @@ struct _GstRollingSum }; ``` -### Algorithm (Simplified from cli.py) +### Algorithm (Simplified from wissotsky's cli.py) **Per Frame Processing:** diff --git a/ini/200fps-2456x4pix-cw.ini b/ini/200fps-2456x4pix-cw.ini deleted file mode 100644 index e8fe2f5..0000000 --- a/ini/200fps-2456x4pix-cw.ini +++ /dev/null @@ -1,222 +0,0 @@ -[Versions] -ueye_api_64.dll=4.93.1730 -ueye_usb_64.sys=4.93.1314 -ueye_boot_64.sys=4.93.1314 - - -[Sensor] -Sensor=UI308xCP-C -Sensor bit depth=0 -Sensor source gain=24 -FPN correction mode=0 -Black reference mode=0 -Sensor digital gain=0 - - -[Image size] -Start X=0 -Start Y=500 -Start X absolute=0 -Start Y absolute=500 -Width=2456 -Height=4 -Binning=0 -Subsampling=0 - - -[Scaler] -Mode=0 -Factor=0.000000 - - -[Multi AOI] -Enabled=0 -Mode=0 -x1=0 -x2=0 -x3=0 -x4=0 -y1=0 -y2=0 -y3=0 -y4=0 - - -[Shutter] -Mode=0 -Linescan number=0 - - -[Log Mode] -Mode=3 -Manual value=0 -Manual gain=0 - - -[Timing] -Pixelclock=237 -Extended pixelclock range=0 -Framerate=200.151466 -Exposure=4.903189 -Long exposure=0 -Dual exposure ratio=0 - - -[Selected Converter] -IS_SET_CM_RGB32=2 -IS_SET_CM_RGB24=2 -IS_SET_CM_RGB16=2 -IS_SET_CM_RGB15=2 -IS_SET_CM_Y8=2 -IS_SET_CM_RGB8=2 -IS_SET_CM_BAYER=8 -IS_SET_CM_UYVY=2 -IS_SET_CM_UYVY_MONO=2 -IS_SET_CM_UYVY_BAYER=2 -IS_CM_CBYCRY_PACKED=0 -IS_SET_CM_RGBY=8 -IS_SET_CM_RGB30=2 -IS_SET_CM_Y12=2 -IS_SET_CM_BAYER12=8 -IS_SET_CM_Y16=2 -IS_SET_CM_BAYER16=8 -IS_CM_BGR12_UNPACKED=2 -IS_CM_BGRA12_UNPACKED=2 -IS_CM_JPEG=0 -IS_CM_SENSOR_RAW10=8 -IS_CM_MONO10=2 -IS_CM_BGR10_UNPACKED=2 -IS_CM_RGBA8_PACKED=2 -IS_CM_RGB8_PACKED=2 -IS_CM_RGBY8_PACKED=8 -IS_CM_RGB10V2_PACKED=8 -IS_CM_RGB12_UNPACKED=2 -IS_CM_RGBA12_UNPACKED=2 -IS_CM_RGB10_UNPACKED=2 -IS_CM_RGB8_PLANAR=2 - - -[Parameters] -Colormode=1 -Gamma=1.000000 -Hardware Gamma=0 -Blacklevel Mode=0 -Blacklevel Offset=4 -Hotpixel Mode=2 -Hotpixel Threshold=0 -Sensor Hotpixel=0 -Adaptive hotpixel correction enable=0 -Adaptive hotpixel correction mode=0 -Adaptive hotpixel correction sensitivity=3 -GlobalShutter=0 -AllowRawWithLut=0 - - -[Gain] -Master=0 -Red=19 -Green=0 -Blue=33 -GainBoost=1 - - -[Processing] -EdgeEnhancementFactor=0 -RopEffect=0 -Whitebalance=0 -Whitebalance Red=1.000000 -Whitebalance Green=1.000000 -Whitebalance Blue=1.000000 -Color correction=4 -Color_correction_factor=1.000000 -Color_correction_satU=100 -Color_correction_satV=100 -Bayer Conversion=1 -JpegCompression=0 -NoiseMode=0 -ImageEffect=0 -LscModel=0 -WideDynamicRange=0 - - -[Auto features] -Auto Framerate control=0 -Brightness exposure control=0 -Brightness gain control=0 -Auto Framerate Sensor control=0 -Brightness exposure Sensor control=0 -Brightness gain Sensor control=0 -Brightness exposure Sensor control photometry=0 -Brightness gain Sensor control photometry=0 -Brightness control once=0 -Brightness reference=128 -Brightness speed=50 -Brightness max gain=100 -Brightness max exposure=4.903189 -Brightness Aoi Left=0 -Brightness Aoi Top=0 -Brightness Aoi Width=2456 -Brightness Aoi Height=4 -Brightness Hysteresis=2 -AutoImageControlMode=2 -AutoImageControlPeakWhiteChannel=0 -AutoImageControlExposureMinimum=0.000000 -AutoImageControlPeakWhiteChannelMode=0 -AutoImageControlPeakWhiteGranularity=0 -Auto WB control=0 -Auto WB type=2 -Auto WB RGB color model=1 -Auto WB RGB color temperature=5000 -Auto WB offsetR=0 -Auto WB offsetB=0 -Auto WB gainMin=0 -Auto WB gainMax=100 -Auto WB speed=50 -Auto WB Aoi Left=0 -Auto WB Aoi Top=0 -Auto WB Aoi Width=2456 -Auto WB Aoi Height=4 -Auto WB Once=0 -Auto WB Hysteresis=2 -Brightness Skip Frames Trigger Mode=4 -Brightness Skip Frames Freerun Mode=4 -Auto WB Skip Frames Trigger Mode=4 -Auto WB Skip Frames Freerun Mode=4 - - -[Trigger and Flash] -Trigger mode=0 -Trigger timeout=200 -Trigger delay=0 -Trigger debounce mode=0 -Trigger debounce delay time=1 -Trigger burst size=1 -Trigger prescaler frame=1 -Trigger prescaler line=1 -Trigger input=1 -Flash strobe=0 -Flash delay=0 -Flash duration=0 -Flash auto freerun=0 -PWM mode=0 -PWM frequency=20000000 -PWM dutycycle=20000000 -GPIO state=3 -GPIO direction=0 -GPIO1 Config=1 -GPIO2 Config=1 - - -[Vertical AOI Merge Mode] -Mode=0 -Position=0 -Additional Position=0 -Height=2 - - -[Level Controlled Trigger Mode] -Mode=0 - - -[Memory] -Camera memory mode=1 diff --git a/scripts/UDP_CONTROL_PROTOCOL.md b/scripts/UDP_CONTROL_PROTOCOL.md new file mode 100644 index 0000000..e446ac4 --- /dev/null +++ b/scripts/UDP_CONTROL_PROTOCOL.md @@ -0,0 +1,288 @@ +# UDP Control Protocol Specification + +## Overview + +This document describes the UDP-based control protocol for dynamically controlling the IDS uEye camera exposure during runtime. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ launch-ids.py Process │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ Main Thread │ │ Control Server Thread │ │ +│ │ │ │ │ │ +│ │ GStreamer │◄────────┤ UDP Socket (Port 5001) │ │ +│ │ Pipeline │ Thread- │ Command Parser │ │ +│ │ - idsueyesrc │ Safe │ Property Setter │ │ +│ │ - videocrop │ Updates │ Response Handler │ │ +│ │ - queue │ │ │ │ +│ │ - udpsink:5000 │ └──────────────────────────┘ │ +│ └──────────────────┘ ▲ │ +│ │ │ +└───────────────────────────────────────────┼───────────────────┘ + │ + │ UDP Commands + │ + ┌────────┴────────┐ + │ Control Client │ + │ (Any UDP tool) │ + └─────────────────┘ +``` + +## Connection Details + +- **Control Port**: 5001 (UDP) +- **Bind Address**: 0.0.0.0 (accepts from any interface) +- **Video Port**: 5000 (UDP) - existing video stream, unchanged +- **Protocol**: UDP (connectionless, stateless) +- **Encoding**: ASCII text +- **Delimiter**: Newline (`\n`) + +## Command Format + +### General Structure +``` +COMMAND [PARAMETERS]\n +``` + +Commands are case-insensitive, but UPPERCASE is recommended for clarity. + +## Supported Commands + +### 1. SET_EXPOSURE + +**Description**: Sets the camera exposure time. + +**Syntax**: +``` +SET_EXPOSURE +``` + +**Parameters**: +- ``: Exposure time in seconds (float) + - Range: 0.001 to 1.0 seconds (1ms to 1000ms) + - Examples: `0.016` (16ms), `0.001` (1ms), `0.100` (100ms) + +**Response**: +``` +OK +``` +or +``` +ERROR +``` + +**Examples**: +``` +Client: SET_EXPOSURE 0.016\n +Server: OK 0.016\n + +Client: SET_EXPOSURE 2.0\n +Server: ERROR Value out of range (0.001-1.0)\n +``` + +### 2. GET_EXPOSURE + +**Description**: Retrieves the current exposure time. + +**Syntax**: +``` +GET_EXPOSURE +``` + +**Parameters**: None + +**Response**: +``` +OK +``` + +**Example**: +``` +Client: GET_EXPOSURE\n +Server: OK 0.016\n +``` + +### 3. SET_FRAMERATE + +**Description**: Sets the camera frame rate. + +**Syntax**: +``` +SET_FRAMERATE +``` + +**Parameters**: +- ``: Frame rate in Hz (float) + - Range: 1.0 to 500.0 fps + - Examples: `22`, `30.5`, `100` + +**Response**: +``` +OK +``` +or +``` +ERROR +``` + +**Example**: +``` +Client: SET_FRAMERATE 30\n +Server: OK 30.0\n +``` + +### 4. GET_FRAMERATE + +**Description**: Retrieves the current frame rate. + +**Syntax**: +``` +GET_FRAMERATE +``` + +**Parameters**: None + +**Response**: +``` +OK +``` + +**Example**: +``` +Client: GET_FRAMERATE\n +Server: OK 22.0\n +``` + +### 5. STATUS + +**Description**: Get overall pipeline status and current settings. + +**Syntax**: +``` +STATUS +``` + +**Parameters**: None + +**Response**: +``` +OK exposure= framerate= state= +``` + +**Example**: +``` +Client: STATUS\n +Server: OK exposure=0.016 framerate=22.0 state=PLAYING\n +``` + +## Error Handling + +### Error Response Format +``` +ERROR : +``` + +### Common Error Codes + +| Code | Description | Example | +|------|-------------|---------| +| `INVALID_COMMAND` | Unknown command | `ERROR INVALID_COMMAND: Unknown command 'FOO'` | +| `INVALID_SYNTAX` | Malformed command | `ERROR INVALID_SYNTAX: Missing parameter` | +| `OUT_OF_RANGE` | Value out of valid range | `ERROR OUT_OF_RANGE: Exposure must be 0.001-1.0` | +| `PIPELINE_ERROR` | Pipeline not running | `ERROR PIPELINE_ERROR: Pipeline not in PLAYING state` | + +## Implementation Notes + +### Thread Safety +- The control server runs in a separate daemon thread +- GStreamer properties are inherently thread-safe (GObject properties) +- The `src.set_property()` method can be safely called from the control thread + +### Non-Blocking Operation +- Control server uses non-blocking socket with timeout +- Does not interfere with GStreamer pipeline operation +- Minimal latency for command processing + +### Response Timing +- Responses are sent immediately after processing +- Property changes take effect on the next frame capture +- No guaranteed synchronization with video stream + +## Usage Examples + +### Python Client Example +```python +import socket + +def send_command(command): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.sendto(command.encode() + b'\n', ('127.0.0.1', 5001)) + sock.settimeout(1.0) + response, _ = sock.recvfrom(1024) + sock.close() + return response.decode().strip() + +# Set exposure to 10ms +print(send_command("SET_EXPOSURE 0.010")) + +# Get current exposure +print(send_command("GET_EXPOSURE")) + +# Set framerate to 30fps +print(send_command("SET_FRAMERATE 30")) +``` + +### Command Line (netcat/nc) +```bash +# Set exposure +echo "SET_EXPOSURE 0.020" | nc -u 127.0.0.1 5001 + +# Get exposure +echo "GET_EXPOSURE" | nc -u 127.0.0.1 5001 + +# Get status +echo "STATUS" | nc -u 127.0.0.1 5001 +``` + +### PowerShell Client +```powershell +$udpClient = New-Object System.Net.Sockets.UdpClient +$endpoint = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("127.0.0.1"), 5001) + +# Send command +$bytes = [System.Text.Encoding]::ASCII.GetBytes("SET_EXPOSURE 0.015`n") +$udpClient.Send($bytes, $bytes.Length, $endpoint) + +# Receive response +$udpClient.Client.ReceiveTimeout = 1000 +$receiveBytes = $udpClient.Receive([ref]$endpoint) +$response = [System.Text.Encoding]::ASCII.GetString($receiveBytes) +Write-Host $response + +$udpClient.Close() +``` + +## Testing + +A test client script is provided: `scripts/test_exposure_control.py` + +```bash +# Run the camera pipeline +uv run scripts/launch-ids.py + +# In another terminal, test exposure control +uv run scripts/test_exposure_control.py +``` + +## Future Enhancements + +Possible extensions to the protocol: +- Add `SET_GAIN` / `GET_GAIN` commands +- Add `SAVE_CONFIG` to save current settings to INI file +- Add `RESET` to restore default settings +- Support batch commands (multiple commands in one packet) +- Add authentication/security for production use \ No newline at end of file diff --git a/scripts/launch-ids.py b/scripts/launch-ids.py new file mode 100644 index 0000000..e4e27b9 --- /dev/null +++ b/scripts/launch-ids.py @@ -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 - Set exposure in seconds (e.g., 0.016) +# GET_EXPOSURE - Get current exposure value +# SET_FRAMERATE - 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 , GET_EXPOSURE, SET_FRAMERATE , 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 " + + 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 " + + 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") diff --git a/scripts/test_exposure_control.py b/scripts/test_exposure_control.py new file mode 100644 index 0000000..4b6c455 --- /dev/null +++ b/scripts/test_exposure_control.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.8" +# dependencies = [] +# /// + +""" +Test client for UDP exposure control +Usage: uv run scripts/test_exposure_control.py + +This script tests the UDP control interface for the IDS uEye camera. +Make sure launch-ids.py is running before executing this test. +""" + +import socket +import time +import sys + +def send_command(command, host="127.0.0.1", port=5001, timeout=1.0): + """Send a command and return the response""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + + try: + # Send command + sock.sendto(command.encode() + b'\n', (host, port)) + + # Receive response + response, _ = sock.recvfrom(1024) + return response.decode().strip() + + except socket.timeout: + return "ERROR: Timeout waiting for response (is launch-ids.py running?)" + except Exception as e: + return f"ERROR: {e}" + finally: + sock.close() + +def print_test(test_num, description, command, response): + """Print formatted test result""" + print(f"\nTest {test_num}: {description}") + print(f" Command: {command}") + print(f" Response: {response}") + + # Check if response indicates success + if response.startswith("OK"): + print(" ✓ PASS") + elif response.startswith("ERROR"): + if "OUT_OF_RANGE" in response or "INVALID" in response: + print(" ✓ PASS (Expected error)") + else: + print(" ✗ FAIL (Unexpected error)") + else: + print(" ? UNKNOWN") + +def main(): + print("=" * 70) + print("UDP Exposure Control Test Client") + print("=" * 70) + print("Testing UDP control interface on 127.0.0.1:5001") + print() + + # Check if server is reachable + print("Checking if control server is reachable...") + response = send_command("STATUS", timeout=2.0) + if "Timeout" in response: + print("✗ FAILED: Control server not responding") + print(" Make sure launch-ids.py is running first!") + sys.exit(1) + print("✓ Control server is reachable\n") + + time.sleep(0.2) + + # Test 1: Get current exposure + response = send_command("GET_EXPOSURE") + print_test(1, "Get current exposure", "GET_EXPOSURE", response) + time.sleep(0.2) + + # Test 2: Set exposure to 10ms + response = send_command("SET_EXPOSURE 0.110") + print_test(2, "Set exposure to 10ms", "SET_EXPOSURE 0.010", response) + time.sleep(5.2) + + # Test 3: Verify exposure was set + response = send_command("GET_EXPOSURE") + print_test(3, "Verify exposure changed", "GET_EXPOSURE", response) + time.sleep(0.2) + + # Test 4: Set exposure to 20ms + response = send_command("SET_EXPOSURE 0.020") + print_test(4, "Set exposure to 20ms", "SET_EXPOSURE 0.020", response) + time.sleep(0.2) + + # Test 5: Get framerate + response = send_command("GET_FRAMERATE") + print_test(5, "Get current framerate", "GET_FRAMERATE", response) + time.sleep(0.2) + + # Test 6: Set framerate + response = send_command("SET_FRAMERATE 30") + print_test(6, "Set framerate to 30 fps", "SET_FRAMERATE 30", response) + time.sleep(0.2) + + # Test 7: Verify framerate + response = send_command("GET_FRAMERATE") + print_test(7, "Verify framerate changed", "GET_FRAMERATE", response) + time.sleep(0.2) + + # Test 8: Get status + response = send_command("STATUS") + print_test(8, "Get pipeline status", "STATUS", response) + time.sleep(0.2) + + # Test 9: Invalid command + response = send_command("INVALID_CMD") + print_test(9, "Send invalid command", "INVALID_CMD", response) + time.sleep(0.2) + + # Test 10: Out of range exposure (too high) + response = send_command("SET_EXPOSURE 5.0") + print_test(10, "Out of range exposure (5.0s)", "SET_EXPOSURE 5.0", response) + time.sleep(0.2) + + # Test 11: Out of range exposure (too low) + response = send_command("SET_EXPOSURE 0.0001") + print_test(11, "Out of range exposure (0.1ms)", "SET_EXPOSURE 0.0001", response) + time.sleep(0.2) + + # Test 12: Invalid syntax (missing parameter) + response = send_command("SET_EXPOSURE") + print_test(12, "Invalid syntax (missing param)", "SET_EXPOSURE", response) + time.sleep(0.2) + + # Test 13: Invalid syntax (non-numeric) + response = send_command("SET_EXPOSURE abc") + print_test(13, "Invalid syntax (non-numeric)", "SET_EXPOSURE abc", response) + time.sleep(0.2) + + # Test 14: Restore original exposure (16ms) + response = send_command("SET_EXPOSURE 0.016") + print_test(14, "Restore exposure to 16ms", "SET_EXPOSURE 0.016", response) + time.sleep(0.2) + + # Test 15: Restore original framerate (22 fps) + response = send_command("SET_FRAMERATE 22") + print_test(15, "Restore framerate to 22 fps", "SET_FRAMERATE 22", response) + + print() + print("=" * 70) + print("Test completed!") + print() + print("Quick reference:") + print(" echo 'SET_EXPOSURE 0.010' | nc -u 127.0.0.1 5001") + print(" echo 'GET_EXPOSURE' | nc -u 127.0.0.1 5001") + print(" echo 'STATUS' | nc -u 127.0.0.1 5001") + print("=" * 70) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/udp_backup.reg b/scripts/udp_backup.reg new file mode 100644 index 0000000..5afb4d3 --- /dev/null +++ b/scripts/udp_backup.reg @@ -0,0 +1,24 @@ +Windows Registry Editor Version 5.00 + +; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters] +; "DefaultReceiveWindow" was not set + +; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters] +; "LargeBufferSize" was not set + +; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters] +; "MediumBufferSize" was not set + +; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters] +; "TcpWindowSize" was not set + +; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters] +; "MaxConnectionsPerServer" was not set + +; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters] +; "MaxFreeTcbs" was not set + +; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters] +; "DefaultTTL" was not set + + diff --git a/scripts/visualize_line_realtime.py b/scripts/visualize_line_realtime.py new file mode 100644 index 0000000..20c7782 --- /dev/null +++ b/scripts/visualize_line_realtime.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.8" +# dependencies = [ +# "numpy>=1.24.0", +# "matplotlib>=3.7.0", +# ] +# /// + +""" +Real-time Line Visualization for Camera Data +Displays RGB/BGR channel values across the line width in real-time + +Usage: uv run visualize_line_realtime.py [--format BGR|RGB] [--port 5000] +""" + +import socket +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import argparse +from collections import deque + +# Parse arguments +parser = argparse.ArgumentParser(description='Real-time line channel visualization') +parser.add_argument('--format', type=str, default='BGR', choices=['BGR', 'RGB'], + help='Input format (default: BGR)') +parser.add_argument('--port', type=int, default=5000, + help='UDP port (default: 5000)') +parser.add_argument('--width', type=int, default=2456, + help='Line width in pixels (default: 2456)') +parser.add_argument('--fps-limit', type=int, default=30, + help='Maximum display fps (default: 30)') +args = parser.parse_args() + +# Stream parameters +LINE_WIDTH = args.width +LINE_HEIGHT = 1 +CHANNELS = 3 +FRAME_SIZE = LINE_WIDTH * LINE_HEIGHT * CHANNELS + +UDP_IP = "0.0.0.0" +UDP_PORT = args.port + +# Create UDP socket with minimal buffer to avoid buffering old packets +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536) # Minimal buffer (64KB) +sock.setblocking(False) # Non-blocking for animation +sock.bind((UDP_IP, UDP_PORT)) + +print(f"Receiving {LINE_WIDTH}x{LINE_HEIGHT} {args.format} on UDP port {UDP_PORT}") +print(f"Display update rate: {args.fps_limit} fps max") +print("Close the plot window to exit") + +# Initialize plot +fig, axes = plt.subplots(2, 1, figsize=(15, 8)) +fig.suptitle(f'Real-time {args.format} Channel Visualization - Line Sensor', + fontsize=14, fontweight='bold') + +# Channel order based on format +if args.format == 'BGR': + channel_names = ['Blue', 'Green', 'Red'] + channel_colors = ['b', 'g', 'r'] + channel_indices = [0, 1, 2] # BGR order +else: # RGB + channel_names = ['Red', 'Green', 'Blue'] + channel_colors = ['r', 'g', 'b'] + channel_indices = [0, 1, 2] # RGB order + +# Initialize line data +x_data = np.arange(LINE_WIDTH) +y_data = [np.zeros(LINE_WIDTH) for _ in range(CHANNELS)] +y_grayscale = np.zeros(LINE_WIDTH) # Combined grayscale + +# Top plot - GRAYSCALE ONLY +line_gray, = axes[0].plot(x_data, y_grayscale, 'k-', linewidth=1.0) + +axes[0].set_xlim(0, LINE_WIDTH) +axes[0].set_ylim(0, 255) +axes[0].set_xlabel('Pixel Position') +axes[0].set_ylabel('Grayscale Value') +axes[0].set_title('Grayscale (Luminance-weighted)') +axes[0].grid(True, alpha=0.3) + +# Bottom plot - RGB/BGR channels with color +lines_separate = [] +for i in range(CHANNELS): + line, = axes[1].plot(x_data, y_data[i], channel_colors[i] + '-', + label=channel_names[i], alpha=0.7, linewidth=0.8) + lines_separate.append(line) + +axes[1].set_xlim(0, LINE_WIDTH) +axes[1].set_ylim(0, 255) +axes[1].set_xlabel('Pixel Position') +axes[1].set_ylabel('Pixel Value') +axes[1].set_title(f'{args.format} Channels: {" | ".join(channel_names)}') +axes[1].legend(loc='upper right') +axes[1].grid(True, alpha=0.3) + +# Statistics text +stats_text = axes[0].text(0.02, 0.98, '', transform=axes[0].transAxes, + verticalalignment='top', fontfamily='monospace', + fontsize=9, bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5)) + +# Frame counter +frame_count = [0] +last_update = [0] +fps_buffer = deque(maxlen=30) + +# Animation update function +def update_plot(frame): + """Update plot with new UDP data""" + import time + + current_time = time.time() + + # Rate limiting + if args.fps_limit > 0: + min_interval = 1.0 / args.fps_limit + if current_time - last_update[0] < min_interval: + return [line_gray] + lines_separate + [stats_text] + + # Drain all buffered packets and only use the latest one + latest_data = None + packets_drained = 0 + try: + # Read all available packets, keep only the last one + while True: + try: + data, addr = sock.recvfrom(65536) + if len(data) == FRAME_SIZE: + latest_data = data + packets_drained += 1 + except BlockingIOError: + # No more packets available + break + + # Only process if we got valid data + if latest_data is None: + return [line_gray] + lines_separate + [stats_text] + + # Parse frame + line_data = np.frombuffer(latest_data, dtype=np.uint8).reshape((LINE_HEIGHT, LINE_WIDTH, CHANNELS)) + + # Extract channels based on format + for i in range(CHANNELS): + y_data[i] = line_data[0, :, channel_indices[i]] + + # Calculate grayscale (luminance using standard weights for RGB) + # For BGR: weights are [0.114, 0.587, 0.299] + # For RGB: weights are [0.299, 0.587, 0.114] + if args.format == 'BGR': + y_grayscale = (0.114 * y_data[0] + 0.587 * y_data[1] + 0.299 * y_data[2]) + else: # RGB + y_grayscale = (0.299 * y_data[0] + 0.587 * y_data[1] + 0.114 * y_data[2]) + + # Update top plot (grayscale only) + line_gray.set_ydata(y_grayscale) + + # Update bottom plot (RGB/BGR channels) + for i, line in enumerate(lines_separate): + line.set_ydata(y_data[i]) + + # Calculate statistics + stats = [] + for i in range(CHANNELS): + ch_data = y_data[i] + stats.append(f"{channel_names[i]:5s}: min={ch_data.min():3d} max={ch_data.max():3d} " + f"mean={ch_data.mean():6.2f} std={ch_data.std():6.2f}") + + # Add grayscale stats + stats.append(f"Gray : min={y_grayscale.min():6.2f} max={y_grayscale.max():6.2f} " + f"mean={y_grayscale.mean():6.2f} std={y_grayscale.std():6.2f}") + + # Calculate FPS + frame_count[0] += 1 + if last_update[0] > 0: + fps = 1.0 / (current_time - last_update[0]) + fps_buffer.append(fps) + avg_fps = np.mean(fps_buffer) + else: + avg_fps = 0 + + last_update[0] = current_time + + # Update stats text + stats_str = f"Frame: {frame_count[0]} FPS: {avg_fps:.1f}\n" + "\n".join(stats) + stats_text.set_text(stats_str) + + except BlockingIOError: + # No data available + pass + except Exception as e: + print(f"Error: {e}") + + return [line_gray] + lines_separate + [stats_text] + +# Set up animation with blit for better performance +ani = animation.FuncAnimation(fig, update_plot, interval=10, blit=True, cache_frame_data=False) + +plt.tight_layout() +plt.show() + +# Cleanup +sock.close() +print(f"\nReceived {frame_count[0]} frames total") \ No newline at end of file