bff6d70b17
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
185 lines
5.9 KiB
Python
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
|