# Conflicts:
#	config/machine_settings/defaults.ini
#	init.sh
#	simulate.sh
#	src/components/__init__.py
#	src/components/component.py
#	src/lib/db/__init__.py
#	src/lib/db/models/recipes.py
#	src/main.py
#	src/requirements.txt
#	src/ui/__init__.py
#	src/ui/crud/crud.py
#	src/ui/dialog/dialog.py
#	src/ui/login/login.py
#	src/ui/recipe_selection/recipe_selection.py
This commit is contained in:
ST-TEN-1 2022-06-20 16:08:02 +02:00
commit f99891cf25
27 changed files with 1261 additions and 183 deletions

View File

@ -0,0 +1,14 @@
CT~~CD,~CC^~CT~
^XA~TA000~JSN^LT0^MNW^MTT^PON^PMN^LH0,0^JMA^PR4,4~SD15^JUS^LRN^CI0^XZ
^XA
^MMT
^PW378
^LL0213
^LS0
^FT213,199^BQN,2,6
^FH\^FDLA,5803001456^FS
^FT28,58^A0N,33,33^FH\^FD5803001456^FS
^FT28,104^A0N,33,33^FH\^FDN: $NUM^FS
^FT28,148^A0N,33,33^FH\^FD$DATE^FS
^FT28,185^A0N,33,33^FH\^FD$TIME^FS
^PQ1,0,1,Y^XZ

View File

@ -3,7 +3,7 @@ parameter: default
[vision_saver]
time_format: %Y-%m-%d_%H-%M-%S
location: ./data/images
path: ./data/images
minimum_disk_free_space_gb: 20
[archive_synchronizer]
@ -13,3 +13,12 @@ poll_time: 60
hold_time: 1
service_account_json: data/secrets/MACHINE-bucket-sevice-account_errecinque-prodserver-HEXHEXHEXHEX.json
bucket_id: MACHINE
[label_printer]
platform: cups
printer:
[tecna_marposs_provaset_t3]
address: COM3
baudrate: 115200

26
init.sh
View File

@ -2,17 +2,6 @@
set -x
here="$(realpath "$(dirname "$0")")"
cd "$here"
python="python3.10"
echo "---------- Create support directories ----------"
mkdir -p "tmp"
mkdir -p "test_images"
mkdir -p "neural_networks"
mkdir -p "data/database"
echo "---------- install APT packages ----------"
sudo apt-get install -y "${python}-venv" "${python}-dev" "python3-pip" "python3-setuptools" "p7zip" "p7zip-full" "p7zip-rar"
sudo apt-get install -y "libxcb-xinerama0"
echo "---------- initialize venv ----------"
lsof "./venv/bin/python" | awk 'NR > 1 {print $2}' | xargs kill || :
@ -23,19 +12,4 @@ source "./venv/bin/activate" || source "./venv/Scripts/activate" || :
python -m pip install --upgrade pip
python -m pip install --upgrade -r "src/requirements.txt"
cd "$here/tmp"
wget --continue --timestamping "http://downloads.get-cameras.com/Galaxy_Linux-x86_Gige-U3_32bits-64bits_1.2.2107.9261.tar.gz"
7z x -y "Galaxy_Linux-x86_Gige-U3_32bits-64bits_1.2.2107.9261.tar.gz"
7z x -y "Galaxy_Linux-x86_Gige-U3_32bits-64bits_1.2.2107.9261.tar"
cd "Galaxy_Linux-x86_Gige-U3_32bits-64bits_1.2.2107.9261"
chmod +x "Galaxy_camera.run"
echo -en "\nY\nY\nEn\nY\n" | ./Galaxy_camera.run
cd "$here/tmp"
wget --continue --timestamping "http://downloads.get-cameras.com/Galaxy_Linux_Python_2.0.2106.9041.tar.gz"
7z x -y "Galaxy_Linux_Python_2.0.2106.9041.tar.gz"
7z x -y "Galaxy_Linux_Python_2.0.2106.9041.tar"
cd "Galaxy_Linux_Python_2.0.2106.9041/api"
"${python}" setup.py build
"${python}" setup.py install
cd "$here"

View File

@ -11,15 +11,20 @@ export QT_FATAL_WARNINGS=0
export QT_NO_DEBUG_OUTPUT=1
export QT_NO_INFO_OUTPUT=1
export QT_NO_WARNING_OUTPUT=0
# export QT_QPA_PLATFORM=${XDG_SESSION_TYPE}
python -B -u "./src/main.py" \
--auto-login \
--auto-login-admin \
--auto-select \
--sim-modbus \
--sim-os-label-printer \
--style windows \
$* 2> >(sed $'s/.*/\e[31m&\e[m/' >&2) # &
# --about \
# --archive \
# --auto-login-user \
# --autotests-archive \
# --sim-archiver \
# --sim-serial-label-printer \
# --users-management \
# sudo renice -n -10 $!
# fg

View File

@ -1,4 +1,8 @@
from .archive_synchronizer import ArchiveSynchronizer
from .modbus_component import ModbusComponent
from .os_label_printer import Os_Label_Printer
from .remote_api import RemoteAPI
from .serial_label_printer import Serial_Label_Printer
from .tecna_marposs_provaset_t3 import TecnaMarpossProvasetT3
from .test_component import TestComponent
from .vision_saver import VisionSaver

View File

@ -35,7 +35,7 @@ class Component(QObject):
self._lock.acquire(max(self._lock.available(), 1))
self._timer = None
self.log = logging.getLogger(f"{self.__class__.__name__} ({self.name})")
if not threaded:
if not self._threaded:
self.start()
def _config_changed(self):
@ -51,8 +51,9 @@ class Component(QObject):
self._resume.connect(self._do_resume)
self._set_sources.connect(self._do_set_sources)
self._set_period.connect(self._do_set_period)
self._init_periodic()
self.config.updated.connect(self._config_changed)
self._config_changed()
self._init_periodic()
self._started = True
if not self._paused:
self._do_resume()

View File

@ -0,0 +1 @@
from .modbus_client import *

View File

@ -0,0 +1,29 @@
class ModbusClient:
def __init__(self, *args, **kwargs):
print(__name__, "initialized with", args, kwargs, flush=True)
def connect(self):
self._is_socket_open = True
return self._is_socket_open
def read_holding_registers(self, r, count=1, unit=None):
# print(__name__, unit, str(r) + "-" + str(r + count - 1), "read", flush=True)
return Readout([0] * count)
def write_register(self, r, d, unit=None):
print(__name__, unit, d, r, "write", flush=True)
return Readout()
def close(self):
self._is_socket_open = False
def is_socket_open(self):
return self._is_socket_open
class Readout:
def __init__(self, data=None):
self.registers = data
def isError(self):
return False

View File

@ -0,0 +1,5 @@
import serial
from .serial import Serial
serial.Serial = Serial

View File

@ -0,0 +1,18 @@
import logging
class Serial:
def __init__(self, *args, **kwargs):
self.log = logging.getLogger(f"dummy {self.__class__.__name__} ({id(self)})")
self.log.debug(f"initialized with *{args}, **{kwargs}")
def write(self, d):
self.log.debug(f"write: {d!r}")
def read(self, n):
d = b"\x00" * n
self.log.debug(f"read: {d!r}")
return d
def close(self):
self.log.debug("close")

View File

