sistemata visualizzazione sar e start acquisitzione mfd
This commit is contained in:
parent
ff64d81a38
commit
f4b190fa48
200
VideoReceiverSFP/core/payload_dispatcher.py
Normal file
200
VideoReceiverSFP/core/payload_dispatcher.py
Normal file
@ -0,0 +1,200 @@
|
||||
"""Dispatch logic for decoded frames (MFD/SAR/generic).
|
||||
|
||||
Provides `dispatch_frame(module, frame, leader)` which encapsulates
|
||||
recording, saving previews, and invoking registered callbacks.
|
||||
"""
|
||||
from typing import Any, Optional
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import pathlib
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageOps, ImageEnhance
|
||||
except Exception:
|
||||
Image = None
|
||||
ImageOps = None
|
||||
ImageEnhance = None
|
||||
|
||||
|
||||
def dispatch_frame(module: Any, frame: Any, leader: Optional[Any]) -> None:
|
||||
"""Handle a decoded frame: start/ write video, save previews, and call callbacks.
|
||||
|
||||
`module` is the `SfpConnectorModule` instance.
|
||||
"""
|
||||
try:
|
||||
# Determine image type
|
||||
image_type_id = None
|
||||
try:
|
||||
if leader is not None:
|
||||
image_type_id = int(leader.HEADER_DATA.TYPE)
|
||||
except Exception:
|
||||
image_type_id = None
|
||||
|
||||
# SAR path
|
||||
if image_type_id == getattr(module, 'IMAGE_TYPE_SAR', 2):
|
||||
try:
|
||||
if Image is not None and hasattr(frame, 'copy'):
|
||||
sar_img = frame.copy()
|
||||
if getattr(module, '_sar_autocontrast', False) and ImageOps is not None:
|
||||
try:
|
||||
sar_img = ImageOps.autocontrast(sar_img)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
bf = max(0.0, 1.0 + (float(getattr(module, '_sar_brightness', 0)) / 100.0))
|
||||
cf = max(0.0, 1.0 + (float(getattr(module, '_sar_contrast', 0)) / 100.0))
|
||||
if bf != 1.0 and ImageEnhance is not None:
|
||||
sar_img = ImageEnhance.Brightness(sar_img).enhance(bf)
|
||||
if cf != 1.0 and ImageEnhance is not None:
|
||||
sar_img = ImageEnhance.Contrast(sar_img).enhance(cf)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# save sar png
|
||||
if getattr(module, '_sar_save_png', False):
|
||||
try:
|
||||
if getattr(module, '_dump_manager', None) is not None:
|
||||
saved = module._dump_manager.save_preview_from_pil(sar_img, category='sar', fmt='png')
|
||||
if saved:
|
||||
logging.getLogger().info("VideoReceiverSFP: saved sar png %s", saved)
|
||||
else:
|
||||
ts = time.strftime('%Y%m%d_%H%M%S') + (f"_{int(time.time() * 1000) % 1000:03d}")
|
||||
dumps_dir = getattr(module, '_dumps_dir', os.path.join(os.getcwd(), 'dumps'))
|
||||
os.makedirs(dumps_dir, exist_ok=True)
|
||||
pngpath = os.path.join(dumps_dir, f'VideoReceiverSFP_sar_{ts}.png')
|
||||
sar_img.save(pngpath)
|
||||
module._saved_sar_pngs.append(pngpath)
|
||||
while len(module._saved_sar_pngs) > module._dump_keep:
|
||||
old = module._saved_sar_pngs.popleft()
|
||||
try:
|
||||
pathlib.Path(old).unlink()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception("VideoReceiverSFP: failed saving sar png frame")
|
||||
|
||||
# sar video
|
||||
try:
|
||||
if getattr(module, '_record_sar_video', False) and getattr(module, '_dump_manager', None) is not None:
|
||||
if not getattr(module, '_sar_video_active', False):
|
||||
try:
|
||||
w, h = None, None
|
||||
if Image is not None and hasattr(sar_img, 'size'):
|
||||
w, h = sar_img.size
|
||||
else:
|
||||
try:
|
||||
import numpy as _np
|
||||
arr = _np.asarray(sar_img)
|
||||
if arr is not None and arr.ndim >= 2:
|
||||
h, w = arr.shape[0], arr.shape[1]
|
||||
except Exception:
|
||||
pass
|
||||
if w and h:
|
||||
started = module._dump_manager.start_video_record('sar', w, h, fps=getattr(module, '_video_fps', 20))
|
||||
if started:
|
||||
module._sar_video_active = True
|
||||
try:
|
||||
logging.getLogger().info("VideoReceiverSFP: started SAR video writer (%dx%d @ %s FPS)", w, h, getattr(module, '_video_fps', 20))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception("VideoReceiverSFP: failed to start sar video writer")
|
||||
if getattr(module, '_sar_video_active', False):
|
||||
try:
|
||||
module._dump_manager.write_video_frame(sar_img, 'sar')
|
||||
except Exception:
|
||||
logging.getLogger().exception("VideoReceiverSFP: failed writing sar frame to video")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# deliver to callbacks
|
||||
for scb in list(getattr(module, '_sar_callbacks', [])):
|
||||
try:
|
||||
scb(sar_img)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception("VideoReceiverSFP: error processing SAR image for SAR callbacks")
|
||||
|
||||
# MFD path
|
||||
if image_type_id == getattr(module, 'IMAGE_TYPE_MFD', 1):
|
||||
try:
|
||||
try:
|
||||
if getattr(module, '_record_mfd_video', False) and getattr(module, '_dump_manager', None) is not None:
|
||||
if not getattr(module, '_mfd_video_active', False):
|
||||
try:
|
||||
w, h = None, None
|
||||
if Image is not None and hasattr(frame, 'size'):
|
||||
w, h = frame.size
|
||||
else:
|
||||
try:
|
||||
import numpy as _np
|
||||
arr = _np.asarray(frame)
|
||||
if arr is not None and arr.ndim >= 2:
|
||||
h, w = arr.shape[0], arr.shape[1]
|
||||
except Exception:
|
||||
pass
|
||||
if w and h:
|
||||
started = module._dump_manager.start_video_record('mfd', w, h, fps=getattr(module, '_video_fps', 20))
|
||||
if started:
|
||||
module._mfd_video_active = True
|
||||
try:
|
||||
logging.getLogger().info("VideoReceiverSFP: started MFD video writer (%dx%d @ %s FPS)", w, h, getattr(module, '_video_fps', 20))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed to start mfd video writer')
|
||||
if getattr(module, '_mfd_video_active', False):
|
||||
try:
|
||||
module._dump_manager.write_video_frame(frame, 'mfd')
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed writing mfd frame to video')
|
||||
try:
|
||||
if not getattr(module, '_mfd_callbacks', False):
|
||||
logging.getLogger().debug('VideoReceiverSFP: mfd frames are being recorded but no MFD callbacks are registered')
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for mcb in list(getattr(module, '_mfd_callbacks', [])):
|
||||
try:
|
||||
mcb(frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# generic callbacks and saving
|
||||
for cb in list(getattr(module, '_callbacks', [])):
|
||||
try:
|
||||
if getattr(module, '_save_png', False) and Image is not None and hasattr(frame, 'save'):
|
||||
try:
|
||||
if getattr(module, '_dump_manager', None) is not None:
|
||||
saved = module._dump_manager.save_preview_from_pil(frame, category='frame', fmt='png')
|
||||
if saved:
|
||||
logging.getLogger().info("VideoReceiverSFP: saved frame to %s", saved)
|
||||
else:
|
||||
ts = time.strftime('%Y%m%d_%H%M%S') + (f"_{int(time.time() * 1000) % 1000:03d}")
|
||||
dumps_dir = getattr(module, '_dumps_dir', os.path.join(os.getcwd(), 'dumps'))
|
||||
os.makedirs(dumps_dir, exist_ok=True)
|
||||
pngpath = os.path.join(dumps_dir, f'VideoReceiverSFP_frame_{ts}.png')
|
||||
frame.save(pngpath)
|
||||
module._saved_pngs.append(pngpath)
|
||||
while len(module._saved_pngs) > module._dump_keep:
|
||||
old = module._saved_pngs.popleft()
|
||||
try:
|
||||
pathlib.Path(old).unlink()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed to save preview png')
|
||||
try:
|
||||
cb(frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: dispatch_frame unexpected error')
|
||||
@ -113,10 +113,16 @@ def save_payload_preview(module, leader, payload, ity, dbgdir, ts):
|
||||
base_name = 'mfd' if ity == 1 else ('sar' if ity == 2 else 'unknown')
|
||||
fmt = 'bmp'
|
||||
if getattr(module, '_dump_manager', None) is not None:
|
||||
saved = module._dump_manager.save_preview_from_array(norm, category=base_name, fmt=fmt)
|
||||
if saved:
|
||||
logging.getLogger().info('VideoReceiverSFP: saved payload preview %s', saved)
|
||||
return
|
||||
try:
|
||||
# Convert numpy array to PIL Image and use existing DumpManager API
|
||||
from PIL import Image as _PILImage
|
||||
pil_img = _PILImage.fromarray(norm, mode='L')
|
||||
saved = module._dump_manager.save_preview_from_pil(pil_img, category=base_name, fmt=fmt)
|
||||
if saved:
|
||||
logging.getLogger().info('VideoReceiverSFP: saved payload preview %s', saved)
|
||||
return
|
||||
except Exception:
|
||||
logger.exception('save_payload_preview: DumpManager save_preview_from_pil failed for array input')
|
||||
else:
|
||||
from PIL import Image as _PILImage
|
||||
ts_name = ts + (f"_{int(__import__('time').time() * 1000) % 1000:03d}")
|
||||
|
||||
@ -4,6 +4,7 @@ This module implements a simple L0-like adapter that can run in simulated
|
||||
mode by reading images from a local directory and publishing frames to
|
||||
registered callbacks, or in network mode by listening for SFP UDP packets.
|
||||
"""
|
||||
|
||||
from typing import Callable, Dict, Any, Optional
|
||||
from collections import deque
|
||||
import pathlib
|
||||
@ -54,24 +55,34 @@ try:
|
||||
from .dump_manager import DumpManager
|
||||
except Exception:
|
||||
DumpManager = None
|
||||
|
||||
try:
|
||||
from .payload_saver import save_bin_payload, save_unknown_sample
|
||||
except Exception:
|
||||
save_bin_payload = None
|
||||
save_unknown_sample = None
|
||||
try:
|
||||
from .payload_dispatcher import dispatch_frame
|
||||
except Exception:
|
||||
dispatch_frame = None
|
||||
|
||||
# --- Image Type Constants ---
|
||||
# Based on actual payload analysis: TYPE=1 with PALTYPE=1 is MFD, TYPE=2 is SAR
|
||||
IMAGE_TYPE_UNKNOWN = 0
|
||||
IMAGE_TYPE_MFD = 1 # MFD images have TYPE=1 and typically PALTYPE=1 (indexed palette)
|
||||
IMAGE_TYPE_SAR = 2 # SAR images have TYPE=2
|
||||
IMAGE_TYPE_MFD = 1
|
||||
IMAGE_TYPE_SAR = 2
|
||||
|
||||
def get_image_type_name(type_id):
|
||||
"""Helper function to get the name of the image type."""
|
||||
return {
|
||||
IMAGE_TYPE_SAR: "SAR",
|
||||
IMAGE_TYPE_MFD: "MFD",
|
||||
}.get(type_id, f"Unknown ({type_id})")
|
||||
|
||||
def get_image_type_name(type_id: int) -> str:
|
||||
"""Return a short name for image type ids used in payloads."""
|
||||
try:
|
||||
if int(type_id) == IMAGE_TYPE_MFD:
|
||||
return 'MFD'
|
||||
if int(type_id) == IMAGE_TYPE_SAR:
|
||||
return 'SAR'
|
||||
except Exception:
|
||||
pass
|
||||
return 'UNKNOWN'
|
||||
|
||||
|
||||
class SfpConnectorModule:
|
||||
@ -83,7 +94,7 @@ class SfpConnectorModule:
|
||||
dispatch completed payloads to callbacks.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "VideoReceiverSFP"):
|
||||
def __init__(self, name: str = "VideoReceiverSFP") -> None:
|
||||
self.module_name = name
|
||||
self.is_running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
@ -102,7 +113,6 @@ class SfpConnectorModule:
|
||||
self._image_type_filter: Optional[int] = None
|
||||
self._mfd_lut = None
|
||||
# saving flags and queues
|
||||
# Saving of received payloads is disabled by default; use config flags
|
||||
self._save_png = False
|
||||
self._save_bin = False
|
||||
# video recording flags (disabled by default)
|
||||
@ -121,7 +131,6 @@ class SfpConnectorModule:
|
||||
self._dump_keep = 100
|
||||
self._dumps_dir = os.path.join(os.getcwd(), 'dumps')
|
||||
|
||||
# centralized dump manager for saving/rotation
|
||||
try:
|
||||
if DumpManager is not None:
|
||||
self._dump_manager = DumpManager(self._dumps_dir, self._dump_keep)
|
||||
@ -129,377 +138,22 @@ class SfpConnectorModule:
|
||||
self._dump_manager = None
|
||||
except Exception:
|
||||
self._dump_manager = None
|
||||
# deques to track saved files for rotation
|
||||
|
||||
self._saved_pngs = deque()
|
||||
self._saved_bins = deque()
|
||||
|
||||
# dedicated callback lists for MFD and SAR viewers
|
||||
self._mfd_callbacks = []
|
||||
self._sar_callbacks = []
|
||||
# callbacks for SAR metadata display (receives a formatted string)
|
||||
self._sar_metadata_callbacks = []
|
||||
|
||||
# SAR-specific display controls
|
||||
self._sar_brightness = 0
|
||||
self._sar_contrast = 0
|
||||
self._sar_autocontrast = False
|
||||
self._saved_sar_pngs = deque()
|
||||
self._sar_save_png = False
|
||||
|
||||
# track whether we've saved the first decoded frame to disk
|
||||
self._first_frame_saved = False
|
||||
self._first_frame_path: Optional[str] = None
|
||||
|
||||
# internal counters for logging
|
||||
self._frames_received = 0
|
||||
|
||||
def initialize(self, config: Dict[str, Any]) -> bool:
|
||||
"""Initialize the module.
|
||||
|
||||
Config keys:
|
||||
- sim_dir: optional directory with sample images for simulated mode
|
||||
- fps: frames per second (default 5)
|
||||
- host: optional host to bind for network mode
|
||||
- port: optional UDP port for network mode
|
||||
"""
|
||||
self._sim_dir = config.get("sim_dir")
|
||||
self._fps = int(config.get("fps", 5))
|
||||
self._host = config.get("host")
|
||||
self._port = config.get("port")
|
||||
self._normalize_method = config.get("normalize", None)
|
||||
|
||||
image_type_str = config.get("image_type_filter")
|
||||
if image_type_str:
|
||||
image_type_str = str(image_type_str).upper()
|
||||
if image_type_str == 'MFD':
|
||||
self._image_type_filter = IMAGE_TYPE_MFD
|
||||
logging.info("VideoReceiverSFP: Filtering for MFD images only.")
|
||||
elif image_type_str == 'SAR':
|
||||
self._image_type_filter = IMAGE_TYPE_SAR
|
||||
logging.info("VideoReceiverSFP: Filtering for SAR images only.")
|
||||
else:
|
||||
logging.warning("VideoReceiverSFP: Unknown image_type_filter=%s", image_type_str)
|
||||
try:
|
||||
logging.getLogger().debug("VideoReceiverSFP.initialize: sim_dir=%s fps=%s host=%s port=%s", self._sim_dir, self._fps, self._host, self._port)
|
||||
except Exception:
|
||||
pass
|
||||
# Read default recording flags from local module config if present
|
||||
try:
|
||||
# Try to prefer centralized app config only if it explicitly defines the keys.
|
||||
# Otherwise fall back to the local VideoReceiverSFP/core/config.py defaults.
|
||||
try:
|
||||
import controlpanel.config as appcfg
|
||||
from . import config as localcfg
|
||||
# If controlpanel.config explicitly defines the attributes, use them;
|
||||
# otherwise use the local module defaults.
|
||||
if hasattr(appcfg, 'RECORD_MFD_VIDEO'):
|
||||
try:
|
||||
self._record_mfd_video = bool(getattr(appcfg, 'RECORD_MFD_VIDEO'))
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self._record_mfd_video = bool(getattr(localcfg, 'RECORD_MFD_VIDEO', self._record_mfd_video))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(appcfg, 'RECORD_SAR_VIDEO'):
|
||||
try:
|
||||
self._record_sar_video = bool(getattr(appcfg, 'RECORD_SAR_VIDEO'))
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self._record_sar_video = bool(getattr(localcfg, 'RECORD_SAR_VIDEO', self._record_sar_video))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(appcfg, 'VIDEO_FPS'):
|
||||
try:
|
||||
self._video_fps = int(getattr(appcfg, 'VIDEO_FPS'))
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self._video_fps = int(getattr(localcfg, 'VIDEO_FPS', self._video_fps))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
# controlpanel.config not available; use local module config
|
||||
from .config import RECORD_MFD_VIDEO, RECORD_SAR_VIDEO, VIDEO_FPS
|
||||
try:
|
||||
self._record_mfd_video = bool(RECORD_MFD_VIDEO)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._record_sar_video = bool(RECORD_SAR_VIDEO)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._video_fps = int(VIDEO_FPS)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# Allow orchestrator to override recording flags/fps via the config dict
|
||||
try:
|
||||
# Keys expected: 'record_mfd_video', 'record_sar_video', 'video_fps'
|
||||
if 'record_mfd_video' in config:
|
||||
try:
|
||||
self._record_mfd_video = bool(config.get('record_mfd_video'))
|
||||
except Exception:
|
||||
pass
|
||||
if 'record_sar_video' in config:
|
||||
try:
|
||||
self._record_sar_video = bool(config.get('record_sar_video'))
|
||||
except Exception:
|
||||
pass
|
||||
if 'video_fps' in config:
|
||||
try:
|
||||
self._video_fps = int(config.get('video_fps'))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
def start_session(self) -> None:
|
||||
if self.is_running:
|
||||
return
|
||||
|
||||
# If network config provided and transport available, start transport
|
||||
if self._host and self._port and SfpTransport is not None:
|
||||
try:
|
||||
# Register distinct handlers per SFP flow so the transport
|
||||
# can call the correct one and we can rely on the flow id
|
||||
# (ord('M') for MFD, ord('S') for SAR) instead of only
|
||||
# trusting the embedded ImageLeader.TYPE which may be 0.
|
||||
handlers = {
|
||||
ord("M"): self._network_payload_handler_mfd,
|
||||
ord("S"): self._network_payload_handler_sar,
|
||||
}
|
||||
self._transport = SfpTransport(self._host, int(self._port), handlers)
|
||||
started = self._transport.start()
|
||||
if started:
|
||||
try:
|
||||
logging.getLogger().info("VideoReceiverSFP: started SfpTransport on %s:%s", self._host, self._port)
|
||||
except Exception:
|
||||
pass
|
||||
self.is_running = True
|
||||
return
|
||||
except Exception:
|
||||
logging.exception("Failed to start SfpTransport; falling back to sim mode")
|
||||
|
||||
# fallback to simulated folder stream
|
||||
self._stop_event.clear()
|
||||
self._thread = threading.Thread(target=self._loop, daemon=True)
|
||||
self._thread.start()
|
||||
self.is_running = True
|
||||
|
||||
def stop_session(self) -> None:
|
||||
if not self.is_running:
|
||||
return
|
||||
|
||||
# Stop transport if active
|
||||
if getattr(self, "_transport", None) is not None:
|
||||
try:
|
||||
self._transport.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
self._transport = None
|
||||
|
||||
# Ensure any active video recordings are closed
|
||||
try:
|
||||
if getattr(self, '_dump_manager', None) is not None:
|
||||
try:
|
||||
logging.getLogger().info("VideoReceiverSFP: requesting stop of MFD video writer")
|
||||
self._dump_manager.stop_video_record('mfd')
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
logging.getLogger().info("VideoReceiverSFP: requesting stop of SAR video writer")
|
||||
self._dump_manager.stop_video_record('sar')
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._stop_event.set()
|
||||
if self._thread is not None:
|
||||
self._thread.join(timeout=2.0)
|
||||
# mark writers inactive locally
|
||||
self._mfd_video_active = False
|
||||
self._sar_video_active = False
|
||||
self.is_running = False
|
||||
|
||||
def register_callback(self, cb: Callable[[Any], None]) -> None:
|
||||
if cb not in self._callbacks:
|
||||
self._callbacks.append(cb)
|
||||
try:
|
||||
logging.getLogger().debug("VideoReceiverSFP: registered callback %s", getattr(cb, '__name__', str(cb)))
|
||||
except Exception:
|
||||
pass
|
||||
# if we already have a last frame, deliver it immediately so UI can show initial image
|
||||
try:
|
||||
if self._last_frame is not None:
|
||||
try:
|
||||
cb(self._last_frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def register_mfd_callback(self, cb: Callable[[Any], None]) -> None:
|
||||
if cb not in self._mfd_callbacks:
|
||||
self._mfd_callbacks.append(cb)
|
||||
# push last frame immediately if available (helps initial UI refresh)
|
||||
try:
|
||||
if self._last_frame is not None:
|
||||
try:
|
||||
cb(self._last_frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def register_sar_callback(self, cb: Callable[[Any], None]) -> None:
|
||||
if cb not in self._sar_callbacks:
|
||||
self._sar_callbacks.append(cb)
|
||||
try:
|
||||
if self._last_frame is not None:
|
||||
try:
|
||||
cb(self._last_frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def register_sar_metadata_callback(self, cb: Callable[[str], None]) -> None:
|
||||
"""Register a callback that receives a formatted metadata string for the last SAR image."""
|
||||
if cb not in self._sar_metadata_callbacks:
|
||||
self._sar_metadata_callbacks.append(cb)
|
||||
|
||||
def _notify_sar_metadata(self, metadata_str: Optional[str]) -> None:
|
||||
if not metadata_str:
|
||||
return
|
||||
for cb in list(self._sar_metadata_callbacks):
|
||||
try:
|
||||
cb(metadata_str)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"is_running": self.is_running,
|
||||
"sim_dir": self._sim_dir,
|
||||
"fps": self._fps,
|
||||
"host": self._host,
|
||||
"port": self._port,
|
||||
}
|
||||
|
||||
def get_data_stream(self) -> Any:
|
||||
with self._frame_lock:
|
||||
return self._last_frame
|
||||
|
||||
def update_mfd_lut(self, new_lut) -> None:
|
||||
"""Update the MFD LUT used for MFD image decoding.
|
||||
|
||||
Args:
|
||||
new_lut: 256x3 RGB LUT array (uint8)
|
||||
"""
|
||||
self._mfd_lut = new_lut
|
||||
logging.info("VideoReceiverSFP: MFD LUT updated")
|
||||
# Optionally re-decode last frame if it was MFD type
|
||||
# (For simplicity, next frame will use new LUT)
|
||||
|
||||
def save_last_frame(self, path: str) -> bool:
|
||||
frame = self.get_data_stream()
|
||||
if frame is None:
|
||||
return False
|
||||
try:
|
||||
if Image is not None and hasattr(frame, "save"):
|
||||
frame.save(path)
|
||||
else:
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(frame)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _list_sim_files(self):
|
||||
if not self._sim_dir or not os.path.isdir(self._sim_dir):
|
||||
return []
|
||||
files = [f for f in sorted(os.listdir(self._sim_dir)) if not f.startswith(".")]
|
||||
return [os.path.join(self._sim_dir, f) for f in files]
|
||||
|
||||
def _load_next_frame(self):
|
||||
files = self._list_sim_files()
|
||||
if not files:
|
||||
return None
|
||||
path = files[self._frame_idx % len(files)]
|
||||
self._frame_idx += 1
|
||||
try:
|
||||
if Image is not None:
|
||||
# Open without forced conversion so we can detect grayscale/index images
|
||||
img = Image.open(path)
|
||||
try:
|
||||
mode = getattr(img, 'mode', None)
|
||||
# If the file is single-channel (likely an MFD index image), attempt to colorize
|
||||
if mode in ('L', 'P'):
|
||||
try:
|
||||
import numpy as _np
|
||||
inds = _np.array(img.convert('L'))
|
||||
self._ensure_mfd_lut()
|
||||
if self._mfd_lut is not None and apply_mfd_lut is not None:
|
||||
rgb = apply_mfd_lut(inds, self._mfd_lut)
|
||||
if rgb is not None and Image is not None:
|
||||
try:
|
||||
frame = Image.fromarray(rgb.astype('uint8'), mode='RGB')
|
||||
dumps_dir = os.path.join(os.getcwd(), 'dumps')
|
||||
os.makedirs(dumps_dir, exist_ok=True)
|
||||
dbg_path = os.path.join(dumps_dir, 'VideoReceiverSFP_colorized_debug.png')
|
||||
frame.save(dbg_path)
|
||||
except Exception:
|
||||
pass
|
||||
return frame
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Some dumps are stored as RGB but actually contain index images (identical channels / low cardinality)
|
||||
if mode == 'RGB':
|
||||
try:
|
||||
import numpy as _np
|
||||
larr = _np.array(img.convert('L'))
|
||||
unique_vals = _np.unique(larr)
|
||||
# If unique index count is small (<=32) treat as indices and colorize
|
||||
if unique_vals.size <= 32:
|
||||
self._ensure_mfd_lut()
|
||||
if self._mfd_lut is not None and apply_mfd_lut is not None:
|
||||
rgb = apply_mfd_lut(larr, self._mfd_lut)
|
||||
if rgb is not None and Image is not None:
|
||||
try:
|
||||
frame = Image.fromarray(rgb.astype('uint8'), mode='RGB')
|
||||
dumps_dir = os.path.join(os.getcwd(), 'dumps')
|
||||
os.makedirs(dumps_dir, exist_ok=True)
|
||||
dbg_path = os.path.join(dumps_dir, 'VideoReceiverSFP_colorized_debug.png')
|
||||
frame.save(dbg_path)
|
||||
except Exception:
|
||||
pass
|
||||
return frame
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Default: return an RGB-converted copy for display
|
||||
return img.convert("RGB")
|
||||
except Exception:
|
||||
return img.convert("RGB")
|
||||
else:
|
||||
with open(path, "rb") as fh:
|
||||
return fh.read()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _loop(self):
|
||||
def _loop(self) -> None:
|
||||
interval = 1.0 / max(1, self._fps)
|
||||
while not self._stop_event.is_set():
|
||||
frame = self._load_next_frame()
|
||||
@ -1437,6 +1091,70 @@ class SfpConnectorModule:
|
||||
self._save_bin = bool(enabled)
|
||||
logging.getLogger().info("VideoReceiverSFP: save_bin set to %s", self._save_bin)
|
||||
|
||||
def initialize(self, config: Dict[str, Any]) -> bool:
|
||||
"""Initialize the module with a config dict.
|
||||
|
||||
Supported keys:
|
||||
- sim_dir, fps, host, port, normalize, image_type_filter
|
||||
- record_mfd_video, record_sar_video, video_fps (overrides)
|
||||
"""
|
||||
try:
|
||||
self._sim_dir = config.get("sim_dir")
|
||||
self._fps = int(config.get("fps", self._fps))
|
||||
self._host = config.get("host")
|
||||
self._port = config.get("port")
|
||||
self._normalize_method = config.get("normalize", None)
|
||||
|
||||
image_type_str = config.get("image_type_filter")
|
||||
if image_type_str:
|
||||
image_type_str = str(image_type_str).upper()
|
||||
if image_type_str == 'MFD':
|
||||
self._image_type_filter = IMAGE_TYPE_MFD
|
||||
logging.info("VideoReceiverSFP: Filtering for MFD images only.")
|
||||
elif image_type_str == 'SAR':
|
||||
self._image_type_filter = IMAGE_TYPE_SAR
|
||||
logging.info("VideoReceiverSFP: Filtering for SAR images only.")
|
||||
|
||||
# Prefer controlpanel.app config when present, otherwise local module config
|
||||
try:
|
||||
import controlpanel.config as appcfg
|
||||
from . import config as localcfg
|
||||
if hasattr(appcfg, 'RECORD_MFD_VIDEO'):
|
||||
self._record_mfd_video = bool(getattr(appcfg, 'RECORD_MFD_VIDEO'))
|
||||
else:
|
||||
self._record_mfd_video = bool(getattr(localcfg, 'RECORD_MFD_VIDEO', self._record_mfd_video))
|
||||
|
||||
if hasattr(appcfg, 'RECORD_SAR_VIDEO'):
|
||||
self._record_sar_video = bool(getattr(appcfg, 'RECORD_SAR_VIDEO'))
|
||||
else:
|
||||
self._record_sar_video = bool(getattr(localcfg, 'RECORD_SAR_VIDEO', self._record_sar_video))
|
||||
|
||||
if hasattr(appcfg, 'VIDEO_FPS'):
|
||||
self._video_fps = int(getattr(appcfg, 'VIDEO_FPS'))
|
||||
else:
|
||||
self._video_fps = int(getattr(localcfg, 'VIDEO_FPS', self._video_fps))
|
||||
except Exception:
|
||||
try:
|
||||
from .config import RECORD_MFD_VIDEO, RECORD_SAR_VIDEO, VIDEO_FPS
|
||||
self._record_mfd_video = bool(RECORD_MFD_VIDEO)
|
||||
self._record_sar_video = bool(RECORD_SAR_VIDEO)
|
||||
self._video_fps = int(VIDEO_FPS)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Allow direct overrides from orchestrator-provided config dict
|
||||
if 'record_mfd_video' in config:
|
||||
self._record_mfd_video = bool(config.get('record_mfd_video'))
|
||||
if 'record_sar_video' in config:
|
||||
self._record_sar_video = bool(config.get('record_sar_video'))
|
||||
if 'video_fps' in config:
|
||||
self._video_fps = int(config.get('video_fps'))
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: initialize failed')
|
||||
return False
|
||||
|
||||
def set_record_mfd_video(self, enabled: bool, fps: Optional[int] = None) -> None:
|
||||
try:
|
||||
self._record_mfd_video = bool(enabled)
|
||||
@ -1496,3 +1214,125 @@ class SfpConnectorModule:
|
||||
logging.getLogger().info("VideoReceiverSFP: sar_save_png set to %s", self._sar_save_png)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Callback registration helpers
|
||||
def register_callback(self, cb: Callable[[Any], None]) -> None:
|
||||
try:
|
||||
if cb not in self._callbacks:
|
||||
self._callbacks.append(cb)
|
||||
# Immediately deliver last frame if available
|
||||
try:
|
||||
if self._last_frame is not None:
|
||||
cb(self._last_frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed to register generic callback')
|
||||
|
||||
def register_mfd_callback(self, cb: Callable[[Any], None]) -> None:
|
||||
try:
|
||||
if cb not in self._mfd_callbacks:
|
||||
self._mfd_callbacks.append(cb)
|
||||
# deliver last frame immediately when present
|
||||
try:
|
||||
if self._last_frame is not None:
|
||||
cb(self._last_frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed to register mfd callback')
|
||||
|
||||
def register_sar_callback(self, cb: Callable[[Any], None]) -> None:
|
||||
try:
|
||||
if cb not in self._sar_callbacks:
|
||||
self._sar_callbacks.append(cb)
|
||||
try:
|
||||
if self._last_frame is not None:
|
||||
cb(self._last_frame)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed to register sar callback')
|
||||
|
||||
def register_sar_metadata_callback(self, cb: Callable[[str], None]) -> None:
|
||||
try:
|
||||
if cb not in self._sar_metadata_callbacks:
|
||||
self._sar_metadata_callbacks.append(cb)
|
||||
try:
|
||||
if getattr(self, '_last_sar_metadata_str', None) is not None:
|
||||
cb(self._last_sar_metadata_str)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed to register sar metadata callback')
|
||||
|
||||
def _notify_sar_metadata(self, metadata_str: Optional[str]) -> None:
|
||||
for cb in list(self._sar_metadata_callbacks):
|
||||
try:
|
||||
cb(metadata_str)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"is_running": bool(self.is_running),
|
||||
"sim_dir": self._sim_dir,
|
||||
"fps": self._fps,
|
||||
"host": self._host,
|
||||
"port": self._port,
|
||||
}
|
||||
|
||||
def start_session(self) -> bool:
|
||||
try:
|
||||
if self.is_running:
|
||||
return True
|
||||
# If host/port provided -> network mode using SfpTransport
|
||||
if self._host and self._port and SfpTransport is not None:
|
||||
handlers = {
|
||||
ord('M'): self._network_payload_handler_mfd,
|
||||
ord('S'): self._network_payload_handler_sar,
|
||||
}
|
||||
# general fallback handler for other flows
|
||||
handlers[0] = self._network_payload_handler
|
||||
try:
|
||||
self._transport = SfpTransport(self._host, int(self._port), handlers)
|
||||
started = self._transport.start()
|
||||
if not started:
|
||||
logging.getLogger().error('VideoReceiverSFP: failed to start SfpTransport')
|
||||
self._transport = None
|
||||
else:
|
||||
logging.getLogger().info('VideoReceiverSFP: SfpTransport started on %s:%s', self._host, self._port)
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: exception while creating SfpTransport')
|
||||
|
||||
# If sim_dir configured -> start simulation loop thread
|
||||
if self._sim_dir:
|
||||
self._stop_event.clear()
|
||||
if self._thread is None or not self._thread.is_alive():
|
||||
self._thread = threading.Thread(target=self._loop, daemon=True)
|
||||
self._thread.start()
|
||||
logging.getLogger().info('VideoReceiverSFP: simulation thread started')
|
||||
|
||||
self.is_running = True
|
||||
return True
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: failed to start session')
|
||||
return False
|
||||
|
||||
def stop_session(self) -> None:
|
||||
try:
|
||||
self._stop_event.set()
|
||||
if getattr(self, '_thread', None) is not None and self._thread.is_alive():
|
||||
try:
|
||||
self._thread.join(timeout=1.0)
|
||||
except Exception:
|
||||
pass
|
||||
if getattr(self, '_transport', None) is not None:
|
||||
try:
|
||||
self._transport.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
self._transport = None
|
||||
self.is_running = False
|
||||
except Exception:
|
||||
logging.getLogger().exception('VideoReceiverSFP: error stopping session')
|
||||
|
||||
@ -67,15 +67,18 @@ class SfpSarViewer:
|
||||
save_cb = ttk.Checkbutton(ctrl_frame, text="Save .png", variable=self._save_png_var, command=self._on_save_png_toggled)
|
||||
save_cb.grid(row=6, column=0, sticky="w", pady=(2,2))
|
||||
|
||||
# Status / FPS
|
||||
self._fps_label = ttk.Label(ctrl_frame, text="FPS: 0.00")
|
||||
self._fps_label.grid(row=6, column=0, sticky="w", pady=(8,0))
|
||||
|
||||
ctrl_frame.columnconfigure(0, weight=1)
|
||||
main.rowconfigure(0, weight=1)
|
||||
main.rowconfigure(1, weight=0)
|
||||
main.columnconfigure(0, weight=3)
|
||||
main.columnconfigure(1, weight=1)
|
||||
|
||||
# Bottom: Status bar (FPS)
|
||||
status_frame = ttk.Frame(main, relief="sunken")
|
||||
status_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(5, 0))
|
||||
self._fps_label = ttk.Label(status_frame, text="FPS: 0.00")
|
||||
self._fps_label.pack(side="left", padx=5)
|
||||
|
||||
# runtime state
|
||||
self._photo = None
|
||||
self._pending_frame = None
|
||||
|
||||
Loading…
Reference in New Issue
Block a user