Index: dialin/common/dg_defs.py =================================================================== diff -u -re0221f00fe32642b76d4a03c61e3f5ce2da2c881 -r053580d8dbe39bf313d61b002d8d41a189d95c39 --- dialin/common/dg_defs.py (.../dg_defs.py) (revision e0221f00fe32642b76d4a03c61e3f5ce2da2c881) +++ dialin/common/dg_defs.py (.../dg_defs.py) (revision 053580d8dbe39bf313d61b002d8d41a189d95c39) @@ -99,7 +99,6 @@ DG_HANDLE_BAD_FILL_STATE_FLUSH_FILL = 2 DG_HANDLE_BAD_FILL_STATE_SECOND_DRAIN = 3 DG_HANDLE_BAD_FILL_STATE_REFILL = 4 - DG_HANDLE_BAD_FILL_STATE_CLEAR_ALARM = 5 NUM_OF_DG_HANDLE_BAD_FILL_STATES = 6 Index: dialin/common/msg_ids.py =================================================================== diff -u -r4a30e790387316388d3b8a1603f5c6b4d46ac011 -r053580d8dbe39bf313d61b002d8d41a189d95c39 --- dialin/common/msg_ids.py (.../msg_ids.py) (revision 4a30e790387316388d3b8a1603f5c6b4d46ac011) +++ dialin/common/msg_ids.py (.../msg_ids.py) (revision 053580d8dbe39bf313d61b002d8d41a189d95c39) @@ -324,6 +324,9 @@ MSG_ID_HD_REQ_CURRENT_TREATMENT_PARAMETERS = 0x807B MSG_ID_HD_RES_CURRENT_TREATMENT_PARAMETERS = 0x807C MSG_ID_HD_SET_FANS_RPM_ALARM_START_TIME_OFFSET = 0x807D + MSG_ID_HD_GET_USAGE_INFO_RECORD = 0x807E + MSG_ID_HD_SET_USAGE_INFO_RECORD = 0x807F + MSG_ID_HD_SEND_USAGE_INFO_RECORD = 0x8080 MSG_ID_DG_TESTER_LOGIN_REQUEST = 0xA000 MSG_ID_DG_ALARM_STATE_OVERRIDE = 0xA001 @@ -406,6 +409,9 @@ MSG_ID_DG_USED_BICARB_VOLUME_ML_OVERRIDE = 0xA050 MSG_ID_FILL_MODE_DATA_PUBLISH_INTERVAL_OVERRIDE = 0xA051 MSG_ID_BAD_FILL_STATES_PUBLISH_INTERVAL_OVERRIDE = 0xA052 + MSG_ID_DG_GET_USAGE_INFO_RECORD = 0xA053 + MSG_ID_DG_SET_USAGE_INFO_RECORD = 0xA054 + MSG_ID_DG_SEND_USAGE_INFO_RECORD = 0xA055 MSG_ID_HD_DEBUG_EVENT = 0xFFF1 MSG_ID_DG_DEBUG_EVENT = 0xFFF2 Index: dialin/dg/dialysate_generator.py =================================================================== diff -u -re5058835522499573ba251c093ae3c56deaf2149 -r053580d8dbe39bf313d61b002d8d41a189d95c39 --- dialin/dg/dialysate_generator.py (.../dialysate_generator.py) (revision e5058835522499573ba251c093ae3c56deaf2149) +++ dialin/dg/dialysate_generator.py (.../dialysate_generator.py) (revision 053580d8dbe39bf313d61b002d8d41a189d95c39) @@ -50,6 +50,7 @@ from .voltages import DGVoltages from .events import DGEvents from .sw_configs import DGSoftwareConfigs +from .usage_info_record import DGUsageNVRecord from ..common.msg_defs import MsgIds, MsgFieldPositions from ..protocols.CAN import DenaliCanMessenger, DenaliMessage, DenaliChannels from ..utils import * @@ -190,6 +191,7 @@ self.voltages = DGVoltages(self.can_interface, self.logger) self.events = DGEvents(self.can_interface, self.logger) self.sw_configs = DGSoftwareConfigs(self.can_interface, self.logger) + self.usage_record = DGUsageNVRecord(self.can_interface, self.logger) def get_version(self): """ Index: dialin/dg/sw_configs.py =================================================================== diff -u -r8407f6a1eb2289ed5764423aa9f27615d313d6af -r053580d8dbe39bf313d61b002d8d41a189d95c39 --- dialin/dg/sw_configs.py (.../sw_configs.py) (revision 8407f6a1eb2289ed5764423aa9f27615d313d6af) +++ dialin/dg/sw_configs.py (.../sw_configs.py) (revision 053580d8dbe39bf313d61b002d8d41a189d95c39) @@ -70,7 +70,6 @@ _RTC_RAM_MAX_BYTES_TO_WRITE = 64 _PAYLOAD_TRANSFER_DELAY_S = 0.2 _FIRMWARE_STACK_NAME = 'DG' - _NON_VOLATILE_RECORD_NAME = 'SW_Config_Report' def __init__(self, can_interface, logger: Logger): """ @@ -197,7 +196,7 @@ """ # Pass the software configuration record dictionary to be updated with the excel document status = self._utilities.get_sw_configs_from_excel(self.dg_sw_config_record, excel_report_path, - self._NON_VOLATILE_RECORD_NAME) + self._utilities.NON_VOLATILE_RECORD_NAME) # The excel document was successfully read initiate a write command if status: self._cmd_set_dg_sw_config_record() @@ -304,7 +303,8 @@ """ # Create the excel report - self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._NON_VOLATILE_RECORD_NAME, report_address) + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.NON_VOLATILE_RECORD_NAME, + report_address) # Create ab object of the observer class to observe the dictionary observer = NVUtilsObserver("dg_sw_config_record") Index: dialin/dg/system_record.py =================================================================== diff -u -r3a70bfb451b74106348c064c34f19934aadd9119 -r053580d8dbe39bf313d61b002d8d41a189d95c39 --- dialin/dg/system_record.py (.../system_record.py) (revision 3a70bfb451b74106348c064c34f19934aadd9119) +++ dialin/dg/system_record.py (.../system_record.py) (revision 053580d8dbe39bf313d61b002d8d41a189d95c39) @@ -33,7 +33,7 @@ class DGSystemNVRecord(AbstractSubSystem): """ - Hemodialysis Device (HD) Dialin API sub-class for system record commands. + Dialysate Generator (DG) Dialin API sub-class for system record commands. """ _RECORD_SPECS_BYTES = 12 Index: dialin/dg/usage_info_record.py =================================================================== diff -u --- dialin/dg/usage_info_record.py (revision 0) +++ dialin/dg/usage_info_record.py (revision 053580d8dbe39bf313d61b002d8d41a189d95c39) @@ -0,0 +1,270 @@ +########################################################################### +# +# Copyright (c) 2022-2022 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 sw_configs.py +# +# @author (last) Dara Navaei +# @date (last) 01-Mar-2022 +# @author (original) Dara Navaei +# @date (original) 01-Mar-2022 +# +############################################################################ + +import struct +import time +from collections import OrderedDict +from logging import Logger +from time import sleep + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.nv_ops_utils import NVOpsUtils, NVUtilsObserver + + +class DGUsageNVRecord(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for setting and getting the usage information record. + """ + + _DEFAULT_USAGE_INFO_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + _RECORD_SPECS_BYTES = 12 + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _FIRMWARE_STACK_NAME = 'DG' + + 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._current_message = 0 + self._total_messages = 0 + self._received_msg_length = 0 + self._usage_info_data = 0 + self._is_getting_usage_info_in_progress = False + self._raw_usage_info_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.dg_usage_info_record = self._prepare_dg_usage_info_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_USAGE_INFO_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_usage_info_sync) + + def _cmd_request_dg_usage_info_record(self) -> int: + """ + Handles getting DG usage information record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_usage_info_in_progress is not True: + self._is_getting_usage_info_in_progress = True + # Clear the list for the next call + self._raw_usage_info_record.clear() + # Run the firmware commands to get the record + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_USAGE_INFO_RECORD.value) + + self.logger.debug('Getting DG usage information record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting DG software configuration record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_dg_usage_info_sync(self, message): + """ + Handles published DG usage information record messages. + + @param message: published DG usage information record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._usage_info_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_usage_info_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_usage_info_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.dg_usage_info_record, self._raw_usage_info_record) + self._handler_received_complete_dg_usage_info_record() + + @publish(["dg_usage_info_record"]) + def _handler_received_complete_dg_usage_info_record(self): + """ + Publishes the received usage information record + + @return: None + """ + self.logger.debug("Received a complete dg usage information record.") + + def cmd_update_dg_usage_info_record(self, excel_report_path: str): + """ + Handles preparing the DG usage information from the provided excel report + + @param excel_report_path: (str) the directory in which the excel report of the information is located + @return: none + """ + # Pass the software configuration record dictionary to be updated with the excel document + self._utilities.write_excel_record_to_fw_record(self.dg_usage_info_record, excel_report_path, + self._utilities.USAGE_INFO_RECORD_TAB_NAME) + self._cmd_set_dg_usage_info_record(self.dg_usage_info_record) + + def _cmd_set_dg_usage_info_record(self, previous_record: OrderedDict) -> bool: + """ + Handles updating the DG usage information record and sends it to FW. + + @return: True upon success, False otherwise + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(previous_record) + + self.logger.debug('Setting DG usage information record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_USAGE_INFO_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Finished sending DG usage information record.") + return True + + def _prepare_dg_usage_info_record(self) -> OrderedDict: + """ + Handles assembling the sub dictionaries of eDG usage information. + + @return: (OrderedDict) the assembled DG usage information + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_usage_info_record()] + + for function in functions: + # Update the groups bytes size so far to be used to padding later + groups_byte_size += function[1] + # Update the calibration record + record.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' tuple: + """ + Handles creating the usage information record dictionary. + + @return: usage information record dictionary and the byte size of this group + """ + groups_byte_size = 0 + usage_info_records = OrderedDict( + {'usage_info_record': + {'ro_total_liters': [' int: + """ + Handles getting HD usage information record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_usage_info_in_progress is not True: + self._is_getting_usage_info_in_progress = True + # Clear the list for the next call + self._raw_usage_info_record.clear() + # Run the firmware commands to get the record + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_USAGE_INFO_RECORD.value) + + self.logger.debug('Getting HD usage information record') + + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Received FW ACK after requesting DG software configuration record.") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_hd_usage_info_sync(self, message): + """ + Handles published HD usage information record messages. + + @param message: published HD usage information record data message + + @return: None + """ + curr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + total = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + length = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self._current_message = curr + self._total_messages = total + self._received_msg_length = length + # The end of calibration_record record payload is from the start index + 12 bytes for the current message +total + # messages + the length of calibration_record. The rest is the CAN messaging CRC that is not needed + # to be kept + end_of_data_index = MsgFieldPositions.START_POS_FIELD_1 + self._RECORD_SPECS_BYTES + self._received_msg_length + + # Get the data only and not specs of it (i.e current message number) + self._usage_info_data = message['message'][MsgFieldPositions.START_POS_FIELD_1:end_of_data_index] + + # Continue getting calibration_record records until the all the calibration_record messages are received. + # Concatenate the calibration_record records to each other + if self._current_message <= self._total_messages: + self._raw_usage_info_record += (message['message'][MsgFieldPositions.START_POS_FIELD_1 + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self._current_message == self._total_messages: + # Done with receiving the messages + self._is_getting_usage_info_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._utilities.process_received_record_from_fw(self.hd_usage_info_record, self._raw_usage_info_record) + self._handler_received_complete_hd_usage_info_record() + + @publish(["hd_usage_info_record"]) + def _handler_received_complete_hd_usage_info_record(self): + """ + Publishes the received usage information record + + @return: None + """ + self.logger.debug("Received a complete HD usage information record.") + + def cmd_update_dg_usage_info_record(self, excel_report_path: str): + """ + Handles preparing the HD usage information from the provided excel report + + @param excel_report_path: (str) the directory in which the excel report of the information is located + @return: none + """ + # Pass the software configuration record dictionary to be updated with the excel document + self._utilities.write_excel_record_to_fw_record(self.hd_usage_info_record, excel_report_path, + self._utilities.USAGE_INFO_RECORD_TAB_NAME) + print(self.hd_usage_info_record) # TODO remove + self._cmd_set_hd_usage_info_record(self.hd_usage_info_record) + + def _cmd_set_hd_usage_info_record(self, previous_record: OrderedDict) -> bool: + """ + Handles updating the HD usage information record and sends it to FW. + + @return: True upon success, False otherwise + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(previous_record) + + self.logger.debug('Setting HD usage information record') + + # Update all the data packets with the last message count since is the number of messages that firmware + # should receive + for packet in record_packets: + # Sleep to let the firmware receive and process the data + time.sleep(self._PAYLOAD_TRANSFER_DELAY_S) + + # Convert the list packet to a bytearray + payload = b''.join(packet) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_USAGE_INFO_RECORD.value, + payload=payload) + + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is None: + self.logger.debug("Timeout!!!!") + return False + + self.logger.debug("Finished sending HD usage information record.") + return True + + def _prepare_hd_usage_info_record(self) -> OrderedDict: + """ + Handles assembling the sub dictionaries of HD usage information. + + @return: (OrderedDict) the assembled HD usage information + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_usage_info_record()] + + for function in functions: + # Update the groups bytes size so far to be used to padding later + groups_byte_size += function[1] + # Update the calibration record + record.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' tuple: + """ + Handles creating the usage information record dictionary. + + @return: usage information record dictionary and the byte size of this group + """ + groups_byte_size = 0 + usage_info_records = OrderedDict( + {'usage_info_record': + {'tx_total_time_hours': ['