@ -0,0 +1,115 @@
import sys
import traceback
import serial
from pymodbus.constants import Endian
# from pymodbus.exceptions import ModbusIOException
from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder
from PyQt5.QtCore import QMutex
if "--sim-modbus" not in sys.argv:
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
else:
from components.dummies.pymodbus import ModbusClient
from .component import Component
# from pymodbus.client.sync import ModbusSerialClient as ModbusClient
# import serial
# client = ModbusClient(method="rtu", port="COM3", stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, baudrate=115200, timeout=1, strict=False)
# client.connect()
# client.read_holding_registers(1, count=1)
class ModbusComponent(Component):
def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, threaded=True, registers=None):
super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded)
self.registers = registers if registers is not None else {}
self.lock = QMutex()
def config_changed(self):
self.address = self.config[self.name]["address"]
self.baudrate = int(self.config[self.name]["baudrate"])
self.stopbits = getattr(serial, self.config[self.name].get("stopbits", "stopbits_one").upper())
self.parity = getattr(serial, self.config[self.name].get("parity", "parity_none").upper())
self.bytesize = getattr(serial, self.config[self.name].get("bytesize", "eightbits").upper())
self.byteorder = getattr(Endian, self.config[self.name].get("byteorder", "Big").title())
self.wordorder = getattr(Endian, self.config[self.name].get("wordorder", "Big").title())
self.timeout = int(self.config[self.name].get("timeout", 1))
self.lock.lock()
self.client = ModbusClient(
method="rtu",
port=self.address,
stopbits=self.stopbits,
bytesize=self.bytesize,
parity=self.parity,
baudrate=self.baudrate,
timeout=self.timeout,
strict=False
)
if not self.client.connect():
raise ConnectionError("device not reachable (could not connect): {} ({})".format(self.name, self.port))
if not self.client.is_socket_open():
raise ConnectionError("device not reachable (socket not open): {} ({})".format(self.name, self.port))
self.lock.unlock()
def _read(self, register, count=1):
self.lock.lock()
read = self.client.read_holding_registers(register, count=count)
self.lock.unlock()
if read.isError():
self.log.exception(traceback.format_exception(read))
return None
return read
def _write(self, register, value):
self.lock.lock()
wrote = self.client.write_register(register, value)
self.lock.unlock()
if wrote.isError():
self.log.exception(traceback.format_exception(wrote))
return False
return True
def _decode(self, read, data_type, *args, **kwargs):
decoder = BinaryPayloadDecoder.fromRegisters(read.registers, byteorder=self.byteorder, wordorder=self.wordorder)
return getattr(decoder, f"decode_{data_type}")(*args, **kwargs)
def _encode(self, data, data_type, *args, **kwargs):
builder = BinaryPayloadBuilder(byteorder=self.byteorder, wordorder=self.wordorder)
getattr(builder, f"add_{data_type}")(data, *args, **kwargs)
return builder.build()
def read(self, register, *args, **kwargs):
if type(register) is str:
register, s = self.registers[register]
if not len(args):
args = [s["dt"], *s.get("a", [])]
if not len(kwargs):
kwargs = s.get("k", {})
if args[0].startswith("16bit_"):
count = 1
elif args[0].startswith("32bit_"):
count = 2
else:
raise NotImplementedError(f"data_type {args[0]!r} is not supported")
return self._decode(self._read(register, count=count), *args, **kwargs)
def write(self, register, data, *args, **kwargs):
if type(register) is str:
register, s = self.registers[register]
if not len(args):
args = [s["dt"], *s.get("a", [])]
if not len(kwargs):
kwargs = s.get("k", {})
return self._write(register, self._encode(data, *args, **kwargs))
# def _get(self, data):
# # print("MODBUS", str(int(QThread.currentThreadId())), flush=True)
# super()._get(data)
def __del__(self, event=None):
self.lock.lock()
if self.client.is_socket_open():
self.client.close()
self.lock.unlock()

View File

@ -0,0 +1,49 @@
import subprocess
import sys
from PyQt5.QtWidgets import QMessageBox
from .component import Component
class Os_Label_Printer(Component):
def __init__(self, config=None, name=None):
super().__init__(config=config, name=name, threaded=False)
if "--sim-os-label-printer" in sys.argv:
self.simulate = True
def config_changed(self):
self.platform = self.config["label_printer"]["platform"]
# for windows:
# cmd
# wmic printer list brief
# powershell
# Get-Printer
# for cups (linux, osx)
# lpstat -p -d
self.printer = self.config["label_printer"]["printer"]
def print_label(self, template, archived):
# LOAD LABEL TEMPLATE
with open(f"config/label_templates/{template}.prn") as f:
label = f.read()
# LABEL PRINT
label = label.replace("$PH1", archived.barcode).replace("$PH2", archived.barcode)
label_file = "a"
if self.platform == "windows":
cmd = f'print /d:"{self.printer}" "{label_file}"'
elif self.platform == "cups":
cmd = f'lp -d "{self.printer}" "{label_file}"'
else:
raise NotImplementedError(f"platform {self.platform!r} is not supported")
if not self.simulate:
p = subprocess.run(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True) # unsafe
if p.returncode != 0:
self.log.exception(f"failed to print: returncode: {p.returncode}\noutput:\n{p.stdout}")
QMessageBox.critical(
None,
"Errore Stampante",
f"Non e stato possibile stampare l'etichetta.\n\nErrore:\nreturncode: {p.returncode}\noutput:\n{p.stdout}"
)
return False
return True

View File

@ -0,0 +1,45 @@
import sys
if "--sim-serial-label-printer" in sys.argv:
import components.dummies.serial as serial
else:
import serial
from PyQt5.QtWidgets import QMessageBox
from .component import Component
class Serial_Label_Printer(Component):
def __init__(self, config=None, name=None):
super().__init__(config=config, name=name, threaded=False)
def config_changed(self):
self.address = self.config["label_printer"]["address"]
self.baudrate = int(self.config["label_printer"]["baudrate"])
self.stopbits = getattr(serial, self.config["label_printer"].get("stopbits", "stopbits_one").upper())
self.parity = getattr(serial, self.config["label_printer"].get("parity", "parity_none").upper())
self.bytesize = getattr(serial, self.config["label_printer"].get("bytesize", "eightbits").upper())
def print_label(self, template, archived):
# LOAD LABEL TEMPLATE
with open(f"config/label_templates/{template}.prn") as f:
label = f.read()
# LABEL PRINT
label = label.replace("$PH1", archived.barcode).replace("$PH2", archived.barcode)
try:
conn = serial.Serial(
self.address,
baudrate=self.baudrate,
stopbits=self.stopbits,
parity=self.parity,
bytesize=self.bytesize,
)
conn.write(bytes(label, encoding="ascii"))
conn.close()
except serial.serialutil.SerialException as e:
QMessageBox.critical(
None,
"Errore Connessione Stampante",
"Non e stato possibile connettersi alla stampante di etichette\nL'etichetta non verra stampata.\n\nErrore:\n" + str(e)
)

View File

