Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/concentrate_pump.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/conductivity_sensors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/dialysate_pump.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/heaters.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/levels.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/pressure_sensors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/temperature_sensors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/modules/valves.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/proxies/__init__.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi-dialin/dd/proxies/td_proxy.py'. Fisheye: No comparison available. Pass `N' to diff? Index: leahi_dialin/common/dd_defs.py =================================================================== diff -u -r2138d06d100fdcf23f2e9069f35ee2fdee62008f -rfd7a25d8f068bcba594c01410a02a03f6afbcd59 --- leahi_dialin/common/dd_defs.py (.../dd_defs.py) (revision 2138d06d100fdcf23f2e9069f35ee2fdee62008f) +++ leahi_dialin/common/dd_defs.py (.../dd_defs.py) (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -19,15 +19,15 @@ @unique class DDOpModes(DialinEnum): - DD_MODE_FAUL = 0 - DD_MODE_SERV = 1 - DD_MODE_INIT = 2 - DD_MODE_STAN = 3 - DD_MODE_GEND = 4 - DD_MODE_HEAT = 5 - DD_MODE_HCOL = 6 - DD_MODE_ROPS = 7 - DD_MODE_NLEG = 8 + MODE_FAUL = 0 + MODE_SERV = 1 + MODE_INIT = 2 + MODE_STAN = 3 + MODE_GEND = 4 + MODE_HEAT = 5 + MODE_HCOL = 6 + MODE_ROPS = 7 + MODE_NLEG = 8 NUM_OF_DD_MODES = 9 @unique Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi_dialin/dd/conductivity_sensors.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi_dialin/dd/constants.py'. Fisheye: No comparison available. Pass `N' to diff? Index: leahi_dialin/dd/dialysate_delivery.py =================================================================== diff -u -r2138d06d100fdcf23f2e9069f35ee2fdee62008f -rfd7a25d8f068bcba594c01410a02a03f6afbcd59 --- leahi_dialin/dd/dialysate_delivery.py (.../dialysate_delivery.py) (revision 2138d06d100fdcf23f2e9069f35ee2fdee62008f) +++ leahi_dialin/dd/dialysate_delivery.py (.../dialysate_delivery.py) (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -1 +1,254 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 treatment_delivery.py +# +# @author (last) Dara Navaei +# @date (last) 26-Feb-2024 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct + +from .modules.concentrate_pump import DDConcentratePumps +from .modules.conductivity_sensors import DDConductivitySensors +from .modules.dialysate_pump import DDDialysatePumps +from .modules.heaters import DDHeaters +from .modules.levels import DDLevels +from .modules.pressure_sensors import DDPressureSensors +from .modules.temperature_sensors import DDTemperatureSensors +from .modules.valves import DDValves + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..common.dd_defs import DDOpModes +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, LogManager +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, unsigned_short_to_bytearray, bytearray_to_integer, \ + bytearray_to_byte + + +class DD(AbstractSubSystem): + """ + Treatment Delivery (DD) Dialin object API. + It provides the basic interface to communicate with the DD firmware. + """ + # DD debug event max count + _DD_DEBUG_EVENT_LIST_COUNT = 10 + _DD_DEBUG_EVENT_MSG_LEN_INDEX = 5 + + # DD login password + DD_LOGIN_PASSWORD = '123' + + # UI version message field positions + START_POS_MAJOR = DenaliMessage.PAYLOAD_START_INDEX + END_POS_MAJOR = START_POS_MAJOR + 1 + START_POS_MINOR = END_POS_MAJOR + END_POS_MINOR = START_POS_MINOR + 1 + START_POS_MICRO = END_POS_MINOR + END_POS_MICRO = START_POS_MICRO + 1 + START_POS_BUILD = END_POS_MICRO + END_POS_BUILD = START_POS_BUILD + 2 + START_POS_COMPATIBILITY_REV = END_POS_BUILD + END_POS_COMPATIBILITY_REV = START_POS_COMPATIBILITY_REV + 4 + + def __init__(self, can_interface="can0", log_level=None): + """ + DD object provides test/service commands for the DD sub-system. + + >> DD_object = DD('can0') + >> DD_object = DD(can_interface='can0', log_level="DEBUG") + + Possible log levels: + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "CAN_ONLY", "PRINT_ONLY"] + + @param can_interface: (str) CANBus interface name, e.g. "can0" + @param log_level: (str) Logging level, defaults to None + """ + + super().__init__() + self._log_manager = LogManager(log_level=log_level, log_filepath=self.__class__.__name__ + ".log") + self.logger = self._log_manager.logger + + # Create listener + self.can_interface = DenaliCanMessenger(can_interface=can_interface, + logger=self.logger) + self.can_interface.start() + self.callback_id = None + # register handler for DD operation mode broadcast messages + if self.can_interface is not None: + 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) + + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_DD_DEBUG_EVENT.value, + self._handler_dd_debug_event_sync) + + # create properties + self.dd_op_mode_timestamp = 0.0 + self.dd_debug_events_timestamp = 0.0 + self.ui_version_info_response_timestamp = 0.0 + self.dd_operation_mode = DDOpModes.MODE_INIT.value + self.dd_operation_sub_mode = 0 + self.dd_logged_in = False + self.dd_set_logged_in_status(False) + self.ui_version = None + self.dd_debug_events = [''] * self._DD_DEBUG_EVENT_LIST_COUNT + self.dd_debug_event_index = 0 + self.dd_last_debug_event = '' + + # Create command groups + + 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.heaters = DDHeaters(self.can_interface, self.logger) + self.levels = DDLevels(self.can_interface, self.logger) + self.pressure_sensors = DDPressureSensors(self.can_interface, self.logger) + self.temperature_sensors = DDTemperatureSensors(self.can_interface, self.logger) + self.valves = DDValves(self.can_interface, self.logger) + + + @publish(["dd_debug_events_timestamp","dd_debug_events"]) + def _handler_dd_debug_event_sync(self, message, timestamp = 0.0): + + payload = message['message'] + message_length = payload[self._DD_DEBUG_EVENT_MSG_LEN_INDEX] + temp_message = '' + + index = MsgFieldPositions.START_POS_FIELD_1 + + for i in range(0, message_length): + # Loop through the length and get the + char, char_index = bytearray_to_byte(payload, index + i, False) + temp_message += chr(char) + + self.dd_debug_events_timestamp = timestamp + self.dd_debug_events.insert(self.dd_debug_event_index, temp_message) + self.dd_last_debug_event = temp_message + + self.dd_debug_event_index += 1 + if self.dd_debug_event_index == self._DD_DEBUG_EVENT_LIST_COUNT: + self.dd_debug_event_index = 0 + + @publish(["dd_logged_in"]) + def dd_set_logged_in_status(self, logged_in: bool = False): + """ + Callback for dd logged in status change. + @param logged_in boolean logged in status for DD + @return: none + """ + self.dd_logged_in = logged_in + + @publish(["dd_op_mode_timestamp","dd_operation_mode", "dd_operation_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_operation_mode = mode[0] + self.dd_operation_sub_mode = smode[0] + self.dd_op_mode_timestamp = timestamp + + def cmd_log_in_to_dd(self, resend: bool = False) -> int: + """ + Constructs and sends a login command via CAN bus. Login required before \n + other commands can be sent to the DD. + + @param resend: (bool) if False (default), try to login once. Otherwise, tries to login indefinitely + @return: 1 if logged in, 0 if log in failed + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_TESTER_LOGIN_REQUEST.value, + payload=list(map(int, map(ord, self.DD_LOGIN_PASSWORD)))) + + self.logger.debug("Logging in...") + + # Send message + received_message = self.can_interface.send(message, resend=resend) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("Success: Logged In") + self.dd_set_logged_in_status(True) + self._send_dd_checkin_message() # Timer starts interval first + self.can_interface.transmit_interval_dictionary[self.callback_id].start() + else: + self.logger.debug("Failure: Log In Failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Login Timeout!!!!") + return False + + def cmd_dd_set_operation_mode(self, new_mode: int = 0) -> int: + """ + Constructs and sends a set operation mode request command via CAN bus. + Constraints: + Must be logged into DD. + Transition from current to requested op mode must be legal. + NOTE: for POST the DD device shall be in Standby Mode + + @param new_mode: ID of operation mode to transition to + + @return: 1 if successful, zero otherwise + + """ + + payload = integer_to_bytearray(new_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_SET_OP_MODE_REQUEST.value, + payload=payload) + + self.logger.debug("Requesting DD mode change to " + str(new_mode)) + + # Send message + received_message = self.can_interface.send(message) + + if received_message is not None: + if received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] == 1: + self.logger.debug("Success: Mode change accepted") + else: + self.logger.debug("Failure: Mode change rejected.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("DD mode change request Timeout!!!!") + return False + + + def cmd_dd_software_reset_request(self) -> None: + """ + Constructs and sends an DD software reset request via CAN bus. + Constraints: + Must be logged into DD. + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_SOFTWARE_RESET_REQUEST.value) + + self.logger.debug("requesting DD software reset") + + # Send message + self.can_interface.send(message, 0) + self.logger.debug("Sent request to DD to reset...") + self.dd_set_logged_in_status(False) + + Index: leahi_dialin/dd/modules/__init__.py =================================================================== diff -u --- leahi_dialin/dd/modules/__init__.py (revision 0) +++ leahi_dialin/dd/modules/__init__.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1 @@ \ No newline at end of file Index: leahi_dialin/dd/modules/concentrate_pump.py =================================================================== diff -u --- leahi_dialin/dd/modules/concentrate_pump.py (revision 0) +++ leahi_dialin/dd/modules/concentrate_pump.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,351 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 concentrate_pumps.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Micahel Garthwaite +# @date (original) 29-Oct-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ConcentratePumpsEnum(DialinEnum): + CP1_ACID = 0 + CP2_BICARB = 1 + + +@unique +class DDConcentratePumpsStates(DialinEnum): + CONCENTRATE_PUMP_OFF_STATE = 0 + CONCENTRATE_PUMP_RAMP_TO_TARGET_SPEED_STATE = 1 + CONCENTRATE_PUMP_CONTROL_TARGET_SPEED_STATE = 2 + + +class DDConcentratePumps(AbstractSubSystem): + """ + ConcentratePumps + + Dialysate Delivery (DD) Dialin API sub-class for concentrate pumps related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DD_CONCENTRATE_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_concentrate_pumps_sync) + + self.concentrate_pump_cp1_current_set_speed = 0.0 + self.concentrate_pump_cp1_measured_speed = 0.0 + self.concentrate_pump_cp1_target_speed = 0.0 + self.concentrate_pump_cp2_current_set_speed = 0.0 + self.concentrate_pump_cp2_measured_speed = 0.0 + self.concentrate_pump_cp2_target_speed = 0.0 + self.concentrate_pump_cp1_current_state = 0.0 + self.concentrate_pump_cp2_current_state = 0.0 + self.concentrate_pump_cp1_parked = False + self.concentrate_pump_cp2_parked = False + self.concentrate_pump_cp1_park_fault = False + self.concentrate_pump_cp2_park_fault = False + self.dd_concentrate_pump_timestamp = 0.0 + + self.cp1_pulse_us = 0.0 + self.cp2_pulse_us = 0.0 + + @publish(["dd_concentrate_pump_timestamp", + "concentrate_pump_cp1_current_set_speed", "concentrate_pump_cp1_measured_speed", "concentrate_pump_cp1_target_speed", + "concentrate_pump_cp2_current_set_speed", "concentrate_pump_cp2_measured_speed", "concentrate_pump_cp2_target_speed", + "concentrate_pump_cp1_current_state", "concentrate_pump_cp2_current_state", + "concentrate_pump_cp1_parked", "concentrate_pump_cp2_parked", + "concentrate_pump_cp1_park_fault", "concentrate_pump_cp2_park_fault"]) + def _handler_concentrate_pumps_sync(self, message, timestamp=0.0): + """ + Handles published concentrate pumps' data messages. Concentrate pumps' speed data are captured + for reference. + + @param message: published concentrate pumps' data message + @return: None + """ + + self.concentrate_pump_cp1_current_set_speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.concentrate_pump_cp1_measured_speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.concentrate_pump_cp1_target_speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self.concentrate_pump_cp2_current_set_speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.concentrate_pump_cp2_measured_speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.concentrate_pump_cp2_target_speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + + self.concentrate_pump_cp1_current_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.concentrate_pump_cp2_current_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + + self.cp1_pulse_us = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.cp2_pulse_us = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + + + self.concentrate_pump_cp1_parked = True if struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] == 1 else False + self.concentrate_pump_cp2_parked = True if struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] == 1 else False + + self.concentrate_pump_cp1_park_fault = True if struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13]))[0] == 1 else False + self.concentrate_pump_cp2_park_fault = True if struct.unpack('I', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14]))[0] == 1 else False + + self.dg_concentrate_pump_timestamp = timestamp + + def cmd_concentrate_pump_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DD_CONCENTRATE_PUMP_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override DG concentrate pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_target_speed_override(self, pump_id: int, speed: float) -> int: + """ + Constructs and sends the concentrate pump target speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param speed: float - target speed value to override concentrate pump with + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(NO_RESET) + speed_byte_array = float_to_bytearray(speed) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + speed_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONCENTRATE_PUMP_TARGET_SPEED_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override target speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_measured_speed_override(self, pump_id: int, speed: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump measured speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param speed: float - measured speed value to override concentrate pump with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + speed_byte_array = float_to_bytearray(speed) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + speed_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONCENTRATE_PUMP_MEASURED_SPEED_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + self.logger.debug("reset back to normal value for pump: " + str(pump_id)) + else: + self.logger.debug("override measured speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_parked_status_override(self, pump_id: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump parked state override command + + @param pump_id: unsigned int - concentrate pump ID + @param status: unsigned int - 1 = parked, 0 = not parked + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + status_byte_array = integer_to_bytearray(status) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + status_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONCENTRATE_PUMP_PARKED_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + self.logger.debug("reset parked status back to normal for pump: " + str(pump_id)) + else: + self.logger.debug("override parked status to: " + str(status) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_park_fault_state_override(self, pump_id: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump park fault state override command + + @param pump_id: unsigned int - concentrate pump ID + @param status: unsigned int - 1 = fault, 0 = no fault + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + status_byte_array = integer_to_bytearray(status) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + status_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONCENTRATE_PUMP_PARK_FAULT_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + self.logger.debug("reset park fault status back to normal for pump: " + str(pump_id)) + else: + self.logger.debug("override park fault status to: " + str(status) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_concentrate_pump_park_command(self, pump_id: int) -> int: + """ + Constructs and sends the concentrate pump park command + + @param pump_id: unsigned int - concentrate pump ID + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + payload = integer_to_bytearray(pump_id) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONCENTRATE_PUMP_PARK_REQUEST_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("park concentrate pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + Index: leahi_dialin/dd/modules/conductivity_sensors.py =================================================================== diff -u --- leahi_dialin/dd/modules/conductivity_sensors.py (revision 0) +++ leahi_dialin/dd/modules/conductivity_sensors.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,304 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 conductivity_sensors.py +# +# @author (last) Dara Navaei +# @date (last) 15-May-2023 +# @author (original) Quang Nguyen +# @date (original) 20-Jul-2020 +# +############################################################################ +import struct +from logging import Logger +from enum import unique + +from .constants import RESET, NO_RESET +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ConductivitySensorsEnum(DialinEnum): + CD1 = 0 + CD2 = 1 + CD3 = 2 + CD4 = 3 + NUM_OF_CONDUCTIVITY_SENSORS = 4 + +class DDConductivitySensors(AbstractSubSystem): + """ + ConductivitySensors + + Dialysate Generator (DG) Dialin API sub-class for conductivity sensors related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DD_CONDUCTIVITY_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_conductivity_sensors_sync) + + self.ro_rejection_ratio = 0.0 + self.conductivity_sensor_cpi = 0.0 + self.conductivity_sensor_cpo = 0.0 + self.conductivity_sensor_cd1 = 0.0 + self.conductivity_sensor_cd2 = 0.0 + + self.raw_conductivity_sensor_cpi = 0.0 + self.raw_conductivity_sensor_cpo = 0.0 + self.raw_conductivity_sensor_cd1 = 0.0 + self.raw_conductivity_sensor_cd2 = 0.0 + + self.cpi_sensor_status = 0 + self.cpo_sensor_status = 0 + self.cd1_sensor_status = 0 + self.cd2_sensor_status = 0 + + self.dg_conductivity_timestamp = 0.0 + + @publish(["dd_conductivity_timestamp", + "conductivity_sensor_cd1", "conductivity_sensor_cd2", + "conductivity_sensor_cd3", "conductivity_sensor_cd4", + "conductivity_sensor_td1", "conductivity_sensor_td2", + "conductivity_sensor_td3", "conductivity_sensor_td4",]) + def _handler_conductivity_sensors_sync(self, message, timestamp=0.0): + """ + Handles published conductivity sensor data messages. Conductivity sensor data are captured + for reference. + + @param message: published conductivity sensor data message + @return: None + """ + + self.conductivity_sensor_cd1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.conductivity_sensor_cd2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.conductivity_sensor_cd3 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.conductivity_sensor_cd4 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + self.conductivity_sensor_td1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.conductivity_sensor_td2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.conductivity_sensor_td3 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.conductivity_sensor_td4 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + + self.dd_conductivity_timestamp = timestamp + + def cmd_conductivity_sensor_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity sensor data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONDUCTIVITY_SENSOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override DD conductivity sensor broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Conductivity sensor data broadcast interval overridden to " + str_res + + str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_conductivity_sensor_readings_override(self, sensor: int, conductivity: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity value override command + + @param sensor: unsigned int - sensor ID + @param conductivity: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Conductivity sensor IDs: \n + 0 = CPI \n + 1 = CPO \n + 2 = CD1 \n + 3 = CD2 \n + """ + + reset_byte_array = integer_to_bytearray(reset) + cond_byte_array = float_to_bytearray(conductivity) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + cond_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONDUCTIVITY_SENSOR_READINGS_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " microsiemens/cm" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_conductivity_sensor_temperature_override(self, sensor: int, temperature: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity value override command + + @param sensor: unsigned int - sensor ID + @param temperature: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + temp_byte_array = float_to_bytearray(temperature) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + temp_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONDUCTIVITY_SENSOR_READINGS_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " microsiemens/cm" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_conductivity_sensor_read_counter_override(self, sensor: int, counter: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity value override command + + @param sensor: unsigned int - sensor ID + @param counter: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + read_byte_array = float_to_bytearray(counter) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + read_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONDUCTIVITY_SENSOR_READ_COUNTER_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " microsiemens/cm" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_conductivity_sensor_error_counter_override(self, sensor: int, counter: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity value override command + + @param sensor: unsigned int - sensor ID + @param counter: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + error_byte_array = float_to_bytearray(counter) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + error_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_CONDUCTIVITY_SENSOR_ERROR_COUNTER_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " microsiemens/cm" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False \ No newline at end of file Index: leahi_dialin/dd/modules/constants.py =================================================================== diff -u --- leahi_dialin/dd/modules/constants.py (revision 0) +++ leahi_dialin/dd/modules/constants.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,18 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 constants.py +# +# @author (last) Michael +# @date (last) 14-Nov-2024 +# @author (original) Michael +# @date (original) 14-Nov-2024 +# +############################################################################ + +RESET = 1 +NO_RESET = 0 Index: leahi_dialin/dd/modules/dialysate_pump.py =================================================================== diff -u --- leahi_dialin/dd/modules/dialysate_pump.py (revision 0) +++ leahi_dialin/dd/modules/dialysate_pump.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,360 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 concentrate_pumps.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Micahel Garthwaite +# @date (original) 29-Oct-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class DialysatePumpsEnum(DialinEnum): + FRESH_DIALYSATE_PUMP = 0 + SPENT_DIALYSATE_PUMP = 1 + + +@unique +class DDDialysatePumpsStates(DialinEnum): + DIALYSATE_PUMP_OFF_STATE = 0 + DIALYSATE_PUMP_RAMP_UP_STATE = 1 + DIALYSATE_PUMP_CONTROL_TO_TARGET_STATE = 2 + + +class DDDialysatePumps(AbstractSubSystem): + """ + DialysatePumps + + Dialysate Delivery (DD) Dialin API sub-class for dialysate pumps related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DIALYSATE_PUMPS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_concentrate_pumps_sync) + + self.dial_pump_target_rpm_fresh = 0.0 + self.dial_pump_target_rpm_spent = 0.0 + self.dial_pump_measured_spd_fresh = 0.0 + self.dial_pump_measured_spd_spent = 0.0 + self.dial_pump_current_spd_fresh = 0.0 + self.dial_pump_current_spd_spent = 0.0 + self.dial_pump_state_fresh = 0 + self.dial_pump_state_spent = 0 + self.dial_pump_tgt_pressure_fresh = 0.0 + self.dial_pump_tgt_pressure_spent = 0.0 + self.dial_pump_measured_pressure_fresh = 0.0 + self.dial_pump_measured_pressure_spent = 0.0 + self.dial_pump_measured_current_fresh = 0.0 + self.dial_pump_measured_current_spent = 0.0 + self.dial_pump_control_fresh = 0 + self.dial_pump_control_spent = 0 + self.dial_pump_dir_err_cnt_fresh = 0 + self.dial_pump_dir_err_cnt_spent = 0 + self.dial_pump_measured_dir_fresh = 0 + self.dial_pump_measured_dir_spent = 0 + + self.dd_dialysate_pump_timestamp = 0 + + @publish(["dd_dialysate_pump_timestamp", + "dial_pump_target_rpm_fresh","dial_pump_target_rpm_spent", + "dial_pump_measured_spd_fresh","dial_pump_measured_spd_spent", + "dial_pump_current_spd_fresh","dial_pump_current_spd_spent", + "dial_pump_state_fresh","dial_pump_state_spent", + "dial_pump_tgt_pressure_fresh","dial_pump_tgt_pressure_spent", + "dial_pump_measured_pressure_fresh","dial_pump_measured_pressure_spent", + "dial_pump_measured_current_fresh","dial_pump_measured_current_spent", + "dial_pump_control_fresh","dial_pump_control_spent", + "dial_pump_dir_err_cnt_fresh","dial_pump_dir_err_cnt_spent", + "dial_pump_measured_dir_fresh","dial_pump_measured_dir_spent", + ]) + def _handler_concentrate_pumps_sync(self, message, timestamp=0.0): + """ + Handles published concentrate pumps' data messages. Concentrate pumps' speed data are captured + for reference. + + @param message: published concentrate pumps' data message + @return: None + """ + + self.dial_pump_target_rpm_fresh = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.dial_pump_target_rpm_spent = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.dial_pump_measured_spd_fresh = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.dial_pump_measured_spd_spent = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.dial_pump_current_spd_fresh = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.dial_pump_current_spd_spent = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.dial_pump_state_fresh = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.dial_pump_state_spent = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.dial_pump_tgt_pressure_fresh = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.dial_pump_tgt_pressure_spent = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.dial_pump_measured_pressure_fresh = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.dial_pump_measured_pressure_spent = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] + self.dial_pump_measured_current_fresh = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13]))[0] + self.dial_pump_measured_current_spent = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14]))[0] + self.dial_pump_control_fresh = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15]))[0] + self.dial_pump_control_spent = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_16:MsgFieldPositions.END_POS_FIELD_16]))[0] + self.dial_pump_dir_err_cnt_fresh = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_17:MsgFieldPositions.END_POS_FIELD_17]))[0] + self.dial_pump_dir_err_cnt_spent = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_18:MsgFieldPositions.END_POS_FIELD_18]))[0] + self.dial_pump_measured_dir_fresh = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_19:MsgFieldPositions.END_POS_FIELD_19]))[0] + self.dial_pump_measured_dir_spent = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_20:MsgFieldPositions.END_POS_FIELD_20]))[0] + + self.dd_dialysate_pump_timestamp = timestamp + + def cmd_dialysate_pump_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the concentrate pump data broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: integer - interval (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + reset_byte_array = integer_to_bytearray(reset) + ms_byte_array = integer_to_bytearray(ms) + payload = reset_byte_array + ms_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_DIALYSATE_PUMPS_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override DD dialysate pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_dialysate_pump_target_speed_override(self, pump_id: int, speed: float) -> int: + """ + Constructs and sends the concentrate pump target speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param speed: float - target speed value to override concentrate pump with + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(NO_RESET) + speed_byte_array = float_to_bytearray(speed) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + speed_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_DIALYSATE_PUMPS_TARGET_SPEED_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override target speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_dialysate_pump_measured_speed_override(self, pump_id: int, speed: float) -> int: + """ + Constructs and sends the concentrate pump target speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param speed: float - target speed value to override concentrate pump with + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(NO_RESET) + speed_byte_array = float_to_bytearray(speed) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + speed_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_DIALYSATE_PUMPS_MEASURED_SPEED_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override target speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_dialysate_pump_target_pressure_override(self, pump_id: int, pressure: float) -> int: + """ + Constructs and sends the concentrate pump target speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param pressure: float - target speed value to override concentrate pump with + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(NO_RESET) + pressure_byte_array = float_to_bytearray(pressure) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + pressure_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_DIALYSATE_PUMPS_TARGET_PRESSURE_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override target speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_dialysate_pump_measured_current_override(self, pump_id: int, current: float) -> int: + """ + Constructs and sends the concentrate pump target speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param current: float - target speed value to override concentrate pump with + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(NO_RESET) + current_byte_array = float_to_bytearray(current) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + current_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_DIALYSATE_PUMPS_MEASURED_CURRENT_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override target speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_dialysate_pump_measured_direction_override(self, pump_id: int, direction: float) -> int: + """ + Constructs and sends the concentrate pump target speed override command + + @param pump_id: unsigned int - concentrate pump ID + @param direction: float - target speed value to override concentrate pump with + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + + reset_byte_array = integer_to_bytearray(NO_RESET) + direction_byte_array = float_to_bytearray(direction) + pump_id_byte_array = integer_to_bytearray(pump_id) + payload = reset_byte_array + direction_byte_array + pump_id_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_DIALYSATE_PUMPS_MEASURED_DIRECTION_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override target speed: " + str(speed) + " - for pump: " + str(pump_id)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False \ No newline at end of file Index: leahi_dialin/dd/modules/heaters.py =================================================================== diff -u --- leahi_dialin/dd/modules/heaters.py (revision 0) +++ leahi_dialin/dd/modules/heaters.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,211 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 heaters.py +# +# @author (last) Michael Garthwaite +# @date (last) 28-Oct-2024 +# @author (original) Dara Navaei +# @date (original) 29-May-2020 +# +############################################################################ + +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class HeatersStartStop(DialinEnum): + STOP = 0 + START = 1 + + +@unique +class HeatersState(DialinEnum): + HEATER_EXEC_STATE_OFF = 0 + HEATER_EXEC_STATE_PRIMARY_RAMP_TO_TARGET = 1 + HEATER_EXEC_STATE_PRIMARY_CONTROL_TO_TARGET = 2 + HEATER_EXEC_STATE_CONTROL_TO_DISINFECT_TARGE = 3 + HEATER_EXEC_STATE_TRIMMER_RAMP_TO_TARGET = 4 + HEATER_EXEC_STATE_TRIMMER_CONTROL_TO_TARGET = 5 + NUM_OF_HEATERS_STATE = 6 + + +@unique +class HeatersNames(DialinEnum): + DD_PRIMARY_HEATER = 0 + DD_TRIMMER_HEATER = 1 + NUM_OF_DD_HEATERS = 2 + + +class DDHeaters(AbstractSubSystem): + """ + + Dialysate Delivery (DD) Dialin API sub-class for heaters related commands. + """ + + 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.main_primary_heater_duty_cycle = 0 + self.trimmer_heater_duty_cycle = 0 + self.primary_heaters_target_temperature = 0.0 + self.trimmer_heater_target_temperature = 0.0 + self.primary_heater_state = 0 + self.trimmer_heater_state = 0 + self.primary_control_counter = 0 + self.trimmer_control_counter = 0 + self.dd_heaters_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DD_HEATERS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_heaters_sync) + + @publish(["dd_heaters_timestamp", + "main_primary_heater_duty_cycle","trimmer_heater_duty_cycle", + "primary_heaters_target_temperature","trimmer_heater_target_temperature", + "primary_heater_state","trimmer_heater_state", + "primary_control_counter","trimmer_control_counter"]) + def _handler_heaters_sync(self, message, timestamp=0.0): + """ + Handles published heaters message + + @param message: published heaters data message + @returns none + """ + self.main_primary_heater_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.trimmer_heater_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.primary_heaters_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.trimmer_heater_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.primary_heater_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.trimmer_heater_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.primary_control_counter = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.trimmer_control_counter = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + + self.dg_heaters_timestamp = timestamp + + def cmd_heaters_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends broadcast time interval. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) Publish time interval in ms + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_HEATERS_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending {} ms publish interval to the Heaters module".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_heater_duty_cycle_override(self, heater: int, duty_cycle: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends heater duty cycle override command + Must be logged into DG + There must be a minimum flow available for the heaters (FMP for primary and FMD for trimmer) + + @param heater the heater to override its value (primary, trimmer) (0-1.0 order) + @param duty_cycle the duty cycle value to override in percent + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + heater_name = integer_to_bytearray(heater) + duty = float_to_bytearray(duty_cycle/100) + payload = reset_value + duty + heater_name + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_HEATERS_DUTY_CYCLE_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Overriding {} heater's duty cycle to {:5.3f} %".format(HeatersNames(heater).name, duty_cycle)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_heater_start_stop_override(self, heater: int, command: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends heater duty cycle override command + Must be logged into DG + There must be a minimum flow available for the heaters (FMP for primary and FMD for trimmer) + + @param heater the heater to override its value (primary, trimmer) (0-1.0 order) + @param cmd the duty cycle value to override in percent + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + heater_name = integer_to_bytearray(heater) + cmd = integer_to_bytearray(command) + payload = reset_value + cmd + heater_name + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_HEATERS_START_STOP_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Overriding {} heater's duty cycle to {:5.3f} %".format(HeatersNames(heater).name, duty_cycle)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + Index: leahi_dialin/dd/modules/levels.py =================================================================== diff -u --- leahi_dialin/dd/modules/levels.py (revision 0) +++ leahi_dialin/dd/modules/levels.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,139 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 heaters.py +# +# @author (last) Michael Garthwaite +# @date (last) 28-Oct-2024 +# @author (original) Dara Navaei +# @date (original) 29-May-2020 +# +############################################################################ + +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray, float_to_bytearray + +class DDLevels(AbstractSubSystem): + """ + + Dialysate Delivery (DD) Dialin API sub-class for levels related commands. + """ + + 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.floater1 = 0 + self.floater2 = 0 + self.bicarb_level = 0 + self.spent_dialysate_level = 0 + self.dd_levels_timestamp = 0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DD_LEVEL_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_heaters_sync) + + @publish(["dd_levels_timestamp", + "",]) + def _handler_heaters_sync(self, message, timestamp=0.0): + """ + Handles published heaters message + + @param message: published heaters data message + @returns none + """ + self.floater1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.floater2 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.bicarb_level = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.dd_levels_timestamp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + self.dd_levels_timestamp = timestamp + + def cmd_levels_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends broadcast time interval. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: (int) Publish time interval in ms + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_LEVELS_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending {} ms publish interval to the Heaters module".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_level_status_override(self, level_sensor: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends heater duty cycle override command + Must be logged into DG + There must be a minimum flow available for the heaters (FMP for primary and FMD for trimmer) + + @param level_sensor the level sensor to override its value (primary, trimmer) (0-1.0 order) + @param status the status value to override + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + level_sensor = integer_to_bytearray(heater) + sts = integer_to_bytearray(status) + payload = reset_value + sts + level_sensor + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_LEVELS_STATUS_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Overriding {} heater's duty cycle to {:5.3f} %".format(HeatersNames(heater).name, duty_cycle)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Index: leahi_dialin/dd/modules/pressure_sensors.py =================================================================== diff -u --- leahi_dialin/dd/modules/pressure_sensors.py (revision 0) +++ leahi_dialin/dd/modules/pressure_sensors.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,415 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 pressures.py +# +# @author (last) Micahel Garthwaite +# @date (last) 07-Mar-2023 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray, float_to_bytearray + +@unique +class PressureNames(DialinEnum): + # Pressure Sensor IDs + PRESSURE_SENSOR_WATER_INLET_INPUT = 0 + PRESSURE_SENSOR_WATER_INLET_OUTPUT = 1 + PRESSURE_SENSOR_HYDRAULICS_OUTLET = 2 + PRESSURE_SENSOR_DRAIN_PUMP_OUTLET = 3 + PRESSURE_SENSOR_BIBAG = 4 + PRESSURE_SENSOR_SPENT_DIALYSATE = 5 + PRESSURE_SENSOR_FRESH_DIALYSATE = 6 + PRESSURE_SENSOR_TRANSMEMBRANE = 7 + PRESSURE_SENSOR_BAROMETRIC = 8 + NUM_OF_PRESSURE_SENSORS = 9 + + +class DDPressureSensors(AbstractSubSystem): + """ + DD interface containing pressure related commands. + """ + + + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: The DenaliCANMessenger object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DD_PRESSURES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_pressures_sync) + + self.filtered_water_inlet_input_pressure = 0.0 + self.filtered_water_inlet_output_pressure = 0.0 + self.filtered_hydraulics_output_pressure = 0.0 + self.filtered_bibag_presssure = 0.0 + self.filtered_spent_dialysate_pressure = 0.0 + self.filtered_fresh_dialysate_pressure = 0.0 + self.filtered_transmembrane_pressure = 0.0 + self.filtered_water_inlet_input_temp = 0.0 + self.filtered_water_inlet_output_temp = 0.0 + self.filtered_hydraulics_output_temp = 0.0 + self.filtered_bibag_temp = 0.0 + self.filtered_spent_dialysate_temp = 0.0 + self.filtered_fresh_dialysate_temp = 0.0 + self.transmembrane_temp = 0.0 + + self.dd_pressures_timestamp = 0 + + @publish([ + "dd_pressures_timestamp", + "water_inlet_input_pressure", + "water_inlet_output_pressure", + "hydraulics_output_pressure", + "bibag_presssure", + "spent_dialysate_pressure", + "fresh_dialysate_pressure", + "transmembrane_pressure", + "water_inlet_input_temp", + "water_inlet_output_temp", + "hydraulics_output_temp", + "bibag_temp", + "spent_dialysate_temp", + "fresh_dialysate_temp", + "transmembrane_temp", + ]) + def _handler_pressures_sync(self, message,timestamp=0.0): + """ + Handles published pressure data messages. Pressure data are captured + for reference. + + @param message: published pressure data message + @return: none + """ + self.filtered_water_inlet_input_pressure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.filtered_water_inlet_output_pressure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.filtered_hydraulics_output_pressure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.filtered_bibag_presssure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.filtered_spent_dialysate_pressure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.filtered_fresh_dialysate_pressure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.filtered_transmembrane_pressure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.filtered_water_inlet_input_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.filtered_water_inlet_output_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.filtered_hydraulics_output_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.filtered_bibag_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.filtered_spent_dialysate_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] + self.filtered_fresh_dialysate_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13]))[0] + self.filtered_transmembrane_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14]))[0] + + self.dd_pressures_timestamp = timestamp + + def cmd_pressure_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure override command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: unsigned int - broadcast interval (in ms) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + ivl = integer_to_bytearray(ms) + payload = rst + ivl + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_PRESSURE_SENSOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override pressure data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_pressure_readings_override(self, sensor: int, pressure: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure override command. + Constraints: + Must be logged into DG. + Given sensor must be one of the sensors listed below. + + pressure sensor IDs: \n + 0 = RO Pump Inlet \n + 1 = RO Pump Outlet \n + 2 = Drain Pump Inlet \n + 3 = Drain Pump Outlet \n + 4 = Barometric pressure \n + + @param sensor: unsigned int - sensor ID + @param pressure: unsigned int - pressure (in PSI) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + prs = float_to_bytearray(pressure) + idx = integer_to_bytearray(sensor) + payload = rst + prs + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_PRESSURE_SENSOR_READINGS_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override pressure reading") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_pressure_temperature_override(self, sensor: int, temperature: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure override command. + Constraints: + Must be logged into DG. + Given sensor must be one of the sensors listed below. + + pressure sensor IDs: \n + 0 = RO Pump Inlet \n + 1 = RO Pump Outlet \n + 2 = Drain Pump Inlet \n + 3 = Drain Pump Outlet \n + 4 = Barometric pressure \n + + @param sensor: unsigned int - sensor ID + @param temperature: unsigned int - pressure (in PSI) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + tmp = float_to_bytearray(temperature) + idx = integer_to_bytearray(sensor) + payload = rst + tmp + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_PRESSURE_SENSOR_TEMPERATURE_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override pressure temperature") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_pressure_sensor_read_counter_override(self, sensor: int, counter: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity value override command + + @param sensor: unsigned int - sensor ID + @param counter: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + read_byte_array = float_to_bytearray(counter) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + read_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_PRESSURE_SENSOR_READ_COUNTER_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " microsiemens/cm" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_pressure_sensor_error_counter_override(self, sensor: int, counter: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the conductivity value override command + + @param sensor: unsigned int - sensor ID + @param counter: float - conductivity value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + error_byte_array = float_to_bytearray(counter) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + error_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_PRESSURE_SENSOR_ERROR_COUNTER_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " microsiemens/cm" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_pressure_filtered_readings_override(self, sensor: int, pressure: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure override command. + Constraints: + Must be logged into DG. + Given sensor must be one of the sensors listed below. + + pressure sensor IDs: \n + 0 = RO Pump Inlet \n + 1 = RO Pump Outlet \n + 2 = Drain Pump Inlet \n + 3 = Drain Pump Outlet \n + 4 = Barometric pressure \n + + @param sensor: unsigned int - sensor ID + @param pressure: unsigned int - pressure (in PSI) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + prs = float_to_bytearray(pressure) + idx = integer_to_bytearray(sensor) + payload = rst + prs + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_PRESSURE_SENSOR_FILTER_READINGS_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override pressure reading") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_pressure_filtered_temperature_override(self, sensor: int, temperature: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure override command. + Constraints: + Must be logged into DG. + Given sensor must be one of the sensors listed below. + + pressure sensor IDs: \n + 0 = RO Pump Inlet \n + 1 = RO Pump Outlet \n + 2 = Drain Pump Inlet \n + 3 = Drain Pump Outlet \n + 4 = Barometric pressure \n + + @param sensor: unsigned int - sensor ID + @param temperature: unsigned int - pressure (in PSI) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + tmp = float_to_bytearray(temperature) + idx = integer_to_bytearray(sensor) + payload = rst + tmp + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_PRESSURE_SENSOR_FILTER_TEMPERATURE_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override pressure temperature") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False \ No newline at end of file Index: leahi_dialin/dd/modules/temperature_sensors.py =================================================================== diff -u --- leahi_dialin/dd/modules/temperature_sensors.py (revision 0) +++ leahi_dialin/dd/modules/temperature_sensors.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,267 @@ +########################################################################### +# +# Copyright (c) 2021-2024 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 temperatures.py +# +# @author (last) Micahel Garthwaite +# @date (last) 28-Jul-2023 +# @author (original) Dara Navaei +# @date (original) 01-Dec-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from leahi_dialin.common.msg_defs import MsgIds, MsgFieldPositions +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class DDTemperaturesNames(DialinEnum): + INLET_HEAT_EXCHANGER = 0 + OUTLET_HEAT_EXCHANGER = 1 + HYDRAULICS_PRIMARY_HEATER = 2 + TRIMMER_HEATER = 3 + BOARD_TEMP = 4 + BARO_TEMP_SENSOR = 5 + + +class DDTemperatureSensors(AbstractSubSystem): + + def __init__(self, can_interface, logger: Logger): + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + # Dictionary of the temperature sensors + self.dd_temperatures_timestamp = 0.0 + self.dd_temperatures = {DDTemperaturesNames.INLET_HEAT_EXCHANGER.name: 0.0, + DDTemperaturesNames.OUTLET_HEAT_EXCHANGER.name: 0.0, + DDTemperaturesNames.HYDRAULICS_PRIMARY_HEATER.name: 0.0, + DDTemperaturesNames.TRIMMER_HEATER.name: 0.0, + DDTemperaturesNames.BOARD_TEMP.name: 0.0, + DDTemperaturesNames.BARO_TEMP_SENSOR.name: 0.0,} + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DD_TEMPERATURE_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_temperature_sensors_sync) + + @publish(["dd_temperatures_timestamp", "dd_temperatures"]) + def _handler_temperature_sensors_sync(self, message,timestamp=0.0): + """ + Handles published temperature sensors message + + @param message: published temperature sensors data message + @returns none + """ + self.temperatures[DGTemperaturesNames.INLET_HEAT_EXCHANGER.name] = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + self.temperatures[DGTemperaturesNames.OUTLET_HEAT_EXCHANGER.name] = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + self.temperatures[DGTemperaturesNames.HYDRAULICS_PRIMARY_HEATER.name] = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self.temperatures[DGTemperaturesNames.TRIMMER_HEATER.name] = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + self.temperatures[DGTemperaturesNames.BOARD_TEMP.name] = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + self.temperatures[DGTemperaturesNames.BARO_TEMP_SENSOR.name] = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + + self.dg_temperatures_timestamp = timestamp + + def cmd_temperatures_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends broadcast time interval. + Constraints: + Must be logged into DD. + Given interval must be non-zero and a multiple of the DD general task interval (50 ms). + + @param ms: (int) Publish time interval in ms + @param reset: (int) 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + if not check_broadcast_interval_override_ms(ms): + return False + + reset_value = integer_to_bytearray(reset) + interval_value = integer_to_bytearray(ms) + payload = reset_value + interval_value + + message = DenaliMessage.build_message( + channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_TEMPERATURE_SENSOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending {} ms publish interval to the Temperature Sensors module".format(ms)) + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_temperatures_value_override(self, sensor_index: int, sensor_value: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the value override of a temperature sensor. + Constraints: + Must be logged into DD. + Given sensor_index must be one of the sensors listed below. + + @param sensor_index : (int) Index of the sensor + @param sensor_value: (float) Value of the sensor to override + @param reset: (int) whether to reset the override value. The default is NO_RESET + @returns 1 if successful, zero otherwise + + """ + rst = integer_to_bytearray(reset) + value = float_to_bytearray(sensor_value) + index = integer_to_bytearray(sensor_index) + + payload = rst + value + index + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_TEMPERATURE_SENSOR_MEASURED_TEMPERATURE_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Setting sensor {} to {} C".format(sensor_index, sensor_value)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_temperature_sensor_read_counter_override(self, sensor: int, counter: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the temperature value override command + + @param sensor: unsigned int - sensor ID + @param counter: float - temperature value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + read_byte_array = float_to_bytearray(counter) + sensor_byte_array = integer_to_bytearray(sensor) + payload = reset_byte_array + read_byte_array + sensor_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_TEMPERATURE_SENSOR_READ_COUNTER_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " celsius" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_baro_sensor_read_counter_override(self, counter: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the temperature value override command + + @param sensor: unsigned int - sensor ID + @param counter: float - temperature value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + read_byte_array = float_to_bytearray(counter) + payload = reset_byte_array + read_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_TEMPERATURE_SENSOR_BARO_READ_COUNTER_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " celsius" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_baro_sensor_crc_override(self, counter: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the temperature value override command + + @param sensor: unsigned int - sensor ID + @param counter: float - temperature value to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + reset_byte_array = integer_to_bytearray(reset) + read_byte_array = float_to_bytearray(counter) + payload = reset_byte_array + read_byte_array + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_TEMPERATURE_SENSOR_BARO_CRC_OVERRIDE_REQUEST.value, + payload=payload) + + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(conductivity) + " celsius" + self.logger.debug("override conductivity sensor value for sensor " + str(sensor) + ": " + str_res) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False \ No newline at end of file Index: leahi_dialin/dd/modules/valves.py =================================================================== diff -u --- leahi_dialin/dd/modules/valves.py (revision 0) +++ leahi_dialin/dd/modules/valves.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1,404 @@ +########################################################################### +# +# Copyright (c) 2020-2024 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 valves.py +# +# @author (last) Micahel Garthwaite +# @date (last) 17-Aug-2023 +# @author (original) Peman Montazemi +# @date (original) 19-May-2020 +# +############################################################################ + +import struct +from enum import unique +from logging import Logger +from collections import OrderedDict + +from .constants import NO_RESET +from leahi_dialin.common.msg_defs import MsgIds +from leahi_dialin.protocols.CAN import DenaliMessage, DenaliChannels +from leahi_dialin.utils.base import AbstractSubSystem, publish, DialinEnum +from leahi_dialin.utils.checks import check_broadcast_interval_override_ms +from leahi_dialin.utils.conversions import integer_to_bytearray + +# Valve states +ENERGIZED = True +DEENERGIZED = False + + +@unique +class DDValveStates(DialinEnum): + VALVE_STATE_CLOSED = 0 + VALVE_STATE_OPEN = 1 + + +@unique +class DDValveNames(DialinEnum): + VDR = 0 # Valve Drain (D53) + VTD = 1 # Valve Thermal Disinfect (D52) + VHB = 2 # Valve Hydraulics Bypass (D8) + VRP = 3 # Valve Rinse Port (D54) + VHO = 4 # Valve Hydraulics Outlet (D14) + VDB1 = 5 # Valve DryBcarb Inlet (D65) + VP1 = 6 # Valve Purge 1 (D64) + VPT = 7 # Valve Pressure Test (D31) + VDB2 = 8 # Valve Dialyzer Bypass (D34) + VDI = 9 # Valve Dialyzer Inlet (D35) + VDO = 10 # Valve Dialyzer Outlet (D40) + VP2 = 11 # Valve Dialysate Out Purge 2 (D47) + VHI = 12 # Valve Hydraulics Inlet (D3) + VWI = 13 # Valve Water Inlet (M4) + RSRVD_SPACE1 = 14 # This space has been reserved + RSRVD_SPACE2 = 15 # This space has been reserved + BCV1 = 16 # Balancing chamber Valve 1 (D23) + BCV2 = 17 # Balancing chamber Valve 2 (D19) + BCV3 = 18 # Balancing chamber Valve 3 (D25) + BCV4 = 19 # Balancing chamber Valve 4 (D21) + BCV5 = 20 # Balancing chamber Valve 5 (D24) + BCV6 = 21 # Balancing chamber Valve 6 (D20) + BCV7 = 22 # Balancing chamber Valve 7 (D26) + BCV8 = 23 # Balancing chamber Valve 8 (D22) + UFI1 = 24 # Ultrafiltration Valve 1 Inlet (D69) + UFI2 = 25 # Ultrafiltration Valve 2 Inlet (D71) + UFO1 = 26 # Ultrafiltration Valve 1 Outlet (D70) + UFO2 = 27 # Ultrafiltration Valve 2 Outlet (D72) + +class DDValves(AbstractSubSystem): + """ + Dialysate Delivery (DG) interface for valve related commands. + + """ + + # Valves states publish message field positions + START_POS_VALVES_STATES = DenaliMessage.PAYLOAD_START_INDEX + END_POS_VALVES_STATES = START_POS_VALVES_STATES + 2 # Valves States come in as a U16 value (2 bytes) + + 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.valves_sensed_states = OrderedDict() + self.dg_valves_states_timestamp = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DD_VALVES_STATES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_valves_sync) + + self.valve_states_all = 0x0000 + + # NOTE: The len function counts the enums with the same number only once. This is not the case in the DG valves + # class because each valve must have a unique ID. + self.valve_states_enum = [0 for _ in range(len(DDValveNames))] + + for valve in DDValveNames.__members__: + self.valves_sensed_states[valve] = '' + + def get_valve_states(self): + """ + Gets the valve states + + @return: All valve states: \n + [\n + Valve Reservoir Fill \n + Valve Reservoir Inlet \n + Reserved Space \n + Valve Reservoir Outlet \n + Valve Pressure Outlet \n + Valve Bypass Filter \n + Valve Recirculate \n + Valve Drain \n + Valve Pressure Inlet \n + Valve Sampling Port \n + Valve Reservoir 1 Drain \n + Valve Reservoir 2 Drain \n + Valve Production Drain \n + ]\n + """ + return [ + self.valve_state_VRF.get("state", None), + self.valve_state_VRI.get("state", None), + self.valve_state_VRO.get("state", None), + self.valve_state_VPO.get("state", None), + self.valve_state_VBF.get("state", None), + self.valve_state_VRC.get("state", None), + self.valve_state_VDR.get("state", None), + self.valve_state_VPI.get("state", None), + self.valve_state_VSP.get("state", None), + self.valve_state_VRD1.get("state", None), + self.valve_state_VRD2.get("state", None), + self.valve_state_VPD.get("state", None) + ] + + @staticmethod + def sort_by_id(observation): + """ + Converts a published dictionary of valve state information to an ordered list + of tuples. + + For example: + hd = DG() + observation = {'datetime': datetime.datetime(2020, 7, 13, 10, 43, 27, 433357), + + 'valve_state_VBF': {'id': 5, 'state': True}, + 'valve_state_VDR': {'id': 7, 'state': True}, + 'valve_state_VPD': {'id': 12, 'state': True}, + 'valve_state_VPI': {'id': 8, 'state': True}, + 'valve_state_VPO': {'id': 4, 'state': True}, + 'valve_state_VR1': {'id': 10, 'state': True}, + 'valve_state_VR2': {'id': 11, 'state': True}, + 'valve_state_VRC': {'id': 6, 'state': True}, + 'valve_state_VRF': {'id': 0, 'state': True}, + 'valve_state_VRI': {'id': 1, 'state': True}, + 'valve_state_VRO': {'id': 3, 'state': True}, + 'valve_state_VSP': {'id': 9, 'state': True}, + 'valve_states_all': 8191} + self.logger.debug(hd.valves.sort_by_id(observation)) + + ('valve_state_VRF', 0, True) + ('valve_state_VRI', 1, True) + ('valve_state_VRO', 3, True) + ('valve_state_VPO', 4, True) + ('valve_state_VBF', 5, True) + ('valve_state_VRC', 6, True) + ('valve_state_VDR', 7, True) + ('valve_state_VPI', 8, True) + ('valve_state_VSP', 9, True) + ('valve_state_VR1', 10, True) + ('valve_state_VR2', 11, True) + ('valve_state_VPD', 12, True) + + @param observation: dictionary of the observed valve states + @return: a list of tuples of the valve states + """ + + result = [] + for key, value in observation.items(): + if isinstance(value, dict): + result.append((key, value.get("id", None), value.get("state", None))) + + result = sorted(result, key=lambda each: each[1]) + return result + + @staticmethod + def _binary_to_valve_state(binary) -> bool: + """ + @param binary: binary value + @return: 1 = energized, otherwise de-energized + """ + + if binary != 0: + return ENERGIZED + else: + return DEENERGIZED + + @publish([ + "dg_valves_states_timestamp", + "valve_states_all", + "valve_state_VRF", + "valve_state_VRI", + "valve_state_VRO", + "valve_state_VPO", + "valve_state_VBF", + "valve_state_VRC", + "valve_state_VDR", + "valve_state_VPI", + "valve_state_VSP", + "valve_state_VRD1", + "valve_state_VRD2", + "valve_state_VPD", + "valve_states_enum" + ]) + def _handler_valves_sync(self, message, timestamp=0.0): + """ + Handles published valves states message. + + @param message: published valves states message + @return: none + """ + + vst = struct.unpack('H', bytearray(message['message'][self.START_POS_VALVES_STATES:self.END_POS_VALVES_STATES])) + self.valve_states_all = vst[0] + # Extract each valve state from U16 valves states using bit-masking + self.valve_state_VRF["state"] = self._binary_to_valve_state(vst[0] & 1) + self.valve_state_VRI["state"] = self._binary_to_valve_state(vst[0] & 2) + self.valve_state_VRO["state"] = self._binary_to_valve_state(vst[0] & 8) + self.valve_state_VPO["state"] = self._binary_to_valve_state(vst[0] & 16) + self.valve_state_VBF["state"] = self._binary_to_valve_state(vst[0] & 32) + self.valve_state_VRC["state"] = self._binary_to_valve_state(vst[0] & 64) + self.valve_state_VDR["state"] = self._binary_to_valve_state(vst[0] & 128) + self.valve_state_VPI["state"] = self._binary_to_valve_state(vst[0] & 256) + self.valve_state_VSP["state"] = self._binary_to_valve_state(vst[0] & 512) + self.valve_state_VRD1["state"] = self._binary_to_valve_state(vst[0] & 1024) + self.valve_state_VRD2["state"] = self._binary_to_valve_state(vst[0] & 2048) + self.valve_state_VPD["state"] = self._binary_to_valve_state(vst[0] & 4096) + + self.valve_states_enum[DDValveNames.VALVE_RESERVOIR_FILL.value] = VRdVRfStates(self._binary_to_valve_state(vst[0] & 1)).name # VRF + self.valve_states_enum[DDValveNames.VALVE_RESERVOIR_INLET.value] = VRoVRiStates(self._binary_to_valve_state(vst[0] & 2)).name # VRI + self.valve_states_enum[DDValveNames.VALVE_RESERVOIR_OUTLET.value] = VRoVRiStates(self._binary_to_valve_state(vst[0] & 8)).name # VRO + self.valve_states_enum[DDValveNames.VALVE_PRESSURE_OUTLET.value] = VPoStates(self._binary_to_valve_state(vst[0] & 16)).name # VPO + self.valve_states_enum[DDValveNames.VALVE_BYPASS_FILTER.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 32)).name # VBF + self.valve_states_enum[DDValveNames.VALVE_RECIRCULATE.value] = VDrVRcStates(self._binary_to_valve_state(vst[0] & 64)).name # VRC + self.valve_states_enum[DDValveNames.VALVE_DRAIN.value] = VDrVRcStates(self._binary_to_valve_state(vst[0] & 128)).name # VDR + self.valve_states_enum[DDValveNames.VALVE_PRESSURE_INLET.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 256)).name # VPI + self.valve_states_enum[DDValveNames.VALVE_SAMPLING_PORT.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 512)).name # VSP + self.valve_states_enum[DDValveNames.VALVE_RESERVOIR_DRAIN_1.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 1024)).name # VRD1 + self.valve_states_enum[DDValveNames.VALVE_RESERVOIR_DRAIN_2.value] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 2048)).name # VRD2 + self.valve_states_enum[DDValveNames.VALVE_PRODUCTION_DRAIN.value] = VPdStates(self._binary_to_valve_state(vst[0] & 4096)).name # VPD + + start = self.END_POS_VALVES_STATES + end = start + 1 + for valve_id in self.valves_sensed_states: + valve_state_number = struct.unpack('B', bytearray(message['message'][start:end]))[0] + self.valves_sensed_states[valve_id] = DGValvesSensedStates(valve_state_number).name + start = end + end += 1 + + self.dg_valves_states_timestamp = timestamp + + def cmd_valve_sensed_state_override(self, valve: int, state: bool, reset: int = NO_RESET) -> int: + """ + Constructs and sends the valve sensed state override command. + Constraints: + Must be logged into DG. + Given valve ID must be one of the valve IDs listed below. + + @param valve: unsigned int - valve ID + @param state: bool - valve state + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + valve IDs: \n + 0 = Valve Reservoir Fill \n + 1 = Valve Reservoir Inlet \n + 2 = Reserved Space \n + 3 = Valve Reservoir Outlet \n + 4 = Valve Pressure Outlet \n + 5 = Valve Bypass Filter \n + 6 = Valve Recirculate \n + 7 = Valve Drain \n + 8 = Valve Pressure Inlet \n + 9 = Valve Sampling Port \n + 10 = Valve Reservoir 1 Drain \n + 11 = Valve Reservoir 2 Drain \n + 12 = Valve Production Drain \n + """ + + rst = integer_to_bytearray(reset) + ste = integer_to_bytearray(int(state)) + vlv = integer_to_bytearray(valve) + payload = rst + ste + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_VALVE_SENSED_STATE_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Override valve sensed state") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_valve_override(self, valve: int, state: bool, reset: int = NO_RESET) -> int: + """ + Constructs and sends the valve state override command. + Constraints: + Must be logged into DG. + Given valve ID must be one of the valve IDs listed below. + + @param valve: unsigned int - valve ID + @param state: bool - valve state + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + valve IDs: \n + 0 = Valve Reservoir Fill \n + 1 = Valve Reservoir Inlet \n + 2 = Reserved Space \n + 3 = Valve Reservoir Outlet \n + 4 = Valve Pressure Outlet \n + 5 = Valve Bypass Filter \n + 6 = Valve Recirculate \n + 7 = Valve Drain \n + 8 = Valve Pressure Inlet \n + 9 = Valve Sampling Port \n + 10 = Valve Reservoir 1 Drain \n + 11 = Valve Reservoir 2 Drain \n + 12 = Valve Production Drain \n + """ + + rst = integer_to_bytearray(reset) + ste = integer_to_bytearray(int(state)) + vlv = integer_to_bytearray(valve) + payload = rst + ste + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_VALVE_STATE_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("Override valve state") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_valve_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the valve state override command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG general task interval (50 ms). + + @param ms: unsigned int - broadcast interval (in ms) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + if not check_broadcast_interval_override_ms(ms): + return False + + rst = integer_to_bytearray(reset) + ivl = integer_to_bytearray(ms) + payload = rst + ivl + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dd_ch_id, + message_id=MsgIds.MSG_ID_DD_VALVE_PUBLISH_INTERVAL_OVERRIDE_REQUEST.value, + payload=payload) + + self.logger.debug("override valves states publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content in message + if received_message is not None: + # Response payload is OK or not + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi_dialin/dd/pressure_sensors.py'. Fisheye: No comparison available. Pass `N' to diff? Index: leahi_dialin/dd/proxies/__init__.py =================================================================== diff -u --- leahi_dialin/dd/proxies/__init__.py (revision 0) +++ leahi_dialin/dd/proxies/__init__.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1 @@ \ No newline at end of file Index: leahi_dialin/dd/proxies/td_proxy.py =================================================================== diff -u --- leahi_dialin/dd/proxies/td_proxy.py (revision 0) +++ leahi_dialin/dd/proxies/td_proxy.py (revision fd7a25d8f068bcba594c01410a02a03f6afbcd59) @@ -0,0 +1 @@ \ No newline at end of file Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi_dialin/dd/td_proxy.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag fd7a25d8f068bcba594c01410a02a03f6afbcd59 refers to a dead (removed) revision in file `leahi_dialin/dd/valves.py'. Fisheye: No comparison available. Pass `N' to diff?