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
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user