@ -0,0 +1,108 @@
from .modbus_component import ModbusComponent
from .tecna_marposs_provaset_t3_registers import registers
# from pymodbus.client.sync import ModbusSerialClient as ModbusClient
# import serial
# client = ModbusClient(method="rtu", port="COM3", stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, baudrate=115200, timeout=1, strict=False)
# client.connect()
# client.read_holding_registers(1, count=1)
class TecnaMarpossProvasetT3(ModbusComponent):
def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, threaded=True):
super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded, registers=registers)
def config_changed(self):
super().config_changed()
# self.set_measure_units()
self.units = self.get_measure_units()
_pressure_units = {"mH2O": 0, "mbar": 1, "kPa": 2, "mmHg": 3, "inH2O": 4, "psi": 5, "mmH2O": 6, } # (se fondoscala <=6 bar)
_leak_units = {"mmH2O": 0, "mbar": 1, "Pa": 2, "mmHg": 3, "inH2O": 4, "psi": 5, }
_leak_flow_units = {"cm3/min": 0, "cm3/h": 1, }
_volume_units = {"litri": 0, "cm3": 1, }
_time_units = {"seconds": 0, }
_flow_units = {"liters/min": 0, "liters/h": 1, "m3/h": 2, }
_pressure_units_map = {v: k for k, v in _pressure_units.items()}
_leak_units_map = {v: k for k, v in _leak_units.items()}
_leak_flow_units_map = {v: k for k, v in _leak_flow_units.items()}
_volume_units_map = {v: k for k, v in _volume_units.items()}
_time_units_map = {v: k for k, v in _time_units.items()}
_flow_units_map = {v: k for k, v in _flow_units.items()}
def set_measure_units(self):
return [
self.write("MEASURE UNITS: pressure measure units", self._pressure_units["mbar"]),
self.write("MEASURE UNITS: Leak measure units", self._leak_units["mbar"]),
self.write("MEASURE UNITS: leak flow rate measure units", self._leak_flow_units["cm3/min"]),
self.write("MEASURE UNITS: Volume", self._volume_units["litri"]),
self.write("MEASURE UNITS: Flow rate measure units", self._flow_units["liters/min"]),
]
def get_measure_units(self):
units = {}
for [register, unit_map, unit_names] in [
["Relative pressure variable format - high resolution", self._pressure_units_map, ["pressure_hr", "red", "r", 21, ]], # also by documentation color and register number
["Relative pressure variable format - low resolution", self._pressure_units_map, ["pressure_lr", "purple", "p", 22, ]], # also by documentation color and register number
["Format of the variables related to the measurement of the differential leak pressure", self._leak_units_map, ["leak", "yellow", "y", 23, ]], # also by documentation color and register number
["Format of the variables related to the calculated leak flow", self._leak_flow_units_map, ["leak_flow", "blue", "b", 24, ]], # also by documentation color and register number
["Format of volume variables", self._volume_units_map, ["volume", "green", "g", 25, ]], # also by documentation color and register number
["Format of time variables", self._time_units_map, ["time", "tangerine", "t", 26, ]], # also by documentation color and register number
["Format of variables related to flow measurements", self._flow_units_map, ["flow", "orange", "o", 27, ]], # also by documentation color and register number
]:
v = self.read(register)
unit = [10**(-((v >> 8) & 0xff)), unit_map[v & 0xff]]
for unit_name in unit_names:
units[unit_name] = unit
return units
def _convert_from_format(self, data, format=None):
if format is None:
return data
# units = self.units[format]
# return [data * units[0], units[1]]
return data * self.units[format][0]
def _convert_to_format(self, data, format=None):
if format is None:
return data
return int(data / self.units[format][0])
def read(self, register, *args, format=None, **kwargs):
if type(register) is str:
_, s = self.registers[register]
if format is None:
format = s.get("f")
return self._convert_from_format(super().read(register, *args, **kwargs), format=format)
def write(self, register, data, *args, format=None, **kwargs):
if type(register) is str:
_, s = self.registers[register]
if format is None:
format = s.get("f")
return super().write(register, self._convert_to_format(data, format=format), *args, **kwargs)
def _get(self):
# print("TECNA", str(int(QThread.currentThreadId())), flush=True)
# READ INFO
if self.units is None:
self.set_measure_units()
self.units = self.get_measure_units()
info = {r: self.read(r) for r in [
"Instrument status: active phase",
"Test circuit pressure, in real time",
"Measured leak, in real time",
"Regulated pressure, in real time",
"Active alarm flags",
"Running test: type of test",
"Testing in progress: progressive sequence index",
]}
super()._get([info])
def get_test_results(self):
return {r: self.read(r) for r in [
"Running test: measured leak",
"Running test: calculated leak flow rate",
"Running test: calculate RVP%",
"Running test: result",
]}

View File

