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,12 @@
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 33 39 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 31 30 32 2E 33 30 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 31 30 32 2E 33 30 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 20 20 30 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 31 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 31 34 31 2E 38 30 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 20 20 30 09 31 34 31 2E 38 30 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 20 20 30 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 31 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 33 2E 33 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 31 34 31 2E 32 30 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 20 20 30 09 31 34 31 2E 32 30 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 34 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 34 30 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 31 2E 34 30 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 34 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 34 30 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 31 2E 34 30 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 34 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 33 35 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 31 2E 33 35 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 34 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 33 30 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 31 2E 33 30 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 35 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 32 35 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 31 2E 32 35 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 35 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 30 2E 39 35 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 30 2E 39 35 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 20 20 30 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 35 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 32 30 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 20 20 30 09 20 20 31 2E 32 30 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 35 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 32 35 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 31 2E 32 35 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
43 4C 53 0D 0A 53 49 5A 45 20 35 37 20 6D 6D 2C 20 34 30 20 6D 6D 0D 0A 47 41 50 20 32 20 6D 6D 2C 30 0D 0A 44 49 52 45 43 54 49 4F 4E 20 31 0D 0A 52 45 46 45 52 45 4E 43 45 20 30 2C 30 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 33 30 30 2C 32 35 30 2C 22 31 30 22 2C 32 37 30 2C 31 2C 31 2C 22 4E 6F 3A 20 36 20 53 48 49 46 54 3A 32 22 0D 0A 54 45 58 54 20 30 33 30 2C 32 33 35 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 4F 72 64 65 72 2E 43 6F 20 3A 20 20 20 32 32 32 0D 0A 54 45 58 54 20 30 33 30 2C 30 38 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 31 34 30 35 2F 30 37 2F 31 39 20 2D 20 31 36 3A 34 35 22 0D 0A 54 45 58 54 20 30 33 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 54 68 69 63 6B 6E 65 73 73 20 3A 20 30 2E 30 22 0D 0A 43 4F 44 45 50 41 47 45 20 31 32 35 33 0D 0A 54 45 58 54 20 32 32 30 2C 31 36 30 2C 22 31 30 22 2C 30 2C 31 2C 32 2C 22 E6 22 0D 0A 43 4F 44 45 50 41 47 45 20 49 72 61 6E 49 49 0D 0A 54 45 58 54 20 30 32 30 2C 30 30 30 2C 22 31 30 22 2C 30 2C 32 2C 32 2C 22 20 20 31 2E 31 35 30 22 0D 0A 54 45 58 54 20 32 30 30 2C 30 31 35 2C 22 31 30 22 2C 30 2C 32 2C 31 2C 22 28 4B 67 29 22 0D 0A 42 41 52 43 4F 44 45 20 33 35 30 2C 32 38 30 2C 20 22 31 32 38 22 2C 30 37 30 2C 31 2C 32 37 30 2C 32 2C 32 2C 20 22 20 20 32 32 32 09 20 20 31 2E 31 35 30 22 0D 0A 50 52 49 4E 54 20 31 0D 0A 0D 0A
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Auto-detection against simulated scales that only understand their own settings."""
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from scale_bridge.detect import Detector, Timings, readability
|
||||
from scale_bridge.simulator import SimulatorBank
|
||||
|
||||
FAST = Timings(passive_s=0.5, probe_s=0.15, confirm_s=0.4, drain_s=0.02)
|
||||
|
||||
|
||||
class Silent:
|
||||
"""A port with nothing connected."""
|
||||
in_waiting = 0
|
||||
|
||||
def __init__(self, settings):
|
||||
self.settings = settings
|
||||
|
||||
def read(self, n=1):
|
||||
time.sleep(0.01)
|
||||
return b""
|
||||
|
||||
def write(self, data):
|
||||
return len(data)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class Chatter(Silent):
|
||||
"""Readable text without any weight in it."""
|
||||
|
||||
def read(self, n=1):
|
||||
time.sleep(0.01)
|
||||
return b"HELLO WORLD\r\n" if self.settings.baudrate == 9600 else b""
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
return 0
|
||||
|
||||
|
||||
def detect(spec, port="COM90", thorough=False):
|
||||
bank = SimulatorBank(spec)
|
||||
events = []
|
||||
result = Detector(bank.opener(), log=lambda code, **kw: events.append(code), thorough=thorough,
|
||||
timings=FAST).run_port(port)
|
||||
return result, events
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spec,protocol,baud,framing,mode", [
|
||||
("mahak_mds", "mahak_mds", 9600, "8N1", "poll"),
|
||||
("toledo_cont:4800:7E1", "toledo_cont", 4800, "7E1", "continuous"),
|
||||
("xk3190_tf0:2400", "xk3190_tf0", 2400, "8N1", "continuous"),
|
||||
("xk3190_reversed:1200", "xk3190_reversed", 1200, "8N1", "continuous"),
|
||||
("mt_sics", "mt_sics", 9600, "8N1", "poll"),
|
||||
("and_std:19200", "and_std", 19200, "8N1", "poll"),
|
||||
("sma:9600:7E1", "sma", 9600, "7E1", "poll"),
|
||||
("sartorius:1200:7E1", "sartorius", 1200, "7E1", "poll"),
|
||||
("cas_enq", "cas_enq", 9600, "8N1", "poll"),
|
||||
])
|
||||
def test_finds_protocol_speed_and_framing(spec, protocol, baud, framing, mode):
|
||||
result, events = detect(spec)
|
||||
assert result["found"], events
|
||||
assert result["protocol"] == protocol
|
||||
assert result["serial"]["baudrate"] == baud
|
||||
s = result["serial"]
|
||||
assert f"{s['bytesize']}{s['parity']}{s['stopbits']}" == framing
|
||||
assert result["mode"] == mode
|
||||
assert 0 < result["confidence"] <= 0.99
|
||||
assert events[0] == "port_start" and "found" in events
|
||||
|
||||
|
||||
def test_thorough_search_reaches_7o1():
|
||||
result, _ = detect("ohaus:9600:7O1", thorough=True)
|
||||
assert result["found"] and result["protocol"] == "ohaus"
|
||||
assert result["serial"]["parity"] == "O"
|
||||
|
||||
|
||||
def test_nothing_connected():
|
||||
detector = Detector(lambda s: Silent(s), timings=FAST)
|
||||
result = detector.run_port("COM5")
|
||||
assert result["found"] is False and result["unknown"] is None
|
||||
|
||||
|
||||
def test_unknown_readable_data_is_reported():
|
||||
detector = Detector(lambda s: Chatter(s), timings=FAST)
|
||||
result = detector.run_port("COM5")
|
||||
assert result["found"] is False
|
||||
assert result["unknown"]["baud"] == 9600
|
||||
assert "HELLO" in result["unknown"]["sample_text"]
|
||||
|
||||
|
||||
def test_cancel_stops_quickly():
|
||||
detector = Detector(lambda s: Silent(s), cancelled=lambda: True, timings=FAST)
|
||||
started = time.monotonic()
|
||||
result = detector.run_port("COM5")
|
||||
assert time.monotonic() - started < 0.5
|
||||
assert result["found"] is False and result["cancelled"] is True
|
||||
|
||||
|
||||
def test_open_failure_is_reported():
|
||||
def refuse(settings):
|
||||
raise OSError("Access is denied")
|
||||
result = Detector(refuse, timings=FAST).run_port("COM5")
|
||||
assert result["found"] is False and "Access is denied" in result["error"]
|
||||
|
||||
|
||||
def test_readability():
|
||||
assert readability(b"ST,+0.55 kg\r\n") == 1.0
|
||||
assert readability(bytes([0xD3, 0xD4])) == 0.0
|
||||
assert readability(bytes([0xD3, 0xD4]), mask7=True) == 1.0
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Indicators that print labels: the PTC400 capture, other label languages, the label log."""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from scale_bridge import profiles
|
||||
from scale_bridge.detect import Detector, Timings
|
||||
from scale_bridge.labels import LabelLog
|
||||
from scale_bridge.protocols import PROTOCOLS
|
||||
from scale_bridge.simulator import SimulatorBank
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
FAST = Timings(passive_s=0.4, probe_s=0.1, confirm_s=0.3, drain_s=0.02)
|
||||
|
||||
|
||||
def capture() -> list[bytes]:
|
||||
with open(os.path.join(HERE, "captures", "ptc400_tspl_labels.hex"), encoding="ascii") as f:
|
||||
return [bytes.fromhex(line) for line in f if line.strip()]
|
||||
|
||||
|
||||
def test_ptc400_capture_all_labels():
|
||||
# Recorded from a PTC400 indicator (TSPL to a TSC label printer), a 1.4 kg
|
||||
# test weight moved around the platform after three other weighings.
|
||||
readings = PROTOCOLS["label_print"].readings(b"".join(capture()))
|
||||
assert [r.value for r in readings] == [102.3, 141.8, 141.2, 1.4, 1.4, 1.35, 1.3, 1.25, 0.95,
|
||||
1.2, 1.25, 1.15]
|
||||
first = readings[0]
|
||||
assert first.unit == "kg" and first.stable is True
|
||||
assert first.fields["Order.Co"] == "222" and first.fields["SHIFT"] == "2"
|
||||
assert first.fields["date"] == "1405/07/19" and first.fields["time"] == "16:39"
|
||||
assert first.fields["Thickness"] == "0.0" and first.fields["No"] == "6"
|
||||
assert readings[2].fields["Thickness"] == "3.3"
|
||||
|
||||
|
||||
def test_label_split_over_two_chunks_is_read_once():
|
||||
label = capture()[0]
|
||||
chunks = [(1000, label[:200]), (1020, label[200:])]
|
||||
timed = profiles.timed_readings("label_print", None, chunks)
|
||||
assert len(timed) == 1 and timed[0].t_ms == 1020 and timed[0].reading.value == 102.3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("label,value,unit", [
|
||||
(b'^XA^FO50,50^A0N,40,40^FDNet: 12.34 kg^FS^FO50,120^FDLOT 7^FS^XZ', 12.34, "kg"),
|
||||
(b'N\r\nA50,0,0,4,1,1,N,"Weight"\r\nA50,40,0,4,1,1,N,"2.500"\r\nA200,40,0,4,1,1,N,"kg"\r\nP1\r\n',
|
||||
2.5, "kg"),
|
||||
(b'CLS\r\nTEXT 10,10,"3",0,1,1,"GROSS 15.6 kg"\r\nPRINT 1,1\r\n', 15.6, "kg"),
|
||||
])
|
||||
def test_other_label_languages(label, value, unit):
|
||||
r = PROTOCOLS["label_print"].readings(label)[-1]
|
||||
assert (r.value, r.unit) == (value, unit)
|
||||
|
||||
|
||||
def test_label_without_weight_is_ignored():
|
||||
assert PROTOCOLS["label_print"].readings(b'CLS\r\nTEXT 1,1,"3",0,1,1,"Hello"\r\nPRINT 1\r\n') == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spec,baud,parity,wrong", [
|
||||
("label_print", 9600, "N", 0), ("label_print:4800", 4800, "N", 2), ("label_print:9600:7E1", 9600, "E", 0),
|
||||
])
|
||||
def test_wait_for_print_detection(spec, baud, parity, wrong):
|
||||
bank = SimulatorBank(spec)
|
||||
events = []
|
||||
detector = Detector(bank.opener(), log=lambda code, **kw: events.append(code), timings=FAST,
|
||||
print_wait_s=5, listen_only=True)
|
||||
result = detector.run_port("COM90")
|
||||
assert result["found"] and result["protocol"] == "label_print" and result["mode"] == "print"
|
||||
assert result["serial"]["baudrate"] == baud and result["serial"]["parity"] == parity
|
||||
assert result["reading"]["fields"]["Order.Co"] == "222"
|
||||
assert events.count("print_wrong_speed") == wrong
|
||||
assert "ask" not in events # listen only: nothing was sent
|
||||
|
||||
|
||||
def test_listen_only_sends_nothing():
|
||||
written = []
|
||||
|
||||
class Quiet:
|
||||
in_waiting = 0
|
||||
|
||||
def __init__(self, s):
|
||||
pass
|
||||
|
||||
def read(self, n=1):
|
||||
time.sleep(0.01)
|
||||
return b""
|
||||
|
||||
def write(self, data):
|
||||
written.append(data)
|
||||
return len(data)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
result = Detector(lambda s: Quiet(s), timings=FAST, print_wait_s=0.3, listen_only=True).run_port("COM5")
|
||||
assert result["found"] is False and written == []
|
||||
|
||||
|
||||
def test_label_log_feeds_pieces_and_writes_files(tmp_path):
|
||||
log = LabelLog(str(tmp_path))
|
||||
profile = profiles.validate({"name": "label-1", "port": "COM8", "protocol": "label_print",
|
||||
"serial": {"baudrate": 9600}})
|
||||
a, b = capture()[3], capture()[4]
|
||||
assert log.feed(profile, a[:100]) == []
|
||||
got = log.feed(profile, a[100:] + b)
|
||||
assert [r["value"] for r in got] == [1.4, 1.4] and [r["seq"] for r in got] == [1, 2]
|
||||
assert [r["seq"] for r in log.list("label-1", after=1)] == [2]
|
||||
files = list(tmp_path.iterdir())
|
||||
assert len(files) == 1 and files[0].name.startswith("label-1-")
|
||||
lines = files[0].read_text(encoding="utf-8").splitlines()
|
||||
assert json.loads(lines[0])["fields"]["Order.Co"] == "222"
|
||||
|
||||
|
||||
def test_bridge_keeps_print_scale_open_and_logs_labels(tmp_path):
|
||||
from scale_bridge.config import Config
|
||||
from scale_bridge.server import Bridge
|
||||
config = Config(str(tmp_path / "bridge.json"))
|
||||
bridge = Bridge(config, simulate="label_print")
|
||||
try:
|
||||
config.save_profile({"name": "label-1", "port": "COM90", "protocol": "label_print",
|
||||
"serial": {"baudrate": 9600}})
|
||||
bridge.keeper.tick()
|
||||
deadline = time.time() + 12
|
||||
while time.time() < deadline and not bridge.labels.list("label-1"):
|
||||
time.sleep(0.3)
|
||||
listed = bridge.label_list({"profile": "label-1"})
|
||||
assert listed["labels"] and listed["listening"] == ["label-1"]
|
||||
assert listed["labels"][-1]["fields"]["SHIFT"] == "2"
|
||||
started = time.monotonic()
|
||||
answer = bridge.weight(config.profile("label-1"), max_age_ms=60000, wait_ms=2500)
|
||||
assert time.monotonic() - started < 1.5 # a print scale never holds the caller
|
||||
assert answer["ok"] and answer["on_print"] and answer["stable"] and answer["unit"] == "kg"
|
||||
assert (tmp_path / "labels").is_dir()
|
||||
finally:
|
||||
bridge.keeper.stop()
|
||||
bridge.registry.stop_all()
|
||||
@@ -0,0 +1,132 @@
|
||||
# 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]
|
||||
@@ -0,0 +1,193 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Every parser against sample frames in the shapes the manufacturers document."""
|
||||
import pytest
|
||||
|
||||
from scale_bridge.protocols import PROTOCOLS, convert, get_protocol, lines, norm_unit
|
||||
from scale_bridge.simulator import frame_for
|
||||
|
||||
|
||||
def one(protocol_id, data, params=None):
|
||||
readings = get_protocol(protocol_id, params).readings(data)
|
||||
assert readings, f"{protocol_id} read nothing from {data!r}"
|
||||
return readings[-1]
|
||||
|
||||
|
||||
def toledo_frame(swa, swb, swc, digits, tare=b"000000"):
|
||||
body = bytes([0x02, swa, swb, swc]) + digits + tare + b"\r"
|
||||
return body + bytes([(-sum(b & 0x7F for b in body)) & 0x7F])
|
||||
|
||||
|
||||
# -- Mettler Toledo ----------------------------------------------------------
|
||||
|
||||
def test_mt_sics_stable_and_dynamic():
|
||||
r = one("mt_sics", b"S S 0.550 kg\r\n")
|
||||
assert (r.value, r.unit, r.stable) == (0.55, "kg", True)
|
||||
r = one("mt_sics", b"S D 12.345 g\r\n")
|
||||
assert (r.value, r.unit, r.stable) == (12.345, "g", False)
|
||||
|
||||
|
||||
def test_mt_sics_overload_and_error():
|
||||
assert one("mt_sics", b"S +\r\n").overload
|
||||
assert PROTOCOLS["mt_sics"].readings(b"S I\r\nES\r\n") == []
|
||||
|
||||
|
||||
def test_toledo_continuous_decimal_point_units_and_checksum():
|
||||
# SWA: bit5 + count-by x1 (bit3) + dp code 3 (XXXXX.X); SWB: bit5 + kg (bit4)
|
||||
r = one("toledo_cont", toledo_frame(0x2B, 0x30, 0x20, b"012345"))
|
||||
assert (r.value, r.unit, r.stable, r.net, r.checksum_ok) == (1234.5, "kg", True, False, True)
|
||||
# motion + negative + net, grams in SWC
|
||||
r = one("toledo_cont", toledo_frame(0x2A, 0x20 | 0x08 | 0x02 | 0x01, 0x21, b"000550"))
|
||||
assert (r.value, r.unit, r.stable, r.net) == (-550, "g", False, True)
|
||||
# lb when SWB bit4 is clear, dp code 5 (X.XXX)
|
||||
r = one("toledo_cont", toledo_frame(0x2D, 0x20, 0x20, b"001210"))
|
||||
assert (r.value, r.unit) == (1.21, "lb")
|
||||
|
||||
|
||||
def test_toledo_continuous_bad_checksum_is_flagged():
|
||||
frame = bytearray(toledo_frame(0x2B, 0x30, 0x20, b"012345"))
|
||||
frame[-1] ^= 0x01
|
||||
assert one("toledo_cont", bytes(frame)).checksum_ok is False
|
||||
|
||||
|
||||
# -- A&D and friends -----------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("frame,value,unit,stable,net", [
|
||||
(b"ST,+00000.550 kg\r\n", 0.55, "kg", True, None),
|
||||
(b"US,-00001.20 kg\r\n", -1.2, "kg", False, None),
|
||||
(b"ST,GS,+0012.34kg\r\n", 12.34, "kg", True, False),
|
||||
(b"ST,NT,+0001.500 lb\r\n", 1.5, "lb", True, True),
|
||||
(b"QT,+000123.4 g\r\n", 123.4, "g", True, None),
|
||||
])
|
||||
def test_and_standard(frame, value, unit, stable, net):
|
||||
r = one("and_std", frame)
|
||||
assert (r.value, r.unit, r.stable, r.net) == (value, unit, stable, net)
|
||||
|
||||
|
||||
def test_and_standard_overload():
|
||||
r = one("and_std", b"OL,+9999999 kg\r\n")
|
||||
assert r.overload and r.value is None
|
||||
|
||||
|
||||
def test_sma_frames():
|
||||
r = one("sma", b"\n 1G 0.550kg \r") # s r n m f, then the weight in 10 characters
|
||||
assert (r.value, r.unit, r.stable, r.net) == (0.55, "kg", True, False)
|
||||
r = one("sma", b"\n 1NM 12.500lb \r")
|
||||
assert (r.value, r.unit, r.stable, r.net) == (12.5, "lb", False, True)
|
||||
assert one("sma", b"\nO1G ----------lb \r").overload
|
||||
|
||||
|
||||
def test_nci():
|
||||
r = one("nci", b"\n 1.23lb\r\n00\r\x03")
|
||||
assert (r.value, r.unit, r.stable) == (1.23, "lb", True)
|
||||
r = one("nci", b"\n 1.23lb\r\n10\r\x03")
|
||||
assert r.stable is False
|
||||
|
||||
|
||||
def test_ohaus():
|
||||
r = one("ohaus", b" 0.550 kg \r\n")
|
||||
assert (r.value, r.unit, r.stable) == (0.55, "kg", True)
|
||||
assert one("ohaus", b" 0.550 kg ? \r\n").stable is False
|
||||
assert one("ohaus", b" 1.234 kg N\r\n").net is True
|
||||
|
||||
|
||||
def test_sartorius_unit_missing_means_unstable():
|
||||
r = one("sartorius", b"+ 0.550 kg \r\n")
|
||||
assert (r.value, r.unit, r.stable) == (0.55, "kg", True)
|
||||
r = one("sartorius", b"+ 0.550 \r\n")
|
||||
assert (r.value, r.stable) == (0.55, False)
|
||||
r = one("sartorius", b"N + 1.234 kg\r\n")
|
||||
assert (r.value, r.net) == (1.234, True)
|
||||
|
||||
|
||||
def test_cas_enq():
|
||||
r = one("cas_enq", b"\x06\x01\x02S 0.550kg\x03\x04")
|
||||
assert (r.value, r.unit, r.stable) == (0.55, "kg", True)
|
||||
|
||||
|
||||
# -- XK3190 ---------------------------------------------------------------------
|
||||
|
||||
def test_xk3190_tf0_with_checksum():
|
||||
body = b"+0012342"
|
||||
x = 0
|
||||
for b in body:
|
||||
x ^= b
|
||||
frame = b"\x02" + body + f"{x >> 4:X}{x & 0xF:X}".encode() + b"\x03"
|
||||
r = one("xk3190_tf0", frame * 2)
|
||||
assert (r.value, r.checksum_ok) == (12.34, True)
|
||||
r = one("xk3190_tf0", b"\x02-0005003" + b"00" + b"\x03")
|
||||
assert r.value == -0.5 and r.checksum_ok is False
|
||||
|
||||
|
||||
def test_xk3190_reversed_stream():
|
||||
r = one("xk3190_reversed", b"05.21000=05.21000=05.21000=")
|
||||
assert r.value == 12.5
|
||||
assert PROTOCOLS["xk3190_reversed"].readings(b"05.21000=") == []
|
||||
|
||||
|
||||
# -- Mahak (field-tested) -----------------------------------------------------------
|
||||
|
||||
def test_mahak_field_capture():
|
||||
# Captured on a Mahak MDS 13000 with a 550 g weight: 58 ?? 07 07 00 02 26
|
||||
r = one("mahak_mds", bytes.fromhex("58 51 07 07 00 02 26"))
|
||||
assert (r.value, r.unit) == (550.0, "g")
|
||||
r = one("mahak_mds", bytes.fromhex("58 A3 07 07 00 02 EE"))
|
||||
assert r.value == 750.0
|
||||
|
||||
|
||||
def test_mahak_ignores_boot_message():
|
||||
assert PROTOCOLS["mahak_mds"].readings(bytes.fromhex("42 03 04 01 42 4F 4F 54 0C 04 8C")) == []
|
||||
|
||||
|
||||
# -- generic and custom ----------------------------------------------------------------
|
||||
|
||||
def test_generic_ascii():
|
||||
r = one("ascii_generic", b"G 12.50 kg\r\n")
|
||||
assert (r.value, r.unit) == (12.5, "kg")
|
||||
assert one("ascii_generic", b"US 3.2 kg\r\n").stable is False
|
||||
|
||||
|
||||
def test_custom_binary_and_ascii():
|
||||
params = {"pattern": "58 ?? 07 07", "value_offset": 4, "value_length": 3, "unit": "g"}
|
||||
r = one("custom_binary", bytes.fromhex("58 51 07 07 00 02 26"), params)
|
||||
assert (r.value, r.unit) == (550.0, "g")
|
||||
r = one("custom_ascii", b"W: 12.5\r\n", {"regex": r"W:\s*([-\d.]+)", "unit": "kg"})
|
||||
assert (r.value, r.unit) == (12.5, "kg")
|
||||
assert get_protocol("custom_ascii").readings(b"W: 1\r\n") == []
|
||||
|
||||
|
||||
# -- the simulator speaks what the parsers read --------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("pid", ["mt_sics", "and_std", "toledo_cont", "sma", "nci", "ohaus", "sartorius",
|
||||
"cas_enq", "xk3190_tf0", "xk3190_reversed", "mahak_mds"])
|
||||
@pytest.mark.parametrize("kg,stable", [(0.55, True), (13.1, False)])
|
||||
def test_simulator_round_trip(pid, kg, stable):
|
||||
frame = frame_for(pid, kg, stable)
|
||||
data = frame * 3 if pid == "xk3190_reversed" else frame
|
||||
r = PROTOCOLS[pid].readings(data)[-1]
|
||||
expected = kg * 1000 if pid == "mahak_mds" else kg
|
||||
assert r.value == pytest.approx(expected)
|
||||
if r.stable is not None and pid != "sartorius":
|
||||
assert r.stable == stable
|
||||
|
||||
|
||||
# -- helpers ----------------------------------------------------------------------------
|
||||
|
||||
def test_units():
|
||||
assert norm_unit(b" KG ") == "kg" and norm_unit("lbs") == "lb" and norm_unit(None) == ""
|
||||
assert convert(550, "g", "kg") == pytest.approx(0.55)
|
||||
assert convert(1, "lb", "g") == pytest.approx(453.59237)
|
||||
assert convert(1, "kg", "pcs") is None
|
||||
|
||||
|
||||
def test_lines_split_on_terminators():
|
||||
assert lines(b"\x02abc\r\ndef\x03\r\n\r\n") == [b"abc", b"def"]
|
||||
|
||||
|
||||
def test_catalogue_is_consistent():
|
||||
from scale_bridge.protocols import CONTINUOUS_IDS, POLL_IDS
|
||||
for pid in CONTINUOUS_IDS + POLL_IDS:
|
||||
assert pid in PROTOCOLS
|
||||
for pid in POLL_IDS:
|
||||
assert PROTOCOLS[pid].request, pid
|
||||
# read-only: never zero / tare / reset / print-off letters on their own
|
||||
assert PROTOCOLS[pid].request.strip(b"\r\n") not in (b"Z", b"T", b"R", b"P")
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""The HTTP interface, end to end, with simulated scales."""
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from scale_bridge.config import Config
|
||||
from scale_bridge.detect import Timings
|
||||
from scale_bridge.server import Bridge, make_server
|
||||
|
||||
LOCAL = {"X-Requested-With": "ScaleBridge"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def bridge_url(tmp_path_factory):
|
||||
config = Config(str(tmp_path_factory.mktemp("bridge") / "bridge.json"))
|
||||
bridge = Bridge(config, simulate="mahak_mds,toledo_cont:4800:7E1")
|
||||
bridge.detect_timings = Timings(passive_s=0.5, probe_s=0.15, confirm_s=0.4, drain_s=0.02)
|
||||
server = make_server(bridge, "127.0.0.1", 0)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
yield f"http://127.0.0.1:{server.server_address[1]}", config
|
||||
server.shutdown()
|
||||
bridge.registry.stop_all()
|
||||
|
||||
|
||||
def call(url, method="GET", body=None, headers=None):
|
||||
req = urllib.request.Request(url, method=method,
|
||||
data=json.dumps(body).encode() if body is not None else None,
|
||||
headers={"Content-Type": "application/json", **(headers or {})})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
return r.status, json.loads(r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read() or b"{}")
|
||||
|
||||
|
||||
def test_health_is_open_and_identifies_the_author(bridge_url):
|
||||
url, _ = bridge_url
|
||||
code, body = call(url + "/health")
|
||||
assert code == 200 and body["service"] == "syncpulse-scale-bridge"
|
||||
assert body["author"] == "Farshad Zamani" and "weight" in body["capabilities"]
|
||||
|
||||
|
||||
def test_key_is_required(bridge_url):
|
||||
url, config = bridge_url
|
||||
assert call(url + "/ports")[0] == 401
|
||||
assert call(url + "/ports", headers={"X-Bridge-Token": "wrong"})[0] == 401
|
||||
code, body = call(url + "/ports", headers={"X-Bridge-Token": config.token})
|
||||
assert code == 200 and any(p["device"] == "COM90" for p in body["ports"])
|
||||
|
||||
|
||||
def test_legacy_read_contract(bridge_url):
|
||||
url, config = bridge_url
|
||||
code, body = call(url + "/read", "POST", {
|
||||
"serial": {"port": "COM90", "baudrate": 9600, "bytesize": 8, "parity": "N", "stopbits": 1},
|
||||
"exchange": {"request_hex": "4D 09 04 07", "poll_interval_ms": 300}, "frames": 3},
|
||||
headers={"X-Bridge-Token": config.token})
|
||||
assert code == 200 and body["ok"] and body["open"]
|
||||
assert body["frames"] and body["frames"][-1]["hex"].startswith("58")
|
||||
assert {"t", "hex"} <= set(body["frames"][-1]) and "now" in body
|
||||
|
||||
|
||||
def test_weight_by_port_and_protocol(bridge_url):
|
||||
url, config = bridge_url
|
||||
code, body = call(url + "/weight?port=COM91&protocol=toledo_cont&baud=4800&framing=7E1",
|
||||
headers={"X-Bridge-Token": config.token})
|
||||
assert code == 200 and body["ok"] and body["unit"] == "kg" and body["checksum_ok"] is True
|
||||
assert isinstance(body["value"], float) and "measured_at" in body
|
||||
|
||||
|
||||
def test_weight_errors(bridge_url):
|
||||
url, config = bridge_url
|
||||
h = {"X-Bridge-Token": config.token}
|
||||
assert call(url + "/weight?profile=missing", headers=h)[0] == 404
|
||||
assert call(url + "/weight?port=COM90", headers=h)[0] == 400
|
||||
assert call(url + "/weight?port=COM90&protocol=mt_sics&framing=9X9", headers=h)[0] == 400
|
||||
|
||||
|
||||
def test_local_api_needs_header_and_local_host(bridge_url):
|
||||
url, _ = bridge_url
|
||||
assert call(url + "/api/state")[0] == 403
|
||||
assert call(url + "/api/state", headers={**LOCAL, "Host": "evil.example"})[0] == 403
|
||||
code, state = call(url + "/api/state", headers=LOCAL)
|
||||
assert code == 200 and state["about"]["author"] == "Farshad Zamani" and state["token"]
|
||||
|
||||
|
||||
def test_page_is_served_with_signature(bridge_url):
|
||||
url, _ = bridge_url
|
||||
with urllib.request.urlopen(url + "/", timeout=5) as r:
|
||||
html = r.read().decode("utf-8")
|
||||
assert "Content-Security-Policy" in r.headers
|
||||
assert "Farshad Zamani" in html and "linkedin.com/in/farshad-zamani92" in html
|
||||
assert "cloudtart.com" in html and "SyncPulse" in html
|
||||
|
||||
|
||||
def test_detect_save_read_delete(bridge_url):
|
||||
url, config = bridge_url
|
||||
code, body = call(url + "/api/detect", "POST", {"ports": ["COM90"]}, headers=LOCAL)
|
||||
assert code == 200 and body["job"]["running"]
|
||||
# a second search while the first runs is refused, and the port is reserved
|
||||
assert call(url + "/api/detect", "POST", {"ports": ["COM90"]}, headers=LOCAL)[0] == 409
|
||||
busy = call(url + "/weight?port=COM90&protocol=mahak_mds", headers={"X-Bridge-Token": config.token})
|
||||
assert busy[0] == 409
|
||||
deadline = time.time() + 60
|
||||
job = None
|
||||
while time.time() < deadline:
|
||||
job = call(url + "/api/detect", headers=LOCAL)[1]["job"]
|
||||
if not job["running"]:
|
||||
break
|
||||
time.sleep(0.3)
|
||||
assert job and job["results"][0]["found"] and job["results"][0]["protocol"] == "mahak_mds"
|
||||
assert job["progress"] == 1.0
|
||||
|
||||
code, saved = call(url + "/api/profiles", "POST",
|
||||
{"detection": job["results"][0], "name": "packing-1", "label": "Packing"},
|
||||
headers=LOCAL)
|
||||
assert code == 200 and saved["profile"]["name"] == "packing-1"
|
||||
|
||||
code, listed = call(url + "/profiles", headers={"X-Bridge-Token": config.token})
|
||||
assert [p["name"] for p in listed["profiles"]] == ["packing-1"]
|
||||
|
||||
deadline = time.time() + 10
|
||||
body = {}
|
||||
while time.time() < deadline:
|
||||
body = call(url + "/weight?profile=packing-1", headers={"X-Bridge-Token": config.token})[1]
|
||||
if body.get("stable"):
|
||||
break
|
||||
time.sleep(0.4)
|
||||
assert body["ok"] and body["unit"] == "g" and body["stable"] is True
|
||||
|
||||
code, _ = call(url + "/api/profiles/packing-1", "DELETE", headers=LOCAL)
|
||||
assert code == 200 and config.profiles() == []
|
||||
|
||||
|
||||
def test_allowed_clients_do_not_lock_out_this_pc(bridge_url):
|
||||
url, config = bridge_url
|
||||
code, _ = call(url + "/api/allowed", "POST", {"clients": ["10.1.2.3"]}, headers=LOCAL)
|
||||
assert code == 200
|
||||
try:
|
||||
assert call(url + "/ports", headers={"X-Bridge-Token": config.token})[0] == 200
|
||||
finally:
|
||||
call(url + "/api/allowed", "POST", {"clients": []}, headers=LOCAL)
|
||||
|
||||
|
||||
def test_bad_json_is_a_400(bridge_url):
|
||||
url, config = bridge_url
|
||||
req = urllib.request.Request(url + "/read", method="POST", data=b"{nope",
|
||||
headers={"X-Bridge-Token": config.token})
|
||||
with pytest.raises(urllib.error.HTTPError) as e:
|
||||
urllib.request.urlopen(req, timeout=5)
|
||||
assert e.value.code == 400
|
||||
Reference in New Issue
Block a user