2025-02-10 18:01:35 +00:00
import time
2024-09-18 12:18:07 +00:00
from lib . db import Recipes , db
2025-02-05 10:38:40 +00:00
from PyQt5 . QtCore import QSemaphore , pyqtSignal , pyqtSlot
2022-10-18 14:41:53 +00:00
from PyQt5 . QtWidgets import QMessageBox
2022-09-06 10:06:43 +00:00
from . component import Component
2022-06-08 07:11:38 +00:00
from . modbus_component import ModbusComponent
2022-09-14 12:47:09 +00:00
from . tecna_marposs_provaset_t3l_registers import registers as t3l_registers
from . tecna_marposs_provaset_t3p_registers import registers as t3p_registers
2022-06-01 16:37:19 +00:00
2022-09-14 12:47:09 +00:00
class TecnaMarpossProvasetT3 ( ModbusComponent ) :
2022-10-18 14:41:53 +00:00
_store_recipes_signal = pyqtSignal ( object )
_store_recipes_lock = QSemaphore ( 0 )
2025-02-05 10:38:40 +00:00
tecna_error_signal = pyqtSignal ( bool , str ) # Emits (True, error_message) if error exists, else (False, "")
2022-10-18 14:41:53 +00:00
2022-06-01 16:37:19 +00:00
def __init__ ( self , config = None , name = None , period = 1 , lazy = True , paused = False , threaded = True ) :
2022-09-14 12:47:09 +00:00
super ( ) . __init__ ( config = config , name = name , period = period , lazy = lazy , paused = paused , threaded = threaded , registers = None )
2025-05-08 12:51:44 +00:00
# Connect to the modbus_error_signal to handle connection issues
self . modbus_error_signal . connect ( self . handle_modbus_error )
self . connection_lost = False
self . _previous_connection_lost = False
2022-06-01 16:37:19 +00:00
def config_changed ( self ) :
2022-06-08 07:11:38 +00:00
super ( ) . config_changed ( )
2022-10-18 14:41:53 +00:00
self . _store_recipes_signal . connect ( self . _store_recipes )
2022-09-14 12:47:09 +00:00
self . model = self . config [ self . name ] [ " model " ] . lower ( )
if self . model == " t3p " :
self . registers = t3p_registers
elif self . model == " t3l " :
self . registers = t3l_registers
else :
2023-07-01 14:16:58 +00:00
raise NotImplementedError ( f " tecna t3 model { self . model !r} not implemented. " )
2022-07-13 08:36:58 +00:00
self . set_measure_units ( )
2025-02-06 16:01:48 +00:00
try :
self . units = self . get_measure_units ( )
self . max_program_number = self . read ( " Max number of programs " )
self . saver_label_count = min ( abs ( int ( self . config [ self . name ] . get ( " saver_label_count " , 1 ) ) ) , 0b1111 )
self . saver_print_on_fail = 1 if self . config [ self . name ] . get ( " saver_print_on_fail " , " no " ) . lower ( ) in { " yes " , " y " , " on " , " true " , " 1 " , " x " } else 0
self . saver_label_template = min ( abs ( int ( self . config [ self . name ] . get ( " saver_label_template " , 1 ) ) ) , 0b11111111 )
self . model = self . config [ self . name ] [ " model " ] . lower ( )
self . log . info ( f " units: { self . units } " )
except Exception as e :
error_message = f " Error during config_changed operation: { str ( e ) } "
2025-05-08 12:51:44 +00:00
# Mark connection as lost if it's a connection error
if " Connection error " in str ( e ) or " Cannot connect " in str ( e ) :
self . connection_lost = True
self . log . warning ( " Connection to Tecna Marposs lost during config_changed operation, will attempt to reconnect on next periodic call " )
# Emit a signal to notify the UI of the connection loss
self . tecna_error_signal . emit ( True , error_message )
2022-06-08 07:11:38 +00:00
_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 , }
2022-09-14 12:47:09 +00:00
_flow_units = { " liters/min " : 0 , " liters/h " : 1 , " m3/h " : 2 , " cfm " : 3 }
2022-06-08 07:11:38 +00:00
_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 ) :
2022-09-14 12:47:09 +00:00
if self . model == " t3p " :
for register , [ unit , decimals ] in {
" MEASURE UNITS: Relative pressure " : [ self . _pressure_units [ " mbar " ] , 0 ] , # red, purple
" MEASURE UNITS: Differential (leak) pressure " : [ self . _leak_units [ " mbar " ] , 0 ] , # yellow
" MEASURE UNITS: Calculated leak flow rate " : [ self . _leak_flow_units [ " cm3/min " ] , 0 ] , # blue
" MEASURE UNITS: Volume " : [ self . _volume_units [ " litri " ] , 0 ] , # green
" MEASURE UNITS: Flow rate " : [ self . _flow_units [ " liters/min " ] , 0 ] , # orange
} . items ( ) :
self . write ( register , ( decimals << 8 ) + unit )
elif self . model == " t3l " :
for register , [ unit , decimals ] in {
" MEASURE UNITS: Relative pressure " : [ self . _pressure_units [ " mbar " ] , 0 ] , # red, purple
" MEASURE UNITS: Differential (leak) pressure " : [ self . _leak_units [ " mbar " ] , 0 ] , # yellow
" MEASURE UNITS: Calculated leak flow rate " : [ self . _leak_flow_units [ " cm3/min " ] , 0 ] , # blue
" MEASURE UNITS: Volume " : [ self . _volume_units [ " litri " ] , 0 ] , # green
" MEASURE UNITS: Flow rate " : [ self . _flow_units [ " liters/min " ] , 0 ] , # orange
} . items ( ) :
self . write ( register , unit ) # (decimals << 8) + unit)
else :
2023-07-01 14:16:58 +00:00
raise NotImplementedError ( f " tecna t3 model { self . model !r} not implemented. " )
2022-06-08 07:11:38 +00:00
def get_measure_units ( self ) :
units = { }
2022-09-14 12:47:09 +00:00
if self . model == " t3p " :
for [ register , unit_map , unit_names ] in [
[ " Running test: relative pressure format " , self . _pressure_units_map , [ " relative_pressure " , " red " , " r " , 21 , ] ] , # also by documentation color and register number
[ " Running test: differential pressure format " , self . _pressure_units_map , [ " differential_pressure " , " purple " , " p " , 22 , ] ] , # also by documentation color and register number
[ " Running test: relative pressure format (low resolution) " , self . _leak_units_map , [ " relative_pressure_lr " , " yellow " , " y " , 23 , ] ] , # also by documentation color and register number
[ " Running test: calculated leak flow rate format " , self . _leak_flow_units_map , [ " leak_flow " , " blue " , " b " , 24 , ] ] , # also by documentation color and register number
[ " Running test: volume format " , self . _volume_units_map , [ " volume " , " green " , " g " , 25 , ] ] , # also by documentation color and register number
[ " Running test: time format " , self . _time_units_map , [ " time " , " orange " , " t " , 26 , ] ] , # also by documentation color and register number
[ " Running test: flow rate format " , self . _flow_units_map , [ " flow " , " white " , " o " , 27 , ] ] , # also by documentation color and register number
] :
v = self . read ( register )
unit_spec = [ 10 * * ( - ( ( v >> 8 ) & 0xff ) ) , unit_map [ v & 0xff ] ]
for unit_name in unit_names :
units [ unit_name ] = unit_spec
elif self . model == " t3l " :
for [ register , unit_map , unit_names ] in [
[ [ " Running test: relative pressure scale " , " Running test: relative pressure decimals " ] , self . _pressure_units_map , [ " relative_pressure " , " red " , " r " , 1501 , ] ] , # also by documentation color and register number
[ [ " Running test: differential pressure scale " , " Running test: differential pressure decimals " ] , self . _pressure_units_map , [ " differential_pressure " , " purple " , " p " , 1503 , ] ] , # also by documentation color and register number
[ [ " Running test: relative pressure scale (low resolution) " , " Running test: relative pressure decimals (low resolution) " ] , self . _leak_units_map , [ " relative_pressure_lr " , " yellow " , " y " , 1505 , ] ] , # also by documentation color and register number
[ " Running test: calculated leak flow rate format " , self . _leak_flow_units_map , [ " leak_flow " , " blue " , " b " , 1507 , ] ] , # also by documentation color and register number
[ " Running test: volume format " , self . _volume_units_map , [ " volume " , " green " , " g " , 1508 , ] ] , # also by documentation color and register number
[ " Running test: time format " , self . _time_units_map , [ " time " , " orange " , " t " , 1509 , ] ] , # also by documentation color and register number
[ " Running test: flow rate format " , self . _flow_units_map , [ " flow " , " white " , " o " , 1510 , ] ] , # also by documentation color and register number
[ " Running test: line pressure format " , self . _pressure_units_map , [ " line_pressure " , " lp " , " l " , 1511 , ] ] , # also by documentation color and register number
] :
if type ( register ) is list :
v = [ self . read ( r ) for r in register ]
unit_spec = [ 10 * * ( - ( v [ 1 ] & 0xff ) ) , v [ 0 ] ]
else :
v = self . read ( register )
unit_spec = [ 10 * * ( - ( ( v >> 8 ) & 0xff ) ) , unit_map [ v & 0xff ] ]
for unit_name in unit_names :
units [ unit_name ] = unit_spec
else :
2023-07-01 14:16:58 +00:00
raise NotImplementedError ( f " tecna t3 model { self . model !r} not implemented. " )
2022-06-08 07:11:38 +00:00
return units
2022-07-04 10:36:51 +00:00
def _convert_from_format ( self , data , formatting = None , decoding_map = None ) :
2022-07-12 14:02:45 +00:00
if decoding_map is not None and data in decoding_map :
2022-07-04 10:36:51 +00:00
data = decoding_map [ data ]
if formatting is not None :
# units = self.units[formatting]
# data = [data * units[0], units[1]]
2025-03-28 11:52:57 +00:00
if data is not None :
data = data * self . units [ formatting ] [ 0 ]
else :
return None
2022-07-04 10:36:51 +00:00
return data
2022-06-08 07:11:38 +00:00
2022-07-04 10:36:51 +00:00
def _convert_to_format ( self , data , formatting = None , encoding_map = None ) :
if formatting is not None :
data = int ( data / self . units [ formatting ] [ 0 ] )
2022-07-12 14:02:45 +00:00
if encoding_map is not None and data in encoding_map :
2022-07-04 10:36:51 +00:00
data = encoding_map [ data ]
return data
2022-06-08 07:11:38 +00:00
2022-09-06 10:06:43 +00:00
@Component.reconfig_on_error
2025-02-05 10:38:40 +00:00
def read ( self , register , * args , data_type = None , gain = None , offset = None , formatting = None , decoding_map = None ,
* * kwargs ) :
try :
if type ( register ) is str :
register , s = self . registers [ register ]
if data_type is None :
data_type = s . get ( " dt " , None )
if gain is None :
gain = s . get ( " g " , None )
if offset is None :
offset = s . get ( " o " , None )
if formatting is None :
formatting = s . get ( " f " , None )
if decoding_map is None :
decoding_map = s . get ( " decoding " , None )
if not len ( args ) :
args = s . get ( " a " , [ ] )
if not len ( kwargs ) :
kwargs = s . get ( " k " , { } )
2022-07-19 09:59:00 +00:00
if data_type is None :
2025-02-05 10:38:40 +00:00
data_type = " 16bit_uint "
2022-07-19 09:59:00 +00:00
if gain is None :
2025-02-05 10:38:40 +00:00
gain = 1
2022-07-19 09:59:00 +00:00
if offset is None :
2025-02-05 10:38:40 +00:00
offset = 0
return self . _convert_from_format (
super ( ) . read (
register ,
* args ,
data_type = data_type ,
gain = gain ,
offset = offset ,
* * kwargs ,
) ,
formatting = formatting ,
decoding_map = decoding_map ,
)
2025-09-22 09:54:03 +00:00
2025-02-05 10:38:40 +00:00
except Exception as e :
error_message = f " Error during read operation on register { register } : { str ( e ) } "
self . log . error ( error_message , exc_info = True )
2025-05-08 12:51:44 +00:00
# Do not use tecna_error_signal as per issue description
# self.tecna_error_signal.emit(True, error_message)
# Mark connection as lost if it's a connection error
if " Connection error " in str ( e ) or " Cannot connect " in str ( e ) :
self . connection_lost = True
self . log . warning ( " Connection to Tecna Marposs lost during read operation, will attempt to reconnect on next periodic call " )
2025-02-05 10:38:40 +00:00
raise # Re-raise the exception for further upstream handling if needed
2025-03-25 09:02:33 +00:00
# @Component.reconfig_on_error
2025-02-05 10:38:40 +00:00
@Component.reconfig_on_error
def write ( self , register , data , * args , data_type = None , gain = None , offset = None , formatting = None , encoding_map = None ,
* * kwargs ) :
try :
if type ( register ) is str :
register , s = self . registers [ register ]
if data_type is None :
data_type = s . get ( " dt " , None )
if gain is None :
gain = s . get ( " g " , None )
if offset is None :
offset = s . get ( " o " , None )
if formatting is None :
formatting = s . get ( " f " , None )
if encoding_map is None :
encoding_map = s . get ( " encoding " , None )
if not len ( args ) :
args = s . get ( " a " , [ ] )
if not len ( kwargs ) :
kwargs = s . get ( " k " , { } )
if data_type is None :
data_type = " 16bit_uint "
if gain is None :
gain = 1
if offset is None :
offset = 0
return super ( ) . write (
2022-07-19 09:59:00 +00:00
register ,
2025-02-05 10:38:40 +00:00
self . _convert_to_format (
data ,
formatting = formatting ,
encoding_map = encoding_map ,
) ,
2022-07-19 09:59:00 +00:00
* args ,
data_type = data_type ,
gain = gain ,
offset = offset ,
* * kwargs ,
2025-02-05 10:38:40 +00:00
)
except Exception as e :
error_message = f " Error during write operation on register { register } with data { data } : { str ( e ) } "
self . log . error ( error_message , exc_info = True )
2025-05-08 12:51:44 +00:00
# Mark connection as lost if it's a connection error
if " Connection error " in str ( e ) or " Cannot connect " in str ( e ) :
self . connection_lost = True
self . log . warning ( " Connection to Tecna Marposs lost during write operation, will attempt to reconnect on next periodic call " )
# Emit a signal to notify the UI of the connection loss
self . tecna_error_signal . emit ( True , error_message )
2025-02-05 10:38:40 +00:00
raise # Re-raise the exception for further upstream handling if needed
2022-06-01 16:37:19 +00:00
2022-09-06 10:06:43 +00:00
@Component.reconfig_on_error
2022-06-01 16:37:19 +00:00
def _get ( self ) :
2025-02-05 10:38:40 +00:00
try :
2025-05-08 12:51:44 +00:00
# If connection was lost, try to reconnect
if self . connection_lost :
try :
self . log . info ( " Attempting to reconnect to Tecna Marposs... " )
if self . connection_type == " ethernet " :
if not self . client . connect ( ) :
self . log . warning ( " Reconnection attempt failed " )
return
else :
if not self . client . connect ( ) :
self . log . warning ( " Reconnection attempt failed " )
return
if not self . client . is_socket_open ( ) :
self . log . warning ( " Reconnection socket not open " )
return
# If we get here, connection was successful
self . log . info ( " Successfully reconnected to Tecna Marposs " )
# Store the previous connection state before updating it
self . _previous_connection_lost = True
self . connection_lost = False
# Emit a signal to notify the UI that the connection has been restored
self . tecna_error_signal . emit ( False , " Connection restored " )
# Force a small delay to ensure the UI has time to process the signal
time . sleep ( 0.1 )
except Exception as e :
self . log . error ( f " Error during reconnection attempt: { str ( e ) } " )
return
2025-02-05 10:38:40 +00:00
# READ INFO
info = { r : self . read ( r ) for r in [
" Real time test pressure output " ,
" Real time differential pressure output " ,
" Real time pressure line regulator " ,
" Active alarm flags " ,
" Active test program number " ,
" Running test: active phase " ,
" Running test: test type " ,
" Running test: sequence index " ,
" Digital inputs status (mask) " ,
# "Digital outputs status (mask)",
] }
if self . model == " t3p " :
pass
elif self . model == " t3l " :
info . update ( { r : self . read ( r ) for r in [
" Active not severe alarm flags " ,
] } )
else :
raise NotImplementedError ( f " Tecna t3 model { self . model !r} not implemented. " )
if info [ " Running test: active phase " ] == " FINE TEST " : # "END TEST, WAITING THE START OF A NEW TEST":
info . update ( self . get_test_results ( ) )
for round_me in [ " measured leak " ] :
if round_me in info . keys ( ) :
info . update ( { round_me : float ( f " { info [ round_me ] : .2f } " ) } )
self . log . debug ( str ( info ) )
super ( ) . _get ( [ info ] )
except Exception as e :
error_message = f " Error during _get operation: { str ( e ) } "
self . log . error ( error_message , exc_info = True )
2025-05-08 12:51:44 +00:00
# Mark connection as lost if it's a connection error
if " Connection error " in str ( e ) or " Cannot connect " in str ( e ) :
self . connection_lost = True
self . log . warning ( " Connection to Tecna Marposs lost, will attempt to reconnect on next periodic call " )
# Emit a signal to notify the UI of the connection loss
self . tecna_error_signal . emit ( True , error_message )
# Don't raise the exception to allow periodic reconnection attempts
# Just return without emitting data
2022-06-08 07:11:38 +00:00
2022-10-04 11:51:36 +00:00
@Component.reconfig_on_error
def _set ( self , val = None ) :
if val is not None : # handle request:
for register , value in val . items ( ) :
print ( register , value )
self . write ( register , value )
super ( ) . _set ( val )
2022-10-18 14:41:53 +00:00
def start_test ( self , table = None ) :
if table is None :
table = self . max_program_number
2022-07-12 08:48:04 +00:00
self . log . info ( f " starting test table { table !r} " )
2022-07-18 10:32:05 +00:00
self . write ( " Source of test program number selection " , " FROM PARAMETER (SET BY LCD OR SERIAL LINE) " )
self . write ( " Selected program " , table )
self . write ( " Start test " , table )
2022-07-04 10:36:51 +00:00
def stop_test ( self ) :
2022-07-12 08:48:04 +00:00
self . log . warning ( " stopping test " )
2022-07-18 10:32:05 +00:00
self . write ( " Reset running test " , 0 )
2022-07-04 10:36:51 +00:00
2022-06-08 07:11:38 +00:00
def get_test_results ( self ) :
2022-07-12 08:48:04 +00:00
self . log . info ( " getting test results " )
2022-06-08 07:11:38 +00:00
return { r : self . read ( r ) for r in [
2023-05-18 14:12:40 +00:00
#"Running test: phase backwards time",
2022-07-18 10:32:05 +00:00
" Running test: filling pressure " ,
" Running test: pressure at the end of settling " ,
2023-05-18 14:12:40 +00:00
#"Running test: burst pressure",
2022-06-08 07:11:38 +00:00
" Running test: measured leak " ,
2023-05-18 14:12:40 +00:00
#"Running test: calculated leak flow rate",
#"Running test: calculate RVP%",
2022-06-08 07:11:38 +00:00
" Running test: result " ,
] }
2022-07-12 08:48:04 +00:00
2022-10-18 14:41:53 +00:00
def write_recipe ( self , recipe , step , table = None ) :
if table is None :
table = self . max_program_number
2022-07-19 09:59:00 +00:00
recipe_name = recipe . part_number [ : 16 ] . encode ( " ascii " )
2022-07-12 13:30:30 +00:00
recipe_name + = b " \x00 " * ( 16 - len ( recipe_name ) )
2022-10-18 14:41:53 +00:00
recipe_barcode = f " j { recipe . part_number } " [ : 16 ] . encode ( " ascii " )
recipe_barcode + = b " \x00 " * ( 24 - len ( recipe_barcode ) )
2025-10-15 13:03:50 +00:00
# Base flags and PID mode
2024-04-22 16:38:34 +00:00
test_flags = 0b0110100001010000 if ( step . spec . get ( " autotest " , False ) in [ " ko_check " ] ) else 0b0110000001010000
2025-10-15 13:03:50 +00:00
pid_mode_text = step . spec . get ( " pid_mod_config " , " AUTO " ) # Get the selected text from the combobox
2025-03-05 15:19:47 +00:00
pid_mode_value = { # Mapping of text to numeric values
" AUTO " : 5 ,
" FAST " : 0 ,
" MEDIUM " : 1 ,
" SLOW " : 2 ,
} . get ( pid_mode_text , 5 )
2025-10-15 13:03:50 +00:00
# Inject PID mode bits (bits 4..6)
2025-03-05 15:19:47 +00:00
test_flags = ( test_flags & ~ ( 7 << 4 ) ) | ( pid_mode_value << 4 )
2025-10-15 13:03:50 +00:00
pid_ramps = 0b0000000000000000 | int ( self . config [ " recipes_defaults " ] [ " pid_level " ] ) << 8 | int ( self . config [ " recipes_defaults " ] [ " pid_speed " ] ) << 12
2025-10-15 11:08:01 +00:00
# Build a robust map of parameters with safe defaults, so Free Fall (minimal spec) won't crash
_defs = self . config . get ( " recipes_defaults " , { } ) or { }
_s = step . spec or { }
2025-10-15 13:03:50 +00:00
2025-10-15 11:08:01 +00:00
def _gv ( key , def_key = None , fallback_key = None ) :
if key in _s :
return _s . get ( key )
if fallback_key and fallback_key in _s :
return _s . get ( fallback_key )
if def_key is not None :
return int ( _defs . get ( def_key , 0 ) ) if isinstance ( _defs . get ( def_key , 0 ) , ( int , float , str ) ) else 0
return 0
2025-10-15 13:03:50 +00:00
# Determine nominal pressure; for Free Fall we allow "filling_pressure" to be the nominal
2025-10-15 11:08:01 +00:00
test_pressure_val = _gv ( " test_pressure " , " pressione_di_test " , fallback_key = " filling_pressure " )
2025-10-15 13:03:50 +00:00
# Free Fall specific behavior
is_free_fall = ( step . step_type == " test_freefall_leak " )
test_type_value = " Leak Test "
if is_free_fall :
# Use Blockage test type for Free Fall as requested (code 2 in registers map)
test_type_value = " Blockage "
# Set/clear T1/Pr (bit 1) based on continuous_filling: True => pressure mode
if bool ( _s . get ( " continuous_filling " , False ) ) :
test_flags | = ( 1 << 1 )
else :
test_flags & = ~ ( 1 << 1 )
# Compute pressure tolerances in % from absolute min/max if provided (for Free Fall)
pr_minus_percent = None
pr_plus_percent = None
try :
nominal = float ( test_pressure_val ) if test_pressure_val is not None else 0.0
if is_free_fall and nominal > 0 :
if _s . get ( " pressure_min " ) is not None :
pmin = max ( 0.0 , float ( _s . get ( " pressure_min " ) ) )
pr_minus_percent = max ( 0.0 , min ( 999.9 , ( ( nominal - pmin ) * 100.0 ) / nominal ) )
if _s . get ( " pressure_max " ) is not None :
pmax = max ( 0.0 , float ( _s . get ( " pressure_max " ) ) )
pr_plus_percent = max ( 0.0 , min ( 999.9 , ( ( pmax - nominal ) * 100.0 ) / nominal ) )
except Exception :
pr_minus_percent = pr_minus_percent if pr_minus_percent is not None else None
pr_plus_percent = pr_plus_percent if pr_plus_percent is not None else None
2022-07-19 09:59:00 +00:00
spec = {
2022-07-18 10:32:05 +00:00
" Flag: Instrument settings " : 0b0000000000000000 ,
" Test program for read/write operation " : table ,
2022-10-18 14:41:53 +00:00
* * { 719 - 1 + i : ( recipe_name [ i * 2 + 1 ] << 8 ) + recipe_name [ i * 2 ] for i in range ( 8 ) } , # program name
* * { 727 - 1 + i : ( recipe_barcode [ i * 2 + 1 ] << 8 ) + recipe_barcode [ i * 2 ] for i in range ( 12 ) } , # program associated bar-code
2022-10-24 10:19:20 +00:00
* * { 761 - 1 + i : ( recipe_name [ i * 2 + 1 ] << 8 ) + recipe_name [ i * 2 ] for i in range ( 8 ) } , # print field 1
# **{769 - 1 + i: (recipe_name[i * 2 + 1] << 8) + recipe_name[i * 2] for i in range(8)}, # print field 2
" Print options " : 0b0000000000000000 | self . saver_label_count << 12 | self . saver_print_on_fail << 8 | self . saver_label_template ,
2025-10-15 13:03:50 +00:00
" Test type " : test_type_value ,
2023-07-24 20:46:19 +00:00
" Test flags " : test_flags ,
2025-10-15 11:08:01 +00:00
" T0 - Pre-filling time " : _gv ( " pre_filling_time " , " tempo_pre_riempimento " ) ,
" P0 - Pre-filling pressure " : _gv ( " pre_filling_pressure " , " pressione_pre_riempimento " ) ,
" T1 - Filling time " : _gv ( " filling_time " , " tempo_riempimento " ) ,
" T2 - Settling time " : _gv ( " settling_time " , " tempo_assestamento " ) ,
2025-10-15 13:03:50 +00:00
" PR- - Min pressure tolerance % " : ( pr_minus_percent if pr_minus_percent is not None else _gv ( " settling_pressure_min_percent " , " percentuale_minima_pressione_assestamento " ) ) ,
" PR+ - Max pressure tolerance % (P+) " : ( pr_plus_percent if pr_plus_percent is not None else _gv ( " settling_pressure_max_percent " , " percentuale_massima_pressione_assestamento " ) ) ,
2025-10-15 11:08:01 +00:00
" T3 - Measure time " : _gv ( " test_time " , " tempo_di_test " ) ,
" Q- Lower test leak limit " : _gv ( " test_pressure_qneg " , " pressione_di_test_delta_minimo " ) ,
" PREL - Nominal test pressure " : test_pressure_val ,
" Q+ Upper test leak limit " : _gv ( " test_pressure_qpos " , " pressione_di_test_delta_massimo " ) ,
" FST - Discharge time " : _gv ( " flush_time " , " tempo_svuotamento " ) ,
" FSL - Discharge limit " : _gv ( " flush_pressure " , " pressione_svuotamento " ) ,
2023-07-01 14:46:33 +00:00
" PSQ - Next sequence program PSOUT mode " : 0 ,
2023-07-24 20:46:19 +00:00
" RAMPS: T1 configuration " : pid_ramps ,
2025-10-15 11:08:01 +00:00
" PID: pressure correction " : _gv ( " pid_pressure_correction " , " pid_pressure_correction " ) ,
2025-03-13 14:41:02 +00:00
" Various flags " : 0b0000000000010000 if self . config [ " recipes_defaults " ] [ " tester_discharge_enable " ] in ( " yes " , " x " ) else 0b0000000000000000
2023-07-01 14:46:33 +00:00
2022-07-12 08:48:04 +00:00
}
2022-09-14 12:47:09 +00:00
if self . model == " t3p " :
pass
elif self . model == " t3l " :
spec . update ( {
" Use programs or use products " : 0 ,
2025-10-15 11:08:01 +00:00
" Nominal peak pressure " : test_pressure_val ,
" Pn - Nominal test pressure " : test_pressure_val ,
2022-09-14 12:47:09 +00:00
} )
else :
2023-07-01 14:16:58 +00:00
raise NotImplementedError ( f " tecna t3 model { self . model !r} not implemented. " )
2022-07-19 09:59:00 +00:00
self . log . debug ( str ( spec ) )
for register , value in spec . items ( ) :
2022-07-12 08:48:04 +00:00
self . write ( register , value )
2025-10-15 13:38:05 +00:00
# Override PR-/PR+ handling for Free Fall (Blockage) to write ABSOLUTE pressures
if is_free_fall :
try :
2025-10-20 09:11:46 +00:00
ptet = _s . get ( " pre_filling_pressure " , None )
2025-10-15 13:38:05 +00:00
pmin = _s . get ( " pressure_min " , None )
pmax = _s . get ( " pressure_max " , None )
# Sanity checks and auto-correct
if pmin is not None and pmax is not None :
try :
pmin_v = float ( pmin )
pmax_v = float ( pmax )
if pmin_v > pmax_v :
self . log . warning ( f " Free Fall: pressure_min ( { pmin_v } ) > pressure_max ( { pmax_v } ); swapping values to maintain consistency " )
pmin_v , pmax_v = pmax_v , pmin_v
pmin , pmax = pmin_v , pmax_v
except Exception :
pass
# Write absolute values using relative pressure low-res format (23) and no gain
if pmin is not None :
2025-10-20 08:09:44 +00:00
self . write ( " PR- - Min pressure tolerance % " , pmin )
2025-10-15 13:38:05 +00:00
self . log . info ( f " Free Fall: wrote PR- (min final pressure) = { pmin } mbar (format 23) " )
if pmax is not None :
2025-10-20 08:09:44 +00:00
self . write ( " PR+ - Max pressure tolerance % (P+) " , pmax )
2025-10-15 13:38:05 +00:00
self . log . info ( f " Free Fall: wrote PR+ (max final pressure) = { pmax } mbar (format 23) " )
2025-10-20 09:11:46 +00:00
if ptet is not None :
self . write ( " P0 - Pre-filling pressure " , ptet )
self . log . info ( f " Free Fall: wrote PR+ (max final pressure) = { ptet } mbar (format 23) " )
2025-10-15 13:38:05 +00:00
except Exception as e :
try :
self . log . exception ( f " Free Fall: failed to write absolute PR-/PR+ values: { e } " )
except Exception :
pass
2024-02-23 17:34:17 +00:00
pass
2022-10-18 14:41:53 +00:00
@db.connection_context ( )
def store_recipes ( self , recipes ) :
2022-10-20 07:20:43 +00:00
if not self . ready :
self . resume ( )
resumed = True
2022-10-18 14:41:53 +00:00
if not self . ready :
QMessageBox . critical (
None ,
2022-10-18 14:45:13 +00:00
" Impossibile salvare le ricette sulla tecna " ,
2022-10-18 14:41:53 +00:00
" La tecna non sembra essere pronta " ,
)
return
recipes = [ ]
for recipe in list ( Recipes . select ( ) . order_by ( Recipes . name . asc ( ) ) ) :
2022-10-20 07:20:43 +00:00
# if recipe.spec["leak_1"]:
2024-09-18 12:18:07 +00:00
recipes . append ( [ recipe , recipe . spec [ " steps " ] [ " leak_1 " ] ] )
2022-10-18 14:41:53 +00:00
# reverve last for our recipe control
if len ( recipes ) > max ( self . max_program_number - 1 , 0 ) :
self . log . warning ( f " too many recipes ( { len ( recipes ) } ), saving only first { max ( self . max_program_number - 1 , 0 ) } " )
QMessageBox . warning (
None ,
2022-10-18 14:45:13 +00:00
" Impossibile salvare tutte le ricette sulla tecna " ,
2022-10-18 14:41:53 +00:00
f " Troppe ricette ( { len ( recipes ) } ), saranno salvate solamente le prime { max ( self . max_program_number - 1 , 0 ) } " ,
)
self . _store_recipes_signal . emit ( recipes [ : max ( self . max_program_number - 1 , 0 ) ] )
self . _store_recipes_lock . acquire ( max ( self . _store_recipes_lock . available ( ) , 1 ) )
2022-10-18 14:47:48 +00:00
QMessageBox . information (
2022-10-18 14:45:13 +00:00
None ,
" Ricette salvate sulla tecna " ,
f " Salvate { min ( len ( recipes ) , max ( self . max_program_number - 1 , 0 ) ) } ricette " ,
)
2022-10-20 07:20:43 +00:00
if resumed :
self . pause ( )
2022-10-18 14:41:53 +00:00
def _store_recipes ( self , recipes ) :
if len ( recipes ) > max ( self . max_program_number - 1 , 0 ) :
self . log . warning ( f " too many recipes ( { len ( recipes ) } ) saving only first { max ( self . max_program_number - 1 , 0 ) } " )
for i , [ recipe , step ] in enumerate ( recipes [ : max ( self . max_program_number - 1 , 0 ) ] , start = 1 ) :
2022-10-24 10:19:20 +00:00
self . log . debug ( f " saving recipe { recipe . part_number } to table { i } " )
self . write_recipe ( recipe , step , table = i )
2022-10-18 14:45:13 +00:00
self . log . info ( f " saved { min ( len ( recipes ) , max ( self . max_program_number - 1 , 0 ) ) } recipes " )
2022-10-18 14:53:17 +00:00
self . _store_recipes_lock . release ( 1 )
2024-09-27 17:11:49 +00:00
2024-10-04 09:39:16 +00:00
# SELECT & READ RECIPE FROM TECNA MEMORY
def read_recipe ( self , recipe_number ) :
2024-09-27 17:11:49 +00:00
spec = {
2024-10-04 13:52:54 +00:00
* * { ( 719 - 1 + i ) : f " R { i } " for i in range ( 8 ) } ,
* * { ( 727 - 1 + i ) : f " C { i } " for i in range ( 12 ) } ,
* * { ( 761 - 1 + i ) : f " F1_ { i } " for i in range ( 8 ) } ,
* * { ( 769 - 1 + i ) : f " F2_ { i } " for i in range ( 8 ) } ,
2024-10-04 09:39:16 +00:00
" T0 - Pre-filling time " : " pre_filling_time " ,
" P0 - Pre-filling pressure " : " pre_filling_pressure " ,
" T1 - Filling time " : " filling_time " ,
" T2 - Settling time " : " settling_time " ,
" PR- - Min pressure tolerance % " : " settling_pressure_min_percent " ,
" PR+ - Max pressure tolerance % (P+) " : " settling_pressure_max_percent " ,
" T3 - Measure time " : " test_time " ,
" Q- Lower test leak limit " : " test_pressure_qneg " ,
" Q+ Upper test leak limit " : " test_pressure_qpos " ,
" FST - Discharge time " : " flush_time " ,
" FSL - Discharge limit " : " flush_pressure " ,
2024-10-04 13:52:54 +00:00
" Print options " : " print_options "
2024-09-27 17:11:49 +00:00
}
if self . model == " t3p " :
spec . update ( {
2024-10-04 09:39:16 +00:00
" PREL - Nominal test pressure " : " test_pressure " ,
2024-09-27 17:11:49 +00:00
} )
elif self . model == " t3l " :
spec . update ( {
2024-11-06 07:57:08 +00:00
" PREL - Nominal test pressure " : " test_pressure " ,
2024-09-27 17:11:49 +00:00
} )
else :
raise NotImplementedError ( f " tecna t3 model { self . model !r} not implemented. " )
# SET RECIPE NUMBER
2024-10-04 13:52:54 +00:00
#self.write("Source of test program number selection", "FROM PARAMETER (SET BY LCD OR SERIAL LINE)")
#self.write("Selected program", recipe_number)
self . write ( " Test program for read/write operation " , recipe_number )
2024-09-27 17:11:49 +00:00
2024-10-04 13:52:54 +00:00
recipe_data = { }
2024-09-27 17:11:49 +00:00
# READ ALL PARAMETERS
for register_name , field_name in spec . items ( ) :
2024-10-04 09:39:16 +00:00
recipe_data [ field_name ] = self . read ( register_name )
2025-02-10 18:01:35 +00:00
time . sleep ( 0.01 )
2024-10-04 13:52:54 +00:00
recipe_name = self . int_array_to_str ( [ recipe_data [ f " R { wn } " ] for wn in range ( 8 ) ] )
recipe_code = self . int_array_to_str ( [ recipe_data [ f " C { wn } " ] for wn in range ( 12 ) ] )
recipe_f1 = self . int_array_to_str ( [ recipe_data [ f " F1_ { wn } " ] for wn in range ( 8 ) ] )
recipe_f2 = self . int_array_to_str ( [ recipe_data [ f " F2_ { wn } " ] for wn in range ( 8 ) ] )
recipe_data [ " recipe_name " ] = recipe_name
recipe_data [ " recipe_code " ] = recipe_code
recipe_data [ " recipe_f1 " ] = recipe_f1
recipe_data [ " recipe_f2 " ] = recipe_f2
2024-11-06 07:57:08 +00:00
recipe_data [ " print_template " ] = int ( recipe_data [ " print_options " ] & 0xFF )
if self . model == " t3p " :
recipe_data [ " test_time " ] = int ( recipe_data [ " test_time " ] / 10 )
recipe_data [ " pre_filling_time " ] = int ( recipe_data [ " pre_filling_time " ] / 10 )
recipe_data [ " filling_time " ] = int ( recipe_data [ " filling_time " ] / 10 )
recipe_data [ " settling_time " ] = int ( recipe_data [ " settling_time " ] / 10 )
recipe_data [ " test_pressure_qneg " ] = int ( recipe_data [ " test_pressure_qneg " ] / 100 )
recipe_data [ " test_pressure_qpos " ] = int ( recipe_data [ " test_pressure_qpos " ] / 100 )
2024-10-04 09:39:16 +00:00
return recipe_data
2024-10-04 13:52:54 +00:00
@staticmethod
def int_array_to_str ( arr ) :
"""
Translates an array of 16 - bit integers , where each integer contains 2 ASCII characters ,
into a string .
Args :
arr : The array of 16 - bit integers .
Returns :
The translated string .
"""
result = " "
for value in arr :
char1 = chr ( value & 0xFF )
char2 = chr ( value >> 8 )
# Stop if we encounter a null byte
if char1 == ' \0 ' :
break
result + = char1
if char2 == ' \0 ' :
break
result + = char2
return result
2025-02-05 10:38:40 +00:00
@pyqtSlot ( str )
def handle_modbus_error ( self , error_message ) :
"""
2025-05-08 12:51:44 +00:00
Handle errors received from the ModbusComponent .
Emit a signal to notify the UI of the error .
2025-02-05 10:38:40 +00:00
"""
if error_message :
2025-05-08 12:51:44 +00:00
# Log the error and emit a signal
2025-02-06 15:17:45 +00:00
self . log . error ( f " Modbus error encountered: { error_message } " )
2025-05-08 12:51:44 +00:00
# Mark connection as lost if it's a connection error
if " Connection error " in error_message or " Cannot connect " in error_message :
self . connection_lost = True
self . log . warning ( " Connection to Tecna Marposs lost, will attempt to reconnect on next periodic call " )
# Emit a signal to notify the UI of the connection loss
self . tecna_error_signal . emit ( True , error_message )
2025-02-05 10:38:40 +00:00
return True , error_message
else :
2025-05-08 12:51:44 +00:00
# Connection restored
if self . connection_lost :
self . connection_lost = False
self . log . info ( " Connection to Tecna Marposs restored " )
# Emit a signal to notify the UI that the connection has been restored
self . tecna_error_signal . emit ( False , " Connection restored " )
return False , None