@ -0,0 +1,169 @@
registers = {
"Instrument type": [1, {"dt": "16bit_uint", }],
"Test counter: FAILED": [2, {"dt": "32bit_uint", }],
"Test counter: TOTAL TESTS": [4, {"dt": "32bit_uint", }],
"Life counter: FAILED": [6, {"dt": "32bit_uint", }],
"Life counter: TOTAL TESTS": [8, {"dt": "32bit_uint", }],
"Test circuit pressure, in real time": [11, {"dt": "32bit_int", "f": 21, }],
"Measured leak, in real time": [13, {"dt": "32bit_int", "f": 22, }],
"Regulated pressure, in real time": [15, {"dt": "16bit_int", "f": 23, }],
"Active alarm flags": [16, {"dt": "16bit_uint", }],
"Relative pressure variable format - high resolution": [21, {"dt": "16bit_uint", }],
"Format of the variables related to the measurement of the differential leak pressure": [22, {"dt": "16bit_uint", }],
"Relative pressure variable format - low resolution": [23, {"dt": "16bit_uint", }],
"Format of the variables related to the calculated leak flow": [24, {"dt": "16bit_uint", }],
"Format of volume variables": [25, {"dt": "16bit_uint", }],
"Format of time variables": [26, {"dt": "16bit_uint", }],
"Format of variables related to flow measurements": [27, {"dt": "16bit_uint", }],
"Instrument status: table of parameters in use": [31, {"dt": "16bit_uint", }],
"Instrument status: active phase": [32, {"dt": "16bit_uint", }],
# 0 = NO TEST EXECUTED: WAITING START NEW TEST
# 10 = TEST IN PROGRESS: CHECK BARCODE CODE (if enabled)
# 20 = TEST IN PROGRESS: WAITING READING BARCODE CODE (if enabled)
# 30 = TEST IN PROGRESS: TEST INITIALIZATION
# 40 = TEST IN PROGRESS: AUTOMATIC CLOSING 1 (CAGE)
# 50 = TEST IN PROGRESS: AUTOMATIC CLOSING 2 (PLUG)
# 60 = TEST IN PROGRESS: ACTIVE TEST PROGRAM INITIALIZATION
# 70 = TEST IN PROGRESS: WAITING ACKNOWLEDGE WITH BI-START COMMAND (if used)
# 80 = TEST IN PROGRESS: WAITING DELAY (PSDEL parameter)
# 90 = TEST IN PROGRESS: WAITING CONSENT SIGNAL (PSIN parameter)
# 100 = TEST IN PROGRESS: PHASE T0 - PRE-FILLING
# 110 = TEST IN PROGRESS: PHASE T1 - FILLING
# 120 = TEST IN PROGRESS: PHASE T2 - ASSESTMENT
# 130 = TEST IN PROGRESS: PHASE T3- SIZE
# 140 = TEST IN PROGRESS: WAITING CONFIRMATION EXAMINED BY OPERATOR (if used)
# 150 = TEST IN PROGRESS: TEST RESULT PRESENT
# 160 = TEST IN PROGRESS: MARKING (only if last table of a sequence or not in sequence mode)
# 170 = TEST IN PROGRESS: PRESSURE DISCHARGE
# 180 = TEST IN PROGRESS: END OF TESTING TABLE - IF IN SEQUENCE MODE THE CYCLE STARTS FROM
# PHASE 60 WITH THE NEXT TABLE OF THE SEQUENCE
# 190 = TEST IN PROGRESS: AUTOMATIC OPENING 2 (BUFFER)
# 200 = TEST IN PROGRESS: AUTOMATIC OPENING 1 (CAGE)
# 210 = TEST TERMINATED: WAITING THE START OF A NEW TEST
"Running test: active phase backwards time": [33, {"dt": "16bit_uint", "f": 26, }],
"Running test: T1 phase end pressure": [34, {"dt": "32bit_int", "f": 21, }],
"Running test: T2 phase end pressure": [36, {"dt": "32bit_int", "f": 21, }],
"Running test: burst pressure": [38, {"dt": "32bit_int", "f": 21, }],
"Running test: measured leak": [40, {"dt": "32bit_int", "f": 22, }],
"Running test: calculated leak flow rate": [42, {"dt": "32bit_int", "f": 24, }],
"Running test: calculate RVP%": [44, {"dt": "32bit_int", }],
"Running test: result": [46, {"dt": "16bit_uint", }],
# 1 = LEAK TEST PASSED
# 2 = BURST TEST PASSED WITH BURST
# 3 = BURST TEST PASSED WITHOUT BURST
# 4 = not used
# 5 = BLOCKAGE TEST PASSED
# 100 = LEAK TEST FAILED - UPPER LIMIT
# 101 = LEAK TEST – FAILED ANOMALY
# 102 = LEAK TEST - MAXIMUM LEAK FAILED
# 103 = BURST - BREAKAGE PRESSURE DEFLECTION
# 104 = VOLUMETRIC CONTROL - RVP% FAILED
# 105 = not used
# 106 = not used
# 107 = BLOCKAGE - MAX PRESSURE FAILED
# 108 = BLOCKAGE - MIN PRESSURE FAILED
# 109 = BURST - MINIMUM PRESSURE FAILED
# 200 = LEAK TEST FAILED – PR% PRESSURE MINUM
# 201 = LEAK TEST FAILED - PR% PRESSURE MAX
# 202 = LEAK TEST FAILED – P0% PRESSURE MINUM
# 203 = LEAK TEST FAILED - P0% PRESSURE MAX
# 204 = ERROR - INTERNAL ALARMS
# 205 = ERROR - RELATIVE PRESSURE OUT OF RANGE
# 206 = ERROR - DIFFERENTIAL PRESSURE OUT OF RANGE
# 207 = ERROR – PRE-FILLING VALVE NOT OPENED
# 250 = TEST ABORTED
"Running test: type of test": [47, {"dt": "16bit_uint", }],
# 1 = LEAK TEST
# 2 = BLOCKAGE TEST
# 3 = LEAK TEST WITH VOLUME CHECK
# 4 = BURST TEST
"Testing in progress: progressive sequence index": [48, {"dt": "16bit_uint", }],
"Testing in progress: graphical sampling rate": [49, {"dt": "16bit_uint", }],
"Testing in progress: number of samples of the graph": [50, {"dt": "16bit_uint", }],
"DISLPAY and SOUND: Language": [601, {"dt": "16bit_uint", }],
# 0=ITALIANO
# 1=ENGLISH
"INSTRUMENT SETTING: Test table from": [602, {"dt": "16bit_uint", }],
# 0=PARAMETER
# 1=PLC BASE 1
# 2=PLC BASE 2
"MEASURE UNITS: pressure measure units": [606, {"dt": "16bit_uint", }],
# 0=mH2O 1=mbar 2=kPa 3=mmHg 4=inH2O 5=psi 6=mmH2O (se fondoscala <=6 bar)
"MEASURE UNITS: Leak measure units": [607, {"dt": "16bit_uint", }],
# 0=mmH2O 1=mbar 2=Pa 3=mmHg 4=inH2O 5=psi
"MEASURE UNITS: leak flow rate measure units": [608, {"dt": "16bit_uint", }],
# 0=cm3/min 1=cm3/h
"MEASURE UNITS: Volume": [609, {"dt": "16bit_uint", }],
# 0=litri 1=cm3
"MEASURE UNITS: Flow rate measure units": [617, {"dt": "16bit_uint", }],
# 0=liters/min 1=liters/h 2=m3/h
"AUTOMATION: Cage - closing time": [618, {"dt": "16bit_uint", }],
# Format: x.x seconds
"AUTOMATION: Cage - opening time": [619, {"dt": "16bit_uint", }],
# Format: x.x seconds
"INSTRUMENT SETTINGS: MAX pressure": [620, {"dt": "16bit_uint", }],
"AUTOMATION: Buffer - closing time": [621, {"dt": "16bit_uint", }],
# Format: x.x seconds
"AUTOMATION: Buffer - opening time": [622, {"dt": "16bit_uint", }],
# Format: x.x seconds
"AUTOMATION: Marking - result": [623, {"dt": "16bit_uint", }],
# 0= Only passed
# 1= Only failed
# 2=All
"AUTOMATION: Marking – driving time": [624, {"dt": "16bit_uint", }],
# Format: x.x seconds
"Type of test": [701, {"dt": "16bit_uint", }],
# 1=LEAK TEST
# 2=BLOCKAGE TEST
# 3=LEAK TEST WITH VOLUME
# 4=BURST TEST
"Test flags": [702, {"dt": "16bit_uint", }],
# T0/Pr: Phase filling mode T0 0= TIME 1= PRESSURE
# T1/Pr: Phase filling mode T1 0= TIME 1=PRESSURE
# T3/Q: Phase mode T3 0= TIME 1=TERMINATE IMMEDIATELY IF FAILED
# TYPE PID: 0=FAST 1=MEDIUM 2=SLOW 4 = FIXED 5 = AUTOMATIC 6= PULSES (if enabled SWLP option)
# P0-: 0=Positive P0 pressure 1=P0 negative pressure (if enabled V or N option for vacuum tests)
# Pr-: 0=Positive pressure Pr 1=Pr negative pressure (if enabled V or N option for vacuum tests)
# Q+: 0=Q + positive parameter 1=parameter Q + negative
# Q-: 0=Q-positive parameter 1=Q-negative parameter
# AT: 0=Tare pressure disabled 1=Tare pressure enabled
# HR: 0=Resolution on loss 1 Pa 1=Resolution on loss 0.1 Pa (models with full scale <= 2 bar)
# CH: Selected test channel (2-channel T3P2C model only)
"T0 – Pre-filling time": [704, {"dt": "16bit_uint", "f": 26, }],
"P0 – Pre-filling pressure": [705, {"dt": "16bit_uint", "f": 23, }],
# In order to use a negative (vacuum) value, this parameter must however be written positively, but the P0- bit must
# also be selected in register 702
"T1 – Filling time": [706, {"dt": "16bit_uint", "f": 26, }],
"T2 – Settling time": [707, {"dt": "16bit_uint", "f": 26, }],
"T3 – Measure time": [708, {"dt": "16bit_uint", "f": 26, }],
"PREL – Nominal test pressure": [709, {"dt": "16bit_uint", "f": 23, }],
# To set a negative pressure (vacuum) this parameters must be written in absolute value and then set
# bit Pr- in register 702
"Pxx Minimum Pressure": [710, {"dt": "16bit_uint", }],
# PR%- Lower tolerance on pressure: LEAK: Format: x.x %
# P- Minimum pressure: BLOCKAGE: Format as indicated in register 23
# PR- Minimum pressure%: BURST: Format x.x%
"Q + Upper limit": [711, {"dt": "16bit_uint", "f": 22, }],
# Format as indicated in the register 22
"Q- Leak limit": [712, {"dt": "16bit_uint", "f": 22, }],
# Format as indicated in the register 22
"FST –discharge time": [713, {"dt": "16bit_uint", "f": 26, }],
# Format as indicated in the register 26
"VP – Equivalent volume": [714, {"dt": "16bit_uint", "f": 25, }],
# Format as indicated in the register 25
"P% Pressure tolerance": [717, {"dt": "16bit_uint", }],
# BLOCKAGE: Format x.x %
"PB – Minimum burst pressure": [743, {"dt": "16bit_uint", "f": 23, }],
# BURST: Format as indicated in the register 23
"BD – Delta burst": [744, {"dt": "16bit_uint", "f": 23, }],
# BURST: Format as indicated in the register 23
"FSL – discharge limit": [745, {"dt": "16bit_uint", "f": 23, }],
# Format as indicated in the register 23
"PR% + Superior tolerance on pressure": [747, {"dt": "16bit_uint", }],
# LEAK: Format: x.x %
# P + Pressure max BURST: Format as indicated in the register 23
"RVP%: volumetric ratio": [750, {"dt": "16bit_uint", }],
# VOLUME+LEAK: Format x.xx (from 0.00 to 649.99)
"RVP%: max tolerance": [751, {"dt": "16bit_uint", }],
# VOLUME+LEAK: Format: x.xx
}

