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:
+77
@@ -0,0 +1,77 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (C) 2026 Farshad Zamani - Cloudtart (https://cloudtart.com)
|
||||
"""
|
||||
Build dist/SyncPulseScaleBridge.exe — one file, nothing to install.
|
||||
|
||||
python scripts/build.py (needs: pip install -r requirements-dev.txt)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
from scale_bridge import ABOUT, COMPANY, EXE_NAME, PRODUCT, __version__ # noqa: E402
|
||||
|
||||
|
||||
def version_file(path: str) -> None:
|
||||
parts = [int(p) for p in __version__.split(".")[:3]] + [0]
|
||||
numbers = ", ".join(str(n) for n in (parts + [0, 0, 0, 0])[:4])
|
||||
strings = {
|
||||
"CompanyName": COMPANY,
|
||||
"FileDescription": f"{PRODUCT} - RS-232 scale reader and network bridge",
|
||||
"FileVersion": __version__,
|
||||
"InternalName": EXE_NAME,
|
||||
"LegalCopyright": f"{ABOUT['copyright']}. Licensed under {ABOUT['license']}.",
|
||||
"OriginalFilename": f"{EXE_NAME}.exe",
|
||||
"ProductName": PRODUCT,
|
||||
"ProductVersion": __version__,
|
||||
"Comments": f"Author: {ABOUT['author']} - {ABOUT['website']} - {ABOUT['repository']}",
|
||||
}
|
||||
entries = ",\n".join(f" StringStruct({k!r}, {v!r})" for k, v in strings.items())
|
||||
text = f"""# UTF-8
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(filevers=({numbers}), prodvers=({numbers}), mask=0x3f, flags=0x0,
|
||||
OS=0x40004, fileType=0x1, subtype=0x0, date=(0, 0)),
|
||||
kids=[
|
||||
StringFileInfo([
|
||||
StringTable('040904B0', [
|
||||
{entries}
|
||||
])
|
||||
]),
|
||||
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
||||
]
|
||||
)
|
||||
"""
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
build = os.path.join(ROOT, "build")
|
||||
os.makedirs(build, exist_ok=True)
|
||||
icon = os.path.join(ROOT, "assets", "icon.ico")
|
||||
if not os.path.exists(icon):
|
||||
subprocess.check_call([sys.executable, os.path.join(ROOT, "scripts", "make_icon.py")])
|
||||
vfile = os.path.join(build, "version_info.txt")
|
||||
version_file(vfile)
|
||||
cmd = [sys.executable, "-m", "PyInstaller", "--noconfirm", "--clean", "--onefile", "--console",
|
||||
"--name", EXE_NAME, "--icon", icon, "--version-file", vfile,
|
||||
"--distpath", os.path.join(ROOT, "dist"), "--workpath", os.path.join(build, "pyinstaller"),
|
||||
"--specpath", build, os.path.join(ROOT, "run_bridge.py")]
|
||||
subprocess.check_call(cmd, cwd=ROOT)
|
||||
exe = os.path.join(ROOT, "dist", EXE_NAME + (".exe" if os.name == "nt" else ""))
|
||||
with open(exe, "rb") as f:
|
||||
digest = hashlib.sha256(f.read()).hexdigest()
|
||||
with open(exe + ".sha256", "w", encoding="ascii", newline="\n") as f:
|
||||
f.write(f"{digest} {os.path.basename(exe)}\n")
|
||||
print(f"\nbuilt {exe}\nSHA-256 {digest}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
rem SyncPulse Scale Bridge - build dist\SyncPulseScaleBridge.exe
|
||||
rem Copyright (C) 2026 Farshad Zamani - Cloudtart (https://cloudtart.com) - GPL-3.0-or-later
|
||||
setlocal
|
||||
cd /d "%~dp0\.."
|
||||
|
||||
if "%PYTHON%"=="" (
|
||||
where py >nul 2>&1 && (set "PYTHON=py -3") || (set "PYTHON=python")
|
||||
)
|
||||
|
||||
if not exist .venv\Scripts\python.exe (
|
||||
echo Creating .venv ...
|
||||
%PYTHON% -m venv .venv || goto :error
|
||||
)
|
||||
.venv\Scripts\python.exe -m pip install --quiet --upgrade pip || goto :error
|
||||
.venv\Scripts\python.exe -m pip install --quiet -r requirements-dev.txt || goto :error
|
||||
.venv\Scripts\python.exe -m pytest -q || goto :error
|
||||
.venv\Scripts\python.exe scripts\build.py || goto :error
|
||||
echo.
|
||||
echo Done: dist\SyncPulseScaleBridge.exe
|
||||
exit /b 0
|
||||
|
||||
:error
|
||||
echo.
|
||||
echo Build failed.
|
||||
exit /b 1
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (C) 2026 Farshad Zamani - Cloudtart (https://cloudtart.com)
|
||||
"""Draw the program icon (a platform scale with a display) into assets/icon.ico — standard library only."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
TEAL = (15, 118, 110)
|
||||
DARK = (8, 64, 60)
|
||||
WHITE = (255, 255, 255)
|
||||
|
||||
|
||||
def _in_round_rect(x, y, x0, y0, x1, y1, r):
|
||||
if not (x0 <= x <= x1 and y0 <= y <= y1):
|
||||
return False
|
||||
cx = min(max(x, x0 + r), x1 - r)
|
||||
cy = min(max(y, y0 + r), y1 - r)
|
||||
return (x - cx) ** 2 + (y - cy) ** 2 <= r * r
|
||||
|
||||
|
||||
def _in_trapezoid(x, y, top, bottom, top_x0, top_x1, bottom_x0, bottom_x1):
|
||||
if not (top <= y <= bottom):
|
||||
return False
|
||||
f = (y - top) / (bottom - top)
|
||||
return top_x0 + (bottom_x0 - top_x0) * f <= x <= top_x1 + (bottom_x1 - top_x1) * f
|
||||
|
||||
|
||||
def colour_at(x, y):
|
||||
"""Colour of the 64x64 design at (x, y), or None outside the icon."""
|
||||
if not _in_round_rect(x, y, 0, 0, 64, 64, 14):
|
||||
return None
|
||||
if _in_round_rect(x, y, 23, 34, 41, 42, 2): # display window
|
||||
if _in_round_rect(x, y, 26, 37, 38, 39, 1): # the reading
|
||||
return WHITE
|
||||
return DARK
|
||||
if _in_round_rect(x, y, 11, 13, 53, 19, 2.5): # weighing plate
|
||||
return WHITE
|
||||
if 29 <= x <= 35 and 19 <= y <= 28: # column
|
||||
return WHITE
|
||||
if _in_trapezoid(x, y, 27, 51, 18, 46, 13, 51): # body
|
||||
return WHITE
|
||||
return TEAL
|
||||
|
||||
|
||||
def render(size: int, samples: int = 4) -> list[list[tuple[int, int, int, int]]]:
|
||||
rows = []
|
||||
step = 64 / size
|
||||
for py in range(size):
|
||||
row = []
|
||||
for px in range(size):
|
||||
acc = [0, 0, 0]
|
||||
covered = 0
|
||||
for sy in range(samples):
|
||||
for sx in range(samples):
|
||||
c = colour_at((px + (sx + 0.5) / samples) * step, (py + (sy + 0.5) / samples) * step)
|
||||
if c is not None:
|
||||
covered += 1
|
||||
acc[0] += c[0]
|
||||
acc[1] += c[1]
|
||||
acc[2] += c[2]
|
||||
if covered:
|
||||
row.append((acc[0] // covered, acc[1] // covered, acc[2] // covered,
|
||||
round(255 * covered / samples ** 2)))
|
||||
else:
|
||||
row.append((0, 0, 0, 0))
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def png(rows) -> bytes:
|
||||
h, w = len(rows), len(rows[0])
|
||||
raw = b"".join(b"\x00" + bytes(v for px in row for v in px) for row in rows)
|
||||
|
||||
def chunk(tag, data):
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def dib(rows) -> bytes:
|
||||
h, w = len(rows), len(rows[0])
|
||||
header = struct.pack("<IiiHHIIiiII", 40, w, h * 2, 1, 32, 0, 0, 0, 0, 0, 0)
|
||||
pixels = b"".join(bytes((b, g, r, a)) for row in reversed(rows) for (r, g, b, a) in row)
|
||||
mask_row = ((w + 31) // 32) * 4
|
||||
return header + pixels + b"\x00" * mask_row * h
|
||||
|
||||
|
||||
def ico(sizes=(16, 24, 32, 48, 64, 256)) -> bytes:
|
||||
images = [(s, png(render(s)) if s >= 256 else dib(render(s))) for s in sizes]
|
||||
out = struct.pack("<HHH", 0, 1, len(images))
|
||||
offset = 6 + 16 * len(images)
|
||||
for s, data in images:
|
||||
out += struct.pack("<BBBBHHII", s % 256, s % 256, 0, 0, 1, 32, len(data), offset)
|
||||
offset += len(data)
|
||||
return out + b"".join(d for _, d in images)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
target = os.path.join(root, "assets", "icon.ico")
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
with open(target, "wb") as f:
|
||||
f.write(ico())
|
||||
with open(os.path.join(root, "assets", "icon.png"), "wb") as f:
|
||||
f.write(png(render(256)))
|
||||
print(target)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user