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:
2026-10-18 23:38:45 +03:00
commit bff6d70b17
107 changed files with 21428 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Utilitare de linie de comanda."""
+122
View File
@@ -0,0 +1,122 @@
"""Utilitar de administrare: stare, backup, reset, verificare, date demo.
Exemple:
python -m cantarire.tools.admin status
python -m cantarire.tools.admin check
python -m cantarire.tools.admin backup --keep 10
python -m cantarire.tools.admin reset --yes
python -m cantarire.tools.admin seed-demo
"""
from __future__ import annotations
import argparse
import sys
from cantarire import create_app
from cantarire import maintenance
def _status() -> int:
status = maintenance.database_status()
print(f"Baza de date : {status['path']}")
print(f"Marime : {status['size_human']}")
print(f"Integritate : {status['integrity']}")
print(f"Schema : v{status['schema_version']}")
print(f"Backup-uri : {status['backups']} in {status['backup_dir']}")
print("Inregistrari :")
for table, count in status["counts"].items():
print(f" {table:<14} {count}")
return 0
def _check(as_json: bool = False) -> int:
summary = maintenance.readiness_summary()
if as_json:
import json
print(json.dumps(summary, indent=2, ensure_ascii=False, default=str))
return 0 if summary["ready"] else 1
print("Verificare pregatire productie:\n")
for item in summary["checks"]:
mark = "OK " if item["ok"] else ("OPT " if item.get("optional") else "LIPSA")
print(f"[{mark}] {item['label']:<22} {item['message']}")
print()
if summary["ready"]:
print("Gata de cantarit: toate elementele obligatorii sunt completate.")
return 0
missing = ", ".join(item["label"] for item in summary["missing"])
print(f"De completat: {missing}")
return 1
def _backup(keep: int) -> int:
path = maintenance.backup_database("manual")
removed = maintenance.prune_backups(keep)
print(f"Backup creat: {path}")
if removed:
print(f"Backup-uri vechi sterse: {removed} (pastrez ultimele {keep})")
return 0
def _reset(confirmed: bool, keep_backup: bool) -> int:
if not confirmed:
print("Operatia goleste toate datele (componente, produse finite, pachete, sabloane).")
print("Ruleaz-o din nou cu --yes daca esti sigur.")
return 2
result = maintenance.reset_database(keep_backup=keep_backup)
print(maintenance.reset_report(result))
return 0
def _seed_demo() -> int:
result = maintenance.seed_demo()
created = result["created"]
print(f"Componente create: {created['components']} · produse finite create: {created['recipes']}")
print(f"Componente: {', '.join(result['components']) or '-'}")
print(f"Produse finite: {', '.join(result['recipes']) or '-'}")
return 0
def main(argv: list[str] | None = None, app=None) -> int:
parser = argparse.ArgumentParser(
prog="cantarire-admin",
description="Administrare: stare, backup, reset, verificare.",
)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("status", help="starea bazei si numarul de inregistrari")
check = sub.add_parser("check", help="verifica daca aplicatia e gata de cantarit")
check.add_argument("--json", action="store_true", help="iesire JSON")
backup = sub.add_parser("backup", help="creeaza un backup al bazei")
backup.add_argument("--keep", type=int, default=maintenance.KEEP_BACKUPS,
help="cate backup-uri pastreaza (implicit 20)")
reset = sub.add_parser("reset", help="goleste datele si readuce setarile initiale")
reset.add_argument("--yes", action="store_true", help="confirma stergerea")
reset.add_argument("--no-backup", action="store_true", help="nu face backup inainte")
sub.add_parser("seed-demo", help="adauga datele demo (instruire/testare)")
args = parser.parse_args(argv)
application = app or create_app()
with application.app_context():
maintenance.ensure_instance_dirs()
if args.command == "status":
return _status()
if args.command == "check":
return _check(args.json)
if args.command == "backup":
return _backup(args.keep)
if args.command == "reset":
return _reset(args.yes, not args.no_backup)
if args.command == "seed-demo":
return _seed_demo()
return 1
if __name__ == "__main__":
sys.exit(main())
+89
View File
@@ -0,0 +1,89 @@
"""Utilitar CLI pentru identificarea unui cantar nou.
python -m cantarire.tools.monitor --list
python -m cantarire.tools.monitor /dev/ttyUSB0 --seconds 8
python -m cantarire.tools.monitor COM3 --scan --seconds 2
python -m cantarire.tools.monitor COM3 --baud 9600 --framing 7E1 --save bizerba
"""
from __future__ import annotations
import argparse
import sys
from ..probe import BAUD_RATES, FRAMINGS, ProbeError, list_ports, probe_port, save_capture, sweep
def _print_ports() -> int:
ports = list_ports()
if not ports:
print("Niciun port serial detectat.")
return 1
for p in ports:
chip = f" [{p['chip']}]" if p.get("chip") else ""
print(f"{p['device']:<20} {p.get('description') or ''}{chip}")
if p.get("note"):
print(f"{'':<20} ! {p['note']}")
return 0
def _print_result(result: dict) -> None:
print(f"\n{result['settings']} ({result['seconds']}s)")
print(f"bytes: {result['bytes']} cadre: {len(result['frames'])}")
if result.get("error"):
print(f"eroare: {result['error']}")
for frame in result["frames"]:
print(f" {frame['hex']:<56} {frame['text']}")
if result["guesses"]:
print("\nprotocoale recunoscute:")
for g in result["guesses"]:
print(f" {g['protocol']:<18} {g['kind']:<10} {g['value']} {g['unit']} "
f"({g['count']} cadre)")
elif result["bytes"]:
print("\nNiciun protocol cunoscut. Salveaza captura si adaugam parserul.")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Monitor port serial cantar")
parser.add_argument("port", nargs="?", help="ex. /dev/ttyUSB0 sau COM3")
parser.add_argument("--list", action="store_true", help="listeaza porturile si iese")
parser.add_argument("--baud", type=int, default=9600)
parser.add_argument("--framing", default="8N1", choices=FRAMINGS)
parser.add_argument("--seconds", type=float, default=5.0)
parser.add_argument("--scan", action="store_true",
help="incearca toate vitezele la framing-ul dat")
parser.add_argument("--save", metavar="NUME", help="scrie captura in instance/captures")
args = parser.parse_args(argv)
if args.list or not args.port:
return _print_ports()
try:
if args.scan:
rows = sweep(args.port, seconds=min(args.seconds, 3.0),
framings=(args.framing,), bauds=BAUD_RATES)
hits = [r for r in rows if r.get("bytes")]
if not hits:
print("Niciun byte primit la vitezele incercate.")
for row in rows:
if row.get("error"):
print(f" {row['baud']} {row['framing']}: {row['error']}")
return 1
for hit in hits:
names = ", ".join(g["protocol"] for g in hit["guesses"]) or "necunoscut"
print(f"{hit['baud']:>7} {hit['framing']} {hit['bytes']:>6} bytes {names}")
return 0
result = probe_port(args.port, args.baud, args.framing, args.seconds)
except ProbeError as exc:
print(f"eroare: {exc}", file=sys.stderr)
return 2
_print_result(result)
if args.save and result["frames"]:
target = save_capture("instance/captures", args.save, result["frames"])
print(f"\ncaptura: {target}")
return 0 if result["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())