View File

@ -5,7 +5,7 @@ import logging
from playhouse.sqlite_ext import JSONField
from .models import Archive, Log, Recipes, Session, Users, db, Autotests
from .models import Archive, Autotests, Log, Recipes, Session, Users, db
models_reference = {
"archive": Archive,
@ -60,10 +60,28 @@ if admin is None or not admin.is_admin:
Users.register(username="USER", password="user")
# register test recipe
Recipes.replace(id=0, name="TEST", spec={
# recipe
"client": "TEST_CLIENT",
"part_number": "TEST_PART_NUMBER",
"station": "TEST_STATION",
# pressure
"pressure_min": 0,
"pressure_test": 1,
"pressure_max": 2,
"pressure_ramp": 3,
# test
"cleaning_time": 4,
"tolerance": 5,
"test_duration": 6,
"flush_duration": 7,
# stabilizarion
"stabilization_time": 8,
"stabilization_level_min": 9,
"stabilization_level_max": 10,
"stabilization_settling_time": 11,
"stabilization_cycles": 12,
# description
"description": "TEST_DESCRIPTION",
"vision_recipe": "TEST_VISION_RECIPE",
}, archived=False).execute()
if True:

View File

@ -7,7 +7,7 @@ from .base_model import BaseModel
class Recipes(BaseModel):
id = AutoField(primary_key=True, unique=True, null=False)
name = TextField(null=False)
spec = JSONField(null=False)
spec = JSONField(null=False) # keys inside spec must not overlap withthe model
archived = BooleanField(null=False, default=False)
@classmethod

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
import faulthandler
import logging
import os
@ -35,7 +34,7 @@ logging.basicConfig(
handlers=[
logging.StreamHandler(stream=sys.stderr),
logging.FileHandler(
logs_dir / f"{datetime.now().strftime('%Y-%m-%dT%H-%M-%S%z')}.log",
logs_dir / f"{datetime.now().isoformat()}.log",
mode="a",
encoding="utf-8",
delay=False,
@ -47,9 +46,10 @@ logging.basicConfig(
errors="surrogateescape",
)
if True:
try:
# IMPORT PROJECT ONLY AFTER SETTING UP SIGNAL, FAULTHANDLER AND LOGGHING
from components import ArchiveSynchronizer, RemoteAPI, TestComponent
from components import (ArchiveSynchronizer, Os_Label_Printer, RemoteAPI,
TecnaMarpossProvasetT3, TestComponent, VisionSaver)
from lib.db import Users
from lib.helpers import ConfigReader
from PyQt5.QtCore import QObject, QThread, pyqtSignal
@ -57,108 +57,110 @@ if True:
from ui import (About, Archive, Autotests_Archive, Login, Main_Window,
Test, Users_Management)
class Main(QObject):
do = pyqtSignal(dict)
class Main(QObject):
do = pyqtSignal(dict)
@staticmethod
def _do(config):
return config["f"](*config.get("a", []), **config.get("k", {}))
@staticmethod
def _do(config):
return config["f"](*config.get("a", []), **config.get("k", {}))
def __init__(self, parent=None):
# print(f"MAIN {int(QThread.currentThreadId())}", flush=True)
super().__init__()
self.do.connect(self._do)
try:
# READ CONFIG
self.config = ConfigReader()
# INIT COMPONENT
self.components_specs = {
"archive_synchronizer": {"c": ArchiveSynchronizer, "a": [], "k": {}},
"remote_api": {"c": RemoteAPI, "a": [], "k": {"main": self}},
"test_component": {"c": TestComponent, "a": [], "k": {}},
}
self.components = {}
self.threads = {}
for component_name, spec in self.components_specs.items():
self.components[component_name] = spec["c"](*spec["a"], config=self.config, name=component_name, **spec["k"])
self.threads[component_name] = QThread()
self.threads[component_name].setTerminationEnabled(True)
self.components[component_name].moveToThread(self.threads[component_name])
for component_name in self.components_specs:
self.threads[component_name].started.connect(self.components[component_name].start)
self.threads[component_name].start()
self.components[component_name].wait_ready()
except Exception as e:
logging.exception(traceback.format_exc())
QMessageBox.critical(None, "Errore Banco", f"Non e stato possibile connettersi al banco:\n\n{e}")
#quit()
# GUI INIT
# self.main_window = Main_Window(self.bench)
self.main_window = Main_Window()
# CONNECT MAIN WINDOW ACTIONS
self.main_window.archive_a.triggered.connect(self.open_archive)
if "--archive" in sys.argv:
self.main_window.archive_a.trigger()
self.main_window.autotests_archive_a.triggered.connect(self.open_autotests_archive)
if "--autotests-archive" in sys.argv:
self.main_window.autotests_archive_a.trigger()
self.main_window.about_a.triggered.connect(self.open_about)
if "--about" in sys.argv:
self.main_window.about_a.trigger()
self.main_window.admin_m.menuAction().setVisible(False) # admin menu should not be visible before an admin logs in
self.main_window.users_management_a.triggered.connect(self.open_users_management)
if "--users-management" in sys.argv:
self.main_window.users_management_a.trigger()
# OPEN LOGIN TAB
self.open_login()
# SHOW MAIN WINDOW
if "--panel" in sys.argv:
self.main_window.show()
elif "--maximized" in sys.argv:
self.main_window.showMaximized()
elif "--full-screen" in sys.argv:
self.main_window.showFullScreen()
else:
self.main_window.show()
def open_archive(self):
self.main_window.open_dialog(Archive())
def open_autotests_archive(self):
self.main_window.open_dialog(Autotests_Archive())
def open_users_management(self):
self.main_window.open_dialog(Users_Management())
def open_about(self):
about_widget = About()
self.main_window.open_dialog(about_widget)
def open_login(self):
tab = Login()
tab.successful_login.connect(self.logghed_in)
self.main_window.open_tab(tab)
def logghed_in(self):
session = Users.get_session()
if session is not None:
if session.is_admin:
self.main_window.admin_m.menuAction().setVisible(True)
def __init__(self, parent=None):
# print(f"MAIN {int(QThread.currentThreadId())}", flush=True)
super().__init__()
self.do.connect(self._do)
try:
# READ CONFIG
self.config = ConfigReader()
# INIT COMPONENT
self.components_specs = {
"archive_synchronizer": {"c": ArchiveSynchronizer},
"remote_api": {"c": RemoteAPI, "k": {"main": self}},
"test_component": {"c": TestComponent},
"vision_savert": {"c": VisionSaver, "t": False},
"label_printer": {"c": Os_Label_Printer, "t": False},
"tecna_marposs_provaset_t3": {"c": TecnaMarpossProvasetT3},
}
self.components = {}
self.threads = {}
for component_name, spec in self.components_specs.items():
self.components[component_name] = spec["c"](*spec.get("a", []), config=self.config, name=component_name, **spec.get("k", {}))
if spec.get("t", True):
self.threads[component_name] = QThread()
self.threads[component_name].setTerminationEnabled(True)
self.components[component_name].moveToThread(self.threads[component_name])
for component_name, thread in self.threads.items():
component = self.components[component_name]
thread.started.connect(component.start)
thread.start()
component.wait_ready()
except Exception as e:
logging.exception(traceback.format_exc())
QMessageBox.critical(None, "Errore Banco", f"Non e stato possibile connettersi al banco:\n\n{e}")
quit()
# GUI INIT
# self.main_window = Main_Window(self.bench)
self.main_window = Main_Window()
# CONNECT MAIN WINDOW ACTIONS
self.main_window.archive_a.triggered.connect(self.open_archive)
if "--archive" in sys.argv:
self.main_window.archive_a.trigger()
self.main_window.autotests_archive_a.triggered.connect(self.open_autotests_archive)
if "--autotests-archive" in sys.argv:
self.main_window.autotests_archive_a.trigger()
self.main_window.about_a.triggered.connect(self.open_about)
if "--about" in sys.argv:
self.main_window.about_a.trigger()
self.main_window.admin_m.menuAction().setVisible(False) # admin menu should not be visible before an admin logs in
self.main_window.users_management_a.triggered.connect(self.open_users_management)
if "--users-management" in sys.argv:
self.main_window.users_management_a.trigger()
# OPEN LOGIN TAB
self.open_login()
# SHOW MAIN WINDOW
if "--panel" in sys.argv:
self.main_window.show()
elif "--maximized" in sys.argv:
self.main_window.showMaximized()
elif "--full-screen" in sys.argv:
self.main_window.showFullScreen()
else:
self.main_window.admin_m.menuAction().setVisible(False)
self.open_test()
self.main_window.show()
def open_test(self):
self.main_window.open_tab(Test(self.config.machine_id))
def open_archive(self):
self.main_window.open_dialog(Archive())
def open_autotests_archive(self):
self.main_window.open_dialog(Autotests_Archive())
if __name__ == "__main__":
try:
def open_users_management(self):
self.main_window.open_dialog(Users_Management())
def open_about(self):
about_widget = About()
self.main_window.open_dialog(about_widget)
def open_login(self):
tab = Login()
tab.successful_login.connect(self.logghed_in)
self.main_window.open_tab(tab)
def logghed_in(self):
session = Users.get_session()
if session is not None:
if session.is_admin:
self.main_window.admin_m.menuAction().setVisible(True)
else:
self.main_window.admin_m.menuAction().setVisible(False)
self.open_test()
def open_test(self):
self.main_window.open_tab(Test(self.config.machine_id))
if __name__ == "__main__":
app = QApplication(sys.argv)
main = Main()
app.exec()
except Exception:
logging.exception(traceback.format_exc())
# extype, value, tb = sys.exc_info()
# pdb.post_mortem(tb)
except Exception:
logging.exception(traceback.format_exc())
# extype, value, tb = sys.exc_info()
# pdb.post_mortem(tb)

View File

@ -4,5 +4,9 @@ google-cloud-storage
numpy
opencv-python-headless
peewee
pillow
pymodbus
pyqt5
pyserial
requests
zebra

View File

@ -8,6 +8,7 @@ from .qml_circular_gauge import Qml_Circular_Gauge
from .qml_led import Qml_Led
from .qml_switch import Qml_Switch
from .qml_widget import Qml_Widget
from .recipe_editor import Recipe_Editor
from .recipe_selection import Recipe_Selection
from .test import Test
from .test_autotest import Test_Autotest

View File

@ -45,8 +45,8 @@ class Cell:
self.set_readonly(self.readonly)
self.value = None
self.is_modified = False
self.connected_modified = False
self.do_autocomplete(self.autocomplete)
self.connect_modified()
def set_readonly(self, readonly):
raise NotImplementedError()
@ -61,6 +61,11 @@ class Cell:
self.value = data
self.is_modified = False
self.render(data, *args, **kwargs)
if not self.connected_modified:
# only connect after first render
# to avoid false modified signals trigghered by autocomplete
self.connect_modified()
self.connected_modified = True
def render(self, data, field_name=None, row_number=None, crud=None):
raise NotImplementedError()
@ -90,7 +95,6 @@ class Cell:
class Line_Edit_Cell_Widget(QLineEdit, Cell):
def __init__(self, readonly=True, autocomplete=None, field_name=None, field_alias=None, field=None):
super().__init__()
Cell.__init__(self, readonly=readonly, autocomplete=autocomplete, field_name=field_name, field_alias=field_alias, field=field)
@ -98,9 +102,6 @@ class Line_Edit_Cell_Widget(QLineEdit, Cell):
def set_readonly(self, readonly):
self.setReadOnly(readonly)
# def do_autocomplete(self, autocomplete):
# pass
def connect_modified(self):
self.textChanged.connect(self.check_modified)
@ -150,9 +151,6 @@ class External_Dialog_Cell_Widget(QPushButton, Cell):
def set_readonly(self, readonly):
self.editor.setReadOnly(readonly)
# def do_autocomplete(self, autocomplete):
# pass
def connect_modified(self):
self.editor.textChanged.connect(self.check_modified)
@ -172,6 +170,9 @@ class Json_External_Dialog_Cell_Widget(External_Dialog_Cell_Widget):
class Crud(Widget):
modified = pyqtSignal(bool)
selected = pyqtSignal(list)
def __init__(self, table_name, readonly=False, select=None, filters=None, fields_aliases=None, autocomplete=None, pagination=250, display_name=None, row_upgrader=None, widget_classes=None, row_filter=None):
super().__init__()
self.table_name = table_name
@ -279,12 +280,14 @@ class Crud(Widget):
return True, row, False
def set_modified(self, is_modified=True):
self.modified = self.modified or is_modified
self._modified = self._modified or is_modified
self.modified.emit(self._modified)
def refresh(self, action=None, page=0):
# IGNORE SIGNALS WHILE UPDATING
self.db_tw.blockSignals(True)
self.modified = False
self._modified = False # force reset modified status
self.set_modified(self._modified)
if action == "init":
self.sorting = {}
self.previous_selection = []
@ -314,7 +317,7 @@ class Crud(Widget):
for fn, cn in self.select_index.items():
w = self.db_tw.cellWidget(rn, cn)
try:
r[fn] = w.parse(row_number=rn, crud=self)
r[fn] = w._parse(row_number=rn, crud=self)
except Exception:
self.log.exception(traceback.format_exc())
self.set_row_color(rn, "red")
@ -381,7 +384,7 @@ class Crud(Widget):
return self.refresh("commit")
def add(self):
self.modified = True
self.set_modified(True)
self.data_index.append(None)
rn = self.db_tw.rowCount()
self.db_tw.setRowCount(rn + 1)
@ -393,7 +396,7 @@ class Crud(Widget):
self.db_tw.scrollToBottom()
def delete(self):
self.modified = True
self.set_modified(True)
selected = self.get_selected_rows()
if len(selected) == 0:
return
@ -422,6 +425,7 @@ class Crud(Widget):
def show_selection(self):
selected = self.get_selected_rows()
self.selected.emit(selected)
if self.previous_selection == selected:
return
for row_number in self.previous_selection:
@ -438,3 +442,7 @@ class Crud(Widget):
w.setStyleSheet(f"background-color: {color};")
else:
w.setStyleSheet("")
def emit(self):
self.set_modified(self._modified)
self.show_selection()

View File

@ -33,4 +33,4 @@ class Dialog(QDialog):
def closeEvent(self, *args):
self._closing.emit()
global dialogs
del dialogs[id(self)]
dialogs.pop(id(self), None)

View File

@ -18,9 +18,13 @@ class Login(Widget):
QShortcut(QKeySequence("Return"), self).activated.connect(self.login_b.click)
self.login_b.clicked.connect(self.try_login)
# TESTING
if "--auto-login" in sys.argv or "--test" in sys.argv:
if "--auto-login-admin" in sys.argv:
self.user_cb.setCurrentText("ADMIN")
self.password_le.setText("123123")
if "--auto-login-user" in sys.argv or "--test" in sys.argv:
self.user_cb.setCurrentText("USER")
self.password_le.setText("user")
if "--auto-login-admin" in sys.argv or "--auto-login-user" in sys.argv or "--test" in sys.argv:
self.test_timer = QTimer()
self.test_timer.setSingleShot(True)
self.test_timer.timeout.connect(self.login_b.clicked.emit)

View File

@ -0,0 +1 @@
from .recipe_editor import Recipe_Editor

View File

@ -0,0 +1,120 @@
from PyQt5.QtWidgets import (QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit,
QRadioButton, QSpinBox, QPlainTextEdit)
from ui.widget import Widget
class Recipe_Editor(Widget):
def __init__(self):
super().__init__()
self.spec = {
# recipe
"client": self.client_le,
"part_number": self.part_number_le,
"station": self.station_le,
# pressure
"pressure_min": self.pressure_min_sb,
"pressure_test": self.pressure_test_sb,
"pressure_max": self.pressure_max_sb,
"pressure_ramp": self.pressure_ramp_sb,
# test
"cleaning_time": self.cleaning_time_sb,
"tolerance": self.tolerance_sb,
"test_duration": self.test_duration_sb,
"flush_duration": self.flush_duration_sb,
# stabilizarion
"stabilization_time": self.stabilization_time_sb,
"stabilization_level_min": self.stabilization_level_min_sb,
"stabilization_level_max": self.stabilization_level_max_sb,
"stabilization_settling_time": self.stabilization_settling_time_sb,
"stabilization_cycles": self.stabilization_cycles_sb,
# description
"description": self.description_pte,
}
def set_readonly(self, readonly):
for w in self.spec.values():
w.setReadOnly(readonly)
def do_autocomplete(self, autocomplete):
if autocomplete is None:
return
for k, v in autocomplete.items():
w = self.spec[k]
if isinstance(w, QCheckBox):
w.setChecked(bool(v))
elif isinstance(w, QComboBox):
w.addItems(list(map(str, v)))
elif isinstance(w, QDoubleSpinBox):
w.setValue(float(v))
elif isinstance(w, QLineEdit):
w.setText(str(v))
elif isinstance(w, QRadioButton):
w.setChecked(bool(v))
elif isinstance(w, QSpinBox):
w.setValue(int(v))
elif isinstance(w, QPlainTextEdit):
w.setPlainText(str(v))
else:
raise NotImplementedError(f"widget of type {type(w)!r} not implemented")
def connect_modified(self, check_modified):
for k, w in self.spec.items():
if isinstance(w, QCheckBox):
w.toggled.connect(check_modified)
elif isinstance(w, QComboBox):
w.currentTextChanged.connect(check_modified)
elif isinstance(w, QDoubleSpinBox):
w.valueChanged.connect(check_modified)
elif isinstance(w, QLineEdit):
w.textChanged.connect(check_modified)
elif isinstance(w, QRadioButton):
w.toggled.connect(check_modified)
elif isinstance(w, QSpinBox):
w.valueChanged.connect(check_modified)
elif isinstance(w, QPlainTextEdit):
w.textChanged.connect(check_modified)
else:
raise NotImplementedError(f"widget of type {type(w)!r} not implemented")
def render(self, data, field_name=None, row_number=None, crud=None):
for k, v in data.items():
if k not in self.spec:
continue
w = self.spec[k]
if isinstance(w, QCheckBox):
w.setChecked(bool(v))
elif isinstance(w, QComboBox):
w.setCurrentText(str(v))
elif isinstance(w, QDoubleSpinBox):
w.setValue(float(v))
elif isinstance(w, QLineEdit):
w.setText(str(v))
elif isinstance(w, QRadioButton):
w.setChecked(bool(v))
elif isinstance(w, QSpinBox):
w.setValue(int(v))
elif isinstance(w, QPlainTextEdit):
w.setPlainText(str(v))
else:
raise NotImplementedError(f"widget of type {type(w)!r} not implemented")
def parse(self, row_number=None, crud=None):
ret = {}
for k, w in self.spec.items():
if isinstance(w, QCheckBox):
ret[k] = w.isChecked()
elif isinstance(w, QComboBox):
ret[k] = w.currentText()
elif isinstance(w, QDoubleSpinBox):
ret[k] = w.value()
elif isinstance(w, QLineEdit):
ret[k] = w.text()
elif isinstance(w, QRadioButton):
ret[k] = w.isChecked()
elif isinstance(w, QSpinBox):
ret[k] = w.value()
elif isinstance(w, QPlainTextEdit):
ret[k] = w.toPlainText()
else:
raise NotImplementedError(f"widget of type {type(w)!r} not implemented")
return ret

View File

@ -0,0 +1,310 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Recipe_Editor</class>
<widget class="QWidget" name="Recipe_Editor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>494</width>
<height>654</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Pressione</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="4" column="2">
<widget class="QSpinBox" name="pressure_max_sb"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Min</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Max</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="pressure_min_sb"/>
</item>
<item row="2" column="3">
<widget class="QLabel" name="label_22">
<property name="text">
<string>bar</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSpinBox" name="pressure_test_sb"/>
</item>
<item row="5" column="2">
<widget class="QSpinBox" name="pressure_ramp_sb"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Test</string>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QLabel" name="label_24">
<property name="text">
<string>bar</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Rampa di salita</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLabel" name="label_23">
<property name="text">
<string>bar</string>
</property>
</widget>
</item>
<item row="5" column="3">
<widget class="QLabel" name="label_18">
<property name="text">
<string>bar/min</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Descrizione</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QPlainTextEdit" name="description_pte"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Test</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="4" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Tempo di prova</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Tempo di scarico</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Tempo di pulizia</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="test_duration_sb"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Tolleranza</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QSpinBox" name="flush_duration_sb"/>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="cleaning_time_sb"/>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="tolerance_sb"/>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>s</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_19">
<property name="text">
<string>bar</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_20">
<property name="text">
<string>s</string>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QLabel" name="label_21">
<property name="text">
<string>s</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Ricetta</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Cliente</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Stazione</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLineEdit" name="station_le"/>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="client_le"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>N° disegno</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="part_number_le"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="2">
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Stabilizzazione</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="2" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Soglia max</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Soglia min</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="stabilization_cycles_sb"/>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="stabilization_time_sb"/>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="stabilization_settling_time_sb"/>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="stabilization_level_max_sb"/>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="stabilization_level_min_sb"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Tempo assestamento</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Numero di cicli</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Tempo</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_25">
<property name="text">
<string>s</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_26">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_27">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_28">
<property name="text">
<string>s</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,27 +1,54 @@
import sys
from lib.db import Recipes
from PyQt5.QtCore import QTimer, pyqtSignal
from lib.db import Recipes, Users
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QShortcut
from ui.crud import Crud
from PyQt5.QtWidgets import QPushButton, QShortcut
from ui.crud import Cell, Crud
from ui.dialog import Dialog
from ui.helpers import replace_widget
from ui.recipe_editor import Recipe_Editor
from ui.widget import Widget
class Json_Spec_External_Dialog_Cell_Widget(QPushButton, Cell):
def __init__(self, readonly=True, autocomplete=None, field_name=None, field_alias=None, field=None):
self.dialog = Dialog()
self.dialog.setAttribute(Qt.WA_DeleteOnClose, on=False)
self.editor = Recipe_Editor()
self.dialog.setCentralWidget(self.editor)
super().__init__(u"\u238B apri")
Cell.__init__(self, readonly=readonly, autocomplete=autocomplete, field_name=field_name, field_alias=field_alias, field=field)
self.dialog.setWindowTitle(self.field_alias)
self.clicked.connect(self.dialog.show)
def set_readonly(self, readonly):
self.editor.set_readonly(readonly)
def do_autocomplete(self, autocomplete):
self.editor.do_autocomplete(autocomplete)
def connect_modified(self):
self.editor.connect_modified(self.check_modified)
def render(self, data, field_name=None, row_number=None, crud=None):
self.editor.render(data, field_name=field_name, row_number=row_number, crud=crud)
def parse(self, row_number=None, crud=None):
return self.editor.parse(row_number=row_number, crud=crud)
def recipes_row_upgrader(row, row_number, crud):
row["client"] = row["spec"]["client"]
row["part_number"] = row["spec"]["part_number"]
row["description"] = row["spec"]["description"]
row["vision_recipe"] = row["spec"]["vision_recipe"]
if len(row.keys() & row["spec"].keys()):
raise AssertionError("field keys in Recipes model MUST NOT be present inside Recipes.spec")
row.update(row["spec"])
return row
def recipes_row_filter(row, row_number, crud):
row["spec"]["client"] = row.pop("client")
row["spec"]["part_number"] = row.pop("part_number")
row["spec"]["description"] = row.pop("description")
row["spec"]["vision_recipe"] = row.pop("vision_recipe")
for k in list(row):
if k in row["spec"]:
row["spec"][k] = row.pop(k)
return True, row, False
@ -30,30 +57,63 @@ class Recipe_Selection(Widget):
def __init__(self):
super().__init__()
crud_aliases = {
"name": "Ricetta",
"client": "Cliente",
"part_number": "N° disegno",
"description": "Descrizione",
"vision_recipe": "Ricetta visione",
}
session = Users.get_session()
if session.is_admin:
readonly = False
crud_aliases = {
"name": "Ricetta",
"spec": "Specifica",
# "client": "Cliente",
# "part_number": "N° disegno",
# "station": "Stazione",
# "cleaning_time": "Tempo di pulizia",
# "pressure_ramp": "Rampa di salita",
# "tolerance": "Tolleranza",
# "test_duration": "Tempo di prova",
# "flush_duration": "Tempo di scarico",
# "pressure_min": "Pressione min",
# "pressure_test": "Pressione test",
# "pressure_max": "Pressione max",
# "stabilization_time": "Tempo di stabilizzazione",
# "stabilization_level_min": "Soglia di stabilizzazione min",
# "stabilization_level_max": "Soglia di stabilizzazione max",
# "stabilization_settling_time": "Tempo di assestamento",
# "stabilization_cycles": "Numero di cicli",
# "description": "Desccrizione",
"archived": "Archiviata",
}
filters = None
else:
readonly = True
crud_aliases = {
"name": "Ricetta",
"client": "Cliente",
"part_number": "N° disegno",
"description": "Desccrizione",
"spec": "Specifica",
}
filters = {"archived": False}
self.crud = Crud(
"recipes",
display_name="SELEZIONE RICETTA",
readonly=True,
readonly=readonly,
select=list(crud_aliases.keys()),
filters={"archived": False},
filters=filters,
fields_aliases=crud_aliases,
autocomplete={"archived": False},
row_upgrader=recipes_row_upgrader,
widget_classes={"spec": Json_Spec_External_Dialog_Cell_Widget, },
row_filter=recipes_row_filter,
)
replace_widget(self, "crud_w", self.crud)
self.crud_modified = None
self.selected = None
self.select_b.setEnabled(False)
QShortcut(QKeySequence("Return"), self).activated.connect(self.select_b.click)
self.crud.db_tw.itemSelectionChanged.connect(self.check)
self.select_b.clicked.connect(self.select)
QShortcut(QKeySequence("Return"), self).activated.connect(self.select_b.click)
self.crud.modified.connect(self.check_modified)
self.crud.selected.connect(self.check_selected)
self.crud.emit()
# TESTING
if "--auto-select" in sys.argv or "--test" in sys.argv:
drawing = "TEST"
@ -69,18 +129,22 @@ class Recipe_Selection(Widget):
self.test_timer.start(500)
# /TESTING
def check(self):
if not self.crud.modified:
selected = self.crud.get_selected_rows()
if len(selected) == 1:
selected = selected[0] - 1 # - 1 because rn starts from 1 (filters line)
if selected >= 0 and selected < len(self.crud.data_index):
selected = self.crud.data_index[selected]
self.selected = self.crud.db.table_model.get_by_id(selected)
self.select_b.setEnabled(True)
return
self.selected = None
self.select_b.setEnabled(False)
def check_modified(self, modified):
self.crud_modified = modified
self.check(self.crud_modified, self.selected)
def check_selected(self, selected):
if len(selected) == 1:
selected = selected[0] - 1 # - 1 because rn starts from 1 (filters line)
if selected >= 0 and selected < len(self.crud.data_index):
selected = self.crud.data_index[selected]
self.selected = self.crud.db.table_model.get_by_id(selected)
else:
self.selected = None
self.check(self.crud_modified, self.selected)
def check(self, modified, selected):
self.select_b.setEnabled(modified is False and selected is not None)
def select(self):
if self.selected is not None: