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

133 lines
5.4 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
import json
import pytest
from scale_bridge import profiles
from scale_bridge.config import Config
from scale_bridge.protocols import Reading
def base(**over):
raw = {"name": "packing-1", "port": "com7", "protocol": "mahak_mds",
"serial": {"baudrate": 9600}, "exchange": {"request_hex": "4d090407"}}
raw.update(over)
return raw
def test_validate_normalises():
p = profiles.validate(base())
assert p["port"] == "COM7"
assert p["serial"]["baudrate"] == 9600 and p["serial"]["parity"] == "N"
assert p["exchange"]["request_hex"] == "4D 09 04 07"
assert p["stable_samples"] == 3
@pytest.mark.parametrize("bad", [
{"name": ""}, {"name": "bad name"}, {"name": "-x"}, {"protocol": "nope"}, {"port": "LPT1"},
{"serial": {"baudrate": 9}}, {"exchange": {"request_hex": "zz"}}, {"output_unit": "pcs"},
{"protocol": "custom_ascii", "params": {"regex": "("}}, {"protocol": "custom_ascii", "params": {}},
])
def test_validate_rejects(bad):
with pytest.raises(ValueError):
profiles.validate(base(**bad))
class FakeWorker:
def __init__(self, chunks, is_open=True, last_error=None):
self.chunks = chunks
self.is_open = is_open
self.last_error = last_error
def recent(self, count):
return self.chunks[-count:]
MAHAK = lambda grams: bytes([0x58, 0x51, 7, 7]) + grams.to_bytes(3, "big") # noqa: E731
def test_evaluate_flagless_stability_needs_equal_recent_readings():
p = profiles.validate(base())
now = 100_000
moving = FakeWorker([(now - 1000, MAHAK(540)), (now - 500, MAHAK(550)), (now - 10, MAHAK(550))])
r = profiles.evaluate(p, moving, 1500, now)
assert r["ok"] and r["value"] == 550 and r["unit"] == "g" and r["stable"] is False
steady = FakeWorker([(now - 1000, MAHAK(550)), (now - 500, MAHAK(550)), (now - 10, MAHAK(550))])
assert profiles.evaluate(p, steady, 1500, now)["stable"] is True
old = FakeWorker([(now - 60_000, MAHAK(550)), (now - 59_000, MAHAK(550)), (now - 10, MAHAK(550))])
assert profiles.evaluate(p, old, 1500, now)["stable"] is False
def test_evaluate_stale_and_conversion():
p = profiles.validate(base(output_unit="kg"))
now = 100_000
r = profiles.evaluate(p, FakeWorker([(now - 5000, MAHAK(550))] * 3), 1500, now)
assert r["stale"] is True and r["stable"] is False
assert r["value"] == pytest.approx(0.55) and r["unit"] == "kg"
def test_evaluate_unit_fallback_for_unitless_protocols():
p = profiles.validate(base(protocol="xk3190_reversed", unit="kg", exchange={}))
r = profiles.evaluate(p, FakeWorker([(1000, b"05.21000=05.21000=05.21000=")]), 1500, 1010)
assert r["value"] == 12.5 and r["unit"] == "kg"
def test_evaluate_errors():
p = profiles.validate(base())
r = profiles.evaluate(p, FakeWorker([], is_open=False, last_error="could not open port COM7"), 1500, 0)
assert r["ok"] is False and "COM7" in r["error"]
r = profiles.evaluate(p, FakeWorker([(0, b"garbage")]), 1500, 10)
assert r["ok"] is False and "protocol" in r["error"]
def test_from_detection_and_ad_hoc():
detection = {"port": "COM9", "protocol": "xk3190_tf0",
"serial": {"port": "COM9", "baudrate": 2400, "bytesize": 8, "parity": "N", "stopbits": 1},
"exchange": {"request_hex": "", "poll_interval_ms": 500}, "reading": {"unit": ""}}
p = profiles.from_detection(detection, "line-2")
assert p["unit"] == "kg" and p["serial"]["baudrate"] == 2400
q = profiles.ad_hoc("COM3", "mt_sics")
assert q["exchange"]["request_hex"] == "53 49 0D 0A"
def test_config_round_trip_and_legacy_file(tmp_path):
path = tmp_path / "bridge.json"
# a file written by an earlier version (1.x)
path.write_text(json.dumps({"listen_host": "0.0.0.0", "listen_port": 8765, "token": "abc",
"allowed_clients": []}), encoding="utf-8")
c = Config(str(path))
assert c.token == "abc" and c.profiles() == []
c.save_profile(base())
assert Config(str(path)).profile("PACKING-1")["port"] == "COM7"
assert c.delete_profile("packing-1") and not c.delete_profile("packing-1")
with pytest.raises(ValueError):
c.set_allowed_clients(["not-an-ip"])
def test_config_creates_token_and_survives_a_broken_file(tmp_path):
path = tmp_path / "bridge.json"
path.write_text("{broken", encoding="utf-8")
c = Config(str(path))
assert len(c.token) >= 24
assert (tmp_path / "bridge.json.broken").exists()
old = c.token
assert c.rotate_token() != old
def test_reversed_stream_one_frame_per_chunk():
# A Yaohua YH-T7E sends "=081.100" about nine times a second with a pause
# between frames, so every frame lands in its own chunk (field capture, 1.18 kg).
p = profiles.validate(base(protocol="xk3190_reversed", unit="kg", exchange={}))
now = 10_000
chunks = [(now - 400 + i * 100, b"=081.100") for i in range(5)]
timed = profiles.timed_readings("xk3190_reversed", None, chunks)
assert [x.reading.value for x in timed] == [1.18] * 4
r = profiles.evaluate(p, FakeWorker(chunks), 1500, now)
assert r["ok"] and r["value"] == 1.18 and r["unit"] == "kg" and r["stable"] is True
def test_chunk_boundaries_do_not_repeat_readings():
frame = bytes([0x58, 0x51, 7, 7]) + (550).to_bytes(3, "big")
timed = profiles.timed_readings("mahak_mds", None, [(1, frame), (2, frame), (3, frame)])
assert [x.t_ms for x in timed] == [1, 2, 3]