Index: leahi_dialin/common/fp_defs.py =================================================================== diff -u -r52aa7af16b98730ba59fc2577dbe8f73b5959775 -rbac0a2d09d57dc27cfa2f2a596d57fdcbed392ee --- leahi_dialin/common/fp_defs.py (.../fp_defs.py) (revision 52aa7af16b98730ba59fc2577dbe8f73b5959775) +++ leahi_dialin/common/fp_defs.py (.../fp_defs.py) (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -24,10 +24,16 @@ MODE_STAN = 3 # Standby mode MODE_PRE_GENP = 4 # Pre Generate Permeate Mode MODE_GENP = 5 # Generate Permeate Mode - MODE_NLEG = 6 # Not legal - an illegal mode transition occurred - NUM_OF_FP_MODES = 7 # Number of TD operation modes - + MODE_DPGP = 6 # Defeatured Pre-Generate Permeate Mode + MODE_DEGP = 7 # Defeatured Generate Permeate Mode + MODE_NLEG = 8 # Not legal - an illegal mode transition occurred + NUM_OF_FP_MODES = 9 # Number of TD operation modes + @unique +class FPServiceStates(DialinEnum): + NUM_OF_FP_SERV_STATES = 0 # TODO populate with FP service states + +@unique class FPPostStates(DialinEnum): FP_POST_STATE_START = 0 # Start initialize & POST mode state FP_POST_STATE_FW_INTEGRITY = 1 # Run firmware integrity test state @@ -40,13 +46,65 @@ FP_POST_STATE_COMPLETED = 8 # POST self-tests completed state FP_POST_STATE_FAILED = 9 # POST self-tests failed state NUM_OF_FP_POST_STATES = 10 # Number of initialize & POST mode states - + @unique +class FPFaultStates(DialinEnum): + FP_FAULT_STATE_START = 0 # FP Fault Start State + FP_FAULT_DEENERGIZED_STATE = 1 # FP Fault De-energized State + FP_FAULT_ENERGIZED_STATE = 2 # FP Fault Energized State + NUM_OF_FP_FAULT_STATES = 3 # Number of Fault Mode State + +@unique +class FPStandbyStates(DialinEnum): + FP_STANDBY_MODE_STATE_IDLE = 0 # Idle standby mode state + NUM_OF_FP_STANDBY_MODE_STATES = 1 # Number of standby mode states + +@unique +class FPPreGenPermeateStates(DialinEnum): + FP_PRE_GENP_INLET_PRES_CHECK = 0 # FP Pre Gen Permeate Inlet Pressure Check State + FP_PRE_GENP_FILTER_FLUSH = 1 # FP Pre Gen Permeate Filter Flush State + FP_PRE_GENP_PERMEATE_FLUSH = 2 # FP Pre Gen Permeate Permeate Flush State + FP_PRE_GENP_CONCENTRATE_FLUSH = 3 # FP Pre Gen Permeate Concentrate Flush State + FP_PRE_GENP_VERIFY_WATER = 4 # FP Pre Gen Permeate Verify Water State + FP_PRE_GENP_PAUSED = 5 # FP Pre Gen Permeate Paused State + NUM_OF_FP_PRE_GENP_MODE_STATES = 6 # Number of Pre-Gen Permeate mode states + +@unique +class FPPreGenPDefStates(DialinEnum): + FP_PRE_GENP_DEF_FLUSH = 0 # Pre Gen Permeate Defeatured Flush state + FP_PRE_GENP_DEF_INLET_WATER_CHECK = 1 # Pre Gen Permeate Defeatured Inlet Water Check state + FP_PRE_GENP_DEF_PAUSED = 2 # Defeatured Pre Gen Permeate Paused state + NUM_OF_FP_PRE_GENP_DEF_MODE_STATES = 3 # Number of Defeatured Pre Gen Permeate states + +@unique +class FPGenPermeateStates(DialinEnum): + FP_GENP_TANK_FILL_STATE = 0 # Gen Permeate Tank Fill low state + FP_GENP_TANK_FULL_STATE = 1 # Gen Permeate Tank Full state + NUM_OF_FP_GENP_MODE_STATES = 2 # Number of Gen permeate states + +@unique +class FPGenPermeateDefStates(DialinEnum): + FP_GENP_DEF_SUPPLY_WATER = 0 # Gen Permeate Defeatured Supply Water state + FP_GENP_DEF_PAUSED = 1 # Gen Permeate Defeatured Paused state + NUM_OF_FP_GENP_DEF_MODE_STATES = 2 # Number of gen Permeate states + +@unique +class FPNotLegalStates(DialinEnum): + NUM_OF_NOT_LEGAL_STATES = 0 # TODO: populate with Not Legal states + +@unique class FPEventList(DialinEnum): FP_EVENT_STARTUP = 0 # FP startup event FP_EVENT_OP_MODE_CHANGE = 1 # FP Op mode change event FP_EVENT_SUB_MODE_CHANGE = 2 # FP Op sub-mode change event - NUM_OF_FP_EVENT_IDS = 3 # Total number of FP events + FP_EVENT_PRE_GEN_RO_SET_PWM = 3 # FP gen permeate ro set pwm event + FP_EVENT_GENP_BOOST_SET_PWM = 4 # FP gen permeate boost set pwm event + FP_EVENT_GENP_CHANGE = 5 # FP gen permeate state change + FP_EVENT_PRE_GEN_CHANGE = 6 # FP pre gen state change + FP_EVENT_PRE_GEN_DEF_CHANGE = 7 # FP defeatured pre gen state change + FP_EVENT_GENP_DEF_CHANGE = 8 # FP defeatured pre gen state change + FP_EVENT_FAULT_ALARM_TRIGGER = 9 # FP event for alarms that would trigger + NUM_OF_FP_EVENT_IDS = 10 # Total number of FP events @unique class FPEventDataType(DialinEnum): Index: leahi_dialin/common/td_defs.py =================================================================== diff -u -r52aa7af16b98730ba59fc2577dbe8f73b5959775 -rbac0a2d09d57dc27cfa2f2a596d57fdcbed392ee --- leahi_dialin/common/td_defs.py (.../td_defs.py) (revision 52aa7af16b98730ba59fc2577dbe8f73b5959775) +++ leahi_dialin/common/td_defs.py (.../td_defs.py) (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -14,6 +14,8 @@ # ############################################################################ from enum import unique +from numbers import Number + from ..utils.base import DialinEnum @@ -71,7 +73,7 @@ NUM_OF_TD_TREATMENT_PARAMS_MODE_STATES = 2 # Number of treatment params mode states @unique -class TDPreTreatmentSubModes(DialinEnum): +class TDPreTreatmentModesStates(DialinEnum): TD_PRE_TREATMENT_WATER_SAMPLE_STATE = 0 # Water sample state TD_PRE_TREATMENT_SELF_TEST_CONSUMABLE_STATE = 1 # Consumable self-tests state TD_PRE_TREATMENT_SELF_TEST_NO_CART_STATE = 2 # No cartridge self-tests state @@ -110,10 +112,9 @@ @unique class TDFaultStates(DialinEnum): - TD_FAULT_STATE_START = 0 # Start fault state - TD_FAULT_STATE_RUN_NV_POSTS = 1 # TD fault run NV posts state - TD_FAULT_STATE_COMPLETE = 2 # TD fault run complete state - NUM_OF_TD_FAULT_STATES = 3 # Number of fault mode states + TD_FAULT_ENERGIZED_STATE = 0 # TD fault mode energized state + TD_FAULT_DEENERGIZED_STATE = 1 # TD fault mode deenergized state + NUM_OF_TD_FAULT_STATES = 2 # Number of fault mode states @unique class TDTreatmentStates(DialinEnum): @@ -136,6 +137,15 @@ NUM_OF_DIALYSIS_STATES = 2 # Number of dialysis sub-mode states @unique +class TDServiceStates(DialinEnum): + TD_SERVICE_STATE_START = 0 # Start service mode state + NUM_OF_TD_SERVICE_STATES = 1 # Number of service mode states + +@unique +class TDNotLegalStates(DialinEnum): + NUM_OF_NOT_LEGAL_STATES = 0 # TODO: populate with Not Legal states + +@unique class TDEventList(DialinEnum): TD_EVENT_STARTUP = 0 # TD startup event TD_EVENT_OP_MODE_CHANGE = 1 # TD Op mode change event @@ -151,24 +161,17 @@ TD_EVENT_SW_CONFIG_UPDATE = 11 # TD new software configuration has been updated TD_EVENT_BUTTON = 12 # TD button pressed/released TD_EVENT_SAFETY_LINE = 13 # TD safety line pulled/released - TD_EVENT_RSRVR_1_LOAD_CELL_START_VALUES = 14 # TD reservoir 1 load cells start values - TD_EVENT_RSRVR_1_LOAD_CELL_END_VALUES = 15 # TD reservoir 2 load cells end values - TD_EVENT_RSRVR_2_LOAD_CELL_START_VALUES = 16 # TD reservoir 2 load cells start values - TD_EVENT_RSRVR_2_LOAD_CELL_END_VALUES = 17 # TD reservoir 2 load cells end values - TD_EVENT_SUB_STATE_CHANGE = 18 # TD Op sub-state change event - TD_EVENT_SYRINGE_PUMP_STATE = 19 # TD syringe pump state change event - TD_EVENT_OCCLUSION_BASELINE = 20 # TD event occlusion baseline event - TD_EVENT_RSRVR_UF_VOLUME_AND_TIME = 21 # TD ultrafiltration volume and time for a reservoir use - TD_EVENT_RSRVR_UF_RATE = 22 # TD ultrafiltration measured and expected rates - TD_EVENT_OPERATION_STATUS = 23 # TD operation status event. - TD_EVENT_AIR_TRAP_FILL = 24 # TD initiated an air trap fill (opened VBT briefly). - TD_EVENT_AIR_PUMP_ON_OFF = 25 # TD turned air pump on or off. - TD_EVENT_BLOOD_LEAK_SELF_TEST_RESULT = 26 # TD Blood leak self test result. - TD_EVENT_BLOOD_LEAK_NUM_OF_SET_POINT_CHECK_FAILURES = 27 # TD blood leak number of setpoint check failures - TD_EVENT_DRY_SELF_TEST_PRESSURE_DECAY_WAIT_PERIOD = 28 # TD dry self test pressure decay wait period - TD_EVENT_INSTIT_RECORD_UPDATE = 29 # TD new institutional record has been updated. - TD_EVENT_PARTIAL_OCCLUSION_BASELINE = 30 # TD event partial occlusion baseline event - NUM_OF_EVENT_IDS = 31 # Total number of TD events + TD_EVENT_SUB_STATE_CHANGE = 14 # TD Op sub-state change event + TD_EVENT_RSRVR_UF_RATE = 15 # TD ultrafiltration measured and expected rates + TD_EVENT_OPERATION_STATUS = 16 # TD operation status event. + TD_EVENT_AIR_TRAP_FILL = 17 # TD initiated an air trap fill (opened VBT briefly). + TD_EVENT_AIR_TRAP_LOWER = 18 # TD started/stopped an air trap lower level operation + TD_EVENT_AIR_PUMP_ON_OFF = 19 # TD turned air pump on or off. + TD_EVENT_DRY_SELF_TEST_PRESSURE_DECAY_WAIT_PERIOD = 20 # TD dry self test pressure decay wait period + TD_EVENT_INSTIT_RECORD_UPDATE = 21 # TD new institutional record has been updated. + TD_EVENT_VALVE_POS_CHANGE = 22 # TD pinch valve position change + TD_EVENT_VALVE_HOMED_POS_SETTING = 23 # TD pinch valve homed encoder positions for A/B/C + NUM_OF_EVENT_IDS = 24 # Total number of TD events @unique class TDEventDataType(DialinEnum): Index: leahi_dialin/dd/dialysate_delivery.py =================================================================== diff -u -rba2793fd2b970fc89af085f1dfe4e8b6fe408353 -rbac0a2d09d57dc27cfa2f2a596d57fdcbed392ee --- leahi_dialin/dd/dialysate_delivery.py (.../dialysate_delivery.py) (revision ba2793fd2b970fc89af085f1dfe4e8b6fe408353) +++ leahi_dialin/dd/dialysate_delivery.py (.../dialysate_delivery.py) (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -22,6 +22,7 @@ from .modules.conductivity_sensors import DDConductivitySensors from .modules.constants import NO_RESET, RESET from .modules.dialysate_pump import DDDialysatePumps +from .modules.events import DDEvents from .modules.gen_dialysate import DDGenDialysate from .modules.heaters import DDHeaters from .modules.levels import DDLevels @@ -118,6 +119,7 @@ self.concentrate_pumps = DDConcentratePumps(self.can_interface, self.logger) self.conductivity_sensors = DDConductivitySensors(self.can_interface, self.logger) self.dialysate_pumps = DDDialysatePumps(self.can_interface, self.logger) + self.events = DDEvents(self.can_interface, self.logger) self.gen_dialysate = DDGenDialysate(self.can_interface, self.logger) self.heaters = DDHeaters(self.can_interface, self.logger) self.levels = DDLevels(self.can_interface, self.logger) Index: leahi_dialin/dd/modules/events.py =================================================================== diff -u -re18326881971a5f9c31a26044b3849e8e91df2ce -rbac0a2d09d57dc27cfa2f2a596d57fdcbed392ee --- leahi_dialin/dd/modules/events.py (.../events.py) (revision e18326881971a5f9c31a26044b3849e8e91df2ce) +++ leahi_dialin/dd/modules/events.py (.../events.py) (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -7,8 +7,8 @@ # # @file events.py # -# @author (last) Micahel Garthwaite -# @date (last) 29-Aug-2023 +# @author (last) Jonny Paguio +# @date (last) 15-Sep-2025 # @author (original) Dara Navaei # @date (original) 12-Oct-2021 # @@ -43,12 +43,12 @@ if self.can_interface is not None: channel_id = DenaliChannels.dd_sync_broadcast_ch_id - msg_id = MsgIds.MSG_ID_DD_EVENT.value - self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_events_sync) + self.msg_id_dd_event = MsgIds.MSG_ID_DD_EVENT.value + self.can_interface.register_receiving_publication_function(channel_id, self.msg_id_dd_event, self._handler_events_sync) channel_id = DenaliChannels.dd_sync_broadcast_ch_id - msg_id = MsgIds.MSG_ID_DD_OP_MODE_DATA.value - self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self.msg_id_dd_op_mode_data = MsgIds.MSG_ID_DD_OP_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, self.msg_id_dd_op_mode_data, self._handler_dd_op_mode_sync) @@ -146,7 +146,7 @@ return list_of_events - @publish(["dd_events_timestamp", '_dd_event_dictionary']) + @publish(["msg_id_dd_event", "dd_events_timestamp", '_dd_event_dictionary']) def _handler_events_sync(self, message, timestamp=0.0): """ Handles published events message @@ -285,7 +285,7 @@ self._dd_event_dictionary[event_state_name].append(event_tuple) self.dd_events_timestamp = timestamp - @publish(["dd_event_op_mode_timestamp", "dd_event_op_mode", "dd_event_sub_mode"]) + @publish(["msg_id_dd_op_mode_data", "dd_event_op_mode_timestamp", "dd_event_op_mode", "dd_event_sub_mode"]) def _handler_dd_op_mode_sync(self, message, timestamp=0.0): """ Handles published DD operation mode messages. Current DD operation mode Index: leahi_dialin/fp/filtration_purification.py =================================================================== diff -u -r592d7ab1f37bc69648bd75f3e0886a6fc6f5043e -rbac0a2d09d57dc27cfa2f2a596d57fdcbed392ee --- leahi_dialin/fp/filtration_purification.py (.../filtration_purification.py) (revision 592d7ab1f37bc69648bd75f3e0886a6fc6f5043e) +++ leahi_dialin/fp/filtration_purification.py (.../filtration_purification.py) (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -19,6 +19,7 @@ from .modules.boost_pump import FPBoostPump from .modules.conductivity_sensors import FPConductivitySensors from .modules.constants import NO_RESET, RESET +from .modules.events import FPEvents from .modules.flow_sensors import FPFlowSensors from .modules.levels import FPLevels from .modules.pressure_sensors import FPPressureSensors @@ -107,6 +108,7 @@ self.alarms = FPAlarms(self.can_interface, self.logger) self.boost_pump = FPBoostPump(self.can_interface, self.logger) self.conductivity = FPConductivitySensors(self.can_interface, self.logger) + self.events = FPEvents(self.can_interface, self.logger) self.flows = FPFlowSensors(self.can_interface, self.logger) self.fluid_pumps = FPPumps(self.can_interface, self.logger) self.levels = FPLevels(self.can_interface, self.logger) Index: leahi_dialin/fp/modules/events.py =================================================================== diff -u --- leahi_dialin/fp/modules/events.py (revision 0) +++ leahi_dialin/fp/modules/events.py (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -0,0 +1,303 @@ +########################################################################### +# +# Copyright (c) 2021-2025 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file events.py +# +# @author (last) Jonny Paguio +# @date (last) 16-Sep-2025 +# @author (original) Dara Navaei +# @date (original) 12-Oct-2021 +# +############################################################################ + +import struct +from logging import Logger +from leahi_dialin.common import * +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish +from datetime import datetime +from time import time + + +class FPEvents(AbstractSubSystem): + """ + FP Dialin API sub-class for events related commands. + """ + UNKNOWN_STATE = "UNKNOWN_PREVIOUS_STATE" + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.fp_events_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.fp_sync_broadcast_ch_id + self.msg_id_fp_event = MsgIds.MSG_ID_FP_EVENT.value + self.can_interface.register_receiving_publication_function(channel_id, self.msg_id_fp_event, self._handler_events_sync) + + channel_id = DenaliChannels.fp_sync_broadcast_ch_id + self.msg_id_fp_op_mode_data = MsgIds.MSG_ID_FP_OP_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, self.msg_id_fp_op_mode_data, + self._handler_fp_op_mode_sync) + + + # Define the dictionaries + self._fp_event_dictionary = dict() + self._fp_event_data_type = dict() + + # Dictionary of the mode as key and the sub mode states enum class as the value + self._fp_op_mode_2_sub_mode = {FPOpModes.MODE_FAUL.name: FPFaultStates, + FPOpModes.MODE_SERV.name: FPServiceStates, + FPOpModes.MODE_INIT.name: FPPostStates, + FPOpModes.MODE_STAN.name: FPStandbyStates, + FPOpModes.MODE_PRE_GENP.name: FPPreGenPermeateStates, + FPOpModes.MODE_GENP.name: FPGenPermeateStates, + FPOpModes.MODE_DPGP.name: FPPreGenPDefStates, + FPOpModes.MODE_DEGP.name: FPGenPermeateDefStates, + FPOpModes.MODE_NLEG.name: FPNotLegalStates} + + # Loop through the list of the FP events enums and initial the event dictionary. Each event is a key in the + # dictionary and the value is a list. + for event in FPEventList: + self._fp_event_dictionary[FPEventList(event).name] = [] + + # Loop through the list of the event data type enum and update the dictionary + for data_type in FPEventDataType: + event_data_type = FPEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self._fp_event_data_type[event_data_type] = struct_unpack_type + + def get_fp_nth_event(self, event_id, event_number=0): + """ + Returns the nth requested FP event + + @param event_id the ID of the FP event types (i.e. FP_EVENT_STARTUP) + @param event_number the event number that is requested. The default is 0 meaning the last occurred event + + @returns the requested FP event number + """ + list_length = len(self._fp_event_dictionary[FPEventList(event_id).name]) + + if list_length == 0: + event = [] + elif event_number > list_length: + event = self._fp_event_dictionary[FPEventList(event_id).name][list_length - 1] + else: + event = self._fp_event_dictionary[FPEventList(event_id).name][list_length - event_number - 1] + + return event + + def clear_fp_event_list(self): + """ + Clears the FP event list + + @returns none + """ + for key in self._fp_event_dictionary: + self._fp_event_dictionary[key].clear() + + def get_fp_events(self, event_id, number_of_events=1): + """ + Returns the requested number of a certain FP event ID + + @param event_id the ID of the FP event types (i.e. FP_EVENT_STARTUP) + @param number_of_events the last number of messages of a certain event type + + @returns a list of the requested FP event type + """ + list_of_events = [] + + # If there are not enough event lists send all the events that are available + if len(self._fp_event_dictionary[FPEventList(event_id).name]) <= number_of_events: + list_of_events = self._fp_event_dictionary[FPEventList(event_id).name] + else: + # Get the all the events + complete_list = self._fp_event_dictionary[FPEventList(event_id).name] + # Since the last are located at the end of the list, iterate backwards for the defined + # event messages + for i in range(len(complete_list) - 1, len(complete_list) - number_of_events - 1, -1): + list_of_events.append(complete_list[i]) + + if number_of_events == 0: + list_of_events = self._fp_event_dictionary[FPEventList(event_id).name] + + return list_of_events + + @publish(["msg_id_fp_event", "fp_events_timestamp", '_fp_event_dictionary']) + def _handler_events_sync(self, message, timestamp=0.0): + """ + Handles published events message + + @param message: published FP events data message + @returns none + """ + event_data_1 = 0 + event_data_2 = 0 + op_mode = 0 + sub_mode = 0 + sub_state = 0 + current_sub_tuple = [] + + event_id = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if event_id == FPEventList.FP_EVENT_OPERATION_STATUS.value: + # Get the data type + event_data_type_1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + struct_data_type = self._fp_event_data_type[FPEventDataType(event_data_type_1).name] + op_mode = struct.unpack(' current_sub_mode_timestamp: + # If the previous and current of the last two tuples do not match, then an operation mode transition + # has occurred and the previous state is converted from the previous class and the current op mode + # is converted from current operation states enum class. + # i.e last = (timestamp, event type, 3, 8) and one before = (timestamp, event type, 8, 3) + # previous and current do not match so in the last type (timestamp, event type, 8, 3) the prev state + # should be from op mode 8 and the current state should be from op mode 3 + previous_op_mode = last_op_tuple[len(last_op_tuple) - 2] + if previous_op_mode != FPEvents.UNKNOWN_STATE: + previous_sub_mode_enum_class = self._fp_op_mode_2_sub_mode[previous_op_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + # Unknown previous state. Display value instead of name. + else: + event_data_1 = str(event_data_1) + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + + if event_data_2 != 0: + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + previous_sub_mode = current_sub_tuple[len(current_sub_tuple) - 2] + previous_sub_mode_enum_class = self._fp_op_mode_2_sub_mode[previous_sub_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + event_tuple = (datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S.%f'), event_state_name, event_data_1, event_data_2) + + elif event_state_name == FPEventList.FP_EVENT_OPERATION_STATUS.name: + event_tuple = (time(), op_mode, sub_mode, sub_state) + + # Update event dictionary + self._fp_event_dictionary[event_state_name].append(event_tuple) + self.fp_events_timestamp = timestamp + + @publish(["msg_id_fp_op_mode_data", "fp_event_op_mode_timestamp", "fp_event_op_mode", "fp_event_sub_mode"]) + def _handler_fp_op_mode_sync(self, message, timestamp=0.0): + """ + Handles published FP operation mode messages. Current FP operation mode + is captured for reference. + + @param message: published FP operation mode broadcast message + @return: None + """ + + mode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + smode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.fp_event_op_mode = mode[0] + self.fp_event_sub_mode = smode[0] + self.fp_event_op_mode_timestamp = timestamp \ No newline at end of file Index: leahi_dialin/td/modules/events.py =================================================================== diff -u --- leahi_dialin/td/modules/events.py (revision 0) +++ leahi_dialin/td/modules/events.py (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -0,0 +1,303 @@ +########################################################################### +# +# Copyright (c) 2021-2025 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file events.py +# +# @author (last) Jonny Paguio +# @date (last) 16-Sep-2025 +# @author (original) Dara Navaei +# @date (original) 12-Oct-2021 +# +############################################################################ + +import struct +from logging import Logger +from leahi_dialin.common import * +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish +from datetime import datetime +from time import time + + +class TDEvents(AbstractSubSystem): + """ + TD Dialin API sub-class for events related commands. + """ + UNKNOWN_STATE = "UNKNOWN_PREVIOUS_STATE" + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.td_events_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.td_sync_broadcast_ch_id + self.msg_id_td_event = MsgIds.MSG_ID_TD_EVENT.value + self.can_interface.register_receiving_publication_function(channel_id, self.msg_id_td_event, self._handler_events_sync) + + channel_id = DenaliChannels.td_sync_broadcast_ch_id + self.msg_id_td_op_mode_data = MsgIds.MSG_ID_TD_OP_MODE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, self.msg_id_td_op_mode_data, + self._handler_td_op_mode_sync) + + + # Define the dictionaries + self._td_event_dictionary = dict() + self._td_event_data_type = dict() + + # Dictionary of the mode as key and the sub mode states enum class as the value + self._td_op_mode_2_sub_mode = {TDOpModes.MODE_FAUL.name: TDFaultStates, + TDOpModes.MODE_SERV.name: TDServiceStates, + TDOpModes.MODE_INIT.name: TDInitStates, + TDOpModes.MODE_STAN.name: TDStandbyStates, + TDOpModes.MODE_TPAR.name: TDTreatmentParamStates, + TDOpModes.MODE_PRET.name: TDPreTreatmentModesStates, + TDOpModes.MODE_TREA.name: TDTreatmentStates, + TDOpModes.MODE_POST.name: TDPostTreatmentStates, + TDOpModes.MODE_NLEG.name: TDNotLegalStates} + + # Loop through the list of the TD events enums and initial the event dictionary. Each event is a key in the + # dictionary and the value is a list. + for event in TDEventList: + self._td_event_dictionary[TDEventList(event).name] = [] + + # Loop through the list of the event data type enum and update the dictionary + for data_type in TDEventDataType: + event_data_type = TDEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self._td_event_data_type[event_data_type] = struct_unpack_type + + def get_td_nth_event(self, event_id, event_number=0): + """ + Returns the nth requested TD event + + @param event_id the ID of the TD event types (i.e. TD_EVENT_STARTUP) + @param event_number the event number that is requested. The default is 0 meaning the last occurred event + + @returns the requested TD event number + """ + list_length = len(self._td_event_dictionary[TDEventList(event_id).name]) + + if list_length == 0: + event = [] + elif event_number > list_length: + event = self._td_event_dictionary[TDEventList(event_id).name][list_length - 1] + else: + event = self._td_event_dictionary[TDEventList(event_id).name][list_length - event_number - 1] + + return event + + def clear_td_event_list(self): + """ + Clears the TD event list + + @returns none + """ + for key in self._td_event_dictionary: + self._td_event_dictionary[key].clear() + + def get_td_events(self, event_id, number_of_events=1): + """ + Returns the requested number of a certain TD event ID + + @param event_id the ID of the TD event types (i.e. TD_EVENT_STARTUP) + @param number_of_events the last number of messages of a certain event type + + @returns a list of the requested TD event type + """ + list_of_events = [] + + # If there are not enough event lists send all the events that are available + if len(self._td_event_dictionary[TDEventList(event_id).name]) <= number_of_events: + list_of_events = self._td_event_dictionary[TDEventList(event_id).name] + else: + # Get the all the events + complete_list = self._td_event_dictionary[TDEventList(event_id).name] + # Since the last are located at the end of the list, iterate backwards for the defined + # event messages + for i in range(len(complete_list) - 1, len(complete_list) - number_of_events - 1, -1): + list_of_events.append(complete_list[i]) + + if number_of_events == 0: + list_of_events = self._td_event_dictionary[TDEventList(event_id).name] + + return list_of_events + + @publish(["msg_id_td_event", "td_events_timestamp", '_td_event_dictionary']) + def _handler_events_sync(self, message, timestamp=0.0): + """ + Handles published events message + + @param message: published TD events data message + @returns none + """ + event_data_1 = 0 + event_data_2 = 0 + op_mode = 0 + sub_mode = 0 + sub_state = 0 + current_sub_tuple = [] + + event_id = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if event_id == TDEventList.TD_EVENT_OPERATION_STATUS.value: + # Get the data type + event_data_type_1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + struct_data_type = self._td_event_data_type[TDEventDataType(event_data_type_1).name] + op_mode = struct.unpack(' current_sub_mode_timestamp: + # If the previous and current of the last two tuples do not match, then an operation mode transition + # has occurred and the previous state is converted from the previous class and the current op mode + # is converted from current operation states enum class. + # i.e last = (timestamp, event type, 3, 8) and one before = (timestamp, event type, 8, 3) + # previous and current do not match so in the last type (timestamp, event type, 8, 3) the prev state + # should be from op mode 8 and the current state should be from op mode 3 + previous_op_mode = last_op_tuple[len(last_op_tuple) - 2] + if previous_op_mode != TDEvents.UNKNOWN_STATE: + previous_sub_mode_enum_class = self._td_op_mode_2_sub_mode[previous_op_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + # Unknown previous state. Display value instead of name. + else: + event_data_1 = str(event_data_1) + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + + if event_data_2 != 0: + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + previous_sub_mode = current_sub_tuple[len(current_sub_tuple) - 2] + previous_sub_mode_enum_class = self._td_op_mode_2_sub_mode[previous_sub_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + event_tuple = (datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S.%f'), event_state_name, event_data_1, event_data_2) + + elif event_state_name == TDEventList.TD_EVENT_OPERATION_STATUS.name: + event_tuple = (time(), op_mode, sub_mode, sub_state) + + # Update event dictionary + self._td_event_dictionary[event_state_name].append(event_tuple) + self.td_events_timestamp = timestamp + + @publish(["msg_id_td_op_mode_data", "td_event_op_mode_timestamp", "td_event_op_mode", "td_event_sub_mode"]) + def _handler_td_op_mode_sync(self, message, timestamp=0.0): + """ + Handles published TD operation mode messages. Current TD operation mode + is captured for reference. + + @param message: published TD operation mode broadcast message + @return: None + """ + + mode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + smode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.td_event_op_mode = mode[0] + self.td_event_sub_mode = smode[0] + self.td_event_op_mode_timestamp = timestamp \ No newline at end of file Index: leahi_dialin/td/treatment_delivery.py =================================================================== diff -u -r592d7ab1f37bc69648bd75f3e0886a6fc6f5043e -rbac0a2d09d57dc27cfa2f2a596d57fdcbed392ee --- leahi_dialin/td/treatment_delivery.py (.../treatment_delivery.py) (revision 592d7ab1f37bc69648bd75f3e0886a6fc6f5043e) +++ leahi_dialin/td/treatment_delivery.py (.../treatment_delivery.py) (revision bac0a2d09d57dc27cfa2f2a596d57fdcbed392ee) @@ -23,6 +23,7 @@ from .modules.buttons import TDButtons from .modules.constants import NO_RESET, RESET from .modules.ejector import TDEjector +from .modules.events import TDEvents from .modules.pressure_sensors import TDPressureSensors from .modules.switches import TDSwitches from .modules.treatment import TDTreatment @@ -121,6 +122,7 @@ self.bubbles = TDBubbleDetector(self.can_interface, self.logger) self.buttons = TDButtons(self.can_interface, self.logger) self.ejector = TDEjector(self.can_interface, self.logger) + self.events = TDEvents(self.can_interface, self.logger) self.pressure_sensors = TDPressureSensors(self.can_interface, self.logger) self.switches = TDSwitches(self.can_interface, self.logger) self.treatment = TDTreatment(self.can_interface, self.logger)