Add DearPyGUI observer/server UI and demo launcher
This commit is contained in:
61
aggiornamento-2026-06-03-21-10.md
Normal file
61
aggiornamento-2026-06-03-21-10.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
## Aggiornamento 2026-06-03 21:10
|
||||||
|
|
||||||
|
### Obiettivo
|
||||||
|
Introdurre una UI DearPyGUI solo per i componenti esterni:
|
||||||
|
- observer di `flywms_navigation`
|
||||||
|
- server `flywms_wms_server`
|
||||||
|
|
||||||
|
Lasciare invariata la parte intelligente del core:
|
||||||
|
- acquisizione
|
||||||
|
- inferenza
|
||||||
|
- tracking
|
||||||
|
- logica di navigazione e snapshot
|
||||||
|
|
||||||
|
### Baseline salvata
|
||||||
|
- Commit locale: `e86c05a`
|
||||||
|
- Tag locale: `gui-observer-in-opencv`
|
||||||
|
|
||||||
|
Nota: il push su Gitea non e' riuscito per problema di risoluzione DNS dell'host remoto.
|
||||||
|
|
||||||
|
### Documentazione aggiunta
|
||||||
|
- `dearpygui_observer_server_spec.md`
|
||||||
|
|
||||||
|
### Modifiche implementate
|
||||||
|
|
||||||
|
#### `flywms_navigation_observer.py`
|
||||||
|
- aggiunto supporto backend UI:
|
||||||
|
- `dearpygui`
|
||||||
|
- `opencv`
|
||||||
|
- `auto`
|
||||||
|
- mantenuto il protocollo socket esistente con il core
|
||||||
|
- aggiunta UI DearPyGUI con:
|
||||||
|
- preview principale navigazione
|
||||||
|
- preview snapshot
|
||||||
|
- preview crop etichetta
|
||||||
|
- pannello stato
|
||||||
|
- pannello metriche
|
||||||
|
- pannello comandi
|
||||||
|
- mantenuto fallback OpenCV
|
||||||
|
|
||||||
|
#### `flywms_wms_server.py`
|
||||||
|
- aggiunto supporto backend UI:
|
||||||
|
- `dearpygui`
|
||||||
|
- `opencv`
|
||||||
|
- `auto`
|
||||||
|
- lasciata invariata la logica FastAPI/OCR/ACK
|
||||||
|
- aggiunta UI DearPyGUI con:
|
||||||
|
- immagine ricevuta
|
||||||
|
- stato server
|
||||||
|
- payload OCR / WMS
|
||||||
|
- mantenuto fallback OpenCV
|
||||||
|
|
||||||
|
### Verifiche eseguite
|
||||||
|
- `python -m py_compile flywms_navigation_observer.py flywms_wms_server.py`
|
||||||
|
- import e selezione backend:
|
||||||
|
- `flywms_navigation_observer.choose_backend('auto') -> dearpygui`
|
||||||
|
- `flywms_wms_server.choose_ui_backend('auto') -> dearpygui`
|
||||||
|
|
||||||
|
### Stato attuale
|
||||||
|
- core intelligente invariato
|
||||||
|
- observer e server pronti per prova con DearPyGUI
|
||||||
|
- fallback OpenCV ancora disponibile per debug
|
||||||
103
dearpygui_observer_server_spec.md
Normal file
103
dearpygui_observer_server_spec.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# DearPyGUI UI Spec - Observer e WMS Server
|
||||||
|
|
||||||
|
## Scopo
|
||||||
|
|
||||||
|
Rendere la demo piu' ordinata e leggibile spostando la presentazione visuale da OpenCV a DearPyGUI nei soli componenti esterni:
|
||||||
|
|
||||||
|
- `flywms_navigation_observer.py`
|
||||||
|
- `flywms_wms_server.py`
|
||||||
|
|
||||||
|
La parte intelligente resta invariata:
|
||||||
|
|
||||||
|
- `flywms_navigation.py`
|
||||||
|
- tracking
|
||||||
|
- snapshot logic
|
||||||
|
- WMS client
|
||||||
|
- logica OCR / FastAPI lato server
|
||||||
|
|
||||||
|
## Principio architetturale
|
||||||
|
|
||||||
|
La UI non entra nel percorso critico.
|
||||||
|
|
||||||
|
### Observer
|
||||||
|
|
||||||
|
- riceve gia' telemetria e preview dal core;
|
||||||
|
- DearPyGUI sostituisce solo le finestre OpenCV;
|
||||||
|
- il protocollo socket e il payload restano invariati.
|
||||||
|
|
||||||
|
### WMS Server
|
||||||
|
|
||||||
|
- continua a ricevere upload e a fare OCR come oggi;
|
||||||
|
- DearPyGUI sostituisce solo le finestre OpenCV di monitoraggio;
|
||||||
|
- nessuna modifica al contratto HTTP.
|
||||||
|
|
||||||
|
## Layout Observer
|
||||||
|
|
||||||
|
### Colonna sinistra
|
||||||
|
|
||||||
|
- preview principale `navigate`
|
||||||
|
|
||||||
|
### Colonna destra superiore
|
||||||
|
|
||||||
|
- snapshot OCR payload
|
||||||
|
- crop etichetta
|
||||||
|
|
||||||
|
### Colonna destra inferiore
|
||||||
|
|
||||||
|
- stato / metriche
|
||||||
|
- comando corrente
|
||||||
|
- lista comandi
|
||||||
|
|
||||||
|
### Metriche minime
|
||||||
|
|
||||||
|
- `frame_id`
|
||||||
|
- `processed_frames` se presente
|
||||||
|
- `loop_fps`
|
||||||
|
- `yolo_fps`
|
||||||
|
- `last_yolo_ms`
|
||||||
|
- `det_count`
|
||||||
|
- `track_count`
|
||||||
|
- `snapshot_counter`
|
||||||
|
- `yolo_mode` se disponibile
|
||||||
|
- `motion_text`
|
||||||
|
|
||||||
|
## Layout WMS Server
|
||||||
|
|
||||||
|
### Colonna sinistra
|
||||||
|
|
||||||
|
- ultima immagine ricevuta dal server
|
||||||
|
|
||||||
|
### Colonna destra
|
||||||
|
|
||||||
|
- payload request corrente
|
||||||
|
- esito OCR
|
||||||
|
- esito ACK/NACK
|
||||||
|
- contatore richieste
|
||||||
|
|
||||||
|
## Requisiti UI
|
||||||
|
|
||||||
|
- finestre ridimensionabili
|
||||||
|
- nessun blocco sul core/server
|
||||||
|
- refresh periodico leggero
|
||||||
|
- fallback a OpenCV se DearPyGUI non e' disponibile
|
||||||
|
|
||||||
|
## Strategia di implementazione
|
||||||
|
|
||||||
|
### Fase 1
|
||||||
|
|
||||||
|
- introdurre backend DearPyGUI opzionale per observer
|
||||||
|
- introdurre backend DearPyGUI opzionale per WMS server
|
||||||
|
- mantenere OpenCV come fallback
|
||||||
|
|
||||||
|
### Fase 2
|
||||||
|
|
||||||
|
- rifinire il layout
|
||||||
|
- eventuali colori/stati piu' chiari
|
||||||
|
- eventuali controlli aggiuntivi
|
||||||
|
|
||||||
|
## Criterio di successo
|
||||||
|
|
||||||
|
1. la demo risulta piu' ordinata;
|
||||||
|
2. il core intelligente non cambia comportamento;
|
||||||
|
3. observer e server possono essere lanciati senza finestre OpenCV;
|
||||||
|
4. il fallback OpenCV resta disponibile in caso di problema.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
|
import ctypes
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
@@ -9,6 +10,44 @@ import numpy as np
|
|||||||
|
|
||||||
import flywms_navigation as nav
|
import flywms_navigation as nav
|
||||||
|
|
||||||
|
try:
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
dpg = None
|
||||||
|
|
||||||
|
|
||||||
|
NAV_TEXTURE = "obs_nav_texture"
|
||||||
|
SNAPSHOT_TEXTURE = "obs_snapshot_texture"
|
||||||
|
LABEL_TEXTURE = "obs_label_texture"
|
||||||
|
NAV_IMAGE = "obs_nav_image"
|
||||||
|
SNAPSHOT_IMAGE = "obs_snapshot_image"
|
||||||
|
LABEL_IMAGE = "obs_label_image"
|
||||||
|
TEXT_STATUS = "obs_status"
|
||||||
|
TEXT_COMMANDS = "obs_commands"
|
||||||
|
TEXT_TIMELINE = "obs_timeline"
|
||||||
|
BADGE_CONNECTION = "obs_badge_connection"
|
||||||
|
BADGE_YOLO = "obs_badge_yolo"
|
||||||
|
BADGE_PHASE = "obs_badge_phase"
|
||||||
|
BADGE_WMS = "obs_badge_wms"
|
||||||
|
METRIC_LOOP_FPS = "obs_metric_loop_fps"
|
||||||
|
METRIC_YOLO_FPS = "obs_metric_yolo_fps"
|
||||||
|
METRIC_YOLO_MS = "obs_metric_yolo_ms"
|
||||||
|
METRIC_SNAPSHOTS = "obs_metric_snapshots"
|
||||||
|
METRIC_FRAME_ID = "obs_metric_frame_id"
|
||||||
|
METRIC_TRACKS = "obs_metric_tracks"
|
||||||
|
METRIC_LABELS = "obs_metric_labels"
|
||||||
|
METRIC_PROGRESS = "obs_metric_progress"
|
||||||
|
NAV_W = 960
|
||||||
|
NAV_H = 540
|
||||||
|
SNAPSHOT_W = 520
|
||||||
|
SNAPSHOT_H = 300
|
||||||
|
LABEL_W = 520
|
||||||
|
LABEL_H = 220
|
||||||
|
THEME_BADGE_OK = "obs_theme_badge_ok"
|
||||||
|
THEME_BADGE_WARN = "obs_theme_badge_warn"
|
||||||
|
THEME_BADGE_ALERT = "obs_theme_badge_alert"
|
||||||
|
THEME_BADGE_IDLE = "obs_theme_badge_idle"
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
pre = argparse.ArgumentParser(add_help=False)
|
pre = argparse.ArgumentParser(add_help=False)
|
||||||
@@ -20,6 +59,8 @@ def parse_args():
|
|||||||
ap.add_argument("--host", default=defaults["observer_host"], help="Host publisher observer")
|
ap.add_argument("--host", default=defaults["observer_host"], help="Host publisher observer")
|
||||||
ap.add_argument("--port", type=int, default=defaults["observer_port"], help="Porta publisher observer")
|
ap.add_argument("--port", type=int, default=defaults["observer_port"], help="Porta publisher observer")
|
||||||
ap.add_argument("--reconnect-sec", type=float, default=1.0, help="Attesa tra tentativi di reconnessione")
|
ap.add_argument("--reconnect-sec", type=float, default=1.0, help="Attesa tra tentativi di reconnessione")
|
||||||
|
ap.add_argument("--ui-backend", choices=["auto", "opencv", "dearpygui"], default="dearpygui",
|
||||||
|
help="Backend UI observer")
|
||||||
return ap.parse_args()
|
return ap.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -35,8 +76,360 @@ def decode_preview(message: dict[str, object]) -> np.ndarray | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def choose_backend(requested: str) -> str:
|
||||||
args = parse_args()
|
if requested == "opencv":
|
||||||
|
return "opencv"
|
||||||
|
if requested == "dearpygui":
|
||||||
|
if dpg is None:
|
||||||
|
raise RuntimeError("DearPyGUI non disponibile")
|
||||||
|
return "dearpygui"
|
||||||
|
if dpg is not None:
|
||||||
|
return "dearpygui"
|
||||||
|
return "opencv"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_progress_text(telemetry: dict[str, object]) -> str:
|
||||||
|
pose = telemetry.get("capture_pose") if isinstance(telemetry.get("capture_pose"), dict) else {}
|
||||||
|
route_progress = pose.get("route_progress_ratio")
|
||||||
|
if isinstance(route_progress, (int, float)):
|
||||||
|
return f"{float(route_progress) * 100.0:.1f}%"
|
||||||
|
return "n/d"
|
||||||
|
|
||||||
|
|
||||||
|
def format_commands(telemetry: dict[str, object]) -> str:
|
||||||
|
command_lines = telemetry.get("command_lines") or ["Observer in attesa dati"]
|
||||||
|
if not isinstance(command_lines, list):
|
||||||
|
command_lines = [str(command_lines)]
|
||||||
|
return "\n".join(str(item) for item in command_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def empty_placeholder(width: int, height: int, text: str) -> np.ndarray:
|
||||||
|
canvas = np.full((height, width, 3), 235, dtype=np.uint8)
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
text,
|
||||||
|
(24, height // 2),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.8,
|
||||||
|
(30, 30, 30),
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
|
def frame_to_texture_data(frame: np.ndarray, target_w: int, target_h: int) -> list[float]:
|
||||||
|
src_h, src_w = frame.shape[:2]
|
||||||
|
if src_h <= 0 or src_w <= 0:
|
||||||
|
canvas = np.zeros((target_h, target_w, 3), dtype=np.uint8)
|
||||||
|
else:
|
||||||
|
scale = min(target_w / float(src_w), target_h / float(src_h))
|
||||||
|
draw_w = max(1, int(round(src_w * scale)))
|
||||||
|
draw_h = max(1, int(round(src_h * scale)))
|
||||||
|
resized = cv2.resize(frame, (draw_w, draw_h), interpolation=cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR)
|
||||||
|
canvas = np.full((target_h, target_w, 3), 18, dtype=np.uint8)
|
||||||
|
x0 = (target_w - draw_w) // 2
|
||||||
|
y0 = (target_h - draw_h) // 2
|
||||||
|
canvas[y0:y0 + draw_h, x0:x0 + draw_w] = resized
|
||||||
|
rgba = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGBA)
|
||||||
|
return (rgba.astype(np.float32).ravel() / 255.0).tolist()
|
||||||
|
|
||||||
|
|
||||||
|
def restore_window_by_title(title: str) -> None:
|
||||||
|
if not hasattr(ctypes, "windll"):
|
||||||
|
return
|
||||||
|
user32 = ctypes.windll.user32
|
||||||
|
hwnd = user32.FindWindowW(None, title)
|
||||||
|
if hwnd:
|
||||||
|
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||||
|
user32.SetForegroundWindow(hwnd)
|
||||||
|
|
||||||
|
|
||||||
|
def classify_phase(telemetry: dict[str, object]) -> str:
|
||||||
|
command_lines = telemetry.get("command_lines")
|
||||||
|
if not isinstance(command_lines, list):
|
||||||
|
command_lines = []
|
||||||
|
joined = " | ".join(str(line) for line in command_lines)
|
||||||
|
if "ATTENDI_WMS" in joined or "ATTENDI_ACK" in joined:
|
||||||
|
return "WAIT WMS"
|
||||||
|
if "SCATTA_FOTO_ETICHETTA" in joined or "INVIA_ROI_REMOTA" in joined:
|
||||||
|
return "CAPTURE"
|
||||||
|
if "CENTRA_ETICHETTA" in joined or "RITORNA_CENTRO_GAYLORD" in joined:
|
||||||
|
return "CENTERING"
|
||||||
|
if "STOP" in joined:
|
||||||
|
return "PAUSED"
|
||||||
|
return "SCANNING"
|
||||||
|
|
||||||
|
|
||||||
|
def classify_wms_status(telemetry: dict[str, object]) -> str:
|
||||||
|
command_lines = telemetry.get("command_lines")
|
||||||
|
if not isinstance(command_lines, list):
|
||||||
|
command_lines = []
|
||||||
|
joined = " | ".join(str(line) for line in command_lines)
|
||||||
|
if "WMS_ACK" in joined or "ACK_RICEVUTO" in joined:
|
||||||
|
return "ACK"
|
||||||
|
if "WMS_NACK" in joined or "NACK_RICEVUTO" in joined:
|
||||||
|
return "NACK"
|
||||||
|
if "WMS_TIMEOUT" in joined or "TIMEOUT" in joined:
|
||||||
|
return "TIMEOUT"
|
||||||
|
if "ATTENDI_WMS" in joined or "ATTENDI_ACK" in joined:
|
||||||
|
return "WAIT"
|
||||||
|
return "IDLE"
|
||||||
|
|
||||||
|
|
||||||
|
class DearPyGuiObserver:
|
||||||
|
def __init__(self, args):
|
||||||
|
if dpg is None:
|
||||||
|
raise RuntimeError("DearPyGUI non disponibile")
|
||||||
|
self.args = args
|
||||||
|
self.latest_navigate: np.ndarray | None = None
|
||||||
|
self.latest_snapshot: np.ndarray | None = None
|
||||||
|
self.latest_label: np.ndarray | None = None
|
||||||
|
self.telemetry: dict[str, object] = {
|
||||||
|
"command_lines": ["Observer in attesa dati"],
|
||||||
|
"motion_text": "MOTO: n/d",
|
||||||
|
}
|
||||||
|
self.sock: socket.socket | None = None
|
||||||
|
self.recv_buffer = ""
|
||||||
|
self.last_connect_attempt = 0.0
|
||||||
|
self.connected = False
|
||||||
|
self.event_history: list[str] = []
|
||||||
|
self.last_command_signature = ""
|
||||||
|
self.last_phase = ""
|
||||||
|
self.last_wms_status = ""
|
||||||
|
|
||||||
|
def run(self) -> int:
|
||||||
|
self._build_ui()
|
||||||
|
try:
|
||||||
|
while dpg.is_dearpygui_running():
|
||||||
|
self._poll_socket()
|
||||||
|
self._refresh_ui()
|
||||||
|
dpg.render_dearpygui_frame()
|
||||||
|
finally:
|
||||||
|
self._close_socket()
|
||||||
|
dpg.destroy_context()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
dpg.create_context()
|
||||||
|
self._build_themes()
|
||||||
|
with dpg.texture_registry(show=False):
|
||||||
|
self._create_texture(NAV_TEXTURE, frame_to_texture_data(empty_placeholder(NAV_W, NAV_H, "In attesa preview"), NAV_W, NAV_H), NAV_W, NAV_H)
|
||||||
|
self._create_texture(SNAPSHOT_TEXTURE, frame_to_texture_data(empty_placeholder(SNAPSHOT_W, SNAPSHOT_H, "In attesa snapshot"), SNAPSHOT_W, SNAPSHOT_H), SNAPSHOT_W, SNAPSHOT_H)
|
||||||
|
self._create_texture(LABEL_TEXTURE, frame_to_texture_data(empty_placeholder(LABEL_W, LABEL_H, "In attesa etichetta"), LABEL_W, LABEL_H), LABEL_W, LABEL_H)
|
||||||
|
|
||||||
|
with dpg.window(label="FlyWMS Observer", tag="main_window", width=1600, height=980):
|
||||||
|
with dpg.group(horizontal=True):
|
||||||
|
with dpg.child_window(width=980, height=920, border=True):
|
||||||
|
dpg.add_text("Preview navigazione")
|
||||||
|
dpg.add_image(NAV_TEXTURE, tag=NAV_IMAGE)
|
||||||
|
with dpg.child_window(width=580, height=920, border=True):
|
||||||
|
with dpg.group(horizontal=True):
|
||||||
|
dpg.add_button(label="CONN", tag=BADGE_CONNECTION, width=86, height=28, enabled=False)
|
||||||
|
dpg.add_button(label="YOLO", tag=BADGE_YOLO, width=86, height=28, enabled=False)
|
||||||
|
dpg.add_button(label="PHASE", tag=BADGE_PHASE, width=130, height=28, enabled=False)
|
||||||
|
dpg.add_button(label="WMS", tag=BADGE_WMS, width=86, height=28, enabled=False)
|
||||||
|
dpg.add_spacer(height=10)
|
||||||
|
dpg.add_text("Snapshot OCR payload")
|
||||||
|
dpg.add_image(SNAPSHOT_TEXTURE, tag=SNAPSHOT_IMAGE)
|
||||||
|
dpg.add_spacer(height=12)
|
||||||
|
dpg.add_text("Crop etichetta")
|
||||||
|
dpg.add_image(LABEL_TEXTURE, tag=LABEL_IMAGE)
|
||||||
|
dpg.add_spacer(height=12)
|
||||||
|
dpg.add_separator()
|
||||||
|
dpg.add_text("Stato observer")
|
||||||
|
dpg.add_text("Observer in attesa connessione", tag=TEXT_STATUS, wrap=540)
|
||||||
|
dpg.add_separator()
|
||||||
|
dpg.add_text("Metriche")
|
||||||
|
with dpg.table(header_row=False, borders_innerH=True, borders_outerH=True, borders_innerV=True, borders_outerV=True, resizable=False, policy=dpg.mvTable_SizingStretchProp):
|
||||||
|
dpg.add_table_column()
|
||||||
|
dpg.add_table_column()
|
||||||
|
for label, tag in (
|
||||||
|
("Loop FPS", METRIC_LOOP_FPS),
|
||||||
|
("YOLO FPS", METRIC_YOLO_FPS),
|
||||||
|
("YOLO ms", METRIC_YOLO_MS),
|
||||||
|
("Snapshot", METRIC_SNAPSHOTS),
|
||||||
|
("Frame", METRIC_FRAME_ID),
|
||||||
|
("Track attive", METRIC_TRACKS),
|
||||||
|
("Etichette", METRIC_LABELS),
|
||||||
|
("Progresso", METRIC_PROGRESS),
|
||||||
|
):
|
||||||
|
with dpg.table_row():
|
||||||
|
dpg.add_text(label)
|
||||||
|
dpg.add_text("n/d", tag=tag)
|
||||||
|
dpg.add_separator()
|
||||||
|
dpg.add_text("Comandi")
|
||||||
|
dpg.add_input_text(tag=TEXT_COMMANDS, multiline=True, readonly=True, width=540, height=260, default_value="Observer in attesa dati")
|
||||||
|
dpg.add_separator()
|
||||||
|
dpg.add_text("Timeline eventi")
|
||||||
|
dpg.add_input_text(tag=TEXT_TIMELINE, multiline=True, readonly=True, width=540, height=180, default_value="In attesa eventi")
|
||||||
|
|
||||||
|
dpg.create_viewport(title="FlyWMS Observer", width=1600, height=980)
|
||||||
|
dpg.setup_dearpygui()
|
||||||
|
dpg.show_viewport()
|
||||||
|
time.sleep(0.05)
|
||||||
|
restore_window_by_title("FlyWMS Observer")
|
||||||
|
self._set_badge(BADGE_CONNECTION, "DISCONNECTED", THEME_BADGE_ALERT)
|
||||||
|
self._set_badge(BADGE_YOLO, "YOLO OFF", THEME_BADGE_IDLE)
|
||||||
|
self._set_badge(BADGE_PHASE, "IDLE", THEME_BADGE_IDLE)
|
||||||
|
self._set_badge(BADGE_WMS, "WMS IDLE", THEME_BADGE_IDLE)
|
||||||
|
|
||||||
|
def _build_themes(self) -> None:
|
||||||
|
def make_badge_theme(tag: str, button_color: tuple[int, int, int], text_color: tuple[int, int, int] = (255, 255, 255)) -> None:
|
||||||
|
with dpg.theme(tag=tag):
|
||||||
|
with dpg.theme_component(dpg.mvButton):
|
||||||
|
dpg.add_theme_color(dpg.mvThemeCol_Button, button_color)
|
||||||
|
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, button_color)
|
||||||
|
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, button_color)
|
||||||
|
dpg.add_theme_color(dpg.mvThemeCol_Text, text_color)
|
||||||
|
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 6)
|
||||||
|
dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 8, 6)
|
||||||
|
|
||||||
|
make_badge_theme(THEME_BADGE_OK, (40, 140, 70))
|
||||||
|
make_badge_theme(THEME_BADGE_WARN, (196, 132, 21))
|
||||||
|
make_badge_theme(THEME_BADGE_ALERT, (176, 52, 52))
|
||||||
|
make_badge_theme(THEME_BADGE_IDLE, (80, 88, 102))
|
||||||
|
|
||||||
|
def _create_texture(self, tag: str, data: list[float], width: int, height: int) -> None:
|
||||||
|
dpg.add_dynamic_texture(width=width, height=height, default_value=data, tag=tag)
|
||||||
|
|
||||||
|
def _set_texture(self, texture_tag: str, frame: np.ndarray, target_w: int, target_h: int) -> None:
|
||||||
|
dpg.set_value(texture_tag, frame_to_texture_data(frame, target_w, target_h))
|
||||||
|
|
||||||
|
def _set_badge(self, tag: str, label: str, theme_tag: str) -> None:
|
||||||
|
dpg.configure_item(tag, label=label)
|
||||||
|
dpg.bind_item_theme(tag, theme_tag)
|
||||||
|
|
||||||
|
def _append_event(self, text: str) -> None:
|
||||||
|
stamp = time.strftime("%H:%M:%S")
|
||||||
|
self.event_history.insert(0, f"{stamp} {text}")
|
||||||
|
self.event_history = self.event_history[:12]
|
||||||
|
|
||||||
|
def _poll_socket(self) -> None:
|
||||||
|
now = time.perf_counter()
|
||||||
|
if self.sock is None:
|
||||||
|
if now - self.last_connect_attempt < max(0.2, self.args.reconnect_sec):
|
||||||
|
return
|
||||||
|
self.last_connect_attempt = now
|
||||||
|
try:
|
||||||
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.sock.connect((self.args.host, self.args.port))
|
||||||
|
self.sock.setblocking(False)
|
||||||
|
self.recv_buffer = ""
|
||||||
|
self.connected = True
|
||||||
|
nav.log("[OBS] connesso al core")
|
||||||
|
self._append_event("Observer connesso al core")
|
||||||
|
except OSError as exc:
|
||||||
|
self._close_socket()
|
||||||
|
self.connected = False
|
||||||
|
dpg.set_value(TEXT_STATUS, f"Observer non connesso: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = self.sock.recv(65536)
|
||||||
|
if not chunk:
|
||||||
|
raise ConnectionError("publisher disconnesso")
|
||||||
|
self.recv_buffer += chunk.decode("utf-8", errors="replace")
|
||||||
|
except BlockingIOError:
|
||||||
|
break
|
||||||
|
except OSError as exc:
|
||||||
|
if getattr(exc, "winerror", None) in (10035,):
|
||||||
|
break
|
||||||
|
raise
|
||||||
|
while True:
|
||||||
|
newline_pos = self.recv_buffer.find("\n")
|
||||||
|
if newline_pos < 0:
|
||||||
|
break
|
||||||
|
line = self.recv_buffer[:newline_pos].strip()
|
||||||
|
self.recv_buffer = self.recv_buffer[newline_pos + 1:]
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
message = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
msg_type = message.get("type")
|
||||||
|
if msg_type == "telemetry":
|
||||||
|
self.telemetry = message
|
||||||
|
elif msg_type == "preview":
|
||||||
|
frame = decode_preview(message)
|
||||||
|
stream = message.get("stream")
|
||||||
|
if frame is None:
|
||||||
|
continue
|
||||||
|
if stream == "navigate":
|
||||||
|
self.latest_navigate = frame
|
||||||
|
elif stream == "snapshot":
|
||||||
|
self.latest_snapshot = frame
|
||||||
|
elif stream == "label":
|
||||||
|
self.latest_label = frame
|
||||||
|
except (ConnectionError, OSError) as exc:
|
||||||
|
nav.log(f"[OBS] connessione persa o non disponibile: {exc}")
|
||||||
|
self._close_socket()
|
||||||
|
self.connected = False
|
||||||
|
dpg.set_value(TEXT_STATUS, f"Connessione persa: {exc}")
|
||||||
|
self._append_event(f"Connessione persa: {exc}")
|
||||||
|
|
||||||
|
def _refresh_ui(self) -> None:
|
||||||
|
if self.latest_navigate is not None:
|
||||||
|
self._set_texture(NAV_TEXTURE, self.latest_navigate, NAV_W, NAV_H)
|
||||||
|
if self.latest_snapshot is not None:
|
||||||
|
self._set_texture(SNAPSHOT_TEXTURE, self.latest_snapshot, SNAPSHOT_W, SNAPSHOT_H)
|
||||||
|
if self.latest_label is not None:
|
||||||
|
self._set_texture(LABEL_TEXTURE, self.latest_label, LABEL_W, LABEL_H)
|
||||||
|
state = "connesso" if self.connected else "in attesa connessione"
|
||||||
|
dpg.set_value(TEXT_STATUS, f"Observer {state} su {self.args.host}:{self.args.port}")
|
||||||
|
dpg.set_value(TEXT_COMMANDS, format_commands(self.telemetry))
|
||||||
|
dpg.set_value(METRIC_LOOP_FPS, f"{float(self.telemetry.get('loop_fps', 0.0)):.2f}")
|
||||||
|
dpg.set_value(METRIC_YOLO_FPS, f"{float(self.telemetry.get('yolo_fps', 0.0)):.2f}")
|
||||||
|
dpg.set_value(METRIC_YOLO_MS, f"{float(self.telemetry.get('last_yolo_ms', 0.0)):.1f}")
|
||||||
|
dpg.set_value(METRIC_SNAPSHOTS, str(self.telemetry.get("snapshot_counter", 0)))
|
||||||
|
dpg.set_value(METRIC_FRAME_ID, str(self.telemetry.get("frame_id", "n/d")))
|
||||||
|
dpg.set_value(METRIC_TRACKS, f"{self.telemetry.get('active_track_count', 0)} / {self.telemetry.get('track_count', 0)}")
|
||||||
|
dpg.set_value(METRIC_LABELS, str(self.telemetry.get("label_count", 0)))
|
||||||
|
dpg.set_value(METRIC_PROGRESS, extract_progress_text(self.telemetry))
|
||||||
|
|
||||||
|
self._set_badge(BADGE_CONNECTION, "CONNECTED" if self.connected else "DISCONNECTED", THEME_BADGE_OK if self.connected else THEME_BADGE_ALERT)
|
||||||
|
run_yolo = bool(self.telemetry.get("run_yolo", False))
|
||||||
|
yolo_mode = str(self.telemetry.get("yolo_mode", "idle")).upper()
|
||||||
|
self._set_badge(BADGE_YOLO, f"YOLO {yolo_mode}" if run_yolo else "YOLO IDLE", THEME_BADGE_OK if run_yolo else THEME_BADGE_IDLE)
|
||||||
|
phase = classify_phase(self.telemetry)
|
||||||
|
phase_theme = THEME_BADGE_WARN if phase in ("CAPTURE", "CENTERING", "WAIT WMS", "PAUSED") else THEME_BADGE_OK
|
||||||
|
self._set_badge(BADGE_PHASE, phase, phase_theme)
|
||||||
|
wms_status = classify_wms_status(self.telemetry)
|
||||||
|
wms_theme = THEME_BADGE_IDLE
|
||||||
|
if wms_status == "ACK":
|
||||||
|
wms_theme = THEME_BADGE_OK
|
||||||
|
elif wms_status in ("WAIT",):
|
||||||
|
wms_theme = THEME_BADGE_WARN
|
||||||
|
elif wms_status in ("NACK", "TIMEOUT"):
|
||||||
|
wms_theme = THEME_BADGE_ALERT
|
||||||
|
self._set_badge(BADGE_WMS, f"WMS {wms_status}", wms_theme)
|
||||||
|
|
||||||
|
command_signature = str(self.telemetry.get("command_text") or "")
|
||||||
|
if command_signature and command_signature != self.last_command_signature:
|
||||||
|
self._append_event(command_signature)
|
||||||
|
self.last_command_signature = command_signature
|
||||||
|
if phase != self.last_phase:
|
||||||
|
self._append_event(f"Fase -> {phase}")
|
||||||
|
self.last_phase = phase
|
||||||
|
if wms_status != self.last_wms_status:
|
||||||
|
self._append_event(f"Stato WMS -> {wms_status}")
|
||||||
|
self.last_wms_status = wms_status
|
||||||
|
dpg.set_value(TEXT_TIMELINE, "\n".join(self.event_history) if self.event_history else "In attesa eventi")
|
||||||
|
|
||||||
|
def _close_socket(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.sock is not None:
|
||||||
|
self.sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self.sock = None
|
||||||
|
self.recv_buffer = ""
|
||||||
|
|
||||||
|
|
||||||
|
def run_opencv_observer(args) -> int:
|
||||||
nav.log(f"[OBS] avvio observer {args.host}:{args.port}")
|
nav.log(f"[OBS] avvio observer {args.host}:{args.port}")
|
||||||
|
|
||||||
latest_navigate: np.ndarray | None = None
|
latest_navigate: np.ndarray | None = None
|
||||||
@@ -84,11 +477,10 @@ def main() -> int:
|
|||||||
cv2.imshow("flywms observer snapshot", latest_snapshot)
|
cv2.imshow("flywms observer snapshot", latest_snapshot)
|
||||||
if latest_label is not None:
|
if latest_label is not None:
|
||||||
cv2.imshow("flywms observer etichetta", latest_label)
|
cv2.imshow("flywms observer etichetta", latest_label)
|
||||||
command_lines = telemetry.get("command_lines") or ["Observer attivo"]
|
panel = nav.draw_commands_window(
|
||||||
if not isinstance(command_lines, list):
|
[str(item) for item in (telemetry.get("command_lines") or ["Observer attivo"])],
|
||||||
command_lines = [str(command_lines)]
|
str(telemetry.get("motion_text") or "MOTO: n/d"),
|
||||||
motion_text = str(telemetry.get("motion_text") or "MOTO: n/d")
|
)
|
||||||
panel = nav.draw_commands_window([str(item) for item in command_lines], motion_text)
|
|
||||||
cv2.imshow("flywms observer comandi", panel)
|
cv2.imshow("flywms observer comandi", panel)
|
||||||
key = cv2.waitKey(1) & 0xFF
|
key = cv2.waitKey(1) & 0xFF
|
||||||
if key in (27, ord("q")):
|
if key in (27, ord("q")):
|
||||||
@@ -110,5 +502,14 @@ def main() -> int:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
backend = choose_backend(args.ui_backend)
|
||||||
|
nav.log(f"[OBS] backend UI: {backend}")
|
||||||
|
if backend == "dearpygui":
|
||||||
|
return DearPyGuiObserver(args).run()
|
||||||
|
return run_opencv_observer(args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import configparser
|
import configparser
|
||||||
|
import ctypes
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
@@ -17,6 +18,11 @@ import numpy as np
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI, File, Form, UploadFile
|
from fastapi import FastAPI, File, Form, UploadFile
|
||||||
|
|
||||||
|
try:
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
dpg = None
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG_PATH = "flywms_navigation.ini"
|
DEFAULT_CONFIG_PATH = "flywms_navigation.ini"
|
||||||
CUDA_MIN_PIXELS = 640 * 360
|
CUDA_MIN_PIXELS = 640 * 360
|
||||||
@@ -258,6 +264,8 @@ def parse_args():
|
|||||||
ap.add_argument("--host", default=defaults["wms_server_host"], help="Host bind server")
|
ap.add_argument("--host", default=defaults["wms_server_host"], help="Host bind server")
|
||||||
ap.add_argument("--port", type=int, default=defaults["wms_server_port"], help="Porta server")
|
ap.add_argument("--port", type=int, default=defaults["wms_server_port"], help="Porta server")
|
||||||
ap.add_argument("--received-dir", default=defaults["wms_received_dir"], help="Directory upload ricevuti")
|
ap.add_argument("--received-dir", default=defaults["wms_received_dir"], help="Directory upload ricevuti")
|
||||||
|
ap.add_argument("--ui-backend", choices=["auto", "opencv", "dearpygui"], default="dearpygui",
|
||||||
|
help="Backend UI server WMS")
|
||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
"--fake-ack-mode",
|
"--fake-ack-mode",
|
||||||
choices=["always-ack", "always-nack", "alternate", "random"],
|
choices=["always-ack", "always-nack", "alternate", "random"],
|
||||||
@@ -670,6 +678,60 @@ def safe_name(value: str) -> str:
|
|||||||
return "".join(ch if ch in allowed else "_" for ch in value)[:160]
|
return "".join(ch if ch in allowed else "_" for ch in value)[:160]
|
||||||
|
|
||||||
|
|
||||||
|
def choose_ui_backend(requested: str) -> str:
|
||||||
|
if requested == "opencv":
|
||||||
|
return "opencv"
|
||||||
|
if requested == "dearpygui":
|
||||||
|
if dpg is None:
|
||||||
|
raise RuntimeError("DearPyGUI non disponibile")
|
||||||
|
return "dearpygui"
|
||||||
|
if dpg is not None:
|
||||||
|
return "dearpygui"
|
||||||
|
return "opencv"
|
||||||
|
|
||||||
|
|
||||||
|
def placeholder_frame(width: int, height: int, text: str) -> np.ndarray:
|
||||||
|
canvas = np.full((height, width, 3), 235, dtype=np.uint8)
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
text,
|
||||||
|
(24, height // 2),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.8,
|
||||||
|
(30, 30, 30),
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
|
def frame_to_texture_data(frame: np.ndarray, target_w: int, target_h: int) -> list[float]:
|
||||||
|
src_h, src_w = frame.shape[:2]
|
||||||
|
if src_h <= 0 or src_w <= 0:
|
||||||
|
canvas = np.zeros((target_h, target_w, 3), dtype=np.uint8)
|
||||||
|
else:
|
||||||
|
scale = min(target_w / float(src_w), target_h / float(src_h))
|
||||||
|
draw_w = max(1, int(round(src_w * scale)))
|
||||||
|
draw_h = max(1, int(round(src_h * scale)))
|
||||||
|
resized = cv2.resize(frame, (draw_w, draw_h), interpolation=cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR)
|
||||||
|
canvas = np.full((target_h, target_w, 3), 18, dtype=np.uint8)
|
||||||
|
x0 = (target_w - draw_w) // 2
|
||||||
|
y0 = (target_h - draw_h) // 2
|
||||||
|
canvas[y0:y0 + draw_h, x0:x0 + draw_w] = resized
|
||||||
|
rgba = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGBA)
|
||||||
|
return (rgba.astype(np.float32).ravel() / 255.0).tolist()
|
||||||
|
|
||||||
|
|
||||||
|
def restore_window_by_title(title: str) -> None:
|
||||||
|
if not hasattr(ctypes, "windll"):
|
||||||
|
return
|
||||||
|
user32 = ctypes.windll.user32
|
||||||
|
hwnd = user32.FindWindowW(None, title)
|
||||||
|
if hwnd:
|
||||||
|
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||||
|
user32.SetForegroundWindow(hwnd)
|
||||||
|
|
||||||
|
|
||||||
def server_ui_loop() -> None:
|
def server_ui_loop() -> None:
|
||||||
try:
|
try:
|
||||||
cv2.namedWindow("wms immagine ricevuta", cv2.WINDOW_NORMAL)
|
cv2.namedWindow("wms immagine ricevuta", cv2.WINDOW_NORMAL)
|
||||||
@@ -714,6 +776,70 @@ def server_ui_loop() -> None:
|
|||||||
cv2.destroyWindow("wms payload")
|
cv2.destroyWindow("wms payload")
|
||||||
|
|
||||||
|
|
||||||
|
def server_dpg_ui_loop() -> None:
|
||||||
|
if dpg is None:
|
||||||
|
log("UI DearPyGUI non disponibile, impossibile avviare il monitor WMS")
|
||||||
|
with ui_lock:
|
||||||
|
ui_state["stop"] = True
|
||||||
|
return
|
||||||
|
|
||||||
|
image_texture = "wms_image_texture"
|
||||||
|
payload_text = "wms_payload_text"
|
||||||
|
status_text = "wms_status_text"
|
||||||
|
image_w = 720
|
||||||
|
image_h = 420
|
||||||
|
|
||||||
|
def create_texture(tag: str, data: list[float], width: int, height: int) -> None:
|
||||||
|
dpg.add_dynamic_texture(width=width, height=height, default_value=data, tag=tag)
|
||||||
|
|
||||||
|
def set_texture(frame: np.ndarray) -> None:
|
||||||
|
dpg.set_value(image_texture, frame_to_texture_data(frame, image_w, image_h))
|
||||||
|
|
||||||
|
dpg.create_context()
|
||||||
|
with dpg.texture_registry(show=False):
|
||||||
|
create_texture(
|
||||||
|
image_texture,
|
||||||
|
frame_to_texture_data(placeholder_frame(image_w, image_h, "In attesa immagine etichetta"), image_w, image_h),
|
||||||
|
image_w,
|
||||||
|
image_h,
|
||||||
|
)
|
||||||
|
|
||||||
|
with dpg.window(label="FlyWMS WMS Server", width=1450, height=900):
|
||||||
|
with dpg.group(horizontal=True):
|
||||||
|
with dpg.child_window(width=760, height=840, border=True):
|
||||||
|
dpg.add_text("Ultima immagine ricevuta")
|
||||||
|
dpg.add_image(image_texture)
|
||||||
|
with dpg.child_window(width=640, height=840, border=True):
|
||||||
|
dpg.add_text("Stato")
|
||||||
|
dpg.add_text("Server WMS in attesa payload", tag=status_text, wrap=600)
|
||||||
|
dpg.add_separator()
|
||||||
|
dpg.add_text("Payload")
|
||||||
|
dpg.add_input_text(tag=payload_text, multiline=True, readonly=True, width=600, height=720, default_value="In attesa di payload WMS")
|
||||||
|
|
||||||
|
dpg.create_viewport(title="FlyWMS WMS Server", width=1450, height=900)
|
||||||
|
dpg.setup_dearpygui()
|
||||||
|
dpg.show_viewport()
|
||||||
|
time.sleep(0.05)
|
||||||
|
restore_window_by_title("FlyWMS WMS Server")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while dpg.is_dearpygui_running():
|
||||||
|
with ui_lock:
|
||||||
|
stop = bool(ui_state["stop"])
|
||||||
|
image = None if ui_state["image"] is None else ui_state["image"].copy()
|
||||||
|
lines = list(ui_state["payload_lines"])
|
||||||
|
if stop:
|
||||||
|
break
|
||||||
|
if image is not None:
|
||||||
|
set_texture(image)
|
||||||
|
dpg.set_value(status_text, f"Richieste ricevute: {server_state['counter']}")
|
||||||
|
dpg.set_value(payload_text, "\n".join(lines))
|
||||||
|
dpg.render_dearpygui_frame()
|
||||||
|
time.sleep(0.03)
|
||||||
|
finally:
|
||||||
|
dpg.destroy_context()
|
||||||
|
|
||||||
|
|
||||||
def resize_preview(frame: np.ndarray, max_width: int) -> np.ndarray:
|
def resize_preview(frame: np.ndarray, max_width: int) -> np.ndarray:
|
||||||
h, w = frame.shape[:2]
|
h, w = frame.shape[:2]
|
||||||
if max_width <= 0 or w <= max_width:
|
if max_width <= 0 or w <= max_width:
|
||||||
@@ -745,6 +871,7 @@ def draw_payload_window(lines: list[str]) -> np.ndarray:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
ui_backend = choose_ui_backend(args.ui_backend) if args.ui_enabled else "disabled"
|
||||||
server_state["received_dir"] = args.received_dir
|
server_state["received_dir"] = args.received_dir
|
||||||
server_state["fake_ack_mode"] = args.fake_ack_mode
|
server_state["fake_ack_mode"] = args.fake_ack_mode
|
||||||
server_state["fake_processing_sec"] = args.fake_processing_sec
|
server_state["fake_processing_sec"] = args.fake_processing_sec
|
||||||
@@ -770,9 +897,11 @@ def main() -> int:
|
|||||||
f"mode={args.fake_ack_mode} received_dir={args.received_dir}"
|
f"mode={args.fake_ack_mode} received_dir={args.received_dir}"
|
||||||
)
|
)
|
||||||
log(f"OpenCV CUDA: {'attivo' if OPENCV_CUDA_AVAILABLE else 'non disponibile'}")
|
log(f"OpenCV CUDA: {'attivo' if OPENCV_CUDA_AVAILABLE else 'non disponibile'}")
|
||||||
|
log(f"UI backend: {ui_backend}")
|
||||||
ui_thread = None
|
ui_thread = None
|
||||||
if args.ui_enabled:
|
if args.ui_enabled:
|
||||||
ui_thread = threading.Thread(target=server_ui_loop, name="wms-server-ui", daemon=True)
|
ui_target = server_dpg_ui_loop if ui_backend == "dearpygui" else server_ui_loop
|
||||||
|
ui_thread = threading.Thread(target=ui_target, name="wms-server-ui", daemon=True)
|
||||||
ui_thread.start()
|
ui_thread.start()
|
||||||
try:
|
try:
|
||||||
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
||||||
|
|||||||
5
run_navigator.bat
Normal file
5
run_navigator.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
title FlyWMS - Navigator
|
||||||
|
cd /d "%~dp0"
|
||||||
|
python flywms_navigation.py --video "testhd2_edit.mp4" --observer-enabled --wms-enabled
|
||||||
|
|
||||||
5
run_observer.bat
Normal file
5
run_observer.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
title FlyWMS - Observer
|
||||||
|
cd /d "%~dp0"
|
||||||
|
python flywms_navigation_observer.py --ui-backend dearpygui
|
||||||
|
|
||||||
5
run_wms_server.bat
Normal file
5
run_wms_server.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
title FlyWMS - WMS Server
|
||||||
|
cd /d "%~dp0"
|
||||||
|
python flywms_wms_server.py --ui-backend dearpygui
|
||||||
|
|
||||||
14
startdemo.bat
Normal file
14
startdemo.bat
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set "ROOT=%~dp0"
|
||||||
|
set "PY_SCRIPT=%ROOT%startdemo.py"
|
||||||
|
|
||||||
|
if not exist "%PY_SCRIPT%" (
|
||||||
|
echo Script non trovato: "%PY_SCRIPT%"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
python "%PY_SCRIPT%"
|
||||||
|
set "EXIT_CODE=%ERRORLEVEL%"
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
138
startdemo.py
Normal file
138
startdemo.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
VIDEO = "testhd2_edit.mp4"
|
||||||
|
|
||||||
|
CHILDREN: list[subprocess.Popen] = []
|
||||||
|
STOPPING = False
|
||||||
|
|
||||||
|
|
||||||
|
def kill_tree(pid: int) -> None:
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["taskkill", "/PID", str(pid), "/T", "/F"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def pids_listening_on_port(port: int) -> list[int]:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["netstat", "-ano", "-p", "tcp"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
pids: set[int] = set()
|
||||||
|
suffix = f":{port}"
|
||||||
|
for raw_line in result.stdout.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 5:
|
||||||
|
continue
|
||||||
|
proto, local_addr, _, state, pid_text = parts[:5]
|
||||||
|
if proto.upper() != "TCP":
|
||||||
|
continue
|
||||||
|
if state.upper() != "LISTENING":
|
||||||
|
continue
|
||||||
|
if not local_addr.endswith(suffix):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
pids.add(int(pid_text))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return sorted(pids)
|
||||||
|
|
||||||
|
|
||||||
|
def preflight_cleanup() -> None:
|
||||||
|
stale_pids = pids_listening_on_port(8088)
|
||||||
|
for pid in stale_pids:
|
||||||
|
print(f"Pulizia preventiva: termino processo in ascolto su 8088 (PID {pid})", flush=True)
|
||||||
|
kill_tree(pid)
|
||||||
|
if stale_pids:
|
||||||
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_all() -> None:
|
||||||
|
global STOPPING
|
||||||
|
if STOPPING:
|
||||||
|
return
|
||||||
|
STOPPING = True
|
||||||
|
print("\nArresto finestre demo...", flush=True)
|
||||||
|
for proc in CHILDREN:
|
||||||
|
if proc.poll() is None:
|
||||||
|
kill_tree(proc.pid)
|
||||||
|
|
||||||
|
|
||||||
|
def on_signal(signum, frame) -> None:
|
||||||
|
stop_all()
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
|
def start_window(title: str, launcher_bat: str) -> subprocess.Popen:
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
startupinfo.wShowWindow = 6 # SW_MINIMIZE
|
||||||
|
return subprocess.Popen(
|
||||||
|
["cmd.exe", "/k", launcher_bat],
|
||||||
|
cwd=str(ROOT),
|
||||||
|
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
||||||
|
startupinfo=startupinfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
signal.signal(signal.SIGINT, on_signal)
|
||||||
|
if hasattr(signal, "SIGTERM"):
|
||||||
|
signal.signal(signal.SIGTERM, on_signal)
|
||||||
|
|
||||||
|
preflight_cleanup()
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Avvio WMS server...", flush=True)
|
||||||
|
CHILDREN.append(start_window("FlyWMS - WMS Server", "run_wms_server.bat"))
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
print("Avvio observer...", flush=True)
|
||||||
|
CHILDREN.append(start_window("FlyWMS - Observer", "run_observer.bat"))
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
print("Avvio navigator...", flush=True)
|
||||||
|
CHILDREN.append(start_window("FlyWMS - Navigator", "run_navigator.bat"))
|
||||||
|
|
||||||
|
print("\nDemo avviata.", flush=True)
|
||||||
|
for proc in CHILDREN:
|
||||||
|
print(f"- PID {proc.pid}", flush=True)
|
||||||
|
print("\nPremi Ctrl+C in questa finestra per chiudere tutto.\n", flush=True)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
alive = [proc for proc in CHILDREN if proc.poll() is None]
|
||||||
|
if not alive:
|
||||||
|
print("Tutte le finestre demo sono terminate.", flush=True)
|
||||||
|
return 0
|
||||||
|
time.sleep(1.0)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Interruzione rilevata.", flush=True)
|
||||||
|
return 130
|
||||||
|
finally:
|
||||||
|
stop_all()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user