Index: leahi_dialin/common/dd_defs.py =================================================================== diff -u -rbebba1d868f26b10c5cc045fbcb7ca2b067267a6 -re18326881971a5f9c31a26044b3849e8e91df2ce --- leahi_dialin/common/dd_defs.py (.../dd_defs.py) (revision bebba1d868f26b10c5cc045fbcb7ca2b067267a6) +++ leahi_dialin/common/dd_defs.py (.../dd_defs.py) (revision e18326881971a5f9c31a26044b3849e8e91df2ce) @@ -33,7 +33,7 @@ NUM_OF_DD_MODES = 11 # Number of Modes for the DD @unique -class DDPostStates(DialinEnum): +class DDInitStates(DialinEnum): DD_POST_STATE_START = 0 # Post Generate Dialysate Start State DD_POST_STATE_FW_COMPATIBILITY = 1 # Firmware Compatibility State DD_POST_STATE_FW_INTEGRITY = 2 # Firmware Integrity State @@ -52,12 +52,19 @@ @unique class DDFaultStates(DialinEnum): - DD_FAULT_STATE_START = 0 # DD fault start state - DD_FAULT_STATE_RUN_NV_POSTS = 1 # DD fault run RTC and NV data management post - DD_FAULT_STATE_COMPLETE = 2 # DD fault complete - NUM_OF_DD_FAULT_STATES = 3 # Number of fault mode states + DD_FAULT_STATE_START = 0 # DD fault start state + DD_FAULT_DEENERGIZED_STATE = 1 # DD fault de-energized state + DD_FAULT_ENERGIZED_STATE_ = 2 # DD fault energized state + NUM_OF_DD_FAULT_STATES = 3 # Number of fault mode states @unique +class DDFaultNVDataStates(DialinEnum): + DD_FAULT_STATE_START = 0 # DD fault start state + DD_FAULT_STATE_RUN_NV_POSTS = 1 # DD fault run RTC and NV data management post + DD_FAULT_STATE_COMPLETE = 2 # DD fault complete + NUM_OF_DD_FAULT_NVDATA_STATES = 3 # Number of fault mode NV Data management states + +@unique class DDStandByModeStates(DialinEnum): DD_STANDBY_MODE_STATE_IDLE = 0 # Idle standby mode state DD_STANDBY_MODE_STATE_PAUSE = 1 # Pause state @@ -66,17 +73,20 @@ @unique class DDPreGenDialysateStates(DialinEnum): DD_PRE_GEN_DIALYSATE_HYD_CHAMBER_FILL_CHECK = 0, # DD Pre-Gen Dialysate hyrochamber fill check - DD_PRE_GEN_DIALYSATE_WAIT_FOR_GEND = 1 # DD Pre-Gen Dialysate Wait for Gen Dialysate state transition - NUM_OF_DD_PRE_GEN_DIALYSATE_STATES = 2 # Number of Pre-Gen Dialysate mode states + DD_PRE_GEN_WET_SELF_TEST = 1 # DD Pre-Gen Wet Self Test + DD_PRE_GEN_DIALYSATE_WAIT_FOR_GEND = 2 # DD Pre-Gen Dialysate Wait for Gen Dialysate state transition + NUM_OF_DD_PRE_GEN_DIALYSATE_STATES = 3 # Number of Pre-Gen Dialysate mode states @unique class DDGenDialysateModeStates(DialinEnum): DD_GEND_STATE_START = 0 # Gen dialysate start state DD_GEND_DIALYSATE_BYPASS_STATE = 1 # Gen dialysate - Bypass dialysate state DD_GEND_DIALYSATE_DELIVERY_STATE = 2 # Gen dialysate - deliver dialysate state - DD_GEND_DIALYSATE_DELIVERY_PAUSE = 3 # Gen dialysate - dialysate delivery pause state - DD_GEND_ISOLATED_UF_STATE = 4 # Gen dialysate - Isolated ultrafiltration state - NUM_OF_DD_GEND_MODE_STATES = 5 # Number of gen dialysate states + DD_GEND_ISOLATED_UF_STATE = 3 # Gen dialysate - Isolated ultrafiltration state + DD_GEND_SPENT_CHAMBER_FILL_STATE = 4 # Gen dialysate - spent chamber fill state + DD_GEND_BICARB_CHAMBER_FILL_STATE = 5 # Gen dialysate - bicarb chamber fill state + DD_GEND_DIALYSATE_DELIVERY_PAUSE = 6 # Gen dialysate - dialysate delivery pause state + NUM_OF_DD_GEND_MODE_STATES = 7 # Number of gen dialysate states @unique class DDPostGenDialysateState(DialinEnum): @@ -211,11 +221,17 @@ @unique class DDHeatersState(DialinEnum): + DD_HEAT_DISINFECT_STATE_START = 0 # Heat disinfect, start mode state + DD_HEAT_DISINFECT_STATE_COMPLETE = 1 # Heat disinfect, complete state + NUM_OF_DD_HEAT_DISINFECT_STATES = 2 # Number of heat disinfect mode states + +@unique +class DDHeatersExecState(DialinEnum): HEATER_EXEC_STATE_OFF = 0 # Heater Execution Off State HEATER_EXEC_STATE_RAMP_TO_TARGET = 1 # Heater Execution Ramp to Target State HEATER_EXEC_STATE_CONTROL_TO_TARGET = 2 # Heater Execution Control to Target State HEATER_EXEC_STATE_CONTROL_TO_DISINFECT_TARGET = 3 # Heater Execution Control to Disinfection State - NUM_OF_HEATERS_STATE = 4 + NUM_OF_HEATERS_EXEC_STATE = 4 @unique class DDHeatersNames(DialinEnum): @@ -235,6 +251,10 @@ NUM_OF_DD_HEATER_ATTRIBUTES = 7 # Number of Heater Attributes @unique +class DDHeatersCoolingStates(DialinEnum): + NUM_OF_DD_HEAT_COOL_STATES = 0 # TODO: populate with heater cooling states + +@unique class DDLevelSensorEnum(DialinEnum): D6_LEVEL = 0 # floater switch low, medium and high status D63_LEVEL = 1 # bicarb level low or high status @@ -274,6 +294,10 @@ TEMP = 1 # Temperature Attribute for Pressure Sensors NUM_OF_PRES_SENSOR_ATTRIBUTES = 2 # Number of Pressure Sensor Attributes +@unique +class DDROPermeateStates(DialinEnum): + NUM_OF_RO_PERMEATE_STATES = 0 # TODO: populate with RO Permeate states + COND_SENSOR_INDEX_OFFSET = 6 # Conductivity Sensor Index Offset for the Temperature Sensors @unique @@ -330,3 +354,7 @@ D70_VALV = 26 # Ultrafiltration Valve 1 Outlet (D70) D72_VALV = 27 # Ultrafiltration Valve 2 Outlet (D72) NUM_OF_DD_VALVES = 28 # Number of Valves for the DD + +@unique +class DDNotLegalStates(DialinEnum): + NUM_OF_NOT_LEGAL_STATES = 0 # TODO: populate with Not Legal states Index: leahi_dialin/dd/modules/events.py =================================================================== diff -u --- leahi_dialin/dd/modules/events.py (revision 0) +++ leahi_dialin/dd/modules/events.py (revision e18326881971a5f9c31a26044b3849e8e91df2ce) @@ -0,0 +1,305 @@ +########################################################################### +# +# 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) Micahel Garthwaite +# @date (last) 29-Aug-2023 +# @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 DDEvents(AbstractSubSystem): + """ + Dialysate Delivery (DD) 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.dd_events_timestamp = 0.0 + + 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) + + 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._handler_dd_op_mode_sync) + + + # Define the dictionaries + self._dd_event_dictionary = dict() + self._dd_event_data_type = dict() + + # Dictionary of the mode as key and the sub mode states enum class as the value + self._dd_op_mode_2_sub_mode = {DDOpModes.MODE_FAUL.name: DDFaultStates, + DDOpModes.MODE_SERV.name: DDServiceModesStates, + DDOpModes.MODE_INIT.name: DDInitStates, + DDOpModes.MODE_STAN.name: DDStandByModeStates, + DDOpModes.MODE_PREG.name: DDPreGenDialysateStates, + DDOpModes.MODE_GEND.name: DDGenDialysateModeStates, + DDOpModes.MODE_POSG.name: DDPostGenDialysateState, + DDOpModes.MODE_HEAT.name: DDHeatersState, + DDOpModes.MODE_HCOL.name: DDHeatersCoolingStates, + DDOpModes.MODE_ROPS.name: DDROPermeateStates, + DDOpModes.MODE_NLEG.name: DDNotLegalStates} + + # Loop through the list of the DD events enums and initial the event dictionary. Each event is a key in the + # dictionary and the value is a list. + for event in DDEventList: + self._dd_event_dictionary[DDEventList(event).name] = [] + + # Loop through the list of the event data type enum and update the dictionary + for data_type in DDEventDataType: + event_data_type = DDEventDataType(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._dd_event_data_type[event_data_type] = struct_unpack_type + + def get_dd_nth_event(self, event_id, event_number=0): + """ + Returns the nth requested DD event + + @param event_id the ID of the DD event types (i.e. DD_EVENT_STARTUP) + @param event_number the event number that is requested. The default is 0 meaning the last occurred event + + @returns the requested DD event number + """ + list_length = len(self._dd_event_dictionary[DDEventList(event_id).name]) + + if list_length == 0: + event = [] + elif event_number > list_length: + event = self._dd_event_dictionary[DDEventList(event_id).name][list_length - 1] + else: + event = self._dd_event_dictionary[DDEventList(event_id).name][list_length - event_number - 1] + + return event + + def clear_dd_event_list(self): + """ + Clears the DD event list + + @returns none + """ + for key in self._dd_event_dictionary: + self._dd_event_dictionary[key].clear() + + def get_dd_events(self, event_id, number_of_events=1): + """ + Returns the requested number of a certain DD event ID + + @param event_id the ID of the DD event types (i.e. DD_EVENT_STARTUP) + @param number_of_events the last number of messages of a certain event type + + @returns a list of the requested DD event type + """ + list_of_events = [] + + # If there are not enough event lists send all the events that are available + if len(self._dd_event_dictionary[DDEventList(event_id).name]) <= number_of_events: + list_of_events = self._dd_event_dictionary[DDEventList(event_id).name] + else: + # Get the all the events + complete_list = self._dd_event_dictionary[DDEventList(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._dd_event_dictionary[DDEventList(event_id).name] + + return list_of_events + + @publish(["dd_events_timestamp", '_dd_event_dictionary']) + def _handler_events_sync(self, message, timestamp=0.0): + """ + Handles published events message + + @param message: published DD 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 == DDEventList.DD_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._dd_event_data_type[DDEventDataType(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 != DDEvents.UNKNOWN_STATE: + previous_sub_mode_enum_class = self._dd_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._dd_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 == DDEventList.DD_EVENT_OPERATION_STATUS.name: + event_tuple = (time(), op_mode, sub_mode, sub_state) + + # Update event dictionary + 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"]) + def _handler_dd_op_mode_sync(self, message, timestamp=0.0): + """ + Handles published DD operation mode messages. Current DD operation mode + is captured for reference. + + @param message: published DD 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.dd_event_op_mode = mode[0] + self.dd_event_sub_mode = smode[0] + self.dd_event_op_mode_timestamp = timestamp \ No newline at end of file Index: leahi_dialin/protocols/CAN.py =================================================================== diff -u -r4f967e4bb79cf0137397c6bad0f408df52f77fe9 -re18326881971a5f9c31a26044b3849e8e91df2ce --- leahi_dialin/protocols/CAN.py (.../CAN.py) (revision 4f967e4bb79cf0137397c6bad0f408df52f77fe9) +++ leahi_dialin/protocols/CAN.py (.../CAN.py) (revision e18326881971a5f9c31a26044b3849e8e91df2ce) @@ -393,9 +393,9 @@ td_to_ui_ch_id = 0x040 ui_to_td_ch_id = 0x041 td_sync_broadcast_ch_id = 0x100 - dd_sync_broadcast_ch_id = 0x101 + dd_sync_broadcast_ch_id = 0x101 # Also acts as the dd_to_ui_ch_id fp_sync_broadcast_ch_id = 0x102 - ui_sync_broadcast_ch_id = 0x103 + ui_sync_broadcast_ch_id = 0x103 # Also acts as the ui_to_dd_ch_id dialin_to_td_ch_id = 0x400 td_to_dialin_ch_id = 0x401 dialin_to_dd_ch_id = 0x402