Files
flywms/flywms_wms_server.py
2026-05-19 08:52:44 +02:00

790 lines
30 KiB
Python

import argparse
import asyncio
import configparser
import json
import random
import re
import shutil
import subprocess
import threading
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
import cv2
import numpy as np
import uvicorn
from fastapi import FastAPI, File, Form, UploadFile
DEFAULT_CONFIG_PATH = "flywms_navigation.ini"
CUDA_MIN_PIXELS = 640 * 360
def opencv_cuda_available() -> bool:
try:
return hasattr(cv2, "cuda") and cv2.cuda.getCudaEnabledDeviceCount() > 0
except cv2.error:
return False
OPENCV_CUDA_AVAILABLE = opencv_cuda_available()
def cuda_resize(
image: np.ndarray,
size: tuple[int, int],
interpolation: int = cv2.INTER_LINEAR,
min_pixels: int = CUDA_MIN_PIXELS,
) -> np.ndarray:
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
return cv2.resize(image, size, interpolation=interpolation)
try:
gpu = cv2.cuda_GpuMat()
gpu.upload(image)
return cv2.cuda.resize(gpu, size, interpolation=interpolation).download()
except cv2.error:
return cv2.resize(image, size, interpolation=interpolation)
def cuda_cvt_color(
image: np.ndarray,
code: int,
min_pixels: int = CUDA_MIN_PIXELS,
) -> np.ndarray:
if not OPENCV_CUDA_AVAILABLE or image.size < min_pixels:
return cv2.cvtColor(image, code)
try:
gpu = cv2.cuda_GpuMat()
gpu.upload(image)
return cv2.cuda.cvtColor(gpu, code).download()
except cv2.error:
return cv2.cvtColor(image, code)
app = FastAPI(title="FlyWMS Demo WMS Server")
server_state = {
"received_dir": "wms_received",
"fake_ack_mode": "always-ack",
"fake_processing_sec": 0.5,
"ui_enabled": True,
"ocr_enabled": True,
"ocr_mode": "paddleocr",
"tesseract_cmd": "",
"paddle_python": r"C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe",
"paddle_worker_script": str(Path(__file__).with_name("flywms_paddleocr_worker.py")),
"paddle_target_heights": "96",
"paddle_variant_set": "fast",
"paddle_expected_digits": 6,
"paddle_min_votes": 2,
"paddle_min_confidence": 0.70,
"fake_ocr_prefix": "UDC",
"undetermined_code_text": "udc non determinato",
"fake_ocr_delay_sec": 0.2,
"operator": "udrone",
"site": "demo-magazzino",
"counter": 0,
}
state_lock = threading.Lock()
ui_lock = threading.Lock()
ui_state = {
"image": None,
"payload_lines": ["In attesa di payload WMS"],
"updated": False,
"stop": False,
}
@dataclass(frozen=True)
class OcrServerResult:
text: str
raw_text: str
confidence: float
backend: str
fallback_used: bool
votes: int = 0
variant: str = ""
class PaddleOcrWorker:
def __init__(
self,
python_path: str,
script_path: str,
target_heights: str,
variant_set: str,
expected_digits: int,
min_votes: int,
min_confidence: float,
) -> None:
self.python_path = python_path
self.script_path = script_path
self.target_heights = target_heights
self.variant_set = variant_set
self.expected_digits = expected_digits
self.min_votes = min_votes
self.min_confidence = min_confidence
self.lock = threading.Lock()
self.process: subprocess.Popen[str] | None = None
def close(self) -> None:
with self.lock:
process = self.process
self.process = None
if process is None:
return
try:
if process.stdin:
process.stdin.write("__quit__\n")
process.stdin.flush()
except Exception:
pass
try:
process.wait(timeout=2.0)
except subprocess.TimeoutExpired:
process.kill()
def predict(self, image_path: Path) -> dict[str, object]:
with self.lock:
process = self._ensure_process()
if process.stdin is None or process.stdout is None:
raise RuntimeError("worker stdio non disponibile")
request = {
"image_path": str(image_path),
"target_heights": self.target_heights,
"variant_set": self.variant_set,
"expected_digits": self.expected_digits,
"min_votes": self.min_votes,
"min_confidence": self.min_confidence,
}
process.stdin.write(json.dumps(request, ensure_ascii=True) + "\n")
process.stdin.flush()
line = process.stdout.readline()
if not line:
self.process = None
raise RuntimeError("worker PaddleOCR terminato")
return json.loads(line)
def _ensure_process(self) -> subprocess.Popen[str]:
if self.process is not None and self.process.poll() is None:
return self.process
python_path = Path(self.python_path)
script_path = Path(self.script_path)
if not python_path.exists():
raise RuntimeError(f"python PaddleOCR non trovato: {python_path}")
if not script_path.exists():
raise RuntimeError(f"worker PaddleOCR non trovato: {script_path}")
self.process = subprocess.Popen(
[str(python_path), str(script_path)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
encoding="utf-8",
errors="replace",
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
)
assert self.process.stdout is not None
ready_line = self.process.stdout.readline()
if not ready_line:
raise RuntimeError("worker PaddleOCR non avviato")
ready = json.loads(ready_line)
if not ready.get("ready"):
raise RuntimeError(f"worker PaddleOCR risposta inattesa: {ready}")
return self.process
_paddle_worker: PaddleOcrWorker | None = None
_paddle_worker_lock = threading.Lock()
def log(msg: str) -> None:
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def load_server_config(path_str: str) -> dict[str, object]:
defaults: dict[str, object] = {
"wms_server_host": "0.0.0.0",
"wms_server_port": 8088,
"wms_received_dir": "wms_received",
"wms_fake_ack_mode": "always-ack",
"wms_fake_processing_sec": 0.5,
"wms_ui_enabled": True,
"wms_ocr_enabled": True,
"wms_ocr_mode": "paddleocr",
"wms_tesseract_cmd": "",
"wms_paddle_python": r"C:\devel\yolo-ocr\.venv-paddle-cpu\Scripts\python.exe",
"wms_paddle_worker_script": str(Path(__file__).with_name("flywms_paddleocr_worker.py")),
"wms_paddle_target_heights": "96",
"wms_paddle_variant_set": "fast",
"wms_paddle_expected_digits": 6,
"wms_paddle_min_votes": 2,
"wms_paddle_min_confidence": 0.70,
"wms_fake_ocr_prefix": "UDC",
"wms_undetermined_code_text": "udc non determinato",
"wms_fake_ocr_delay_sec": 0.2,
"wms_operator": "udrone",
"wms_site": "demo-magazzino",
}
path = Path(path_str)
if not path.exists():
return defaults
parser = configparser.ConfigParser()
parser.read(path, encoding="utf-8")
section = parser["navigation"] if parser.has_section("navigation") else {}
for key, default_value in defaults.items():
if key not in section:
continue
if isinstance(default_value, bool):
defaults[key] = parser.getboolean("navigation", key, fallback=default_value)
elif isinstance(default_value, int):
defaults[key] = parser.getint("navigation", key, fallback=default_value)
elif isinstance(default_value, float):
defaults[key] = parser.getfloat("navigation", key, fallback=default_value)
else:
defaults[key] = section.get(key, str(default_value)).strip()
return defaults
def parse_args():
pre = argparse.ArgumentParser(add_help=False)
pre.add_argument("--config", default=DEFAULT_CONFIG_PATH, help="File configurazione INI")
pre_args, _ = pre.parse_known_args()
defaults = load_server_config(pre_args.config)
ap = argparse.ArgumentParser(parents=[pre])
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("--received-dir", default=defaults["wms_received_dir"], help="Directory upload ricevuti")
ap.add_argument(
"--fake-ack-mode",
choices=["always-ack", "always-nack", "alternate", "random"],
default=defaults["wms_fake_ack_mode"],
help="Modalita risposta WMS demo",
)
ap.add_argument(
"--fake-processing-sec",
type=float,
default=defaults["wms_fake_processing_sec"],
help="Ritardo elaborazione finta",
)
ap.add_argument("--ui-enabled", action="store_true", default=defaults["wms_ui_enabled"], help="Mostra finestre OpenCV server")
ap.add_argument("--no-ui", action="store_false", dest="ui_enabled", help="Disabilita finestre OpenCV server")
ap.add_argument("--ocr-enabled", action="store_true", default=defaults["wms_ocr_enabled"], help="Esegue OCR reale se disponibile")
ap.add_argument("--no-ocr", action="store_false", dest="ocr_enabled", help="Disabilita OCR reale e usa fallback demo")
ap.add_argument("--ocr-mode", choices=["paddleocr", "easyocr", "tesseract", "fake"], default=defaults["wms_ocr_mode"], help="Motore OCR server")
ap.add_argument("--tesseract-cmd", default=defaults["wms_tesseract_cmd"], help="Percorso esplicito tesseract.exe")
ap.add_argument("--paddle-python", default=defaults["wms_paddle_python"], help="Python del virtualenv PaddleOCR")
ap.add_argument("--paddle-worker-script", default=defaults["wms_paddle_worker_script"], help="Script worker PaddleOCR")
ap.add_argument("--paddle-target-heights", default=defaults["wms_paddle_target_heights"], help="Altezze resize PaddleOCR, es. 96 oppure 64,96,128")
ap.add_argument("--paddle-variant-set", choices=["fast", "balanced", "full"], default=defaults["wms_paddle_variant_set"], help="Numero varianti preprocessing PaddleOCR")
ap.add_argument("--paddle-expected-digits", type=int, default=defaults["wms_paddle_expected_digits"], help="Numero cifre atteso codice UDC")
ap.add_argument("--paddle-min-votes", type=int, default=defaults["wms_paddle_min_votes"], help="Consenso minimo varianti PaddleOCR")
ap.add_argument("--paddle-min-confidence", type=float, default=defaults["wms_paddle_min_confidence"], help="Confidenza minima PaddleOCR")
ap.add_argument("--fake-ocr-prefix", default=defaults["wms_fake_ocr_prefix"], help="Prefisso fallback OCR demo")
ap.add_argument("--undetermined-code-text", default=defaults["wms_undetermined_code_text"], help="Testo usato se OCR non determina il codice")
ap.add_argument("--fake-ocr-delay-sec", type=float, default=defaults["wms_fake_ocr_delay_sec"], help="Ritardo OCR demo/fallback")
ap.add_argument("--operator", default=defaults["wms_operator"], help="Operatore WMS demo")
ap.add_argument("--site", default=defaults["wms_site"], help="Sito/magazzino demo")
return ap.parse_args()
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/api/v1/navigation-snapshot")
async def receive_navigation_snapshot(
metadata: str = Form(...),
label_image: UploadFile = File(...),
gaylord_image: UploadFile | None = File(None),
) -> dict[str, object]:
started = time.perf_counter()
try:
meta = json.loads(metadata)
except json.JSONDecodeError:
meta = {"raw_metadata": metadata}
with state_lock:
server_state["counter"] = int(server_state["counter"]) + 1
counter = int(server_state["counter"])
request_id = str(meta.get("request_id") or f"server-{counter:06d}")
snapshot_id = meta.get("snapshot_id", counter)
received_dir = Path(str(server_state["received_dir"]))
request_dir = received_dir / f"{counter:06d}_{safe_name(request_id)}"
request_dir.mkdir(parents=True, exist_ok=True)
label_path = request_dir / safe_upload_name(label_image.filename, "label.jpg")
label_bytes = await label_image.read()
label_path.write_bytes(label_bytes)
gaylord_path = None
gaylord_bytes = b""
if gaylord_image is not None:
gaylord_path = request_dir / safe_upload_name(gaylord_image.filename, "gaylord.jpg")
gaylord_bytes = await gaylord_image.read()
gaylord_path.write_bytes(gaylord_bytes)
(request_dir / "metadata.json").write_text(
json.dumps(meta, indent=2, ensure_ascii=True),
encoding="utf-8",
)
image = cv2.imdecode(np.frombuffer(label_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
ocr_result = await asyncio.to_thread(run_server_ocr, image, int(snapshot_id), label_path)
fake_processing_sec = float(server_state["fake_processing_sec"])
if fake_processing_sec > 0:
await asyncio.sleep(fake_processing_sec)
status = choose_status(str(server_state["fake_ack_mode"]), counter)
udc_code = ocr_result.text if status == "ACK" else ""
message = (
f"codice valido su WMS: {udc_code}"
if status == "ACK"
else f"codice non riconosciuto: {ocr_result.text}"
)
processing_ms = (time.perf_counter() - started) * 1000.0
received_at = datetime.now(timezone.utc).isoformat()
wms_payload = {
"request_id": request_id,
"client_id": meta.get("client_id", ""),
"site": server_state["site"],
"operator": server_state["operator"],
"received_at": received_at,
"snapshot_id": snapshot_id,
"position": meta.get("simulated_position", ""),
"track_id": meta.get("track_id", ""),
"udc_code": udc_code,
"ocr_raw_text": ocr_result.raw_text,
"ocr_confidence": ocr_result.confidence,
"ocr_backend": ocr_result.backend,
"ocr_fallback_used": ocr_result.fallback_used,
"ocr_votes": ocr_result.votes,
"ocr_variant": ocr_result.variant,
"validation_status": status,
"validation_message": message,
"label_bbox": meta.get("label_bbox", []),
"gaylord_bbox": meta.get("gaylord_bbox", []),
"movement_vector_px": meta.get("movement_vector_px", {}),
"label_image_path": str(label_path),
"gaylord_image_path": str(gaylord_path) if gaylord_path else "",
}
(request_dir / "wms_payload.json").write_text(
json.dumps(wms_payload, indent=2, ensure_ascii=True),
encoding="utf-8",
)
update_ui_state(image, wms_payload)
log(
f"received request_id={request_id} snapshot={snapshot_id} "
f"label_bytes={len(label_bytes)} gaylord_bytes={len(gaylord_bytes)} "
f"ocr='{ocr_result.text}' status={status}"
)
return {
"status": status,
"request_id": request_id,
"message": message,
"ocr_text": udc_code,
"ocr_raw_text": ocr_result.raw_text,
"ocr_confidence": ocr_result.confidence,
"ocr_backend": ocr_result.backend,
"ocr_fallback_used": ocr_result.fallback_used,
"ocr_votes": ocr_result.votes,
"ocr_variant": ocr_result.variant,
"processing_ms": round(processing_ms, 1),
"saved_dir": str(request_dir),
"wms_payload_path": str(request_dir / "wms_payload.json"),
"label_image_path": str(label_path),
"gaylord_image_path": str(gaylord_path) if gaylord_path else "",
}
def run_server_ocr(image: np.ndarray | None, snapshot_id: int, label_path: Path | None = None) -> OcrServerResult:
fake_delay = float(server_state["fake_ocr_delay_sec"])
if fake_delay > 0:
time.sleep(fake_delay)
fallback = str(server_state["undetermined_code_text"])
if image is None:
return OcrServerResult(fallback, "", 0.0, "fallback", True)
if not bool(server_state["ocr_enabled"]) or str(server_state["ocr_mode"]) == "fake":
return OcrServerResult(fallback, fallback, 1.0, "fake", True)
if str(server_state["ocr_mode"]) == "paddleocr":
if label_path is None:
return OcrServerResult(fallback, "", 0.0, "paddleocr-missing-path", True)
return run_paddleocr_ocr(label_path, fallback)
if str(server_state["ocr_mode"]) == "tesseract":
return run_tesseract_ocr(image, fallback)
try:
reader = get_easyocr_reader()
ocr_input = preprocess_label_for_ocr(image)
results = reader.readtext(
ocr_input,
allowlist="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
detail=1,
paragraph=False,
)
except Exception as exc:
log(f"OCR fallback: {exc}")
return OcrServerResult(fallback, "", 0.0, "easyocr-error", True)
raw_parts: list[str] = []
best_text = ""
best_conf = 0.0
for item in results:
text = str(item[1]).strip()
conf = float(item[2]) if len(item) > 2 else 0.0
if text:
raw_parts.append(text)
candidate = normalize_ocr_code(text)
if candidate and conf >= best_conf:
best_text = candidate
best_conf = conf
raw_text = " ".join(raw_parts)
if not best_text:
digits = re.sub(r"\D+", "", raw_text)
if digits:
best_text = digits
best_conf = max(best_conf, 0.01)
if best_text:
return OcrServerResult(best_text, raw_text, best_conf, "easyocr", False)
return OcrServerResult(fallback, raw_text, best_conf, "easyocr-fallback", True)
def get_paddle_worker() -> PaddleOcrWorker:
global _paddle_worker
with _paddle_worker_lock:
if _paddle_worker is None:
_paddle_worker = PaddleOcrWorker(
python_path=str(server_state["paddle_python"]),
script_path=str(server_state["paddle_worker_script"]),
target_heights=str(server_state["paddle_target_heights"]),
variant_set=str(server_state["paddle_variant_set"]),
expected_digits=int(server_state["paddle_expected_digits"]),
min_votes=int(server_state["paddle_min_votes"]),
min_confidence=float(server_state["paddle_min_confidence"]),
)
return _paddle_worker
def close_paddle_worker() -> None:
global _paddle_worker
with _paddle_worker_lock:
worker = _paddle_worker
_paddle_worker = None
if worker is not None:
worker.close()
def run_paddleocr_ocr(label_path: Path, fallback: str) -> OcrServerResult:
try:
response = get_paddle_worker().predict(label_path)
except Exception as exc:
log(f"PaddleOCR fallback: {exc}")
close_paddle_worker()
return OcrServerResult(fallback, "", 0.0, "paddleocr-error", True)
text = str(response.get("text") or "")
best_text = str(response.get("best_text") or text)
raw_text = str(response.get("raw_text") or best_text)
confidence = float(response.get("confidence") or 0.0)
votes = int(response.get("votes") or 0)
variant = str(response.get("variant") or "")
if response.get("ok") and text:
return OcrServerResult(text, raw_text, confidence, "paddleocr", False, votes=votes, variant=variant)
reason = str(response.get("reason") or "fallback")
raw_with_reason = f"{raw_text} [{reason}]".strip()
return OcrServerResult(fallback, raw_with_reason, confidence, "paddleocr-fallback", True, votes=votes, variant=variant)
def run_tesseract_ocr(image: np.ndarray, fallback: str) -> OcrServerResult:
try:
import pytesseract
except Exception as exc:
log(f"Tesseract fallback: pytesseract non disponibile: {exc}")
return OcrServerResult(fallback, "", 0.0, "tesseract-unavailable", True)
tesseract_cmd = str(server_state.get("tesseract_cmd") or "").strip()
if tesseract_cmd:
if not Path(tesseract_cmd).exists():
log(f"Tesseract fallback: binario non trovato: {tesseract_cmd}")
return OcrServerResult(fallback, "", 0.0, "tesseract-missing", True)
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
elif shutil.which("tesseract") is None:
log("Tesseract fallback: tesseract.exe non trovato nel PATH")
return OcrServerResult(fallback, "", 0.0, "tesseract-missing", True)
variants = build_tesseract_variants(image)
candidates: list[tuple[str, float, str]] = []
config = "--psm 7 --oem 3 -c tessedit_char_whitelist=0123456789"
for name, variant in variants:
try:
data = pytesseract.image_to_data(
variant,
config=config,
output_type=pytesseract.Output.DICT,
)
except Exception as exc:
log(f"Tesseract fallback su variante {name}: {exc}")
return OcrServerResult(fallback, "", 0.0, "tesseract-error", True)
texts = [text.strip() for text in data.get("text", []) if text and text.strip()]
confs: list[float] = []
for conf in data.get("conf", []):
try:
value = float(conf)
except (TypeError, ValueError):
continue
if value >= 0:
confs.append(value / 100.0)
raw = "".join(texts)
digits = re.sub(r"\D+", "", raw)
confidence = sum(confs) / len(confs) if confs else 0.0
if digits:
candidates.append((digits, confidence, name))
if not candidates:
return OcrServerResult(fallback, "", 0.0, "tesseract-fallback", True)
votes: dict[str, list[float]] = {}
for digits, confidence, _ in candidates:
votes.setdefault(digits, []).append(confidence)
best_digits, best_confs = max(
votes.items(),
key=lambda item: (len(item[1]), sum(item[1]) / max(len(item[1]), 1)),
)
consensus_count = len(best_confs)
avg_conf = sum(best_confs) / consensus_count
raw_text = "; ".join(f"{digits}@{conf:.2f}/{name}" for digits, conf, name in candidates)
if consensus_count < 2 or avg_conf < 0.45:
return OcrServerResult(fallback, raw_text, avg_conf, "tesseract-ambiguous", True)
return OcrServerResult(best_digits, raw_text, avg_conf, "tesseract", False)
def build_tesseract_variants(image: np.ndarray) -> list[tuple[str, np.ndarray]]:
h, w = image.shape[:2]
scale = 4.0 if w < 500 else 2.0
resized = cuda_resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_CUBIC)
gray = cuda_cvt_color(resized, cv2.COLOR_BGR2GRAY)
gray = cv2.bilateralFilter(gray, 7, 45, 45)
sharpen = cv2.addWeighted(gray, 1.6, cv2.GaussianBlur(gray, (0, 0), 1.2), -0.6, 0)
_, otsu = cv2.threshold(sharpen, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
adaptive = cv2.adaptiveThreshold(
sharpen,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
31,
7,
)
return [
("gray", gray),
("sharpen", sharpen),
("otsu", otsu),
("adaptive", adaptive),
("otsu_inv", cv2.bitwise_not(otsu)),
]
_easyocr_reader = None
_easyocr_lock = threading.Lock()
def get_easyocr_reader():
global _easyocr_reader
with _easyocr_lock:
if _easyocr_reader is None:
import easyocr
_easyocr_reader = easyocr.Reader(["en"], gpu=False, verbose=False)
return _easyocr_reader
def preprocess_label_for_ocr(image: np.ndarray) -> np.ndarray:
h, w = image.shape[:2]
scale = 3.0 if w < 450 else 2.0
resized = cuda_resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_CUBIC)
gray = cuda_cvt_color(resized, cv2.COLOR_BGR2GRAY)
gray = cv2.bilateralFilter(gray, 7, 45, 45)
return cuda_cvt_color(gray, cv2.COLOR_GRAY2BGR)
def normalize_ocr_code(text: str) -> str:
compact = re.sub(r"[^0-9A-Za-z-]+", "", text).strip("-")
digits = re.sub(r"\D+", "", compact)
return digits if digits else compact
def update_ui_state(image: np.ndarray | None, payload: dict[str, object]) -> None:
if not bool(server_state["ui_enabled"]):
return
lines = [
"ULTIMO PAYLOAD WMS",
f"request_id: {payload.get('request_id', '')}",
f"client_id: {payload.get('client_id', '')}",
f"site: {payload.get('site', '')}",
f"operator: {payload.get('operator', '')}",
f"received_at: {payload.get('received_at', '')}",
f"snapshot_id: {payload.get('snapshot_id', '')}",
f"position: {payload.get('position', '')}",
f"track_id: {payload.get('track_id', '')}",
f"udc_code: {payload.get('udc_code', '')}",
f"ocr_raw: {payload.get('ocr_raw_text', '')}",
f"ocr_backend: {payload.get('ocr_backend', '')}",
f"ocr_fallback: {payload.get('ocr_fallback_used', '')}",
f"ocr_votes: {payload.get('ocr_votes', '')}",
f"ocr_variant: {payload.get('ocr_variant', '')}",
f"status: {payload.get('validation_status', '')}",
f"label_bbox: {payload.get('label_bbox', '')}",
f"movement: {payload.get('movement_vector_px', '')}",
]
with ui_lock:
ui_state["image"] = None if image is None else image.copy()
ui_state["payload_lines"] = lines
ui_state["updated"] = True
def choose_status(mode: str, counter: int) -> str:
if mode == "always-nack":
return "NACK"
if mode == "alternate":
return "ACK" if counter % 2 == 1 else "NACK"
if mode == "random":
return "ACK" if random.random() >= 0.5 else "NACK"
return "ACK"
def safe_upload_name(filename: str | None, fallback: str) -> str:
name = Path(filename or fallback).name
return safe_name(name) or fallback
def safe_name(value: str) -> str:
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-"
return "".join(ch if ch in allowed else "_" for ch in value)[:160]
def server_ui_loop() -> None:
try:
cv2.namedWindow("wms immagine ricevuta", cv2.WINDOW_NORMAL)
cv2.namedWindow("wms payload", cv2.WINDOW_NORMAL)
except cv2.error as exc:
log(f"UI OpenCV disabilitata: highgui non disponibile ({exc})")
with ui_lock:
ui_state["stop"] = True
return
cv2.resizeWindow("wms immagine ricevuta", 640, 360)
cv2.resizeWindow("wms payload", 900, 560)
cv2.moveWindow("wms immagine ricevuta", 40, 40)
cv2.moveWindow("wms payload", 720, 40)
while True:
with ui_lock:
if ui_state["stop"]:
break
image = None if ui_state["image"] is None else ui_state["image"].copy()
lines = list(ui_state["payload_lines"])
if image is None:
image_display = np.full((360, 640, 3), 240, dtype=np.uint8)
cv2.putText(
image_display,
"In attesa immagine etichetta",
(30, 180),
cv2.FONT_HERSHEY_SIMPLEX,
0.9,
(0, 0, 0),
2,
cv2.LINE_AA,
)
else:
image_display = resize_preview(image, 640)
cv2.imshow("wms immagine ricevuta", image_display)
cv2.imshow("wms payload", draw_payload_window(lines))
key = cv2.waitKey(100) & 0xFF
if key in (27, ord("q")):
with ui_lock:
ui_state["stop"] = True
break
cv2.destroyWindow("wms immagine ricevuta")
cv2.destroyWindow("wms payload")
def resize_preview(frame: np.ndarray, max_width: int) -> np.ndarray:
h, w = frame.shape[:2]
if max_width <= 0 or w <= max_width:
return frame
scale = max_width / float(w)
return cuda_resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR)
def draw_payload_window(lines: list[str]) -> np.ndarray:
canvas = np.full((560, 900, 3), 245, dtype=np.uint8)
y = 42
for idx, line in enumerate(lines[:18]):
color = (0, 0, 160) if idx == 0 else (0, 0, 0)
scale = 0.9 if idx == 0 else 0.63
thickness = 2 if idx == 0 else 1
cv2.putText(
canvas,
str(line)[:110],
(24, y),
cv2.FONT_HERSHEY_SIMPLEX,
scale,
color,
thickness,
cv2.LINE_AA,
)
y += 34 if idx == 0 else 30
return canvas
def main() -> int:
args = parse_args()
server_state["received_dir"] = args.received_dir
server_state["fake_ack_mode"] = args.fake_ack_mode
server_state["fake_processing_sec"] = args.fake_processing_sec
server_state["ui_enabled"] = args.ui_enabled
server_state["ocr_enabled"] = args.ocr_enabled
server_state["ocr_mode"] = args.ocr_mode
server_state["tesseract_cmd"] = args.tesseract_cmd
server_state["paddle_python"] = args.paddle_python
server_state["paddle_worker_script"] = args.paddle_worker_script
server_state["paddle_target_heights"] = args.paddle_target_heights
server_state["paddle_variant_set"] = args.paddle_variant_set
server_state["paddle_expected_digits"] = args.paddle_expected_digits
server_state["paddle_min_votes"] = args.paddle_min_votes
server_state["paddle_min_confidence"] = args.paddle_min_confidence
server_state["fake_ocr_prefix"] = args.fake_ocr_prefix
server_state["undetermined_code_text"] = args.undetermined_code_text
server_state["fake_ocr_delay_sec"] = args.fake_ocr_delay_sec
server_state["operator"] = args.operator
server_state["site"] = args.site
Path(args.received_dir).mkdir(parents=True, exist_ok=True)
log(
f"FlyWMS WMS demo server host={args.host} port={args.port} "
f"mode={args.fake_ack_mode} received_dir={args.received_dir}"
)
log(f"OpenCV CUDA: {'attivo' if OPENCV_CUDA_AVAILABLE else 'non disponibile'}")
ui_thread = None
if args.ui_enabled:
ui_thread = threading.Thread(target=server_ui_loop, name="wms-server-ui", daemon=True)
ui_thread.start()
try:
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
finally:
close_paddle_worker()
with ui_lock:
ui_state["stop"] = True
if ui_thread is not None:
ui_thread.join(timeout=2.0)
return 0
if __name__ == "__main__":
raise SystemExit(main())