Index: dialin/dg/alarms.py =================================================================== diff -u -r115cccfba180844d47d6afd541f0af14043b8642 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/dg/alarms.py (.../alarms.py) (revision 115cccfba180844d47d6afd541f0af14043b8642) +++ dialin/dg/alarms.py (.../alarms.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2020-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. Index: dialin/dg/calibration_record.py =================================================================== diff -u -r3a70bfb451b74106348c064c34f19934aadd9119 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/dg/calibration_record.py (.../calibration_record.py) (revision 3a70bfb451b74106348c064c34f19934aadd9119) +++ dialin/dg/calibration_record.py (.../calibration_record.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -17,11 +17,12 @@ 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 +from ..utils.nv_ops_utils import NVOpsUtils, Observer class DGCalibrationNVRecord(AbstractSubSystem): @@ -43,6 +44,9 @@ _DEFAULT_TIME_VALUE = 0 _DEFAULT_CRC_VALUE = 0 _DEFAULT_FLUSH_LINES_VOLUME = 0.01 + _DEFAULT_ULTRAFILTER_TAU_C_PER_MIN = -4.565 + _DEFAULT_RESERVOIR_TAU_C_PER_MIN = -0.512 + _DEFAULT_ULTRAFILTER_VOLUME_ML = 700 # Maximum allowed bytes that are allowed to be written to EEPROM in firmware # The padding size then is calculated to be divisions of 16 @@ -52,6 +56,8 @@ _PAYLOAD_TRANSFER_DELAY_S = 0.2 _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + _FIRMWARE_STACK_NAME = 'DG' + def __init__(self, can_interface, logger: Logger): """ @@ -89,6 +95,28 @@ return status + def cmd_get_dg_calibration_record_report(self, report_destination: str = None): + """ + Handles getting DG calibration_record record from firmware and writing it to excel. + + @param report_destination: (str) the destination that the report should be written to + + @return: none + """ + # Prepare the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.CAL_RECORD_TAB_NAME, + report_destination, protect_sheet=True) + # Request the DG calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_dg_calibration_record() + observer = Observer("dg_calibration_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + # Pass the DG calibration record to the function to write the excel + self._utilities.write_fw_record_to_excel(self.dg_calibration_record) + def cmd_request_dg_calibration_record(self) -> bool: """ Handles getting DG calibration_record record from firmware. @@ -166,6 +194,28 @@ """ self.logger.debug("Received a complete dg calibration record.") + def cmd_set_dg_calibration_excel_to_fw(self, report_address: str): + """ + Handles setting the calibration data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written to excel + + @return: none + """ + + # Request the DG calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_dg_calibration_record() + observer = Observer("dg_calibration_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.dg_calibration_record, report_address, + self._utilities.CAL_RECORD_TAB_NAME) + + self.cmd_set_dg_calibration_record(self.dg_calibration_record) + def cmd_set_dg_calibration_record(self, previous_record: OrderedDict) -> bool: """ Handles updating the DG calibration record with the newest calibration_record data of a hardware and @@ -218,7 +268,8 @@ self._prepare_conductivity_sensors_record(), self._prepare_pumps_record(), self._prepare_volume_record(), self._prepare_acid_concentrates_record(), self._prepare_bicarb_concentrates_record(), self._prepare_filters_record(), - self._prepare_fans_record(), self._prepare_accelerometer_sensor_record()] + self._prepare_fans_record(), self._prepare_accelerometer_sensor_record(), + self._prepare_heating_constants_record()] for record, byte_size in records_with_sizes: # Update the groups bytes size so far to be use to padding later @@ -249,16 +300,8 @@ @return: pressure sensors hardware group dictionary and the byte size of this hardware group """ - hardware_names = ['ppi', - 'ppo', - 'prd', - 'pdr', - 'reserved_1', - 'reserved_2', - 'reserved_3', - 'reserved_4', - 'reserved_5', - 'reserved_6'] + hardware_names = ['ppi', 'ppo', 'prd', 'pdr', 'reserved_1', 'reserved_2', 'reserved_3', 'reserved_4', + 'reserved_5', 'reserved_6'] group_byte_size = 0 group_name = 'pressure_sensors' hardware_group = OrderedDict({group_name: OrderedDict()}) @@ -283,7 +326,7 @@ @return: flow sensors hardware group dictionary and the byte size of this hardware group """ - hardware_names = ['ro_flow_sensor', 'dialysate_flow_sensor', 'reserved_2', 'reserved_3'] + hardware_names = ['ro_flow_sensor', 'dialysate_flow_sensor', 'reserved_1', 'reserved_2'] group_byte_size = 0 group_name = 'flow_sensors' hardware_group = OrderedDict({group_name: OrderedDict()}) @@ -534,7 +577,7 @@ @return: DG fans hardware group dictionary and the byte size of this hardware group """ - hardware_names = ['fan_1', 'fan_2'] + hardware_names = ['inlet_fan_1', 'inlet_fan_2', 'inlet_fan_3', 'outlet_fan_1', 'outlet_fan_2', 'outlet_fan_3'] group_byte_size = 0 group_name = 'fans' hardware_group = OrderedDict({group_name: OrderedDict()}) @@ -557,7 +600,6 @@ @return: accelerometer sensor hardware group dictionary and the byte size of this hardware group """ hardware_names = ['accelerometer'] - group_byte_size = 0 group_name = 'accelerometer_sensor' hardware_group = OrderedDict({group_name: OrderedDict()}) @@ -573,3 +615,26 @@ hardware_group[group_name] = hardware return hardware_group, group_byte_size + + def _prepare_heating_constants_record(self): + """ + Handles creating the calibration dictionary of the heating constants group. + + @return: Heating hardware group dictionary and the byte size of this hardware group + """ + hardware_names = ['heating'] + group_byte_size = 0 + group_name = 'heating_constants' + hardware_group = OrderedDict({group_name: OrderedDict()}) + hardware = OrderedDict() + + for i in hardware_names: + hardware[i] = {'rsrvr_temp_tau_c_per_min': [' int: """ @@ -235,21 +238,19 @@ self.logger.debug("Timeout!!!!") return False - def cmd_fans_rpm_alarm_start_time_offset_override(self, hours: int, minutes: int, reset: int = NO_RESET) -> int: + def cmd_fans_rpm_alarm_start_time_offset_override(self, seconds: int, reset: int = NO_RESET) -> int: """ Constructs and sends the HD fan RPM alarm start time override command Constraints: - Must be logged into HD. + Must be logged into DG. - @param hours: (int) hours that the fan alarm start time must be overridden to - @param minutes: (int) minutes that the fan alarm start time must be overridden to + @param seconds: (int) seconds that the fan alarm start time must be overridden to @param reset: (int) 1 to reset a previous override, 0 to override @return: 1 if successful, zero otherwise """ reset_value = integer_to_bytearray(reset) - h = integer_to_bytearray(hours) - m = integer_to_bytearray(minutes) - payload = reset_value + h + m + s = integer_to_bytearray(seconds) + payload = reset_value + s message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, message_id=MsgIds.MSG_ID_DG_FAN_RPM_ALARM_START_TIME_OFFSET_OVERRIDE.value, @@ -270,3 +271,37 @@ else: self.logger.debug("Timeout!!!!") return False + + def cmd_fans_duty_cycle_override(self, duty_cycle: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG fans duty cycle override command + Constraints: + Must be logged into DG. + + @param duty_cycle: (float) the duty cycle that the fans are overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + dc = float_to_bytearray(duty_cycle / 100.0) + payload = reset_value + dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FANS_DUTY_CYCLE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans duty cycle") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Set fans duty cycle to: " + + 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.debug("Timeout!!!!") + return False Index: dialin/dg/temperatures.py =================================================================== diff -u -r87851abe77a6658952480117b317132a17544f41 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/dg/temperatures.py (.../temperatures.py) (revision 87851abe77a6658952480117b317132a17544f41) +++ dialin/dg/temperatures.py (.../temperatures.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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. Index: dialin/hd/alarms.py =================================================================== diff -u -rb1332a9a27e5d7c47d378fef58afaf272cfeb7af -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/hd/alarms.py (.../alarms.py) (revision b1332a9a27e5d7c47d378fef58afaf272cfeb7af) +++ dialin/hd/alarms.py (.../alarms.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2020-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. Index: dialin/hd/calibration_record.py =================================================================== diff -u -r3a70bfb451b74106348c064c34f19934aadd9119 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/hd/calibration_record.py (.../calibration_record.py) (revision 3a70bfb451b74106348c064c34f19934aadd9119) +++ dialin/hd/calibration_record.py (.../calibration_record.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -17,11 +17,12 @@ 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 +from ..utils.nv_ops_utils import NVOpsUtils, Observer class HDCalibrationNVRecord(AbstractSubSystem): @@ -50,6 +51,8 @@ _PAYLOAD_TRANSFER_DELAY_S = 0.2 _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + _FIRMWARE_STACK_NAME = 'HD' + def __init__(self, can_interface, logger: Logger): """ @@ -64,7 +67,6 @@ self.total_messages = 0 self.received_msg_length = 0 self._is_getting_cal_in_progress = False - self._write_fw_data_to_excel = True self.cal_data = 0 self._raw_cal_record = [] self._utilities = NVOpsUtils(logger=self.logger) @@ -89,6 +91,28 @@ return status + def cmd_get_hd_calibration_record_report(self, report_destination: str = None): + """ + Handles getting HD calibration_record record from firmware and writing it to excel. + + @param report_destination: (str) the destination that the report should be written to + + @return: none + """ + # Prepare the excel report + self._utilities.prepare_excel_report(self._FIRMWARE_STACK_NAME, self._utilities.CAL_RECORD_TAB_NAME, + report_destination, protect_sheet=True) + # Request the HD calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_hd_calibration_record() + observer = Observer("hd_calibration_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + # Pass the HD calibration record to the function to write the excel + self._utilities.write_fw_record_to_excel(self.hd_calibration_record) + def cmd_request_hd_calibration_record(self) -> int: """ Handles getting HD calibration_record record from firmware. @@ -166,12 +190,34 @@ """ self.logger.debug("Received a complete hd calibration record.") + def cmd_set_hd_calibration_excel_to_fw(self, report_address: str): + """ + Handles setting the calibration data that is in an excel report to the firmware. + + @param report_address: (str) the address in which its data must be written to excel + + @return: none + """ + + # Request the HD calibration record and set and observer class to callback when the calibration record is read + # back + self.cmd_request_hd_calibration_record() + observer = Observer("hd_calibration_record") + # Attach the observer to the list + self.attach(observer) + while not observer.received: + sleep(0.1) + self._utilities.write_excel_record_to_fw_record(self.hd_calibration_record, report_address, + self._utilities.CAL_RECORD_TAB_NAME) + + self.cmd_set_hd_calibration_record(self.hd_calibration_record) + def cmd_set_hd_calibration_record(self, hd_calibration_record: OrderedDict) -> bool: """ Handles updating the HD calibration_record record with the newest calibration_record data of a hardware and sends it to FW. - @param hd_calibration_record: (OrderedDict) the hd calibration record to be send + @param hd_calibration_record: (OrderedDict) the hd calibration record to be sent @return: none """ record_packets = self._utilities.prepare_record_to_send_to_fw(hd_calibration_record) @@ -212,10 +258,9 @@ groups_byte_size = 0 # create a list of the functions of the sub dictionaries functions = [self._prepare_pumps_record(), self._prepare_valves_record(), - self._prepare_occlusion_sensors_record(), self._prepare_flow_sensors_record(), - self._prepare_pressure_sensors_record(), self._prepare_temperature_sensors_record(), - self._prepare_heparin_force_sensor_record(), self._prepare_accelerometer_sensor_record(), - self._prepare_blood_leak_sensor_record()] + self._prepare_occlusion_sensors_record(), self._prepare_pressure_sensors_record(), + self._prepare_temperature_sensors_record(), self._prepare_heparin_force_sensor_record(), + self._prepare_accelerometer_sensor_record(), self._prepare_blood_leak_sensor_record()] for function in functions: # Update the groups bytes size so far to be use to padding later @@ -294,7 +339,7 @@ @return: occlusion sensors hardware group dictionary and the byte size of this hardware group """ - hardware_names = ['ob', 'odi', 'odo'] + hardware_names = ['ob'] group_byte_size = 0 group_name = 'occlusion_sensors' @@ -313,31 +358,6 @@ return hardware_group, group_byte_size - def _prepare_flow_sensors_record(self): - """ - Handles creating the calibration_record dictionary of the flow sensors hardware group. - - @return: flow sensors hardware group dictionary and the byte size of this hardware group - """ - hardware_names = ['fmb', 'fmd'] - - group_byte_size = 0 - group_name = 'flow_sensors' - hardware_group = OrderedDict({group_name: OrderedDict()}) - hardware = OrderedDict() - - for i in hardware_names: - hardware[i] = {'fourth_order': [' int: + def cmd_fans_rpm_alarm_start_time_offset_override(self, seconds: int, reset: int = NO_RESET) -> int: """ Constructs and sends the HD fan RPM alarm start time override command Constraints: Must be logged into HD. - @param hours: (int) hours that the fan alarm start time must be overridden to - @param minutes: (int) minutes that the fan alarm start time must be overridden to + @param seconds: (int) seconds that the fan alarm start time must be overridden to @param reset: (int) 1 to reset a previous override, 0 to override @return: 1 if successful, zero otherwise """ reset_value = integer_to_bytearray(reset) - h = integer_to_bytearray(hours) - m = integer_to_bytearray(minutes) - payload = reset_value + h + m + s = integer_to_bytearray(seconds) + payload = reset_value + s message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, message_id=MsgIds.MSG_ID_HD_FAN_RPM_ALARM_START_TIME_OFFSET_OVERRIDE.value, @@ -208,9 +206,43 @@ if received_message is not None: self.logger.debug("Set RPM alarm start time to: " + - str(received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + 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.debug("Timeout!!!!") return False + + def cmd_fans_duty_cycle_override(self, duty_cycle: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD fans duty cycle override command + Constraints: + Must be logged into HD. + + @param duty_cycle: (float) the duty cycle that the fans are overridden to + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + dc = float_to_bytearray(duty_cycle / 100.0) + payload = reset_value + dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FANS_DUTY_CYCLE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans duty cycle") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Set fans duty cycle to: " + + 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.debug("Timeout!!!!") + return False Index: dialin/hd/hemodialysis_device.py =================================================================== diff -u -r6286da1bcfac5f1d43659196fb1baf27af50d746 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/hd/hemodialysis_device.py (.../hemodialysis_device.py) (revision 6286da1bcfac5f1d43659196fb1baf27af50d746) +++ dialin/hd/hemodialysis_device.py (.../hemodialysis_device.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,14 +1,14 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2020-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 hemodialysis_device.py # -# @author (last) Dara Navaei -# @date (last) 23-Nov-2021 +# @author (last) Micahel Garthwaite +# @date (last) 22-Feb-2022 # @author (original) Peter Lucia # @date (original) 02-Apr-2020 # @@ -61,6 +61,20 @@ # HD login password HD_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): """ HD object provides test/service commands for the HD sub-system. @@ -92,13 +106,19 @@ self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_hd_op_mode_sync) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, + MsgIds.MSG_ID_HD_UI_VERSION_INFO_RESPONSE.value, + self._handler_ui_version_response_sync) + # create properties self.hd_operation_mode = HDOpModes.MODE_INIT.value self.hd_operation_sub_mode = 0 self.hd_logged_in = False self.hd_set_logged_in_status(False) self.hd_no_transmit_msg_list = [0,0,0,0,0,0,0,0] + self.ui_version = None + # Create command groups self.accel = HDAccelerometer(self.can_interface, self.logger) self.air_bubbles = HDAirBubbles(self.can_interface, self.logger) @@ -157,6 +177,14 @@ """ return self.hd_no_transmit_msg_list + def get_ui_version(self): + """ + Gets the last recieved ui_version from the HD + + @return: ui_version in a string. + """ + return self.ui_version + @publish(["hd_logged_in"]) def hd_set_logged_in_status(self, logged_in: bool = False): """ @@ -183,6 +211,38 @@ self.hd_operation_mode = mode[0] self.hd_operation_sub_mode = smode[0] + def _handler_ui_version_response_sync(self,message): + """ + Handler for response from HD regarding its version. + + @param message: response message from HD regarding valid treatment parameter ranges.\n + U08 Major \n + U08 Minor \n + U08 Micro \n + U16 Build \n + U32 Compatibility revision + + @return: None if not successful, the version string if unpacked successfully + """ + major = struct.unpack(' 0 for each in [major, minor, micro, build, compatibility]]): + self.ui_version = f"v{major[0]}.{minor[0]}.{micro[0]}-{build[0]}.{compatibility[0]}" + self.logger.debug(f"UI VERSION: {self.ui_version}") + + else: + self.ui_version = None + self.logger.debug("Failed to retrieve UI Version.") + def cmd_log_in_to_hd(self, resend: bool = False) -> int: """ Constructs and sends a login command via CAN bus. Login required before \n @@ -608,3 +668,18 @@ else: self.logger.debug("Timeout!!!!") return False + + def cmd_request_ui_version(self) -> None: + """ + Constructs and sends a ui version request to the HD. + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_UI_VERSION_INFO_REQUEST.value) + + self.logger.debug("Sending an UI version request to the HD.") + self.can_interface.send(message, 0) + + Index: dialin/hd/syringe_pump.py =================================================================== diff -u -r8fcde658a845e638e91e7ab15b9738fcc2f0318c -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/hd/syringe_pump.py (.../syringe_pump.py) (revision 8fcde658a845e638e91e7ab15b9738fcc2f0318c) +++ dialin/hd/syringe_pump.py (.../syringe_pump.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,14 +1,14 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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 syringe_pump.py # -# @author (last) Sean Nash -# @date (last) 12-Nov-2021 +# @author (last) Hung Nguyen +# @date (last) 16-Feb-2022 # @author (original) Sean Nash # @date (original) 12-Mar-2021 # @@ -745,3 +745,32 @@ self.logger.debug("Timeout!!!!") return False + def cmd_heprin_target_rate_override(self, rate: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the heprin bolus target rate value override command + Constraints: + Must be logged into HD. + + @param rate: (float) the heparin bolus target rate to be set in mL/hour + @param reset: (int) 1 to reset a previous override, 0 to override + @return 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlu = float_to_bytearray(rate) # HD expects the rate in mL/hour + payload = reset_value + vlu + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_HEPRIN_BOLUS_TARGET_RATE_OVERRIDE.value, + payload=payload) + self.logger.debug("Overriding heprin bolus target rate value override") + + # 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("Heprin bolus target value override Timeout!!!") + return False Index: dialin/hd/temperatures.py =================================================================== diff -u -ra7f6b7938acc3b1e5e8dd9b8d5f8237a3b298e6b -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/hd/temperatures.py (.../temperatures.py) (revision a7f6b7938acc3b1e5e8dd9b8d5f8237a3b298e6b) +++ dialin/hd/temperatures.py (.../temperatures.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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. Index: dialin/hd/treatment.py =================================================================== diff -u -r9dcbb4f172b53560d8bae26363a16b906a1ba135 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/hd/treatment.py (.../treatment.py) (revision 9dcbb4f172b53560d8bae26363a16b906a1ba135) +++ dialin/hd/treatment.py (.../treatment.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,14 +1,14 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2020-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 treatment.py # -# @author (last) Sean Nash -# @date (last) 12-Nov-2021 +# @author (last) Micahel Garthwaite +# @date (last) 22-Feb-2022 # @author (original) Peter Lucia # @date (original) 02-Apr-2020 # @@ -110,6 +110,10 @@ self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_treatment_stop_timer_data_sync) + msg_id = MsgIds.MSG_ID_HD_RES_CURRENT_TREATMENT_PARAMETERS.value + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_dialin_ch_id, msg_id, + self._handler_treatment_current_parameters) + # treatment duration data self.treatment_time_prescribed = 0 self.treatment_time_elapsed = 0 @@ -144,6 +148,27 @@ self.treatment_stop_timeout_secs = 0 self.treatment_stop_timeout_coundown_secs = 0 + self.current_treatment_param_dict = {} + self.current_blood_flow = 0 + self.current_dialysate_flow = 0 + self.current_treatment_duration = 0 + self.current_heparin_pre_stop = 0 + self.current_saline_bolus_volume = 0 + self.current_acid_concentrate = 0 + self.current_bicarb_concentrate = 0 + self.current_dialyzer_type = 0 + self.current_heparin_type = 0 + self.current_blood_pressure_measurement_interval = 0 + self.current_rinseback_flow_rate = 0 + self.current_arterial_low = 0 + self.current_arterial_high = 0 + self.current_venous_low = 0 + self.current_venous_high = 0 + self.current_heparin_bolus = 0 + self.current_heparin_dispense = 0 + self.current_dialysate_temp = 0 + self.current_uf_volume = 0 + def reset(self) -> None: """ Reset all treatment variables @@ -383,6 +408,37 @@ """ return self.recirc_countdown_secs + def get_current_treatment_parameters(self) -> dict: + """ + Returns current treatment parameters in a dictionary + + @return: self.current_treatment_param_dict + """ + self.current_treatment_param_dict = { + "blood_flow": self.current_blood_flow, + "dialysate_flow" : self.current_dialysate_flow, + "treatment_duration" : self.current_treatment_duration, + "heparin_pre_stop" : self.current_heparin_pre_stop, + "saline_bolus" : self.current_saline_bolus_volume, + "acid_concentrate" : self.current_acid_concentrate, + "bicarb_concetrate" : self.current_bicarb_concentrate, + "dialyzer_type" : self.current_dialyzer_type, + "heparin_type" : self.current_heparin_type, + "blood_pressure_interval" : self.current_blood_pressure_measurement_interval, + "rinseback_flow_rate" : self.current_rinseback_flow_rate, + "arterial_low" : self.current_arterial_low, + "arterial_high" : self.current_arterial_high, + "venous_low" : self.current_venous_low, + "venous_high" : self.current_venous_high, + "heparin_bolus" : self.current_heparin_bolus, + "heparin_dispense" : self.current_heparin_dispense, + "dialysate_temp" : self.current_dialysate_temp, + "uf_volume" : self.current_uf_volume + } + + return self.current_treatment_param_dict + + @publish([ "treatment_time_prescribed", "treatment_time_elapsed", @@ -521,8 +577,7 @@ @publish([ "blood_prime_tgt_vol", - "blood_prime_cum_vol", - "blood_prime_ind_cum_vol" + "blood_prime_cum_vol" ]) def _handler_blood_prime_data_sync(self, message): """ @@ -537,12 +592,9 @@ message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) cum = struct.unpack('f', bytearray( message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) - ind = struct.unpack('f', bytearray( - message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) self.blood_prime_tgt_vol = tgt[0] self.blood_prime_cum_vol = cum[0] - self.blood_prime_ind_cum_vol = ind[0] @publish([ "recirc_timeout_secs", @@ -582,6 +634,82 @@ self.treatment_stop_timeout_secs = tmo[0] self.treatment_stop_timeout_coundown_secs = cnd[0] + @publish(["accepted", "current_blood_flow", "current_dialysate_flow", + "current_treatment_duration", "current_heparin_pre_stop", "current_saline_bolus_volume", + "current_acid_concentrate", "current_bicarb_concentrate", "current_dialyzer_type", + "current_heparin_type", "current_blood_pressure_measurement_interval", "current_rinseback_flow_rate", + "current_arterial_low", "current_arterial_high", "current_venous_low", + "current_venous_high", "current_heparin_bolus", "current_heparin_dispense", + "current_dialysate_temp", "current_uf_volume"]) + def _handler_treatment_current_parameters(self, message) -> None: + """ + Handles published current treatment parameters messages. + + @param message: published current treatment parameters data message. + @return: None + """ + accepted = struct.unpack(' int: """ Constructs and sends the set blood flow rate treatment parameter command. @@ -1550,3 +1678,27 @@ self.logger.debug("Timeout!!!!") return False + def cmd_request_current_treatment_parameters(self): + """ + Constructs and sends the current treatment parameters request message + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_REQ_CURRENT_TREATMENT_PARAMETERS.value) + + self.logger.debug("Requesting current Treatment Parameters from HD.") + + # Send message + received_message = self.can_interface.send(message) + # If there is content... + if received_message is not None: + self.logger.debug("Current Treatment Parameter Request recieved.") + # response payload is OK or not OK + return True + else: + self.logger.debug("Timeout!!!!") + return False Index: dialin/ui/unittests.py =================================================================== diff -u -radae506afce35a0063c6c2baf7e8580986f3bee7 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/ui/unittests.py (.../unittests.py) (revision adae506afce35a0063c6c2baf7e8580986f3bee7) +++ dialin/ui/unittests.py (.../unittests.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,19 +1,18 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2020-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 unittests.py # -# @author (last) Quang Nguyen -# @date (last) 22-Jul-2021 +# @author (last) Behrouz NematiPour +# @date (last) 01-Feb-2022 # @author (original) Peter Lucia # @date (original) 11-Nov-2020 # ############################################################################ - import test import sys Index: dialin/utils/excel_ops.py =================================================================== diff -u -r3a70bfb451b74106348c064c34f19934aadd9119 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/utils/excel_ops.py (.../excel_ops.py) (revision 3a70bfb451b74106348c064c34f19934aadd9119) +++ dialin/utils/excel_ops.py (.../excel_ops.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -17,7 +17,7 @@ import os import math import datetime -from openpyxl.styles import PatternFill, Font, Alignment +from openpyxl.styles import PatternFill, Font, Alignment, Protection from openpyxl.utils import get_column_letter from openpyxl import Workbook, load_workbook @@ -28,6 +28,7 @@ YELLOW = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid') BLUE = PatternFill(start_color='00CCFF', end_color='00CCFF', fill_type='solid') RED = PatternFill(start_color='FF0000', end_color='FF0000', fill_type='solid') +NO_COLOR = PatternFill(fill_type='none') def get_an_excel_workbook(): @@ -39,14 +40,15 @@ return Workbook() -def setup_excel_worksheet(workbook_obj, title, index=None): +def setup_excel_worksheet(workbook_obj, title, index=None, protection=False): """ Creates the worksheets in the created excel file with the name of each software_project_name as the name of the worksheet. @param workbook_obj: Excel workbook object @param title: Title of the created worksheet @param index: Index of the worksheet. If a sheet needs to be at a certain place (default none) + @param protection: Flag to indicate whether the worksheet is write protected or not (default False) @return none """ @@ -65,6 +67,10 @@ # Create a tab and name the tab with the name of the projects dictionary workbook_obj.create_sheet(title=title, index=index) + # Check if the created worksheet must be write protected and if yes, protect it + if protection: + workbook_obj[title].protection.sheet = True + # If the first tab is Sheet or Sheet1, remove it # The other tabs must be created first before removing the default tab if workbook_obj.sheetnames[0] == 'Sheet' or workbook_obj.sheetnames[0] == 'Sheet1': @@ -73,7 +79,7 @@ def write_to_excel(workbook_obj, project, row, column, data, name='Calibri', font=11, bold=False, color=None, - merge=None, horizontal='left', freeze=False, max_col_len=None): + merge=None, horizontal='left', freeze=False, max_col_len=None, protect_cell=False): """ This function writes data at the specified row and column in an excel file (object). @@ -90,6 +96,7 @@ @param horizontal: Horizontal alignment (default left) @param freeze: Freeze top row (default false) @param max_col_len: maximum length of a column (default none, means it is not restricted) + @param protect_cell: flag to indicate whether to write protect cell or not (default False) @return: None """ @@ -148,7 +155,10 @@ # To freeze row 1, make the cell is not row 1, that's why A2 was chosen active_sheet.freeze_panes = 'A2' + # Enforce the cell protection whether it is a False or True + active_sheet.cell(row, column).protection = Protection(locked=protect_cell) + def load_excel_report(path): """ This function returns an object of a currently existing excel workbook @@ -194,14 +204,25 @@ active_sheet.merge_cells(str(merge_start_cell) + ':' + str(merge_end_cell)) -def save_report(excel_workbook, save_dir, record_name): +def save_report(excel_workbook, save_dir, record_name, stack_name=None): """ This function overrides the save function in the Base class. The function saves the excel file. + @param excel_workbook: Excel workbook object + @param save_dir: Saving directory + @param record_name: Type of record being saved (i.e. calibration, software configuration) + @param stack_name: Name of the software stack name (i.e. HD) default none + @returns none """ # Get the current date current_date = str(datetime.datetime.now().date()) + # Some of the records might want to add the name of the stack. For instance, software configurations might want to + # mention that this is an HD or DG software configuration report. + if stack_name is not None: + address = current_date + '-' + str(stack_name) + '-' + str(record_name).upper() + '-Record.xlsx' + else: + address = current_date + '-' + str(record_name).upper() + '-Record.xlsx' # Create the save path by using the path, date and current code review excel count out of total number of them - path = os.path.join(save_dir, current_date + '-' + str(record_name).upper() + '-Record.xlsx') + path = os.path.join(save_dir, address) excel_workbook.save(filename=path) Index: dialin/utils/nv_ops_utils.py =================================================================== diff -u -r3a70bfb451b74106348c064c34f19934aadd9119 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- dialin/utils/nv_ops_utils.py (.../nv_ops_utils.py) (revision 3a70bfb451b74106348c064c34f19934aadd9119) +++ dialin/utils/nv_ops_utils.py (.../nv_ops_utils.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -13,14 +13,36 @@ # @date (original) 21-Feb-2021 # ############################################################################ +import os.path import struct import time from logging import Logger from typing import List from collections import OrderedDict from .excel_ops import * +from dialin.utils.base import AbstractObserver +class Observer(AbstractObserver): + """ + + Observation class + """ + def __init__(self, prop): + self.received = False + self.prop = prop + + def update(self, message): + """ + Publicly accessible function to provide an update of the object that is being observed + + @param message: (str) the message to update its status + + @return none + """ + self.received = message.get(self.prop, False) + + class NVOpsUtils: """ @@ -62,7 +84,11 @@ 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 ) + # Public defines DEFAULT_CHAR_VALUE = ' ' + CAL_RECORD_TAB_NAME = 'Calibration Record' + NON_VOLATILE_RECORD_NAME = 'SW_Config_Report' + _RECORD_START_INDEX = 6 _RECORD_SPECS_BYTES = 12 _RECORD_SPECS_BYTE_ARRAY = 3 @@ -81,6 +107,14 @@ _PAYLOAD_TOTAL_MSG_INDEX = 1 _PAYLOAD_TOTAL_BYTES_INDEX = 2 + _SW_CONFIGS_TITLE_COL = 'SW Configurations' + _SW_CONFIGS_VALUE_COL = 'Status' + _SW_CONFIGS_REPORT_NAME = 'SW-Configs' + _CAL_TIME_NAME = 'cal_time' + _NEW_CAL_TIME_SIGNAL = 'new' + _CRC_NAME = 'crc' + _PADDING_GROUP_NAME = 'padding' + def __init__(self, logger: Logger): """ Constructor for the NVOptsUtils class @@ -104,24 +138,56 @@ # of the group. self._temp_groups_data_to_calculate_crc = [] - def prepare_excel_report(self, firmware_stack: str, record_name: str, directory: str): + def _create_workspace(self, dir_name): """ + Publicly accessible function to get create a workspace for the script that is running. + + @param dir_name: Name of the workspace directory + + @return none + """ + # Get the root directory of the current script + scripts_root_dir = os.path.dirname(os.path.dirname(__file__)) + # Get the root directory of the entire scripts folder. The workspace that holds the + # code review reports and clones other scripts and repositories must be outside of the scripts + root_dir = os.path.dirname(os.path.dirname(scripts_root_dir)) + # Create the address of the workspace + self._workspace_dir = os.path.join(root_dir, dir_name) + # If the path does not exist, make it, otherwise, change to that directory + if not os.path.isdir(self._workspace_dir): + # Create the directory and go to it + os.mkdir(self._workspace_dir) + os.chdir(self._workspace_dir) + + def prepare_excel_report(self, firmware_stack: str, record_name: str, directory: str, protect_sheet: bool = False): + """ Publicly accessible function to prepare the excel report @param firmware_stack: (str) firmware stack name (e.g. "HD" or "DG") @param record_name: (str) record type to check such as calibration, system, ... - @param directory: (str) the directory in which to write the excel doc + @param directory: (str) the directory in which to write the excel document. + @param protect_sheet: (bool) flag to indicate whether to write protect the sheet or not (default False) @return none """ path = '' is_report_found = False - if not os.path.isdir(directory): - # Create the directory and go to it - os.mkdir(directory) + # If a directory is provided and there is not a folder in that address, create the + # directory. Set the workspace directory to the provided directory. If a directory was not + # provided, create a workspace in the default position + default_nv_directory = firmware_stack + '_NV_Records' - self._workspace_dir = directory + if directory is not None: + directory = os.path.join(directory, default_nv_directory) + if not os.path.isdir(directory): + # Create the directory and go to it + os.mkdir(directory) + self._workspace_dir = directory + os.chdir(self._workspace_dir) + else: + self._create_workspace(default_nv_directory) + self._record_name = record_name self._firmware_stack = firmware_stack @@ -132,12 +198,22 @@ # Check if the firmware stack (i.e. DG) is in the file and name of the file # does not have lock in it. When the file is open, there is a hidden lock file # in there and it is ignored - if self._firmware_stack in str(file) and 'lock' not in str(file): - # Create the file path and exit the loop - path = os.path.join(self._workspace_dir, file) - is_report_found = True - break + file = str(file) + if self._firmware_stack in file and 'lock' not in file: + if self._SW_CONFIGS_REPORT_NAME in file and self._SW_CONFIGS_REPORT_NAME == record_name and \ + str(datetime.datetime.now().date()) in file: + # Create the file path and exit the loop + path = os.path.join(self._workspace_dir, file) + is_report_found = True + break + if self._SW_CONFIGS_REPORT_NAME in file and self._SW_CONFIGS_REPORT_NAME == record_name and \ + str(datetime.datetime.now().date()) in file: + # Create the file path and exit the loop + path = os.path.join(self._workspace_dir, file) + is_report_found = True + break + if is_report_found: # Load the excel workbook self._excel_workbook = load_excel_report(path) @@ -146,149 +222,160 @@ self._excel_workbook = get_an_excel_workbook() # Setup worksheet and create the current tab - setup_excel_worksheet(self._excel_workbook, self._record_name) + setup_excel_worksheet(self._excel_workbook, self._record_name, protection=protect_sheet) - def write_fw_record_to_excel(self, calibration_record: dict): + def write_fw_record_to_excel(self, calibration_record: OrderedDict): """ Writes a calibration record to excel @param calibration_record: (dict) the record to write to excel @return: None """ - try: - row = 1 + # Let's say the calibration record is: + # Get the keys of the calibration group {'pressure_sensors': 'ppi', {'fourth_order': [' bytearray: """ @@ -340,6 +427,9 @@ @return record (OrderedDict) the record with updated calibration time and crc """ for key, value in record.items(): + # Check if there is a CRC in the inner dictionary since some of the structures might not have it. + # For instance, the software configuration record does not have an inner CRC and it only has a global CRC + # with the padding if isinstance(value, dict): crc = NVOpsUtils.get_group_record_crc(value) value['crc'][1] = crc @@ -373,15 +463,6 @@ return crc_value @staticmethod - def get_current_time_in_epoch(): - """ - Returns the current date and time in epoch in integer format. This is a static method. - - @return: (int) data and time in epoch - """ - return int(datetime.datetime.now().timestamp()) - - @staticmethod def get_current_date_time(epoch: int): """ Returns the current date and time from an epoch time @@ -392,6 +473,15 @@ return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(epoch)) @staticmethod + def get_current_time_in_epoch(): + """ + Returns the current date and time in epoch in integer format. This is a static method. + + @return: (int) data and time in epoch + """ + return int(datetime.datetime.now().timestamp()) + + @staticmethod def get_date_time_in_epoch(data_time: str): """ Returns the date time in epoch @@ -420,7 +510,7 @@ # Get the list of keys of the main dictionary (i.e. pressure_sensors, temperature_sensors, ....} # These keys are the list of hardware groups for group in cal_record.keys(): - # All the hardware groups (i.e pressure sensors) are nested dictionaries, except the final CRC. So it is + # All the hardware groups (i.e. pressure sensors) are nested dictionaries, except the final CRC. It is # unpacked here. The CRC is an unsigned 16-bit (short). if group == 'crc': # Get the data type ( it must be '= max_col_to_go: + break + + # Check if the value of the read cell is not none + if value is not None: + # If the value of the title row is name of the sw configs title column name, update the title col number + if value.strip() == self._SW_CONFIGS_TITLE_COL: + title_col = col + # If the value of the title row is the name of the values of the sw configs, update the value col number + if value.strip() == self._SW_CONFIGS_VALUE_COL: + value_col = col + # If the title col and value col numbers are both found and they are not none, then exit the while loop + # since the values have been found, otherwise, increment the column number and keep looking + if title_col is not None and value_col is not None: + break + else: + col += 1 + + if title_col is not None and value_col is not None: + # Get the last non-empty row number of the current active sheet + last_non_empty_row = active_sheet.max_row + # Get the dictionary of the provided sw configurations dictionary, this can be either HD or DG + fw_sw_configs = sw_configs_dict['sw_configs'] + # Loop through the excel from row 2 since row 1 is the titles row until the last non-empty row + 1 since the + # range method does not include that last element so the last non-empty row will not be covered if there is + # not a +1. + for row in range(2, last_non_empty_row + 1): + config = active_sheet.cell(row=row, column=title_col).value + + if config is not None: + # Check if the software configuration that has been read from excel exists in dictionary that has + # been prepared in Dialin + if config.strip() in fw_sw_configs: + excel_config_value = active_sheet.cell(row=row, column=value_col).value + # Check if the value is an integer and it is a 1 or a 0 + # The only acceptable values are 1 for enable and 0 for disable + if isinstance(excel_config_value, int) and excel_config_value == 1 or excel_config_value == 0: + fw_sw_configs[config.strip()][1] = excel_config_value + else: + # If the value is not acceptable, set the default value that is sent down to firmware to 0 + # and write incorrect into the report so the user will notice that they had and empty cell + # or a cell with a non-acceptable value (i.e. 123). Color the cell as red. + fw_sw_configs[config.strip()][1] = 0 + write_to_excel(self._excel_workbook, sw_config_excel_tab, row, value_col, 'Incorrect Value', + color=RED) + # Save back the excel workbook with the changes + # This save function is the openpyxl save and not the internal save function that creates + # the save path. The save path already exists + self._excel_workbook.save(filename=excel_path) + status = True + + return status Index: tests/dg_tests.py =================================================================== diff -u -r6d6c2318a81c130b7875bfad31424e56fd8f59f2 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- tests/dg_tests.py (.../dg_tests.py) (revision 6d6c2318a81c130b7875bfad31424e56fd8f59f2) +++ tests/dg_tests.py (.../dg_tests.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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. Index: tests/hd_blood_leak_data.py =================================================================== diff -u -r8841ca865ff616ea3de171bc4037c349aaac7934 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- tests/hd_blood_leak_data.py (.../hd_blood_leak_data.py) (revision 8841ca865ff616ea3de171bc4037c349aaac7934) +++ tests/hd_blood_leak_data.py (.../hd_blood_leak_data.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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. Index: tests/hd_valves_test.py =================================================================== diff -u -r6286da1bcfac5f1d43659196fb1baf27af50d746 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- tests/hd_valves_test.py (.../hd_valves_test.py) (revision 6286da1bcfac5f1d43659196fb1baf27af50d746) +++ tests/hd_valves_test.py (.../hd_valves_test.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2020-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. Index: tests/peter/set_RTCs.py =================================================================== diff -u -r6286da1bcfac5f1d43659196fb1baf27af50d746 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- tests/peter/set_RTCs.py (.../set_RTCs.py) (revision 6286da1bcfac5f1d43659196fb1baf27af50d746) +++ tests/peter/set_RTCs.py (.../set_RTCs.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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. Index: tests/peter/test_dg_records.py =================================================================== diff -u -rbe8a844a39195fc593bcf79b1c1b2b3add33436a -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- tests/peter/test_dg_records.py (.../test_dg_records.py) (revision be8a844a39195fc593bcf79b1c1b2b3add33436a) +++ tests/peter/test_dg_records.py (.../test_dg_records.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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. Index: tests/peter/test_hd_records.py =================================================================== diff -u -ree278f5a643613b8e7acbc44c3e70ead692cd0f0 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- tests/peter/test_hd_records.py (.../test_hd_records.py) (revision ee278f5a643613b8e7acbc44c3e70ead692cd0f0) +++ tests/peter/test_hd_records.py (.../test_hd_records.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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. Index: tests/test_hd_dg_fans.py =================================================================== diff -u -r8841ca865ff616ea3de171bc4037c349aaac7934 -rc3b33cf5796df77eb213523c1e06ba4adbb9501d --- tests/test_hd_dg_fans.py (.../test_hd_dg_fans.py) (revision 8841ca865ff616ea3de171bc4037c349aaac7934) +++ tests/test_hd_dg_fans.py (.../test_hd_dg_fans.py) (revision c3b33cf5796df77eb213523c1e06ba4adbb9501d) @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2019-2022 Diality Inc. - All Rights Reserved. +# Copyright (c) 2021-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.