diff --git a/VideoReceiverSFP/core/dump_manager.py b/VideoReceiverSFP/core/dump_manager.py index 6c98489..696b528 100644 --- a/VideoReceiverSFP/core/dump_manager.py +++ b/VideoReceiverSFP/core/dump_manager.py @@ -49,6 +49,8 @@ class DumpManager: self._video_fps_map: Dict[str, float] = {} # Maximum duplicate frames to write when spacing is large (safety cap) self._max_duplicate_frames = 1000 + # Track last produced video file paths per category + self._video_paths: Dict[str, str] = {} # prune existing files at startup for cat in list(self._saved.keys()): @@ -112,6 +114,8 @@ class DumpManager: return False self._video_writers[category] = writer self._video_start_times[category] = time.time() + # remember video path for later retrieval + self._video_paths[category] = path # store configured fps per category try: self._video_fps_map[category] = float(fps) @@ -171,6 +175,10 @@ class DumpManager: self._video_writers.pop(category, None) self._video_start_times.pop(category, None) self._last_frame_timestamps.pop(category, None) + try: + self._video_paths.pop(category, None) + except Exception: + pass def _enqueue(self, path: str, category: str) -> None: """Track saved file and trigger pruning.""" @@ -178,6 +186,38 @@ class DumpManager: self._saved[cat].append(path) self._prune_category(cat) + def get_last_saved(self, category: str) -> Optional[str]: + """Return the last saved preview path for a category, or None.""" + try: + cat = category if category in self._saved else 'unknown' + if not self._saved.get(cat): + return None + return self._saved[cat][-1] + except Exception: + return None + + def get_last_video_path(self, category: Optional[str] = None) -> Optional[str]: + """Return the last started video path. + + If `category` is provided, return that category's last video path. + Otherwise return the most-recently-started video across categories. + """ + try: + if category: + return self._video_paths.get(category) + # choose latest by _video_start_times + best_cat = None + best_ts = 0.0 + for cat, ts in self._video_start_times.items(): + if ts and ts > best_ts: + best_ts = ts + best_cat = cat + if best_cat: + return self._video_paths.get(best_cat) + return None + except Exception: + return None + def _prune_category(self, category: str) -> None: """Remove oldest files if they exceed the keep limit.""" prefix = f'VideoReceiverSFP_{category}_' diff --git a/VideoReceiverSFP/core/sfp_module.py b/VideoReceiverSFP/core/sfp_module.py index 0aede76..7802d2d 100644 --- a/VideoReceiverSFP/core/sfp_module.py +++ b/VideoReceiverSFP/core/sfp_module.py @@ -1262,6 +1262,36 @@ class SfpConnectorModule: except Exception: pass + # --- API: last saved file paths ------------------------------------- + def get_last_saved_mfd(self) -> Optional[str]: + try: + if getattr(self, '_dump_manager', None) is not None: + return self._dump_manager.get_last_saved('mfd') + # fallback: check module-level saved lists + if getattr(self, '_saved_pngs', None): + return self._saved_pngs[-1] if len(self._saved_pngs) else None + except Exception: + pass + return None + + def get_last_saved_sar(self) -> Optional[str]: + try: + if getattr(self, '_dump_manager', None) is not None: + return self._dump_manager.get_last_saved('sar') + if getattr(self, '_saved_sar_pngs', None): + return self._saved_sar_pngs[-1] if len(self._saved_sar_pngs) else None + except Exception: + pass + return None + + def get_last_video_path(self, category: Optional[str] = None) -> Optional[str]: + try: + if getattr(self, '_dump_manager', None) is not None: + return self._dump_manager.get_last_video_path(category) + except Exception: + pass + return None + def update_mfd_lut(self, lut) -> None: """Receive an MFD LUT (numpy array or similar) from the UI and store it. diff --git a/VideoReceiverSFP/core/test_orchestrator.py b/VideoReceiverSFP/core/test_orchestrator.py index 4f179db..552e78d 100644 --- a/VideoReceiverSFP/core/test_orchestrator.py +++ b/VideoReceiverSFP/core/test_orchestrator.py @@ -128,7 +128,119 @@ def run_orchestrator(): main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(1, weight=1) # The viewer row is the one that grows + # --- Row 0b: Last saved paths (for copy/paste) --- + paths_frame = ttk.Frame(controls_box) + paths_frame.pack(fill='x', expand=False, pady=(6,0)) + + mfd_img_save_var = tk.BooleanVar(value=bool(getattr(module, '_save_png', False))) + mfd_img_cb = ttk.Checkbutton(paths_frame, text='Last MFD file image', variable=mfd_img_save_var) + mfd_img_cb.grid(row=0, column=0, sticky='w', padx=5) + last_mfd_var = tk.StringVar(value="") + last_mfd_entry = ttk.Entry(paths_frame, textvariable=last_mfd_var, width=80, state='readonly') + last_mfd_entry.grid(row=0, column=1, padx=5, sticky='w') + def _copy_mfd(): + try: + root.clipboard_clear() + root.clipboard_append(last_mfd_var.get() or '') + except Exception: + pass + ttk.Button(paths_frame, text='Copy', command=_copy_mfd, width=6).grid(row=0, column=2, padx=4) + + sar_img_save_var = tk.BooleanVar(value=bool(getattr(module, '_sar_save_png', False))) + sar_img_cb = ttk.Checkbutton(paths_frame, text='Last SAR file image', variable=sar_img_save_var) + sar_img_cb.grid(row=1, column=0, sticky='w', padx=5) + last_sar_var = tk.StringVar(value="") + last_sar_entry = ttk.Entry(paths_frame, textvariable=last_sar_var, width=80, state='readonly') + last_sar_entry.grid(row=1, column=1, padx=5, sticky='w') + def _copy_sar(): + try: + root.clipboard_clear() + root.clipboard_append(last_sar_var.get() or '') + except Exception: + pass + ttk.Button(paths_frame, text='Copy', command=_copy_sar, width=6).grid(row=1, column=2, padx=4) + + mfd_vid_save_var = tk.BooleanVar(value=bool(getattr(module, '_record_mfd_video', False))) + mfd_vid_cb = ttk.Checkbutton(paths_frame, text='Last MFD file video', variable=mfd_vid_save_var) + mfd_vid_cb.grid(row=2, column=0, sticky='w', padx=5) + last_mfd_vid_var = tk.StringVar(value="") + last_mfd_vid_entry = ttk.Entry(paths_frame, textvariable=last_mfd_vid_var, width=80, state='readonly') + last_mfd_vid_entry.grid(row=2, column=1, padx=5, sticky='w') + def _copy_mfd_vid(): + try: + root.clipboard_clear() + root.clipboard_append(last_mfd_vid_var.get() or '') + except Exception: + pass + ttk.Button(paths_frame, text='Copy', command=_copy_mfd_vid, width=6).grid(row=2, column=2, padx=4) + + sar_vid_save_var = tk.BooleanVar(value=bool(getattr(module, '_record_sar_video', False))) + sar_vid_cb = ttk.Checkbutton(paths_frame, text='Last SAR file video', variable=sar_vid_save_var) + sar_vid_cb.grid(row=3, column=0, sticky='w', padx=5) + last_sar_vid_var = tk.StringVar(value="") + last_sar_vid_entry = ttk.Entry(paths_frame, textvariable=last_sar_vid_var, width=80, state='readonly') + last_sar_vid_entry.grid(row=3, column=1, padx=5, sticky='w') + def _copy_sar_vid(): + try: + root.clipboard_clear() + root.clipboard_append(last_sar_vid_var.get() or '') + except Exception: + pass + ttk.Button(paths_frame, text='Copy', command=_copy_sar_vid, width=6).grid(row=3, column=2, padx=4) + + def _refresh_paths(): + try: + m = module.get_last_saved_mfd() + s = module.get_last_saved_sar() + vm = module.get_last_video_path('mfd') + vs = module.get_last_video_path('sar') + try: + last_mfd_var.set(m or '') + except Exception: + pass + try: + last_sar_var.set(s or '') + except Exception: + pass + try: + last_mfd_vid_var.set(vm or '') + except Exception: + pass + try: + last_sar_vid_var.set(vs or '') + except Exception: + pass + except Exception: + pass + + ttk.Button(paths_frame, text='Refresh Paths', command=_refresh_paths).grid(row=0, column=3, rowspan=4, padx=8) + + # initial refresh + try: + _refresh_paths() + except Exception: + pass + + # Periodic automatic refresh of last-saved paths (every 2 seconds) + def _periodic_refresh(): + try: + _refresh_paths() + except Exception: + pass + try: + root.after(2000, _periodic_refresh) + except Exception: + pass + + try: + root.after(2000, _periodic_refresh) + except Exception: + pass + # Callbacks for Parameters + # Track whether the user has started a 'manual' recording session + user_recording_active_var = tk.BooleanVar(value=False) + def on_mfd_param_changed(param_type, name, value): try: if param_type == 'save_png': @@ -208,20 +320,108 @@ def run_orchestrator(): # Button Actions def on_start_recording(): + # Read desired targets from checkboxes and only apply when starting + save_mfd = bool(mfd_img_save_var.get()) + save_sar = bool(sar_img_save_var.get()) + rec_mfd = bool(mfd_vid_save_var.get()) + rec_sar = bool(sar_vid_save_var.get()) + + # If nothing selected, do nothing (per user request) + if not (save_mfd or save_sar or rec_mfd or rec_sar): + return + + # Apply selections to module: image saving and video recording only while 'recording' + try: + module.set_save_png(save_mfd) + except Exception: + pass + try: + module.set_sar_save_png(save_sar) + except Exception: + pass + try: + module.set_record_mfd_video(rec_mfd) + except Exception: + pass + try: + module.set_record_sar_video(rec_sar) + except Exception: + pass + + # Mark that user requested recording (even if only image saving) + try: + user_recording_active_var.set(True) + except Exception: + pass + + # Lock checkboxes while recording/armed + try: + mfd_img_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + sar_img_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + mfd_vid_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + sar_vid_cb.config(state=tk.DISABLED) + except Exception: + pass + start_rec_btn.config(state=tk.DISABLED) stop_rec_btn.config(state=tk.NORMAL) rec_status_var.set("Recording...") rec_status_label.config(foreground="red") - module.set_record_mfd_video(True) - module.set_record_sar_video(True) - + def on_stop_recording(): + # Stop all recording/saving targets and unlock checkboxes + try: + module.set_record_mfd_video(False) + except Exception: + pass + try: + module.set_record_sar_video(False) + except Exception: + pass + try: + module.set_save_png(False) + except Exception: + pass + try: + module.set_sar_save_png(False) + except Exception: + pass + + try: + mfd_img_cb.config(state=tk.NORMAL) + except Exception: + pass + try: + sar_img_cb.config(state=tk.NORMAL) + except Exception: + pass + try: + mfd_vid_cb.config(state=tk.NORMAL) + except Exception: + pass + try: + sar_vid_cb.config(state=tk.NORMAL) + except Exception: + pass + + try: + user_recording_active_var.set(False) + except Exception: + pass + start_rec_btn.config(state=tk.NORMAL) stop_rec_btn.config(state=tk.DISABLED) rec_status_var.set("Not Recording") rec_status_label.config(foreground="gray") - module.set_record_mfd_video(False) - module.set_record_sar_video(False) start_rec_btn.config(command=on_start_recording) stop_rec_btn.config(command=on_stop_recording) @@ -229,7 +429,9 @@ def run_orchestrator(): # Periodically update recording status indicator based on module state def _update_record_status(): try: - armed = bool(getattr(module, '_record_mfd_video', False) or getattr(module, '_record_sar_video', False)) + # consider user-initiated recording as 'armed' even if video flags are not set + manual = bool(user_recording_active_var.get()) + armed = manual or bool(getattr(module, '_record_mfd_video', False) or getattr(module, '_record_sar_video', False)) active_mfd = bool(getattr(module, '_mfd_video_active', False)) active_sar = bool(getattr(module, '_sar_video_active', False)) if active_mfd or active_sar: @@ -242,16 +444,64 @@ def run_orchestrator(): rec_status_label.config(foreground="red") start_rec_btn.config(state=tk.DISABLED) stop_rec_btn.config(state=tk.NORMAL) + try: + mfd_vid_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + sar_vid_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + mfd_img_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + sar_img_cb.config(state=tk.DISABLED) + except Exception: + pass elif armed: rec_status_var.set("Armed (waiting for first frame)") rec_status_label.config(foreground="orange") start_rec_btn.config(state=tk.DISABLED) stop_rec_btn.config(state=tk.NORMAL) + try: + mfd_vid_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + sar_vid_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + mfd_img_cb.config(state=tk.DISABLED) + except Exception: + pass + try: + sar_img_cb.config(state=tk.DISABLED) + except Exception: + pass else: rec_status_var.set("Not Recording") rec_status_label.config(foreground="gray") start_rec_btn.config(state=tk.NORMAL) stop_rec_btn.config(state=tk.DISABLED) + try: + mfd_vid_cb.config(state=tk.NORMAL) + except Exception: + pass + try: + sar_vid_cb.config(state=tk.NORMAL) + except Exception: + pass + try: + mfd_img_cb.config(state=tk.NORMAL) + except Exception: + pass + try: + sar_img_cb.config(state=tk.NORMAL) + except Exception: + pass except Exception: pass try: diff --git a/todos.md b/todos.md index 3c24d30..f4e6921 100644 --- a/todos.md +++ b/todos.md @@ -7,7 +7,7 @@ - [ ] VRSFP: aggiungere la possibilità di modificare la configurazione rutime, com e poter aggiungere la possibilità di salvare le immagini invece che il video o altro: creare una funzione setConfig che permetta di modificare runtime la configurazione del modulo. La stessa informazioni come formato deve essere la stessa che viene passata in fase di inizializzazione. Visto che la configurazione possa essere modificata anche in altre parti sarebbe utile anche a vere una funzione che restituisca il dizionario della configurazione come get_config(). - [x] VRSFP: poter aggiungere le funzioni di start salvataggio video e stop salvataggio video come api - [ ] VRSFP: poter impostare runtime il nome e la posizione del file immagine o video prodotto -- [ ] VRSFP: avere una funzione che restituisce nome e path dell'ultimo file salvato per mfd, sar e video +- [x] VRSFP: avere una funzione che restituisce nome e path dell'ultimo file salvato per mfd, sar e video - [x] VRSFP: aggiungere un log stile modulo aggiuntivo e collegarlo al modulo che poi sarà comune a tutti i moduli - [x] VRSFP: aggiungere un pannello al test_orchestator dove poter aggiungere i controlli per testare le funzionalità di registrazione ondemand ecc - [x] VRSFP: poter impostare a priori la dimensione in pixel della finestra mfd e della finestra sar. Per la finestra mfd, se viene indicato "0, 0 " viene usata la dimensione dell'immagine che viene spedita. se è diversa da 0,0 viene ridmensionata la finestra. Invece per il sar la dimensione è sempre quella specificata e deve sempre essere diversa da 0,0.