Files
ske087 bff6d70b17 Initial commit: aplicatie de cantarire si etichetare
Aplicatie Flask + SQLite pentru productie alimentara: cantarirea
componentelor unui produs finit si tiparirea etichetei prin QZ Tray.

- produse finite (retete) formate din componente cantarite
- designer vizual de etichete (mm/px, zona sigura, campuri variabile,
  cod QR cu continut din mai multe bucati)
- legare produs finit -> sablon de eticheta
- navigare pe doua taburi: Cantarire / Setari
- administrare: componente, produse finite, sabloane, campuri, istoric
- backup, resetare baza de date, checklist de pregatire, /healthz
- CLI de administrare si serviciu systemd pentru productie
- vendor/syncpulse-scale-bridge (GPL-3.0-or-later) pentru citirea cantarului
2026-10-18 23:38:45 +03:00

185 lines
5.9 KiB
Python

"""Monitor de port serial: ce trimite de fapt cantarul.
Instrument pentru un model nou (ex. Bizerba BC800): deschide portul in mod
ascultare (nu scrie nimic catre cantar), strange cadrele brute si le trece
prin toate protocoalele cunoscute. Rezultatul spune daca formatul este deja
suportat sau daca trebuie adaugat un protocol nou.
"""
from __future__ import annotations
import re
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
VENDOR_DIR = Path(__file__).resolve().parent.parent / "vendor" / "syncpulse-scale-bridge"
BAUD_RATES = (9600, 4800, 19200, 2400, 38400, 1200, 57600, 115200)
FRAMINGS = ("8N1", "7E1", "7O1", "8E1", "8O1")
MAX_FRAMES = 60
class ProbeError(RuntimeError):
"""Portul nu poate fi deschis sau setarile sunt invalide."""
def _bridge():
path = str(VENDOR_DIR)
if path not in sys.path:
sys.path.insert(0, path)
from scale_bridge import serialio # noqa: PLC0415
return serialio
def valid_framing(text: str) -> str:
value = (text or "").strip().upper()
if not re.fullmatch(r"[5678][NEOMS][12]", value):
raise ProbeError(f"framing invalid: {text!r} (ex. 8N1, 7E1)")
return value
def list_ports() -> list[dict[str, Any]]:
serialio = _bridge()
return serialio.list_serial_ports()
def decode(data: bytes) -> list[dict[str, Any]]:
"""Ce protocoale din biblioteca recunosc datele primite."""
_bridge()
from scale_bridge.protocols import PROTOCOLS # noqa: PLC0415
guesses: list[dict[str, Any]] = []
if not data:
return guesses
for pid, proto in PROTOCOLS.items():
if pid.startswith("custom"):
continue
try:
readings = [r for r in proto.readings(data) if r.value is not None]
except Exception: # noqa: BLE001 - un parser nu trebuie sa rupa monitorul
continue
if not readings:
continue
last = readings[-1]
guesses.append({
"protocol": pid,
"name": proto.name,
"kind": proto.kind,
"count": len(readings),
"value": last.value,
"unit": last.unit,
"stable": last.stable,
"specificity": proto.specificity,
})
guesses.sort(key=lambda g: (-g["specificity"], -g["count"]))
return guesses
def probe_port(
port: str,
baud: int = 9600,
framing: str = "8N1",
seconds: float = 5.0,
opener: Optional[Any] = None,
) -> dict[str, Any]:
"""Asculta portul `seconds` secunde si intoarce cadrele brute."""
serialio = _bridge()
framing = valid_framing(framing)
if not port:
raise ProbeError("Alege portul serial.")
if not 50 <= int(baud) <= 921600:
raise ProbeError(f"baud invalid: {baud}")
seconds = min(max(float(seconds), 0.5), 60.0)
serial_cfg = {
"port": port,
"baudrate": int(baud),
"bytesize": int(framing[0]),
"parity": framing[1],
"stopbits": int(framing[2]),
}
try:
settings = serialio.LineSettings.from_dicts(serial_cfg, {})
except ValueError as exc:
raise ProbeError(str(exc)) from exc
worker = serialio.PortWorker(settings, opener or serialio.open_real)
worker.start()
deadline = time.monotonic() + seconds
try:
while time.monotonic() < deadline:
if worker.last_error and not worker.is_open:
break
time.sleep(0.05)
finally:
worker.stop()
worker.join(timeout=3)
chunks = worker.recent(MAX_FRAMES)
joined = b"".join(data for _, data in chunks)
now = serialio.now_ms()
return {
"ok": bool(chunks) or bool(worker.is_open),
"port": settings.port,
"settings": settings.describe(),
"baud": int(baud),
"framing": framing,
"seconds": seconds,
"error": None if (chunks or worker.is_open) else worker.last_error,
"bytes": len(joined),
"frames": [
{"age_ms": max(0, now - t), "hex": serialio.format_hex(data),
"text": serialio.printable(data), "size": len(data)}
for t, data in chunks
],
"guesses": decode(joined),
"raw": joined,
}
def sweep(
port: str,
seconds: float = 2.0,
framings: tuple[str, ...] = ("8N1", "7E1"),
bauds: tuple[int, ...] = BAUD_RATES,
opener: Optional[Any] = None,
) -> list[dict[str, Any]]:
"""Incearca mai multe setari de linie si pastreaza doar cele cu date."""
results = []
for framing in framings:
for baud in bauds:
try:
result = probe_port(port, baud, framing, seconds, opener)
except ProbeError as exc:
results.append({"baud": baud, "framing": framing, "ok": False,
"error": str(exc), "bytes": 0, "guesses": []})
continue
results.append({
"baud": baud,
"framing": framing,
"ok": result["ok"],
"error": result["error"],
"bytes": result["bytes"],
"frames": len(result["frames"]),
"guesses": result["guesses"],
"raw": result["raw"],
})
if result["bytes"] and result["guesses"]:
# Un protocol recunoscut este un rezultat bun: nu mai insistam.
pass
return results
def save_capture(folder: str | Path, name: str, frames: list[dict[str, Any]]) -> Path:
"""Scrie cadrele ca hex, un cadru pe linie (formatul din tests/captures)."""
safe = re.sub(r"[^A-Za-z0-9_-]+", "-", str(name or "capture")).strip("-") or "capture"
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
target = Path(folder) / f"{safe}-{stamp}.hex"
target.parent.mkdir(parents=True, exist_ok=True)
lines = [f["hex"] for f in frames if f.get("hex")]
target.write_text("\n".join(lines) + "\n", encoding="ascii")
return target