sistemata visualizzazione sar e start acquisitzione mfd

This commit is contained in:
VALLONGOL 2026-01-16 13:31:56 +01:00
parent ff64d81a38
commit f4b190fa48
4 changed files with 425 additions and 376 deletions

View 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')

View File

@ -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)
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}")

View File

@ -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')

View File

@ -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