Fisheye: Tag a06097b6c375bec6088f8fa6feecc8851eaa5889 refers to a dead (removed) revision in file `shared/scripts/dialin'. Fisheye: No comparison available. Pass `N' to diff? Index: shared/scripts/dialin/.gitignore =================================================================== diff -u --- shared/scripts/dialin/.gitignore (revision 0) +++ shared/scripts/dialin/.gitignore (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,2 @@ +__pycache__ + Index: shared/scripts/dialin/__init__.py =================================================================== diff -u --- shared/scripts/dialin/__init__.py (revision 0) +++ shared/scripts/dialin/__init__.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,18 @@ +from .hd import * +from .dg import * +from .ui import * +from .utils import * + +__version__ = "" + +try: + from importlib import metadata +except ImportError: # for Python < 3.8 + import importlib_metadata as metadata + +try: + __version__ = metadata.version(__name__) +except metadata.PackageNotFoundError: + # package is not installed + pass + Index: shared/scripts/dialin/common/__init__.py =================================================================== diff -u --- shared/scripts/dialin/common/__init__.py (revision 0) +++ shared/scripts/dialin/common/__init__.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,7 @@ +from .alarm_defs import * +from .alarm_priorities import * +from .msg_defs import * +from .prs_defs import * +from .hd_defs import * +from .dg_defs import * +from .ui_defs import * Index: shared/scripts/dialin/common/alarm_defs.py =================================================================== diff -u --- shared/scripts/dialin/common/alarm_defs.py (revision 0) +++ shared/scripts/dialin/common/alarm_defs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,311 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 alarm_defs.py +# +# @author (last) Quang Nguyen +# @date (last) 04-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 07-Aug-2020 +# +############################################################################ +from enum import unique +from ..utils.base import AlarmEnum + + +# Branch: staging +@unique +class AlarmList(AlarmEnum): + ALARM_ID_NO_ALARM = 0 + ALARM_ID_STUCK_BUTTON_TEST_FAILED = 1 + ALARM_ID_HD_FPGA_POST_TEST_FAILED = 2 + ALARM_ID_DG_FPGA_POST_TEST_FAILED = 3 + ALARM_ID_WATCHDOG_POST_TEST_FAILED = 4 + ALARM_ID_DG_WATCHDOG_POST_TEST_FAILED = 5 + ALARM_ID_UI_COMM_POST_FAILED = 6 + ALARM_ID_RTC_CONFIG_ERROR = 7 + ALARM_ID_HD_ACCELEROMETER_SELF_TEST_FAILURE = 8 + ALARM_ID_DG_ACCELEROMETER_SELF_TEST_FAILURE = 9 + ALARM_ID_RTC_OR_TIMER_ACCURACY_FAILURE = 10 + ALARM_ID_DG_HEATERS_SELF_TEST_FAILURE = 11 + ALARM_ID_HD_INTEGRITY_POST_TEST_FAILED = 12 + ALARM_ID_DG_INTEGRITY_POST_TEST_FAILED = 13 + ALARM_ID_HD_BLOOD_FLOW_INVALID_CALIBRATION = 14 + ALARM_ID_HD_DIALYSATE_FLOW_INVALID_CALIBRATION = 15 + ALARM_ID_HD_ALARM_AUDIO_SELF_TEST_FAILURE = 16 + ALARM_ID_HD_UI_POST_FAILED = 17 + ALARM_ID_DG_PRESSURE_SENSORS_INVALID_CAL_RECORD = 18 + ALARM_ID_DG_FLOW_SENSORS_INVALID_CAL_RECORD = 19 + ALARM_ID_DG_COND_SENSORS_INVALID_CAL_RECORD = 20 + ALARM_ID_DG_DRAIN_LINE_VOLUME_INVALID_CAL_RECORD = 21 + ALARM_ID_DG_RESERVOIRS_INVALID_CAL_RECORD = 22 + ALARM_ID_DG_ACID_CONCENTRATE_INVALID_CAL_RECORD = 23 + ALARM_ID_DG_BICARB_CONCENTRATE_INVALID_CAL_RECORD = 24 + ALARM_ID_DG_ACCELEROMETERS_INVALID_CAL_RECORD = 25 + ALARM_ID_HD_ACCELEROMETERS_INVALID_CAL_RECORD = 26 + ALARM_ID_HD_BLOOD_FLOW_INVALID_CAL_RECORD = 27 + ALARM_ID_HD_DIALYSATE_FLOW_INVALID_CAL_RECORD = 28 + ALARM_ID_HD_HEPARIN_FORCE_SENSOR_INVALID_CAL_RECORD = 29 + ALARM_ID_HD_SOFTWARE_FAULT = 30 + ALARM_ID_BLOOD_PUMP_MC_CURRENT_CHECK = 31 + ALARM_ID_BLOOD_PUMP_OFF_CHECK = 32 + ALARM_ID_BLOOD_PUMP_MC_DIRECTION_CHECK = 33 + ALARM_ID_BLOOD_PUMP_ROTOR_SPEED_CHECK = 34 + ALARM_ID_DIAL_IN_PUMP_MC_CURRENT_CHECK = 35 + ALARM_ID_DIAL_IN_PUMP_OFF_CHECK = 36 + ALARM_ID_DIAL_IN_PUMP_MC_DIRECTION_CHECK = 37 + ALARM_ID_DIAL_IN_PUMP_ROTOR_SPEED_CHECK = 38 + ALARM_ID_DIAL_OUT_PUMP_MC_CURRENT_CHECK = 39 + ALARM_ID_DIAL_OUT_PUMP_OFF_CHECK = 40 + ALARM_ID_DIAL_OUT_PUMP_MC_DIRECTION_CHECK = 41 + ALARM_ID_DIAL_OUT_PUMP_ROTOR_SPEED_CHECK = 42 + ALARM_ID_WATCHDOG_EXPIRED = 43 + ALARM_ID_UI_COMM_TIMEOUT = 44 + ALARM_ID_COMM_TOO_MANY_BAD_CRCS = 45 + ALARM_ID_CAN_MESSAGE_NOT_ACKED = 46 + ALARM_ID_UF_RATE_TOO_HIGH_ERROR = 47 + ALARM_ID_UF_VOLUME_ACCURACY_ERROR = 48 + ALARM_ID_HD_FPGA_COMM_TIMEOUT = 49 + ALARM_ID_VALVE_CONTROL_FAILURE = 50 + ALARM_ID_BLOOD_PUMP_MOTOR_SPEED_CHECK = 51 + ALARM_ID_DIAL_IN_PUMP_MOTOR_SPEED_CHECK = 52 + ALARM_ID_DIAL_OUT_PUMP_MOTOR_SPEED_CHECK = 53 + ALARM_ID_HD_CRITICAL_DATA_ERROR = 54 + ALARM_ID_DG_CRITICAL_DATA_ERROR = 55 + ALARM_ID_HD_ACCELEROMETER_FAILURE = 56 + ALARM_ID_DG_ACCELEROMETER_FAILURE = 57 + ALARM_ID_HD_VALVE_HOMING_FAILED = 58 + ALARM_ID_HD_VALVE_TRANSITION_TIMEOUT = 59 + ALARM_ID_HD_VALVE_NOT_FUNCTIONAL = 60 + ALARM_ID_HD_VALVE_CURRENT_OUT_OF_RANGE = 61 + ALARM_ID_HD_VALVE_POSITION_OUT_OF_RANGE = 62 + ALARM_ID_ARTERIAL_PRESSURE_SENSOR_FAULT = 63 + ALARM_ID_VENOUS_PRESSURE_SENSOR_FAULT = 64 + ALARM_ID_DG_COMMAND_INVALID_PARAMETER_FAULT = 65 + ALARM_ID____AVAILABLE_21 = 66 + ALARM_ID_HD_BP_OCCLUSION_SELF_TEST_FAILURE = 67 + ALARM_ID_HD_ACTIVE_RESERVOIR_RECIRCULATION_OUT_OF_RANGE = 68 + ALARM_ID____AVAILABLE_6 = 69 + ALARM_ID_HD_ARTERIAL_PRESSURE_SELF_TEST_FAILURE = 70 + ALARM_ID_HD_VENOUS_PRESSURE_SELF_TEST_FAILURE = 71 + ALARM_ID_HD_BLOOD_FLOW_STATUS_SELF_TEST_FAILURE = 72 + ALARM_ID_HD_DIALYSATE_FLOW_STATUS_SELF_TEST_FAILURE = 73 + ALARM_ID_HD_BLOOD_LEAK_SELF_TEST_FAILURE = 74 + ALARM_ID_HD_SYRINGE_PUMP_SELF_TEST_FAILURE = 75 + ALARM_ID_HD_VOLTAGE_OUT_OF_RANGE = 76 + ALARM_ID_DG_VOLTAGE_OUT_OF_RANGE = 77 + ALARM_ID_HD_SYRINGE_PUMP_ENCODER_DIRECTION_ERROR = 78 + ALARM_ID_HD_SYRINGE_PUMP_CONTROLLER_DIRECTION_ERROR = 79 + ALARM_ID_HD_SYRINGE_PUMP_FAULT = 80 + ALARM_ID_HD_SYRINGE_PUMP_OVER_TRAVEL_ERROR = 81 + ALARM_ID_HD_SYRINGE_PUMP_DAC_WRITE_ERROR = 82 + ALARM_ID_HD_SYRINGE_PUMP_RUNNING_WHILE_BP_OFF_ERROR = 83 + ALARM_ID_HD_VENOUS_PRESSURE_READ_TIMEOUT_ERROR = 84 + ALARM_ID_HD_VENOUS_PRESSURE_SENSOR_TEMP_OUT_OF_RANGE = 85 + ALARM_ID_HD_BP_OCCLUSION_READ_TIMEOUT_ERROR = 86 + ALARM_ID____AVAILABLE_10 = 87 + ALARM_ID____AVAILABLE_11 = 88 + ALARM_ID_HD_BP_OCCLUSION_SENSOR_ERROR = 89 + ALARM_ID____AVAILABLE_12 = 90 + ALARM_ID_HD_DIALYSATE_FLOW_DATA_NOT_RECEIVE = 91 + ALARM_ID____AVAILABLE_14 = 92 + ALARM_ID____AVAILABLE_15 = 93 + ALARM_ID____AVAILABLE_16 = 94 + ALARM_ID____AVAILABLE_17 = 95 + ALARM_ID____AVAILABLE_18 = 96 + ALARM_ID____AVAILABLE_19 = 97 + ALARM_ID_HD_BATTERY_COMM_FAULT = 98 + ALARM_ID_HD_SYRINGE_PUMP_STALL = 99 + ALARM_ID_HD_NO_CART_SELF_TEST_TIMEOUT = 100 + ALARM_ID_HD_DRY_SELF_TEST_TIMEOUT = 101 + ALARM_ID_RTC_COMM_ERROR = 102 + ALARM_ID_NVDATA_MFG_RECORD_CRC_ERROR = 103 + ALARM_ID_AIR_TRAP_ILLEGAL_LEVELS = 104 + ALARM_ID_NVDATAMGMT_INDIVIDUAL_RECORD_CRC_INVALID = 105 + ALARM_ID____AVAILABLE_20 = 106 + ALARM_ID_DG_RESTARTED_FAULT = 107 + ALARM_ID_HD_SYRINGE_PUMP_ADC_ERROR = 108 + ALARM_ID_HD_SYRINGE_PUMP_VOLUME_ERROR = 109 + ALARM_ID_HD_SYRINGE_PUMP_SPEED_ERROR = 110 + ALARM_ID_HD_SYRINGE_PUMP_NOT_STOPPED_ERROR = 111 + ALARM_ID_HD_BLOOD_LEAK_FAULT = 112 + ALARM_ID_HD_ARTERIAL_BUBBLE_SELF_TEST_FAILURE = 113 + ALARM_ID_HD_VENOUS_BUBBLE_SELF_TEST_FAILURE = 114 + ALARM_ID_DG_TEMPERATURE_SENSOR_OUT_OF_RANGE = 115 + ALARM_ID_DG_TEMPERATURE_SENSOR_ADC_OUT_OF_RANGE = 116 + ALARM_ID_DG_PRIMARY_HEATER_INTERNAL_TEMP_OUT_OF_RANGE = 117 + ALARM_ID_DG_PRIMARY_HEATER_CJ_TEMP_OUT_OF_RANGE = 118 + ALARM_ID_DG_TRIMMER_HEATER_INTERNAL_TEMP_OUT_OF_RANGE = 119 + ALARM_ID_DG_TRIMMER_HEATER_CJ_TEMP_OUT_OF_RANGE = 120 + ALARM_ID_DG_MAIN_PRIMARY_HEATER_VOLTAGE_OUT_OF_RANGE = 121 + ALARM_ID_DG_SMALL_PRIMARY_HEATER_VOLTAGE_OUT_OF_RANGE = 122 + ALARM_ID_DG_TRIMMER_HEATER_VOLTAGE_OUT_OF_RANGE = 123 + ALARM_ID_END_OF_TREATMENT_HIGH = 124 + ALARM_ID_TREATMENT_STOPPED_NO_RINSEBACK = 125 + ALARM_ID_HD_BLOOD_LEAK_DETECTED = 126 + ALARM_ID_VENOUS_PRESSURE_LOW = 127 + ALARM_ID_HD_VENOUS_BUBBLE_DETECTED = 128 + ALARM_ID_HD_VENOUS_BUBBLE_DETECTED_RINSEBACK = 129 + ALARM_ID_VENOUS_PRESSURE_HIGH = 130 + ALARM_ID_ARTERIAL_PRESSURE_LOW = 131 + ALARM_ID_ARTERIAL_PRESSURE_HIGH = 132 + ALARM_ID_DG_FLUID_LEAK_DETECTED = 133 + ALARM_ID_HD_FLUID_LEAK_DETECTED = 134 + ALARM_ID_HD_SHOCK = 135 + ALARM_ID_DG_SHOCK = 136 + ALARM_ID_HD_EXCESSIVE_TILT = 137 + ALARM_ID_DG_EXCESSIVE_TILT = 138 + ALARM_ID_HD_AC_POWER_LOST = 139 + ALARM_ID_DG_COMM_TIMEOUT = 140 + ALARM_ID_AIR_TRAP_FILL_DURING_TREATMENT = 141 + ALARM_ID_OCCLUSION_BLOOD_PUMP = 142 + ALARM_ID_OCCLUSION_DIAL_IN_PUMP = 143 + ALARM_ID_OCCLUSION_DIAL_OUT_PUMP = 144 + ALARM_ID_ACID_CONDUCTIVITY_OUT_OF_RANGE = 145 + ALARM_ID_DIALYSATE_CONDUCTIVITY_OUT_OF_RANGE = 146 + ALARM_ID_DIALYSATE_CONDUCTIVITY_FAULT = 147 + ALARM_ID_INLET_WATER_HIGH_TEMPERATURE = 148 + ALARM_ID_INLET_WATER_LOW_TEMPERATURE = 149 + ALARM_ID_INLET_WATER_HIGH_CONDUCTIVITY = 150 + ALARM_ID_INLET_WATER_LOW_CONDUCTIVITY = 151 + ALARM_ID_INLET_WATER_LOW_PRESSURE = 152 + ALARM_ID_PRIME_COMPLETED_HIGH = 153 + ALARM_ID_NVDATA_EEPROM_OPS_FAILURE = 154 + ALARM_ID____AVAILABLE_22 = 155 + ALARM_ID_NVDATA_HW_USAGE_DATA_CRC_ERROR = 156 + ALARM_ID____AVAILABLE_23 = 157 + ALARM_ID_BLOOD_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = 158 + ALARM_ID_DIAL_IN_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = 159 + ALARM_ID_BLOOD_PUMP_ROTOR_SPEED_TOO_HIGH = 160 + ALARM_ID_BLOOD_FLOW_SIGNAL_STRENGTH_TOO_LOW = 161 + ALARM_ID_DIALYSATE_FLOW_SIGNAL_STRENGTH_TOO_LOW = 162 + ALARM_ID_HD_LOAD_CELL_ACCELERATION_RES_1_ALARM = 163 + ALARM_ID_HD_LOAD_CELL_ACCELERATION_RES_2_ALARM = 164 + ALARM_ID_TREATMENT_RINSEBACK_TIMEOUT_ALARM = 165 + ALARM_ID_TREATMENT_RECIRC_TIMEOUT_ALARM = 166 + ALARM_ID_CARTRIDGE_DOOR_OPENED = 167 + ALARM_ID_HD_ACTIVE_RESERVOIR_DEPLETION_TIME_OUT = 168 + ALARM_ID_DIALYSATE_FLOW_RATE_OUT_OF_RANGE = 169 + ALARM_ID_HD_SYRINGE_PUMP_SYRINGE_EMPTY = 170 + ALARM_ID_HD_SYRINGE_PUMP_OCCLUSION = 171 + ALARM_ID_HD_SYRINGE_PUMP_NOT_ENOUGH_HEPARIN_ALARM = 172 + ALARM_ID_HD_ARTERIAL_BUBBLE_DETECTED = 173 + ALARM_ID_HD_ARTERIAL_BUBBLE_DETECTED_RINSEBACK = 174 + ALARM_ID_HD_PUMP_DIRECTION_STATUS_ERROR = 175 + ALARM_ID_HD_RESERVOIR_FULL_AND_DG_NOT_READY_TO_SWITCH = 176 + ALARM_ID_DG_SOFTWARE_FAULT = 177 + ALARM_ID_HD_COMM_TIMEOUT = 178 + ALARM_ID_DG_FPGA_COMM_TIMEOUT = 179 + ALARM_ID_DG_LOAD_CELL_ADC_ERROR = 180 + ALARM_ID_DG_LOAD_CELLS_TARE_WEIGHT_OUT_OF_RANGE = 181 + ALARM_ID_DG_LOAD_CELLS_INVALID_CALIBRATION = 182 + ALARM_ID_DG_INVALID_LOAD_CELL_VALUE = 183 + ALARM_ID_UV_REACTOR_NOT_HEALTHY = 184 + ALARM_ID_DG_FAN_RPM_OUT_OF_RANGE = 185 + ALARM_ID_DG_CONCENTRATE_PUMP_FAULT = 186 + ALARM_ID_CP1_SPEED_CONTROL_ERROR = 187 + ALARM_ID_CP2_SPEED_CONTROL_ERROR = 188 + ALARM_ID_DRAIN_PUMP_RPM_OUT_OF_RANGE = 189 + ALARM_ID_DRAIN_PUMP_OFF_FAULT = 190 + ALARM_ID_FLOW_RATE_OUT_OF_UPPER_RANGE = 191 + ALARM_ID_FLOW_RATE_OUT_OF_LOWER_RANGE = 192 + ALARM_ID_RO_PUMP_FLOW_RATE_OUT_OF_RANGE = 193 + ALARM_ID_RO_PUMP_OFF_FAULT = 194 + ALARM_ID_RO_PUMP_PRESSURE_OUT_OF_RANGE = 195 + ALARM_ID_DG_TEMPERATURE_SENSOR_FAULT = 196 + ALARM_ID_DG_TEMPERATURE_SENSORS_ADC_FAULT = 197 + ALARM_ID_DG_HEATERS_NEGATIVE_COLD_JUNCTION_TEMPERATURE = 198 + ALARM_ID_DG_HEATERS_FAULT = 199 + ALARM_ID_DG_THERMISTORS_TEMPERATURE_OUT_OF_RANGE = 200 + ALARM_ID_DG_BAD_INLET_WATER_QUALITY = 201 + ALARM_ID_INLET_WATER_PRESSURE_FAULT = 202 + ALARM_ID_PRESSURE_SENSOR_FAULT = 203 + ALARM_ID_RO_REJECTION_RATIO_OUT_OF_RANGE = 204 + ALARM_ID_CONDUCTIVITY_SENSOR_FAULT = 205 + ALARM_ID_DG_DIALYSATE_FILL_OUT_OF_TIME = 206 + ALARM_ID_DG_FLOW_METER_CHECK_FAILURE = 207 + ALARM_ID_DG_HEATERS_ON_WITH_NO_FLOW_TIMEOUT = 208 + ALARM_ID_DG_DRAIN_CIRCULATION_LINE_TIMEOUT = 209 + ALARM_ID_HD_BATTERY_PACK_ERROR_DETECTED = 210 + ALARM_ID_BLOOD_SITTING_WARNING = 211 + ALARM_ID_END_OF_TREATMENT_ALARM = 212 + ALARM_ID_PRIME_COMPLETED_MEDIUM = 213 + ALARM_ID_SALINE_BOLUS_VOLUME_CHECK_FAILURE = 214 + ALARM_ID_RINSEBACK_VOLUME_CHECK_FAILURE = 215 + ALARM_ID_END_TREATMENT_TIMEOUT_ALARM = 216 + ALARM_ID_BLOOD_PRIME_VOLUME_CHECK_FAILURE = 217 + ALARM_ID_HD_SYRINGE_DETECTED = 218 + ALARM_ID_HD_SYRINGE_PUMP_SYRINGE_REMOVED = 219 + ALARM_ID_HD_BATTERY_PACK_CHARGE_TOO_LOW = 220 + ALARM_ID_EMPTY_SALINE_BAG = 221 + ALARM_ID_DIALYSATE_TEMPERATURE_HIGH = 222 + ALARM_ID_DIALYSATE_TEMPERATURE_LOW = 223 + ALARM_ID_TREATMENT_STOPPED_BY_USER = 224 + ALARM_ID_END_OF_TREATMENT_WARNING = 225 + ALARM_ID_PRIME_COMPLETED_LOW_PRIORITY = 226 + ALARM_ID_PRIME_OUT_OF_TIME = 227 + ALARM_ID_HD_PRIME_PURGE_AIR_TIME_OUT = 228 + ALARM_ID_PRIME_DIALYSATE_DIALYZER_TIME_OUT = 229 + ALARM_ID_PRIME_DIALYSATE_BYPASS_TIME_OUT = 230 + ALARM_ID_PRE_TREATMENT_DRY_SELF_TEST_FAILURE = 231 + ALARM_ID_PRE_TREATMENT_WET_SELF_TEST_FAILURE = 232 + ALARM_ID_RTC_BATTERY_LOW = 233 + ALARM_ID_RTC_RAM_OPS_ERROR = 234 + ALARM_ID_TREATMENT_STOPPED_AFTER_RINSEBACK = 235 + ALARM_ID_INSTALL_NEW_CARTRIDGE = 236 + ALARM_ID_PRIME_SALINE_DIALYZER_TIME_OUT = 237 + ALARM_ID_NO_CARTRIDGE_LOADED = 238 + ALARM_ID_CARTRIDGE_REMOVAL_FAILURE = 239 + ALARM_ID_BICARB_CONDUCTIVITY_OUT_OF_RANGE = 240 + ALARM_ID_DG_RESERVOIR_DRAIN_TIMEOUT = 241 + ALARM_ID_DG_RESERVOIR_FILL_TIMEOUT = 242 + ALARM_ID_DG_RESERVOIR_LEAK_TIMEOUT = 243 + ALARM_ID_DG_TEMP_SENSORS_DIFF_OUT_OF_RANGE = 244 + ALARM_ID_DG_HEAT_DISINFECT_TARGET_TEMP_TIMEOUT = 245 + ALARM_ID_DG_HEAT_DISINFECT_INLET_PRES_AND_TEMP_SNSRS_OUT = 246 + ALARM_ID_DG_HEAT_DISINFECT_INLET_COND_AND_TEMP_OUT = 247 + ALARM_ID_DG_CHEM_DISINFECT_TARGET_TEMP_TIMEOUT = 248 + ALARM_ID_DG_CHEM_DISINFECT_INLET_PRES_AND_TEMP_SNSRS_OUT = 249 + ALARM_ID_DG_CHEM_DISINFECT_INLET_COND_AND_TEMP_OUT = 250 + ALARM_ID_HD_INVALID_SYSTEM_RECORD = 251 + ALARM_ID_HD_INVALID_SERVICE_RECORD = 252 + ALARM_ID_DG_INVALID_SYSTEM_RECORD = 253 + ALARM_ID_DG_INVALID_SERVICE_RECORD = 254 + ALARM_ID_HD_UI_COMPATIBILITY_ERROR = 255 + ALARM_ID_HD_DG_COMPATIBILITY_ERROR = 256 + ALARM_ID_DG_FPGA_POWER_OUT_TIMEOUT = 257 + ALARM_ID_HD_FPGA_POWER_OUT_TIMEOUT = 258 + ALARM_ID_HD_TEMPERATURES_OUT_OF_RANGE = 259 + ALARM_ID_UI_POST_FAILURE_FILESYSTEM = 260 + ALARM_ID_UI_POST_FAILURE_CANBUS = 261 + ALARM_ID_UI_POST_FAILURE_DISPLAY = 262 + ALARM_ID_UI_POST_FAILURE_TOUCH = 263 + ALARM_ID_UI_POST_FAILURE_SDCARD = 264 + ALARM_ID_UI_POST_FAILURE_RTC = 265 + ALARM_ID_UI_POST_FAILURE_WIFI = 266 + ALARM_ID_UI_POST_FAILURE_BLUETOOTH = 267 + ALARM_ID_UI_POST_FAILURE_ETHERNET = 268 + ALARM_ID_UI_POST_FAILURE_SOUND = 269 + ALARM_ID_HD_SAFETY_SHUTDOWN_POST_TEST_FAILED = 270 + ALARM_ID_DG_SAFETY_SHUTDOWN_POST_TEST_FAILED = 271 + ALARM_ID_HD_FAN_RPM_OUT_OF_RANGE = 272 + ALARM_ID_HD_BLOOD_FLOW_OUT_OF_RANGE = 273 + ALARM_ID_HD_DIAL_IN_FLOW_OUT_OF_RANGE = 274 + ALARM_ID_HD_ARTERIAL_PRESSURE_OUT_OF_RANGE = 275 + ALARM_ID_HD_VENOUS_PRESSURE_OUT_OF_RANGE = 276 + ALARM_ID_HD_BP_OCCLUSION_OUT_OF_RANGE = 277 + ALARM_ID_HD_ACTIVE_RESERVOIR_WEIGHT_OUT_OF_RANGE = 278 + ALARM_ID_DG_DIALYSATE_DRAIN_TIME_OUT = 279 + ALARM_ID_HD_ARTERIAL_PRESSURE_READ_TIMEOUT_ERROR = 280 + ALARM_ID_DG_ACID_BOTTLE_LOW_VOLUME = 281 + ALARM_ID_DG_BICARB_BOTTLE_LOW_VOLUME = 282 + ALARM_ID_DG_LOAD_CELL_WEIGHT_OUT_OF_RANGE = 283 + ALARM_ID_DG_LOAD_CELL_PRIMARY_BACKUP_DRIFT_OUT_OF_RANGE = 284 + ALARM_ID_HD_TREATMENT_RECIRC_TIMEOUT_WARNING = 285 + ALARM_ID_HD_TREATMENT_RINSEBACK_TIMEOUT_WARNING = 286 + ALARM_ID_HD_FPGA_CLOCK_SPEED_CHECK_FAILURE = 287 + ALARM_ID_DG_TRIMMER_HEATER_ON_WITH_NO_FLOW_TIMEOUT = 288 Index: shared/scripts/dialin/common/alarm_priorities.py =================================================================== diff -u --- shared/scripts/dialin/common/alarm_priorities.py (revision 0) +++ shared/scripts/dialin/common/alarm_priorities.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,26 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 alarm_priorities.py +# +# @author (last) Quang Nguyen +# @date (last) 06-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 05-Apr-2021 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +@unique +class AlarmPriorities(DialinEnum): + ALARM_PRIORITY_NONE = 0 # Indicates not an alarm or no alarms active at this time + ALARM_PRIORITY_LOW = 1 # Low priority alarm + ALARM_PRIORITY_MEDIUM = 2 # Medium priority alarm + ALARM_PRIORITY_HIGH = 3 # High priority alarm + NUM_OF_ALARM_PRIORITIES = 4 # Total number of alarm priorities Index: shared/scripts/dialin/common/config.py =================================================================== diff -u --- shared/scripts/dialin/common/config.py (revision 0) +++ shared/scripts/dialin/common/config.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,40 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 config.py +# +# @author (last) Joseph Varghese +# @date (last) 17-Jan-2022 +# +############################################################################ + + + + + + + +def aha_setup(): + + test.startSection("AHA SETUP") + + cleanup() # cleanup before setup + + test.log("Launching a new instance of PARSIM/AHA/PATSIM") + try: + os.chdir(f"{config.COMMON_PATH}") + res = os.system("./maintenance_script.sh") + squish.snooze(5) + if (res != 0) and (res != 256): + raise Exception("Wrong path to the simulator executable was given. Script not executed") + except Exception as msg: + test.log(pyStr(msg)) + test.log("Launched a new instance of PARSIM/AHA/PATSIM") + + startApplication(config.RainierMonitorApplication, "Rainier Monitor") + + test.endSection() \ No newline at end of file Index: shared/scripts/dialin/common/dg_defs.py =================================================================== diff -u --- shared/scripts/dialin/common/dg_defs.py (revision 0) +++ shared/scripts/dialin/common/dg_defs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,230 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 dg_defs.py +# +# @author (last) Dara Navaei +# @date (last) 10-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 22-Jun-2021 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +@unique +class DGOpModes(DialinEnum): + DG_MODE_FAUL = 0 # Fault mode + DG_MODE_SERV = 1 # Service mode + DG_MODE_INIT = 2 # Initialization & POST mode + DG_MODE_STAN = 3 # Standby mode - connected to HD + DG_MODE_SOLO = 4 # Standby Solo mode - no HD connected + DG_MODE_GENE = 5 # Generation Idle mode + DG_MODE_FILL = 6 # Fill mode + DG_MODE_DRAI = 7 # Drain mode + DG_MODE_FLUS = 8 # Flush mode + DG_MODE_HEAT = 9 # Heat Disinfect mode + DG_MODE_CHEM = 10 # Chemical Disinfect mode + DG_MODE_NLEG = 11 # Not legal - an illegal mode transition occurred + NUM_OF_DG_MODES = 12 # Number of DG operation modes + + +@unique +class DGInitStates(DialinEnum): + DG_POST_STATE_START = 0 + DG_POST_STATE_FW_COMPATIBILITY = 1 + DG_POST_STATE_FW_INTEGRITY = 2 + DG_POST_STATE_FPGA = 3 + DG_POST_STATE_RTC = 4 + DG_POST_STATE_NVDATAMGMT = 5 + DG_POST_STATE_TEMPERATURE_SENSORS = 6 + DG_POST_STATE_ACCELEROMETER = 7 + DG_POST_STATE_PRESSURES = 8 + DG_POST_STATE_RO_PUMP = 9 + DG_POST_STATE_DRAIN_PUMP = 10 + DG_POST_STATE_CONCENTRATE_PUMPS = 11 + DG_POST_STATE_CONDUCTIVITY_SENSORS = 12 + DG_POST_STATE_RESERVOIRS = 13 + DG_POST_STATE_UV_REACTORS = 14 + DG_POST_STATE_THERMISTORS = 15 + DG_POST_STATE_FANS = 16 + DG_POST_STATE_WATCHDOG = 17 + DG_POST_STATE_SAFETY_SHUTDOWN = 18 + DG_POST_STATE_LOAD_CELL = 19 + DG_POST_STATE_COMPLETED = 20 + DG_POST_STATE_FAILED = 21 + NUM_OF_DG_POST_STATES = 22 + + +@unique +class DGFaultStates(DialinEnum): + DG_FAULT_STATE_START = 0 + NUM_OF_DG_FAULT_STATES = 1 + + +@unique +class DGStandByModeStates(DialinEnum): + DG_STANDBY_MODE_STATE_START = 0 # Start standby mode state + DG_STANDBY_MODE_STATE_IDLE = 1 # Idle standby mode state + DG_STANDBY_MODE_STATE_FLUSH_FILTER = 2 # Sample water flush filter state + DG_STANDBY_MODE_STATE_FLUSH_FILTER_IDLE = 3 # Sample water flush filter idle state + DG_STANDBY_MODE_STATE_SAMPLE_WATER = 4 # Sample water state + NUM_OF_DG_STANDBY_MODE_STATES = 5 # Number of standby mode states + + +@unique +class DGGenIdleModeStates(DialinEnum): + DG_GEN_IDLE_MODE_STATE_START = 0 + DG_GEN_IDLE_MODE_STATE_FLUSH_LINES = 1 + DG_GEN_IDLE_MODE_STATE_FLUSH_WATER = 2 + NUM_OF_DG_GEN_IDLE_MODE_STATES = 3 + + +@unique +class DGFillModeStates(DialinEnum): + DG_FILL_MODE_STATE_START = 0 # Start fill mode state + DG_FILL_MODE_STATE_CHECK_INLET_WATER = 1 # Check inlet water quality state + DG_FILL_MODE_STATE_BICARB_PUMP_CHECK = 2 # Run bicarb concentrate pump and check conductivity range + DG_FILL_MODE_STATE_ACID_PUMP_CHECK = 3 # Run acid concentrate pump and check conductivity range + DG_FILL_MODE_STATE_DIALYSATE_PRODUCTION = 4 # Dialysate production state + DG_FILL_MODE_STATE_DELIVER_DIALYSATE = 5 # Deliver dialysate state + DG_FILL_MODE_STATE_PAUSED = 6 # Dialysate generation pause state + NUM_OF_DG_FILL_MODE_STATES = 7 # Number of fill mode states + + +@unique +class DGDrainModeStates(DialinEnum): + DG_DRAIN_STATE_START = 0 # Start drain mode state + DG_DRAIN_STATE_DRAIN = 1 # Drain drain mode state + DG_DRAIN_STATE_TARE = 2 # Tare drain mode state + NUM_OF_DG_DRAIN_STATES = 3 # Number of drain mode states + + +@unique +class DGFlushStates(DialinEnum): + DG_FLUSH_STATE_START = 0 + DG_FLUSH_STATE_DRAIN_R1 = 1 + DG_FLUSH_STATE_DRAIN_R2 = 2 + DG_FLUSH_STATE_FLUSH_DRAIN = 3 + DG_FLUSH_STATE_FLUSH_DIALYSATE = 4 + DG_FLUSH_STATE_FLUSH_CONCENTRATE_STRAWS = 5 + DG_FLUSH_STATE_FLUSH_R1_TO_R2 = 6 + DG_FLUSH_STATE_FLUSH_R2_AND_DRAIN_R1 = 7 + DG_FLUSH_STATE_FLUSH_CIRCULATION_DRAIN_LINE = 8 + DG_FLUSH_STATE_FLUSH_CIRCULATION = 9 + DG_FLUSH_STATE_FLUSH_WITH_FRESH_WATER = 10 + DG_FLUSH_STATE_CANCEL_BASIC_PATH = 11 + DG_FLUSH_STATE_CANCEL_WATER_PATH = 12 + DG_FLUSH_STATE_COMPLETE = 13 + NUM_OF_DG_FLUSH_STATES = 14 + + +@unique +class DGHeatDisinfectStates(DialinEnum): + DG_HEAT_DISINFECT_STATE_START = 0 # Heat disinfect, start mode state + DG_HEAT_DISINFECT_STATE_DRAIN_R1 = 1 # Heat disinfect, drain R1 state + DG_HEAT_DISINFECT_STATE_DRAIN_R2 = 2 # Heat disinfect, drain R2 state + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN = 3 # Heat disinfect, flush drain state + DG_HEAT_DISINFECT_STATE_FLUSH_CIRCULATION = 4 # Heat disinfect, flush circulation state + DG_HEAT_DISINFECT_STATE_FLUSH_R1_AND_R2 = 5 # Heat disinfect, flush R1 and R2 state + DG_HEAT_DISINFECT_STATE_FLUSH_R2_AND_DRAIN_R1 = 6 # Heat disinfect, flush R2 and drain R1 state + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R2 = 7 # Heat disinfect, flush drain R2 state + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R1 = 8 # Heat disinfect, flush drain R1 state + DG_HEAT_DISINFECT_STATE_FILL_WITH_WATER = 9 # Heat disinfect, fill with water state + DG_HEAT_DISINFECT_STATE_DISINFECT_R1_TO_R2 = 10 # Heat disinfect, disinfect R1 to R2 state + DG_HEAT_DISINFECT_STATE_FILL_R2_WITH_HOT_WATER = 11 # Heat disinfect, fill R2 with hot water state + DG_HEAT_DISINFECT_STATE_DISINFECT_R2_TO_R1 = 12 # Heat disinfect, disinfect R2 to R1 state + DG_HEAT_DISINFECT_STATE_COOL_DOWN_HEATERS = 13 # Heat disinfect, cool down heaters state + DG_HEAT_DISINFECT_STATE_COOL_DOWN_RO_FILTER = 14 # Heat disinfect, cool down RO filter state + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R1 = 15 # Heat disinfect, mix drain R1 state + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R2 = 16 # Heat disinfect, mix drain R2 state + DG_HEAT_DISINFECT_STATE_RINSE_R1_TO_R2 = 17 # Heat disinfect, rinse R1 to R2 state + DG_HEAT_DISINFECT_STATE_RINSE_R2_TO_R1_AND_DRAIN_R1 = 18 # Heat disinfect, rinse R2 to R1 and drain R1 state + DG_HEAT_DISINFECT_STATE_RINSE_CIRCULATION = 19 # Heat disinfect, rinse circulation state + DG_HEAT_DISINFECT_STATE_CANCEL_BASIC_PATH = 20 # Heat disinfect, cancel mode basic path state + DG_HEAT_DISINFECT_STATE_CANCEL_WATER_PATH = 21 # Heat disinfect, cancel mode water path state + DG_HEAT_DISINFECT_STATE_COMPLETE = 22 # Heat disinfect, complete state + NUM_OF_DG_HEAT_DISINFECT_STATES = 23 # Number of heat disinfect mode states + + +@unique +class DGHeatDisinfectUIStates(DialinEnum): + HEAT_DISINFECT_UI_STATE_NOT_RUNNING = 0 + HEAT_DISINFECT_UI_STATE_FLUSH_BEFORE_DISINFECT = 1 + HEAT_DISINFECT_UI_STATE_HEAT_UP_WATER = 2 + HEAT_DISINFECT_UI_STATE_DISINFECT_RESERVOIR_1 = 3 + HEAT_DISINFECT_UI_STATE_TRANSITION_HOT_WATER = 4 + HEAT_DISINFECT_UI_STATE_DISINFECT_RESERVOIR_2 = 5 + HEAT_DISINFECT_UI_STATE_COOL_DOWN_DEVICE = 6 + HEAT_DISINFECT_UI_STATE_FLUSH_AFTER_DISINFECT = 7 + HEAT_DISINFECT_UI_STATE_CANCEL_DISINFECT = 8 + HEAT_DISINFECT_UI_STATE_COMPLETE = 9 + NUM_OF_HEAT_DISINFECT_UI_STATES = 10 + + +@unique +class DGChemicalDisinfectStates(DialinEnum): + DG_CHEM_DISINFECT_STATE_START = 0 + DG_CHEM_DISINFECT_STATE_DRAIN_R1 = 1 + DG_CHEM_DISINFECT_STATE_DRAIN_R2 = 2 + DG_CHEM_DISINFECT_STATE_FLUSH_DRAIN = 3 + DG_CHEM_DISINFECT_STATE_FLUSH_CIRCULATION = 4 + DG_CHEM_DISINFECT_STATE_FLUSH_R1_AND_R2 = 5 + DG_CHEM_DISINFECT_STATE_FLUSH_R2_AND_DRAIN_R1 = 6 + DG_CHEM_DISINFECT_STATE_FLUSH_DRAIN_R2 = 7 + DG_CHEM_DISINFECT_STATE_FLUSH_DRAIN_R1 = 8 + DG_CHEM_DISINFECT_STATE_PRIME_ACID_LINE = 9 + DG_CHEM_DISINFECT_STATE_FILL_WITH_WATER_AND_DISINFECTANT = 10 + DG_CHEM_DISINFECT_STATE_REMOVE_ACID_BOTTLE_FROM_UI = 11 + DG_CHEM_DISINFECT_STATE_DISINFECT_R1_TO_R2 = 12 + DG_CHEM_DISINFECT_STATE_FILL_R2_WITH_DISINFECTANT = 13 + DG_CHEM_DISINFECT_STATE_DISINFECT_R2_TO_R1 = 14 + DG_CHEM_DISINFECT_STATE_COOL_DOWN_HEATERS = 15 + DG_CHEM_DISINFECT_STATE_DISINFECTANT_DRAIN_R1 = 16 + DG_CHEM_DISINFECT_STATE_DISINFECTANT_DRAIN_R2 = 17 + DG_CHEM_DISINFECT_STATE_RINSE_R1_TO_R2 = 18 + DG_CHEM_DISINFECT_STATE_RINSE_R2_TO_R1_AND_DRAIN_R1 = 19 + DG_CHEM_DISINFECT_STATE_RINSE_R1_TO_R2_AND_DRAIN_R2 = 20 + DG_CHEM_DISINFECT_STATE_RINSE_CIRCULATION = 21 + DG_CHEM_DISINFECT_STATE_CANCEL_BASIC_PATH = 22 + DG_CHEM_DISINFECT_STATE_CANCEL_WATER_PATH = 23 + DG_CHEM_DISINFECT_STATE_COMPLETE = 24 + NUM_OF_DG_CHEM_DISINFECT_STATES = 25 + + +@unique +class DGChemDisinfectUIStates(DialinEnum): + CHEM_DISINFECT_UI_STATE_NOT_RUNNING = 0 + CHEM_DISINFECT_UI_STATE_FLUSH_BEFORE_DISINFECT = 1 + CHEM_DISINFECT_UI_STATE_MIX_WATER_AND_ACID = 2 + CHEM_DISINFECT_UI_STATE_REMOVE_ACID = 3 + CHEM_DISINFECT_UI_STATE_DISINFECT_DEVICE = 4 + CHEM_DISINFECT_UI_STATE_FLUSH_AFTER_DISINFECT = 5 + CHEM_DISINFECT_UI_STATE_CANCEL_DISINFECT = 6 + CHEM_DISINFECT_UI_STATE_COMPLETE = 7 + NUM_OF_CHEM_DISINFECT_UI_STATES = 8 + + +@unique +class DGEventList(DialinEnum): + DG_EVENT_STARTUP = 0 + DG_EVENT_OP_MODE_CHANGE = 1 + DG_EVENT_SUB_MODE_CHANGE = 2 + NUM_OF_DG_EVENT_IDS = 3 + + +@unique +class DGEventDataType(DialinEnum): + EVENT_DATA_TYPE_NONE = 0 + EVENT_DATA_TYPE_U32 = 1 + EVENT_DATA_TYPE_S32 = 2 + EVENT_DATA_TYPE_F32 = 3 + EVENT_DATA_TYPE_BOOL = 4 + NUM_OF_EVENT_DATA_TYPES = 5 + + Index: shared/scripts/dialin/common/hd_defs.py =================================================================== diff -u --- shared/scripts/dialin/common/hd_defs.py (revision 0) +++ shared/scripts/dialin/common/hd_defs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,391 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 hd_defs.py +# +# @author (last) Dara Navaei +# @date (last) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 04-Dec-2020 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +@unique +class HDOpModes(DialinEnum): + MODE_FAUL = 0 # Fault mode + MODE_SERV = 1 # Service mode + MODE_INIT = 2 # Initialization & POST mode + MODE_STAN = 3 # Standby mode + MODE_TPAR = 4 # Treatment Parameters mode + MODE_PRET = 5 # Pre-Treatment mode + MODE_TREA = 6 # Treatment mode + MODE_POST = 7 # Post-Treatment mode + NUM_OF_MODES = 8 # Number of HD operation modes + + +@unique +class HDOpSubModes(DialinEnum): + SUBMODE_START = 0 + SUBMODE_WAIT_FOR_TREATMENT = 1 + SUBMODE_WAIT_FOR_DISINFECT = 2 + SUBMODE_DG_FLUSH_IN_PROGRESS = 3 + SUBMODE_DG_HEAT_DISINFECT_IN_PROGRESS = 4 + SUBMODE_DG_CHEMICAL_DISINFECT_IN_PROGRESS = 5 + NUM_OF_MODES = 6 + + +@unique +class HDInitStates(DialinEnum): + POST_STATE_START = 0 + POST_STATE_FW_INTEGRITY = 1 + POST_STATE_AC = 2 + POST_STATE_WATCHDOG = 3 + POST_STATE_SAFETY_SHUTDOWN = 4 + POST_STATE_RTC = 5 + POST_STATE_NVDATAMGMT = 6 + POST_STATE_BLOOD_FLOW = 7 + POST_STATE_DIALYSATE_FLOW = 8 + POST_STATE_VALVES = 9 + POST_STATE_SYRINGE_PUMP = 10 + POST_STATE_ALARM_AUDIO = 11 + POST_STATE_ALARM_LAMP = 12 + POST_STATE_ACCELEROMETER = 13 + POST_STATE_STUCK_BUTTON = 14 + POST_STATE_UI_POST = 15 + POST_STATE_FW_COMPATIBILITY = 16 + POST_STATE_FPGA = 17 + POST_STATE_COMPLETED = 18 + POST_STATE_FAILED = 19 + NUM_OF_POST_STATES = 20 + + +@unique +class PreTreatmentSubModes(DialinEnum): + HD_PRE_TREATMENT_START_STATE = 0 + HD_PRE_TREATMENT_WATER_SAMPLE_STATE = 1 + HD_PRE_TREATMENT_CONSUMABLE_SELF_TEST_STATE = 2 + HD_PRE_TREATMENT_SELF_TEST_NO_CART_STATE = 3 + HD_PRE_TREATMENT_CART_INSTALL_STATE = 4 + HD_PRE_TREATMENT_SELF_TEST_DRY_STATE = 5 + HD_PRE_TREATMENT_PRIME_STATE = 6 + HD_PRE_TREATMENT_RECIRCULATE_STATE = 7 + HD_PRE_TREATMENT_PATIENT_CONNECTION_STATE = 8 + + +@unique +class PreTreatmentSampleWaterStates(DialinEnum): + SAMPLE_WATER_SETUP_STATE = 0 + SAMPLE_WATER_STATE = 1 + + +@unique +class PreTreatmentConsumableSelfTestStates(DialinEnum): + CONSUMABLE_SELF_TESTS_INSTALL = 0 + CONSUMABLE_SELF_TESTS_FILL_CMD_STATE = 1 + CONSUMABLE_SELF_TESTS_WATER_QUALITY_CHECK_STATE = 2 + CONSUMABLE_SELF_TESTS_BICARB_PUMP_CHECK_STATE = 3 + CONSUMABLE_SELF_TESTS_ACID_PUMP_CHECK_STATE = 4 + CONSUMABLE_SELF_TESTS_COMPLETE_STATE = 5 + + +@unique +class PreTreatmentNoCartSelfTestStates(DialinEnum): + NO_CART_SELF_TESTS_START_STATE = 0 + NO_CART_SELF_TESTS_WAIT_FOR_DOOR_CLOSE_STATE = 1 + NO_CART_SELF_TESTS_OCCLUSION_SENSORS_STATE = 2 + NO_CART_SELF_TESTS_BLOOD_FLOW_METERS_STATE = 3 + NO_CART_SELF_TESTS_DIALYSATE_FLOW_METERS_STATE = 4 + NO_CART_SELF_TESTS_PUMPS_STATE = 5 + NO_CART_SELF_TESTS_LEAK_DETECTORS_STATE = 6 + NO_CART_SELF_TESTS_BOARD_TEMPERATURE_STATE = 7 + NO_CART_SELF_TESTS_DOOR_SWITCH_STATE = 8 + NO_CART_SELF_TESTS_HOME_VALVES_AND_PUMPS_STATE = 9 + NO_CART_SELF_TESTS_HOME_IDLE_STATE = 10 + NO_CART_SELF_TESTS_STOPPED_STATE = 11 + NO_CART_SELF_TESTS_COMPLETE_STATE = 12 + + +@unique +class PreTreatmentDrySelfTestStates(DialinEnum): + DRY_SELF_TESTS_START_STATE = 0 + DRY_SELF_TESTS_WAIT_FOR_DOOR_CLOSE_STATE = 1 + DRY_SELF_TESTS_USED_CARTRIDGE_CHECK_STATE = 2 + DRY_SELF_TESTS_OCCLUSION_SENSORS_STATE = 3 + DRY_SELF_TESTS_PRESSURE_SENSORS_SETUP_STATE = 4 + DRY_SELF_TESTS_PRESSURE_SENSORS_STATE = 5 + DRY_SELF_TESTS_PRESSURE_SENSORS_NORMAL_STATE = 6 + DRY_SELF_TESTS_SYRINGE_PUMP_PRIME_STATE = 7 + DRY_SELF_TESTS_STOPPED_STATE = 8 + DRY_SELF_TESTS_COMPLETE_STATE = 9 + + +@unique +class PreTreatmentPrimeStates(DialinEnum): + HD_PRIME_START_STATE = 0 + HD_PRIME_WAIT_FOR_USER_START_STATE = 1 + HD_PRIME_SALINE_SETUP_STATE = 2 + HD_PRIME_SALINE_PURGE_AIR_STATE = 3 + HD_PRIME_SALINE_CIRC_BLOOD_CIRCUIT_STATE = 4 + HD_PRIME_RESERVOIR_ONE_FILL_COMPLETE_STATE = 5 + HD_PRIME_DIALYSATE_DIALYZER_STATE = 6 + HD_PRIME_RESERVOIR_TWO_FILL_COMPLETE_STATE = 7 + HD_PRIME_DIALYSATE_BYPASS_STATE = 8 + HD_PRIME_WET_SELF_TESTS_STATE = 9 + HD_PRIME_PAUSE = 10 + HD_PRIME_COMPLETE = 11 + + +@unique +class PreTreatmentRecircStates(DialinEnum): + PRE_TREATMENT_RECIRC_STATE = 0 + PRE_TREATMENT_RECIRC_STOPPED_STATE = 1 + + +# Heparin states +@unique +class HeparinStates(DialinEnum): + HEPARIN_STATE_OFF = 0 # Heparin treatment parameters set to zero or not yet set + HEPARIN_STATE_STOPPED = 1 # Heparin delivery is stopped because treatment paused or not yet started + HEPARIN_STATE_PAUSED = 2 # Heparin delivery is paused by user + HEPARIN_STATE_INITIAL_BOLUS = 3 # Heparin bolus is currently being delivered + HEPARIN_STATE_DISPENSING = 4 # Heparin continuous delivery is in progress + HEPARIN_STATE_COMPLETED = 5 # Heparin delivery is completed + HEPARIN_STATE_EMPTY = 6 # Heparin syringe is empty + + +# Syringe pump states +@unique +class SyringePumpStates(DialinEnum): + SYRINGE_PUMP_INIT_STATE = 0 # Syringe pump initialize state + SYRINGE_PUMP_OFF_STATE = 1 # Syringe pump off state + SYRINGE_PUMP_RETRACT_STATE = 2 # Syringe pump retract state + SYRINGE_PUMP_SEEK_STATE = 3 # Syringe pump seek state + SYRINGE_PUMP_PRIME_STATE = 4 # Syringe pump prime state + SYRINGE_PUMP_HEP_BOLUS_STATE = 5 # Syringe pump bolus state + SYRINGE_PUMP_HEP_CONTINUOUS_STATE = 6 # Syringe pump continuous state + SYRINGE_PUMP_CONFIG_FORCE_SENSOR_STATE = 7 # Syringe pump configure force sensor state + + +# Syringe pump operations +@unique +class SyringePumpOperations(DialinEnum): + SYRINGE_PUMP_OP_STOP = 0 # Stop syringe pump + SYRINGE_PUMP_OP_RETRACT = 1 # Retract syringe pump + SYRINGE_PUMP_OP_SEEK = 2 # Seek plunger + SYRINGE_PUMP_OP_PRIME = 3 # Prime Heparin line + SYRINGE_PUMP_OP_BOLUS = 4 # Deliver Heparin bolus of set volume over 5 minutes + SYRINGE_PUMP_OP_CONTINUOUS = 5 # Continuous dispense of Heparin at set rate + + +@unique +class PostTreatmentStates(DialinEnum): + HD_POST_TREATMENT_DRAIN_RESERVOIRS_STATE = 0 + HD_POST_TREATMENT_PATIENT_DISCONNECTION_STATE = 1 + HD_POST_TREATMENT_DISPOSABLE_REMOVAL_STATE = 2 + HD_POST_TREATMENT_VERIFY_STATE = 3 + + +@unique +class PreTreatmentModeStates(DialinEnum): + HD_PRE_TREATMENT_START_STATE = 0 # Start pre-treatment mode state + HD_PRE_TREATMENT_WATER_SAMPLE_STATE = 1 # Water sample state + HD_PRE_TREATMENT_SELF_TEST_NO_CART_STATE = 2 # Self tests with no cartridge state + HD_PRE_TREATMENT_CART_INSTALL_STATE = 3 # Consumable and cartridge installation state + HD_PRE_TREATMENT_SELF_TEST_DRY_STATE = 4 # Self tests dry state + HD_PRE_TREATMENT_PRIME_STATE = 5 # Prime blood and dialysate circuits and run wet self-tests state + HD_PRE_TREATMENT_RECIRCULATE_STATE = 6 # Re-circulate blood and dialysate circuits state + HD_PRE_TREATMENT_PATIENT_CONNECTION_STATE = 7 # Patient connection state + NUM_OF_HD_PRE_TREATMENT_STATES = 8 # Number of pre-treatment mode states + + +@unique +class PreTreatmentSampleWaterStates(DialinEnum): + SAMPLE_WATER_SETUP_STATE = 0 # Sample water setup state of the sample water sub-mode state machine + SAMPLE_WATER_STATE = 1 # Sample water state of the sample water sub-mode state machine + SAMPLE_WATER_COMPLETE_STATE = 2 # Sample water complete state + NUM_OF_SAMPLE_WATER_STATES = 3 # Number of sample water sub-mode states + + +@unique +class PreTreatmentConsumableSelfTestStates(DialinEnum): + CONSUMABLE_SELF_TESTS_INSTALL_STATE = 0 # Consumable self-tests install state + CONSUMABLE_SELF_TESTS_WATER_QUALITY_CHECK_STATE = 1 # Consumable self-tests water quality check state + CONSUMABLE_SELF_TESTS_BICARB_PUMP_CHECK_STATE = 2 # Consumable self-tests bicarb concentrate pump check state + CONSUMABLE_SELF_TESTS_ACID_PUMP_CHECK_STATE = 3 # Consumable self-tests acid concentrate pump check state + CONSUMABLE_SELF_TESTS_COMPLETE_STATE = 4 # Consumable self-tests complete state + NUM_OF_CONSUMABLE_SELF_TESTS_STATES = 5 # Number of consumable install sub-mode states + + +@unique +class PreTreatmentNoCartSelfTestsStates(DialinEnum): + NO_CART_SELF_TESTS_START_STATE = 0 # No cartridge self-tests starting state + NO_CART_SELF_TESTS_WAIT_FOR_DOOR_CLOSE_STATE = 1 # Wait for door to be closed before running self-tests + NO_CART_SELF_TESTS_OCCLUSION_SENSORS_STATE = 2 # No cartridge occlusion sensors self-test state + NO_CART_SELF_TESTS_BLOOD_FLOW_METERS_STATE = 3 # No cartridge blood flow meter self-test state + NO_CART_SELF_TESTS_DIALYSATE_FLOW_METERS_STATE = 4 # No cartridge dialysate flow meter self-test state + NO_CART_SELF_TESTS_BOARD_TEMPERATURE_STATE = 5 # No cartridge board temperature self-test state + NO_CART_SELF_TESTS_HOME_VALVES_STATE = 6 # No cartridge home valves state + NO_CART_SELF_TESTS_HOME_SYRINGE_PUMP_STATE = 7 # No cartridge home syringe pump state + NO_CART_SELF_TESTS_PUMPS_STATE = 8 # No cartridge self-test for pumps state + NO_CART_SELF_TESTS_HOME_IDLE_STATE = 9 # Wait for valves and pumps finish homing state + NO_CART_SELF_TESTS_STOPPED_STATE = 10 # No cart self-test stopped state + NO_CART_SELF_TESTS_COMPLETE_STATE = 11 # No cartridge self-test complete state + NUM_OF_NO_CART_SELF_TESTS_STATES = 12 # Number of no cartridge self-tests states + + +@unique +class PreTreatmentCartridgeInstallStates(DialinEnum): + CARTRIDGE_INSTALL_STATE = 0 # Pre-treatment Cartridge Install state. + NUM_OF_CARTRIDGE_INSTALL_STATES = 1 # Number of pre-treatment Cartridge Install states. + + +@unique +class PreTreatmentDrySelfTestsStates(DialinEnum): + DRY_SELF_TESTS_START_STATE = 0 # Dry self-tests starting state + DRY_SELF_TESTS_WAIT_FOR_DOOR_CLOSE_STATE = 1 # Wait for door to close before executing self-tests + DRY_SELF_TESTS_USED_CARTRIDGE_CHECK_STATE = 2 # Used cartridge check dry self-test state + DRY_SELF_TESTS_OCCLUSION_SENSORS_STATE = 3 # Occlusion sensors dry self-test state + DRY_SELF_TESTS_PRESSURE_SENSORS_SETUP_STATE = 4 # Pressure sensors dry self-test setup valves and pump state + DRY_SELF_TESTS_PRESSURE_SENSORS_STATE = 5 # Pressure sensors verify pressure readings state + DRY_SELF_TESTS_PRESSURE_SENSORS_NORMAL_STATE = 6 # Pressure sensors verify normal pressure readings state + DRY_SELF_TESTS_SYRINGE_PUMP_PRIME_STATE = 7 # Prime syringe pump state + DRY_SELF_TESTS_STOPPED_STATE = 8 # Dry self-test stopped state + DRY_SELF_TESTS_COMPLETE_STATE = 9 # Dry self-test complete state + NUM_OF_DRY_SELF_TESTS_STATES = 10 # Number of dry self-tests states + + +@unique +class PreTreatmentPrimeStates(DialinEnum): + HD_PRIME_START_STATE = 0 # Prime start state + HD_PRIME_WAIT_FOR_USER_START_STATE = 1 # Wait for user to start prime state + HD_PRIME_SALINE_SETUP_STATE = 2 # Saline setup state + HD_PRIME_SALINE_PURGE_AIR_STATE = 3 # Saline purge air state + HD_PRIME_SALINE_CIRC_BLOOD_CIRCUIT_STATE = 4 # Circulate blood circuit state + HD_PRIME_RESERVOIR_ONE_FILL_COMPLETE_STATE = 5 # Wait for reservoir 1 fill complete + HD_PRIME_DIALYSATE_DIALYZER_STATE = 6 # Dialysate dialyzer fluid path state + HD_PRIME_RESERVOIR_TWO_FILL_COMPLETE_STATE = 7 # Wait for reservoir 2 fill complete + HD_PRIME_DIALYSATE_BYPASS_STATE = 8 # Dialysate bypass fluid path state + HD_PRIME_WET_SELF_TESTS_STATE = 9 # Perform wet self-tests after priming complete + HD_PRIME_PAUSE = 10 # Prime pause state = waits to be resumed + HD_PRIME_COMPLETE = 11 # Prime complete state + NUM_OF_HD_PRIME_STATES = 12 # Number of prime sub-mode states + + +@unique +class PreTreatmentRecircStates(DialinEnum): + PRE_TREATMENT_RECIRC_STATE = 0 # Pre-treatment recirculate state. + PRE_TREATMENT_RECIRC_STOPPED_STATE = 1 # Pre-treatment recirculate stopped state. + NUM_OF_PRE_TREATMENT_RECIRC_STATES = 2 # Number of pre-treatment recirculate states. + + +@unique +class PreTreatmentPatientConnectionStates(DialinEnum): + PATIENT_CONNECTION_STATE = 0 # Pre-treatment Patient Connection state. + NUM_OF_PATIENT_CONNECTION_STATES = 1 # Number of pre-treatment Patient Connection states. + + +@unique +class TreatmentParametersStates(DialinEnum): + HD_TREATMENT_PARAMS_MODE_STATE_START = 0 + HD_TREATMENT_PARAMS_MODE_STATE_WAIT_4_UI_2_SEND = 1 + HD_TREATMENT_PARAMS_MODE_STATE_WAIT_4_UI_2_CONFIRM = 2 + NUM_OF_HD_TREATMENT_PARAMS_MODE_STATES = 3 + + +@unique +class StandbyStates(DialinEnum): + STANDBY_START_STATE = 0 # Start standby + STANDBY_WAIT_FOR_TREATMENT_STATE = 1 # Wait for treatment + STANDBY_WAIT_FOR_DISINFECT_STATE = 2 # Wait for disinfection + STANDBY_DG_FLUSH_IN_PROGRESS_STATE = 3 # DG flush operation is in progress + STANDBY_DG_HEAT_DISINFECT_IN_PROGRESS_STATE = 4 # DG heat disinfection operation is in progress + STANDBY_DG_CHEM_DISINFECT_IN_PROGRESS_STATE = 5 # DG chemical disinfection operation is in progress + NUM_OF_STANDBY_STATES = 6 # Number of standby states (sub-modes) + + +@unique +class TreatmentStates(DialinEnum): + TREATMENT_START_STATE = 0 + TREATMENT_BLOOD_PRIME_STATE = 1 + TREATMENT_DIALYSIS_STATE = 2 + TREATMENT_STOP_STATE = 3 + TREATMENT_RINSEBACK_STATE = 4 + TREATMENT_RECIRC_STATE = 5 + TREATMENT_END_STATE = 6 + NUM_OF_TREATMENT_STATES = 7 + + +@unique +class TreatmentBloodPrimeStates(DialinEnum): + BLOOD_PRIME_RAMP_STATE = 0 + NUM_OF_BLOOD_PRIME_STATES = 1 + + +@unique +class TreatmentDialysisStates(DialinEnum): + DIALYSIS_START_STATE = 0 + DIALYSIS_UF_STATE = 1 + DIALYSIS_SALINE_BOLUS_STATE = 2 + NUM_OF_DIALYSIS_STATES = 3 + + +@unique +class TreatmentStopStates(DialinEnum): + TREATMENT_STOP_RECIRC_STATE = 0 + TREATMENT_STOP_NO_RECIRC_STATE = 1 + NUM_OF_TREATMENT_STOP_STATES = 2 + + +@unique +class TreatmentRinsebackStates(DialinEnum): + RINSEBACK_STOP_INIT_STATE = 0 + RINSEBACK_RUN_STATE = 1 + RINSEBACK_PAUSED_STATE = 2 + RINSEBACK_STOP_STATE = 3 + RINSEBACK_RUN_ADDITIONAL_STATE = 4 + NUM_OF_RINSEBACK_STATES = 5 + + +@unique +class TreatmentRecircStates(DialinEnum): + TREATMENT_RECIRC_RECIRC_STATE = 0 + TREATMENT_RECIRC_STOPPED_STATE = 1 + NUM_OF_TREATMENT_RECIRC_STATES = 2 + + +@unique +class TreatmentEndStates(DialinEnum): + TREATMENT_END_WAIT_FOR_RINSEBACK_STATE = 0 + TREATMENT_END_PAUSED_STATE = 1 + NUM_OF_TREATMENT_END_STATES = 2 + + +@unique +class HDFaultStates(DialinEnum): + HD_FAULT_STATE_START = 0 + NUM_OF_HD_FAULT_STATES = 1 + + +@unique +class HDEventList(DialinEnum): + HD_EVENT_STARTUP = 0 + HD_EVENT_OP_MODE_CHANGE = 1 + HD_EVENT_SUB_MODE_CHANGE = 2 + NUM_OF_HD_EVENT_IDS = 3 + + +@unique +class HDEventDataType(DialinEnum): + EVENT_DATA_TYPE_NONE = 0 + EVENT_DATA_TYPE_U32 = 1 + EVENT_DATA_TYPE_S32 = 2 + EVENT_DATA_TYPE_F32 = 3 + EVENT_DATA_TYPE_BOOL = 4 + NUM_OF_EVENT_DATA_TYPES = 5 Index: shared/scripts/dialin/common/msg_defs.py =================================================================== diff -u --- shared/scripts/dialin/common/msg_defs.py (revision 0) +++ shared/scripts/dialin/common/msg_defs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,132 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 msg_defs.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 07-Aug-2020 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum +from .msg_ids import MsgIds + + +# Define msg ids that are not yet added to common but are needed in dialin + +@unique +class MsgIdsDialin(DialinEnum): + + MSG_DIALIN_ID_HD_SERIAL_NUMBER_RESPONSE = 0X87 + MSG_DIALIN_ID_DG_SERIAL_NUMBER_RESPONSE = 0X88 + MSG_DIALIN_ID_UI_SYSTEM_USAGE_REQUEST = 0x89 + MSG_DIALIN_ID_HD_SYSTEM_USAGE_RESPONSE = 0x8A + MSG_DIALIN_ID_DG_SYSTEM_USAGE_RESPONSE = 0x8C + MSG_DIALIN_ID_HD_FLUID_LEAK_STATE_DETECTOR_OVERRIDE = 0x8047 + MSG_DIALIN_ID_HD_VALVES_POSITION_COUNT_OVERRIDE = 0x8058 + MSG_DIALIN_ID_HD_DISINFECT_STATE = 0x7E + MSG_DIALIN_ID_HD_VERSION_REQUEST = 0x9E + MSG_DIALIN_ID_UI_POST_REPORT_VERSION = 0x9F + +ACK_NOT_REQUIRED = [ + MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value +] + + +@unique +class RequestRejectReasons(DialinEnum): + REQUEST_REJECT_REASON_NONE = 0 + REQUEST_REJECT_REASON_NOT_ALLOWED_IN_CURRENT_MODE = 1 + REQUEST_REJECT_REASON_TIMEOUT_WAITING_FOR_USER_CONFIRM = 2 + REQUEST_REJECT_REASON_NOT_IN_TREATMENT_MODE = 3 + REQUEST_REJECT_REASON_INVALID_TREATMENT_STATE = 4 + REQUEST_REJECT_REASON_TREATMENT_TOO_CLOSE_TO_FINISHED = 5 + REQUEST_REJECT_REASON_TREATMENT_TIME_OUT_OF_RANGE = 6 + EQUEST_REJECT_REASON_TREATMENT_TIME_LESS_THAN_CURRENT = 7 + REQUEST_REJECT_REASON_BLOOD_FLOW_OUT_OF_RANGE = 8 + REQUEST_REJECT_REASON_DIAL_FLOW_OUT_OF_RANGE = 9 + REQUEST_REJECT_REASON_DIAL_VOLUME_OUT_OF_RANGE = 10 + REQUEST_REJECT_REASON_UF_VOLUME_OUT_OF_RANGE = 11 + REQUEST_REJECT_REASON_UF_RATE_OUT_OF_RANGE = 12 + REQUEST_REJECT_REASON_TREATMENT_TIME_LESS_THAN_MINIMUM = 13 + REQUEST_REJECT_REASON_UF_NOT_IN_PROGESS = 14 + REQUEST_REJECT_REASON_UF_NOT_PAUSED = 15 + REQUEST_REJECT_REASON_SALINE_BOLUS_IN_PROGRESS = 16 + REQUEST_REJECT_REASON_PARAM_OUT_OF_RANGE = 17 + REQUEST_REJECT_REASON_HEPARIN_PRESTOP_EXCEEDS_DURATION = 18 + REQUEST_REJECT_REASON_ARTERIAL_PRESSURE_LOW_VS_HIGH = 19 + REQUEST_REJECT_REASON_VENOUS_PRESSURE_LOW_VS_HIGH = 20 + REQUEST_REJECT_REASON_SALINE_MAX_VOLUME_REACHED = 21 + REQUEST_REJECT_REASON_SALINE_BOLUS_NOT_IN_PROGRESS = 22 + REQUEST_REJECT_REASON_ACTION_DISABLED_IN_CURRENT_STATE = 23 + REQUEST_REJECT_REASON_ALARM_IS_ACTIVE = 24 + REQUEST_REJECT_REASON_INVALID_COMMAND = 25 + REQUEST_REJECT_REASON_TREATMENT_IS_COMPLETED = 26 + REQUEST_REJECT_REASON_RINSEBACK_MAX_VOLUME_REACHED = 27 + REQUEST_REJECT_REASON_UF_VOLUME_NOT_SET = 28 + REQUEST_REJECT_REASON_NO_PATIENT_CONNECTION_CONFIRM = 29 + REQUEST_REJECT_REASON_HEPARIN_PAUSE_INVALID_IN_THIS_STATE = 30 + REQUEST_REJECT_REASON_HEPARIN_NOT_PAUSED = 31 + REQUEST_REJECT_REASON_DG_COMM_LOST = 32 + REQUEST_REJECT_REASON_DRAIN_NOT_COMPLETE = 33 + NUM_OF_REQUEST_REJECT_REASONS = 34 + + +class MsgFieldPositions: + # Generic response msg field byte positions (where 32-bit data fields are used) + START_POS_FIELD_1 = 6 # Hardcoded for now to avoid cyclic import issue. See protocols.CAN.DenaliMessage class + END_POS_FIELD_1 = START_POS_FIELD_1 + 4 + START_POS_FIELD_2 = END_POS_FIELD_1 + END_POS_FIELD_2 = START_POS_FIELD_2 + 4 + START_POS_FIELD_3 = END_POS_FIELD_2 + END_POS_FIELD_3 = START_POS_FIELD_3 + 4 + START_POS_FIELD_4 = END_POS_FIELD_3 + END_POS_FIELD_4 = START_POS_FIELD_4 + 4 + START_POS_FIELD_5 = END_POS_FIELD_4 + END_POS_FIELD_5 = START_POS_FIELD_5 + 4 + START_POS_FIELD_6 = END_POS_FIELD_5 + END_POS_FIELD_6 = START_POS_FIELD_6 + 4 + START_POS_FIELD_7 = END_POS_FIELD_6 + END_POS_FIELD_7 = START_POS_FIELD_7 + 4 + START_POS_FIELD_8 = END_POS_FIELD_7 + END_POS_FIELD_8 = START_POS_FIELD_8 + 4 + START_POS_FIELD_9 = END_POS_FIELD_8 + END_POS_FIELD_9 = START_POS_FIELD_9 + 4 + START_POS_FIELD_10 = END_POS_FIELD_9 + END_POS_FIELD_10 = START_POS_FIELD_10 + 4 + START_POS_FIELD_11 = END_POS_FIELD_10 + END_POS_FIELD_11 = START_POS_FIELD_11 + 4 + START_POS_FIELD_12 = END_POS_FIELD_11 + END_POS_FIELD_12 = START_POS_FIELD_12 + 4 + START_POS_FIELD_13 = END_POS_FIELD_12 + END_POS_FIELD_13 = START_POS_FIELD_13 + 4 + START_POS_FIELD_14 = END_POS_FIELD_13 + END_POS_FIELD_14 = START_POS_FIELD_14 + 4 + START_POS_FIELD_15 = END_POS_FIELD_14 + END_POS_FIELD_15 = START_POS_FIELD_15 + 4 + START_POS_FIELD_16 = END_POS_FIELD_15 + END_POS_FIELD_16 = START_POS_FIELD_16 + 4 + START_POS_FIELD_17 = END_POS_FIELD_16 + END_POS_FIELD_17 = START_POS_FIELD_17 + 4 + START_POS_FIELD_18 = END_POS_FIELD_17 + END_POS_FIELD_18 = START_POS_FIELD_18 + 4 + START_POS_FIELD_19 = END_POS_FIELD_18 + END_POS_FIELD_19 = START_POS_FIELD_19 + 4 + START_POS_FIELD_20 = END_POS_FIELD_19 + END_POS_FIELD_20 = START_POS_FIELD_20 + 4 + START_POS_FIELD_21 = END_POS_FIELD_20 + END_POS_FIELD_21 = START_POS_FIELD_21 + 4 + START_POS_FIELD_22 = END_POS_FIELD_21 + END_POS_FIELD_22 = START_POS_FIELD_22 + 4 + START_POS_FIELD_23 = END_POS_FIELD_22 + END_POS_FIELD_23 = START_POS_FIELD_23 + 4 + START_POS_FIELD_24 = END_POS_FIELD_23 + END_POS_FIELD_24 = START_POS_FIELD_24 + 4 + START_POS_FIELD_25 = END_POS_FIELD_24 + END_POS_FIELD_25 = START_POS_FIELD_25 + 4 Index: shared/scripts/dialin/common/msg_ids.py =================================================================== diff -u --- shared/scripts/dialin/common/msg_ids.py (revision 0) +++ shared/scripts/dialin/common/msg_ids.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,392 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 msg_ids.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 06-Apr-2021 +# +############################################################################ +from enum import unique +from ..utils.base import DialinEnum + + +# Branch: staging +@unique +class MsgIds(DialinEnum): + MSG_ID_UNUSED = 0X0 + MSG_ID_OFF_BUTTON_PRESS = 0X1 + MSG_ID_ALARM_STATUS = 0X2 + MSG_ID_ALARM_TRIGGERED = 0X3 + MSG_ID_ALARM_CLEARED = 0X4 + MSG_ID_BLOOD_FLOW_DATA = 0X5 + MSG_ID_DG_CHECK_IN = 0X6 + MSG_ID_UI_CHECK_IN = 0X7 + MSG_ID_DIALYSATE_FLOW_DATA = 0X8 + MSG_ID_PRESSURE_OCCLUSION_DATA = 0X9 + MSG_ID_RTC_EPOCH = 0XA + MSG_ID_DIALYSATE_OUT_FLOW_DATA = 0XB + MSG_ID_LOAD_CELL_READINGS = 0XC + MSG_ID_TREATMENT_TIME = 0XD + MSG_ID_POWER_OFF_WARNING = 0XE + MSG_ID_TREATMENT_STATE = 0XF + MSG_ID_USER_UF_PAUSE_RESUME_REQUEST = 0X10 + MSG_ID_USER_UF_SETTINGS_CHANGE_REQUEST = 0X11 + MSG_ID_USER_SALINE_BOLUS_REQUEST = 0X12 + MSG_ID_USER_UF_SETTINGS_CHANGE_RESPONSE = 0X13 + MSG_ID_USER_SALINE_BOLUS_RESPONSE = 0X14 + MSG_ID_USER_CONFIRM_UF_SETTINGS_CHANGE = 0X15 + MSG_ID_USER_TREATMENT_TIME_CHANGE_REQUEST = 0X16 + MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_REQUEST = 0X17 + MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_RESPONSE = 0X18 + MSG_ID_SET_DG_DIALYSATE_TEMP_TARGETS = 0X19 + MSG_ID_TREATMENT_PARAM_CHANGE_RANGES = 0X1A + MSG_ID_USER_TREATMENT_TIME_CHANGE_RESPONSE = 0X1B + MSG_ID_REQUEST_FW_VERSIONS = 0X1C + MSG_ID_HD_VERSION = 0X1D + MSG_ID_DG_VERSION = 0X1E + MSG_ID_RO_PUMP_DATA = 0X1F + MSG_ID_DG_PRESSURES_DATA = 0X20 + MSG_ID_DG_SWITCH_RESERVOIR_CMD = 0X21 + MSG_ID_DG_FILL_CMD = 0X22 + MSG_ID_DG_DRAIN_CMD = 0X23 + MSG_ID_DRAIN_PUMP_DATA = 0X24 + MSG_ID_HD_OP_MODE = 0X25 + MSG_ID_STARTING_STOPPING_TREATMENT_CMD = 0X26 + MSG_ID_DG_OP_MODE = 0X27 + MSG_ID_DG_RESERVOIRS_DATA = 0X28 + MSG_ID_DG_SAMPLE_WATER_CMD = 0X29 + MSG_ID_DG_VALVES_STATES = 0X2A + MSG_ID_DG_START_STOP_TRIMMER_HEATER_CMD = 0X2B + MSG_ID_DG_HEATERS_DATA = 0X2C + MSG_ID_DG_TEMPERATURE_DATA = 0X2D + MSG_ID_USER_UF_SETTINGS_CHANGE_CONFIRMATION_RESPONSE = 0X2E + MSG_ID_SALINE_BOLUS_DATA = 0X2F + MSG_ID_DG_START_STOP_HEAT_DISINFECT = 0X30 + MSG_ID_DG_CONDUCTIVITY_DATA = 0X31 + MSG_ID_USER_REQUEST_ALARM_SILENCE = 0X32 + MSG_ID_HD_ACCELEROMETER_DATA = 0X33 + MSG_ID_DG_ACCELEROMETER_DATA = 0X34 + MSG_ID_UI_NEW_TREATMENT_PARAMS = 0X35 + MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE = 0X36 + MSG_ID_DG_HEAT_DISINFECT_DATA = 0X37 + MSG_ID_UI_INITIATE_TREATMENT_REQUEST = 0X38 + MSG_ID_HD_INITIATE_TREATMENT_RESPONSE = 0X39 + MSG_ID_HD_VALVES_DATA = 0X3A + MSG_ID_UI_USER_CONFIRM_TREATMENT_PARAMS = 0X3B + MSG_ID_UI_START_PRIME_REQUEST = 0X3C + MSG_ID_HD_START_PRIME_RESPONSE = 0X3D + MSG_ID_HD_AIR_TRAP_DATA = 0X3E + MSG_ID_ALARM_CONDITION_CLEARED = 0X3F + MSG_ID_UI_ALARM_USER_ACTION = 0X40 + MSG_ID_USER_UF_PAUSE_RESUME_RESPONSE = 0X41 + MSG_ID_DG_CONCENTRATE_PUMP_DATA = 0X42 + MSG_ID_HD_PRIMING_STATUS_DATA = 0X43 + MSG_ID_DG_UV_REACTORS_DATA = 0X44 + MSG_ID_DG_THERMISTORS_DATA = 0X45 + MSG_ID_UI_PRESSURE_LIMITS_CHANGE_REQUEST = 0X46 + MSG_ID_HD_PRESSURE_LIMITS_CHANGE_RESPONSE = 0X47 + MSG_ID_DG_FANS_DATA = 0X48 + MSG_ID_HD_TREATMENT_STOP_TIMER_DATA = 0X49 + MSG_ID_UI_PATIENT_DISCONNECTION_CONFIRM = 0X4A + MSG_ID_UI_HEPARIN_PAUSE_RESUME_REQUEST = 0X4B + MSG_ID_HD_HEPARIN_PAUSE_RESUME_RESPONSE = 0X4C + MSG_ID_HD_HEPARIN_DATA_BROADCAST = 0X4D + MSG_ID_UI_SET_ALARM_AUDIO_VOLUME_LEVEL_CMD = 0X4E + MSG_ID_UI_SET_UF_VOLUME_PARAMETER = 0X4F + MSG_ID_HD_SET_UF_VOLUME_PARAMETER_RESPONSE = 0X50 + MSG_ID_DG_COMMAND_RESPONSE = 0X51 + MSG_ID_UI_RINSEBACK_CMD = 0X52 + MSG_ID_HD_RINSEBACK_CMD_RESPONSE = 0X53 + MSG_ID_UI_RECIRC_CMD = 0X54 + MSG_ID_HD_RECIRC_CMD_RESPONSE = 0X55 + MSG_ID_HD_RINSEBACK_PROGRESS = 0X56 + MSG_ID_UI_TX_END_CMD = 0X57 + MSG_ID_HD_TX_END_CMD_RESPONSE = 0X58 + MSG_ID_HD_BLOOD_PRIME_PROGRESS = 0X59 + MSG_ID_HD_RECIRC_PROGRESS = 0X5A + MSG_ID_DG_CHANGE_VALVE_SETTING_CMD = 0X5B + MSG_ID_PRE_TREATMENT_STATE = 0X5C + MSG_ID_UI_SAMPLE_WATER_CMD = 0X5D + MSG_ID_HD_SAMPLE_WATER_CMD_RESPONSE = 0X5E + MSG_ID_UI_SAMPLE_WATER_RESULT = 0X5F + MSG_ID_DG_FILTER_FLUSH_PROGRESS = 0X60 + MSG_ID_HD_NO_CART_SELF_TEST_PROGRESS = 0X61 + MSG_ID_UI_INSTALLATION_CONFIRM = 0X62 + MSG_ID_HD_DRY_SELF_TEST_PROGRESS = 0X63 + MSG_ID_UI_PATIENT_CONNECTION_BEGIN_REQUEST = 0X64 + MSG_ID_HD_PATIENT_CONNECTION_BEGIN_RESPONSE = 0X65 + MSG_ID_UI_PATIENT_CONNECTION_CONFIRM = 0X66 + MSG_ID_HD_PATIENT_CONNECTION_CONFIRM_RESPONSE = 0X67 + MSG_ID_UI_CONSUMABLE_INSTALL_CONFIRM = 0X68 + MSG_ID_HD_SYRINGE_PUMP_DATA = 0X69 + MSG_ID_HD_FLUID_LEAK_STATE = 0X6A + MSG_ID_DG_FLUID_LEAK_STATE = 0X6B + MSG_ID_HD_BLOOD_LEAK_DATA = 0X6C + MSG_ID_UI_HD_SET_RTC_REQUEST = 0X6D + MSG_ID_HD_UI_SET_RTC_RESPONSE = 0X6E + MSG_ID_UI_DG_SET_RTC_REQUEST = 0X6F + MSG_ID_DG_UI_SET_RTC_RESPONSE = 0X70 + MSG_ID_UI_START_TREATMENT_REQUEST = 0X71 + MSG_ID_HD_START_TREATMENT_RESPONSE = 0X72 + MSG_ID_UI_DISPOSABLE_REMOVAL_CONFIRM = 0X73 + MSG_ID_HD_DISPOSABLE_REMOVAL_CONFIRM_RESPONSE = 0X74 + MSG_ID_UI_TREATMENT_LOG_DATA_REQUEST = 0X75 + MSG_ID_HD_TREATMENT_LOG_DATA_RESPONSE = 0X76 + MSG_ID_HD_POST_TREATMENT_STATE = 0X77 + MSG_ID_DG_START_STOP_CHEM_DISINFECT = 0X78 + MSG_ID_DG_START_STOP_FLUSH = 0X79 + MSG_ID_DG_FLUSH_DATA = 0X7A + MSG_ID_HD_VOLTAGES_DATA = 0X7B + MSG_ID_HD_ALARM_AUDIO_VOLUME_SET_RESPONSE = 0X7C + MSG_ID_HD_ALARM_INFORMATION = 0X7D + MSG_ID_HD_DISINFECT_STANDBY_DATA = 0X7E + MSG_ID_UI_DISINFECT_REQUEST = 0X7F + MSG_ID_HD_DISINFECT_RESPONSE = 0X80 + MSG_ID_UI_CHEM_DISINFECT_CONFIRM = 0X81 + MSG_ID_HD_CHEM_DISINFECT_CONFIRM_RESPONSE = 0X82 + MSG_ID_DG_FLUSH_TIME_DATA = 0X83 + MSG_ID_DG_HEAT_DISINFECT_TIME_DATA = 0X84 + MSG_ID_DG_CHEM_DISINFECT_TIME_DATA = 0X85 + MSG_ID_DG_VOLTAGES_DATA = 0X86 + MSG_ID_DG_CHEM_DISINFECT_DATA = 0X87 + MSG_ID_DG_SERIAL_NUMBER = 0X88 + MSG_ID_UI_REQUEST_SERVICE_INFO = 0X89 + MSG_ID_HD_SERVICE_SCHEDULE_DATA = 0X8A + MSG_ID_HD_USAGE_DATA = 0X8B + MSG_ID_DG_SERVICE_SCHEDULE_DATA = 0X8C + MSG_ID_DG_USAGE_DATA = 0X8D + MSG_ID_HD_POST_SINGLE_TEST_RESULT = 0X8E + MSG_ID_HD_POST_FINAL_TEST_RESULT = 0X8F + MSG_ID_DG_POST_SINGLE_TEST_RESULT = 0X90 + MSG_ID_DG_POST_FINAL_TEST_RESULT = 0X91 + MSG_ID_UI_POST_FINAL_TEST_RESULT = 0X92 + MSG_ID_HD_BUBBLES_DATA = 0X93 + MSG_ID_HD_TREATMENT_LOG_PERIODIC_DATA = 0X94 + MSG_ID_HD_TREATMENT_LOG_ALARM_EVENT = 0X95 + MSG_ID_HD_TREATMENT_LOG_EVENT = 0X96 + MSG_ID_UI_ACTIVE_ALARMS_LIST_REQUEST = 0X97 + MSG_ID_HD_ACTIVE_ALARMS_LIST_REQUEST_RESPONSE = 0X98 + MSG_ID_HD_SERIAL_NUMBER = 0X99 + MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_REQUEST = 0X9A + MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_RESPONSE = 0X9B + MSG_ID_HD_DG_POST_RESULT_REQUEST = 0X9C + MSG_ID_HD_TEMPERATURES_DATA = 0X9D + MSG_ID_HD_UI_VERSION_INFO_REQUEST = 0X9E + MSG_ID_HD_UI_VERSION_INFO_RESPONSE = 0X9F + MSG_ID_REQUEST_HD_USAGE_INFO = 0XA0 + MSG_ID_DG_SWITCHES_DATA = 0XA1 + MSG_ID_HD_SWITCHES_DATA = 0XA2 + MSG_ID_HD_FANS_DATA = 0XA3 + MSG_ID_HD_EVENT = 0xA4 + MSG_ID_DG_EVENT = 0xA5 + MSG_ID_DG_DIALYSATE_FLOW_METER_DATA = 0xA6 + MSG_ID_DG_ALARM_INFO = 0xA7 + MSG_ID_HD_RESERVOIRS_DATA = 0xA8 + MSG_ID_HD_REQUEST_DG_CONCENTRATE_MIXING_RATIOS = 0xA9 + MSG_ID_DG_CONCENTRATE_MIXING_RATIOS_DATA = 0xAA + + MSG_ID_CAN_ERROR_COUNT = 0X999 + + MSG_ID_TESTER_LOGIN_REQUEST = 0X8000 + MSG_ID_DIAL_OUT_FLOW_SET_PT_OVERRIDE = 0X8001 + MSG_ID_OFF_BUTTON_STATE_OVERRIDE = 0X8002 + MSG_ID_STOP_BUTTON_STATE_OVERRIDE = 0X8003 + MSG_ID_ALARM_LAMP_PATTERN_OVERRIDE = 0X8004 + MSG_ID_WATCHDOG_TASK_CHECKIN_OVERRIDE = 0X8005 + MSG_ID_ALARM_STATE_OVERRIDE = 0X8006 + MSG_ID_ALARM_TIME_OVERRIDE = 0X8007 + MSG_ID_BLOOD_FLOW_SET_PT_OVERRIDE = 0X8008 + MSG_ID_BLOOD_FLOW_MEAS_OVERRIDE = 0X8009 + MSG_ID_BLOOD_PUMP_MC_MEAS_SPEED_OVERRIDE = 0X800A + MSG_ID_BLOOD_PUMP_MC_MEAS_CURR_OVERRIDE = 0X800B + MSG_ID_BLOOD_FLOW_SEND_INTERVAL_OVERRIDE = 0X800C + MSG_ID_TREATMENT_TIME_REMAINING_OVERRIDE = 0X800D + MSG_ID_BLOOD_PUMP_MEAS_SPEED_OVERRIDE = 0X800E + MSG_ID_BLOOD_PUMP_MEAS_ROTOR_SPEED_OVERRIDE = 0X800F + MSG_ID_DIAL_IN_FLOW_SET_PT_OVERRIDE = 0X8010 + MSG_ID_DIAL_IN_FLOW_MEAS_OVERRIDE = 0X8011 + MSG_ID_DIAL_IN_PUMP_MC_MEAS_SPEED_OVERRIDE = 0X8012 + MSG_ID_DIAL_IN_PUMP_MC_MEAS_CURR_OVERRIDE = 0X8013 + MSG_ID_DIAL_IN_FLOW_SEND_INTERVAL_OVERRIDE = 0X8014 + MSG_ID_DIAL_IN_PUMP_MEAS_SPEED_OVERRIDE = 0X8015 + MSG_ID_DIAL_IN_PUMP_MEAS_ROTOR_SPEED_OVERRIDE = 0X8016 + MSG_ID_PRESSURE_ARTERIAL_OVERRIDE = 0X8017 + MSG_ID_PRESSURE_VENOUS_OVERRIDE = 0X8018 + MSG_ID_OCCLUSION_BLOOD_PUMP_OVERRIDE = 0X8019 + MSG_ID_BLOOD_PUMP_ROTOR_COUNT_OVERRIDE = 0X801A + MSG_ID_SET_ARTERIAL_PRESSURE_OFFSET = 0X801B + MSG_ID_PRES_OCCL_SEND_INTERVAL_OVERRIDE = 0X801C + MSG_ID_SET_RTC_DATE_TIME = 0X801D + MSG_ID_DIAL_OUT_FLOW_SEND_INTERVAL_OVERRIDE = 0X801E + MSG_ID_DIAL_OUT_UF_REF_VOLUME_OVERRIDE = 0X801F + MSG_ID_DIAL_OUT_UF_MEAS_VOLUME_OVERRIDE = 0X8020 + MSG_ID_DIAL_OUT_PUMP_MC_MEAS_SPEED_OVERRIDE = 0X8021 + MSG_ID_DIAL_OUT_PUMP_MC_MEAS_CURR_OVERRIDE = 0X8022 + MSG_ID_DIAL_OUT_PUMP_MEAS_SPEED_OVERRIDE = 0X8023 + MSG_ID_DIAL_OUT_PUMP_MEAS_ROTOR_SPEED_OVERRIDE = 0X8024 + MSG_ID_DIAL_OUT_LOAD_CELL_WEIGHT_OVERRIDE = 0X8025 + MSG_ID_HD_SAFETY_SHUTDOWN_OVERRIDE = 0X8026 + MSG_ID_HD_ACCEL_OVERRIDE = 0X8027 + MSG_ID_HD_ACCEL_MAX_OVERRIDE = 0X8028 + MSG_ID_HD_ACCEL_SEND_INTERVAL_OVERRIDE = 0X8029 + MSG_ID_HD_SYRINGE_PUMP_SEND_INTERVAL_OVERRIDE = 0X802A + MSG_ID_HD_SYRINGE_PUMP_OPERATION_REQUEST = 0X802B + MSG_ID_HD_SYRINGE_PUMP_MEASURED_RATE_OVERRIDE = 0X802C + MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER = 0X802D + MSG_ID_HD_VALVES_HOME = 0X802E + MSG_ID_HD_VALVES_POSITION_OVERRIDE = 0X802F + MSG_ID_HD_VALVES_SET_AIR_TRAP_VALVE = 0X8030 + MSG_ID_HD_VALVES_SET_PWM_OVERRIDE = 0X8031 + MSG_ID_HD_AIR_TRAP_SEND_INTERVAL_OVERRIDE = 0X8032 + MSG_ID_HD_AIR_TRAP_LEVEL_SENSOR_OVERRIDE = 0X8033 + MSG_ID_HD_SOFTWARE_RESET_REQUEST = 0X8034 + MSG_ID___AVAILABLE_3 = 0X8035 + MSG_ID___AVAILABLE_4 = 0X8036 + MSG_ID_BLOOD_PUMP_HOME_CMD = 0X8037 + MSG_ID_DIAL_IN_PUMP_HOME_CMD = 0X8038 + MSG_ID_DIAL_OUT_PUMP_HOME_CMD = 0X8039 + MSG_ID_SUPER_CLEAR_ALARMS_CMD = 0X803A + MSG_ID_HD_SYRINGE_PUMP_MEASURED_FORCE_OVERRIDE = 0X803B + MSG_ID_HD_SYRINGE_PUMP_SYRINGE_DETECT_OVERRIDE = 0X803C + MSG_ID_HD_SET_CALIBRATION_RECORD = 0X803D + MSG_ID_HD_GET_CALIBRATION_RECORD = 0X803E + MSG_ID_HD_SEND_CALIBRATION_RECORD = 0X803F + MSG_ID_HD_SET_SYSTEM_RECORD = 0X8040 + MSG_ID_HD_GET_SYSTEM_RECORD = 0X8041 + MSG_ID_HD_SEND_SYSTEM_RECORD = 0X8042 + MSG_ID_HD_GET_SERVICE_RECORD = 0X8043 + MSG_ID_HD_SET_SERVICE_RECORD = 0X8044 + MSG_ID_HD_SEND_SERVICE_RECORD = 0X8045 + MSG_ID_HD_SET_OP_MODE_REQUEST = 0X8046 + MSG_ID_HD_FLUID_LEAK_SEND_INTERVAL_OVERRIDE = 0X8047 + MSG_ID_HD_FLUID_LEAK_STATE_OVERRIDE = 0X8048 + MSG_ID_HD_SYRINGE_PUMP_MEASURED_HOME_OVERRIDE = 0X8049 + MSG_ID_HD_SYRINGE_PUMP_MEASURED_POSITION_OVERRIDE = 0X804A + MSG_ID_HD_SYRINGE_PUMP_MEASURED_VOLUME_OVERRIDE = 0X804B + MSG_ID_HD_BLOOD_LEAK_DATA_SEND_INTERVAL_OVERRIDE = 0X804C + MSG_ID_HD_BLOOD_LEAK_STATUS_OVERRIDE = 0X804D + MSG_ID_HD_BLOOD_LEAK_ZERO_REQUEST = 0X804E + MSG_ID_HD_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE = 0X8050 + MSG_ID_HD_MONITORED_VOLTAGES_OVERRIDE = 0X8051 + MSG_ID_HD_ALARM_INFO_SEND_INTERVAL_OVERRIDE = 0X8052 + MSG_ID_HD_ALARM_AUDIO_VOLUME_LEVEL_OVERRIDE = 0X8053 + MSG_ID_HD_ALARM_AUDIO_CURRENT_HG_OVERRIDE = 0X8054 + MSG_ID_HD_ALARM_AUDIO_CURRENT_LG_OVERRIDE = 0X8055 + MSG_ID_HD_ALARM_BACKUP_AUDIO_CURRENT_OVERRIDE = 0X8056 + MSG_ID_HD_VALVES_CURRENT_OVERRIDE = 0X8057 + MSG_ID_HD_VALVES_POSITION_COUNT_OVERRIDE = 0X8058 + MSG_ID_HD_SYRINGE_PUMP_STATUS_OVERRIDE = 0X8059 + MSG_ID_HD_SYRINGE_PUMP_ENCODER_STATUS_OVERRIDE = 0X805A + MSG_ID_HD_SYRINGE_PUMP_ADC_DAC_STATUS_OVERRIDE = 0X805B + MSG_ID_HD_SYRINGE_PUMP_ADC_READ_COUNTER_OVERRIDE = 0X805C + MSG_ID_HD_BUBBLES_DATA_SEND_INTERVAL_OVERRIDE = 0X805D + MSG_ID_HD_BUBBLE_STATUS_OVERRIDE = 0X805E + MSG_ID_HD_BLOOD_PRIME_VOLUME_OVERRIDE = 0X805F + MSG_ID_HD_BUBBLE_SELF_TEST_REQUEST = 0X8060 + MSG_ID_HD_FAN_RPM_ALARM_START_TIME_OFFSET_OVERRIDE = 0x8061 + MSG_ID_HD_SWITCHES_STATUS_OVERRIDE = 0x8062 + MSG_ID_HD_SWITCHES_PUBLISH_INTERVAL_OVERRIDE = 0x8063 + MSG_ID_HD_BATTERY_REMAINING_PERCENT_OVERRIDE = 0x8064 + MSG_ID_HD_TEMPERATURES_VALUE_OVERRIDE = 0x8065 + MSG_ID_HD_TEMPERATURES_PUBLISH_INTERVAL_OVERRIDE = 0x8066 + MSG_ID_HD_FANS_PUBLISH_INTERVAL_OVERRIDE = 0x8067 + MSG_ID_HD_FANS_RPM_OVERRIDE = 0x8068 + MSG_ID_HD_RINSEBACK_VOLUME_OVERRIDE = 0x8069 + MSG_ID_HD___AVAILABLE_2 = 0x806A + MSG_ID_HD_ALARM_STATUS_PUBLISH_INTERVAL_OVERRIDE = 0x806B + MSG_ID_HD_TREATMENT_TIME_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x806C + MSG_ID_HD_TREATMENT_RANGES_PUBLISH_INTERVAL_OVERRIDE = 0x806D + MSG_ID_HD_TREATMENT_STOP_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x806E + MSG_ID_HD_BLOOD_PRIME_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x806F + MSG_ID_HD_RINSEBACK_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8070 + MSG_ID_HD_STANDBY_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8071 + MSG_ID_HD_OP_MODE_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8072 + MSG_ID_HD_PRE_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8073 + MSG_ID_HD_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8074 + MSG_ID_HD_POST_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE = 0x8075 + MSG_ID_HD_BLOCK_MESSAGE_TRANSMISSION = 0x8076 + MSG_ID_HD_SYRINGE_PUMP_FORCE_SENSOR_DAC_CALIBRATE = 0x8077 + MSG_ID_HD_STOP_RTC_CLOCK = 0x8078 + + MSG_ID_DG_TESTER_LOGIN_REQUEST = 0XA000 + MSG_ID_DG_ALARM_STATE_OVERRIDE = 0XA001 + MSG_ID_DG_WATCHDOG_TASK_CHECKIN_OVERRIDE = 0XA002 + MSG_ID_DG_SET_RTC_DATE_TIME = 0XA004 + MSG_ID_LOAD_CELL_OVERRIDE = 0XA005 + MSG_ID_PRESSURE_OVERRIDE = 0XA006 + MSG_ID_PRESSURE_SEND_INTERVAL_OVERRIDE = 0XA007 + MSG_ID_RO_MEASURED_FLOW_OVERRIDE = 0XA009 + MSG_ID_RO_PUMP_SEND_INTERVAL_OVERRIDE = 0XA00A + MSG_ID_DRAIN_PUMP_SET_RPM = 0XA00B + MSG_ID_DRAIN_PUMP_SEND_INTERVAL_OVERRIDE = 0XA00C + MSG_ID_LOAD_CELL_SEND_INTERVAL_OVERRIDE = 0XA00D + MSG_ID_VALVE_STATE_OVERRIDE = 0XA00E + MSG_ID_VALVES_STATES_PUBLISH_INTERVAL_OVERRIDE = 0XA00F + MSG_ID_TEMPERATURE_SENSORS_VALUE_OVERRIDE = 0XA010 + MSG_ID_START_STOP_PRIMARY_HEATER = 0XA011 + MSG_ID_TEMPERATURE_SENSORS_PUBLISH_INTERVAL_OVERRIDE = 0XA012 + MSG_ID_HEATERS_PUBLISH_INTERVAL_ORVERRIDE = 0XA013 + MSG_ID_DG_SAFETY_SHUTDOWN_OVERRIDE = 0XA014 + MSG_ID_CONDUCTIVITY_OVERRIDE = 0XA015 + MSG_ID_CONDUCTIVITY_PUBLISH_INTERVAL_OVERRIDE = 0XA016 + MSG_ID_DG_ACCEL_OVERRIDE = 0XA017 + MSG_ID_DG_ACCEL_MAX_OVERRIDE = 0XA018 + MSG_ID_DG_ACCEL_SEND_INTERVAL_OVERRIDE = 0XA019 + MSG_ID_DG_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE = 0XA01A + MSG_ID_DG_MONITORED_VOLTAGES_OVERRIDE = 0XA01B + MSG_ID_DRAIN_PUMP_TARGET_OUTLET_PRESSURE = 0XA01C + MSG_ID_DG_SWITCHES_STATUS_OVERRIDE = 0XA01D + MSG_ID_DG_SWITCHES_PUBLISH_INTERVAL_OVERRIDE = 0XA01E + MSG_ID_DG_OP_MODE_PUBLISH_INTERVAL_OVERRIDE = 0XA01F + MSG_ID_DG_BLOCK_MESSAGE_TRANSMISSION = 0XA020 + MSG_ID_DIALYSATE_MEASURED_FLOW_OVERRIDE = 0xA021 + MSG_ID_DG_SOFTWARE_RESET_REQUEST = 0XA022 + MSG_ID_DG_CONCENTRATE_PUMP_MEASURED_SPEED_OVERRIDE = 0XA023 + MSG_ID_CONCENTRATE_PUMP_TARGET_SPEED_OVERRIDE = 0XA024 + MSG_ID_UV_REACTORS_DATA_PUBLISH_INTERVAL_OVERRIDE = 0XA025 + MSG_ID_CONCENTRATE_PUMP_STATE_CHANGE_REQUEST = 0XA026 + MSG_ID_CONCENTRATE_PUMP_PUBLISH_INTERVAL_OVERRIDE = 0XA027 + MSG_ID_DG_START_STOP_UV_REACTORS = 0XA028 + MSG_ID_DG_REQUEST_CALIBRATION_DATA = 0XA029 + MSG_ID_DG_FANS_DATA_PUBLISH_INTERVAL_OVERRIDE = 0XA02A + MSG_ID_DG_UV_REACTORS_HEALTH_OVERRIDE = 0XA02C + MSG_ID_DG_THERMISTORS_DATA_PUBLISH_INTERVAL_OVERRIDE = 0XA02D + MSG_ID_DG_THERMISTORS_VALUE_OVERRIDE = 0XA02E + MSG_ID_DG_RO_PUMP_DUTY_CYCLE_OVERRIDE = 0XA02F + MSG_ID_DG_RO_FLOW_RATE_OVERRIDE = 0XA030 + MSG_ID_DG_SET_RO_PUMP_TARGET_FLOW = 0XA031 + MSG_ID_DG_RO_PUMP_TARGET_PRESSURE_OVERRIDE = 0XA032 + MSG_ID_DG_SET_CALIBRATION_RECORD = 0XA033 + MSG_ID_DG_GET_CALIBRATION_RECORD = 0XA034 + MSG_ID_DG_SEND_CALIBRATION_RECORD = 0XA035 + MSG_ID_DG_SET_SYSTEM_RECORD = 0XA036 + MSG_ID_DG_GET_SYSTEM_RECORD = 0XA037 + MSG_ID_DG_SEND_SYSTEM_RECORD = 0XA038 + MSG_ID_DG_GET_SERVICE_RECORD = 0XA039 + MSG_ID_DG_SET_SERVICE_RECORD = 0XA03A + MSG_ID_DG_SEND_SERVICE_RECORD = 0XA03B + MSG_ID_DG_GET_SCHEDULED_RUNS_RECORD = 0XA03C + MSG_ID_DG_SET_SCHEDULED_RUNS_RECORD = 0XA03D + MSG_ID_DG_SEND_SCHEDULED_RUNS_RECORD = 0XA03E + MSG_ID_DG_FLUID_LEAK_SEND_INTERVAL_OVERRIDE = 0XA03F + MSG_ID_DG_FLUID_LEAK_STATE_DETECTOR_OVERRIDE = 0XA040 + MSG_ID_DG_FLUSH_PUBLISH_INTERVAL_OVERRIDE = 0XA041 + MSG_ID_FILTER_FLUSH_TIME_PERIOD_OVERRIDE = 0XA042 + MSG_ID_DG_FANS_RPM_OVERRIDE = 0XA043 + MSG_ID_DIALYSATE_FLOW_SEND_INTERVAL_OVERRIDE = 0XA044 + MSG_ID_DG_STOP_RTC_CLOCK = 0XA045 + MSG_ID_DG_DRAIN_PUMP_MEASURED_RPM_OVERRIDE = 0XA046 + MSG_ID_DG_SUPER_CLEAR_ALARMS_CMD = 0XA047 + MSG_ID_DG_ALARM_INFO_SEND_INTERVAL_OVERRIDE = 0XA048 + MSG_ID_DG_FAN_RPM_ALARM_START_TIME_OFFSET_OVERRIDE = 0xA049 + + MSG_ID_HD_DEBUG_EVENT = 0XFFF1 + MSG_ID_DG_DEBUG_EVENT = 0XFFF2 + MSG_ID_ACK_MESSAGE_THAT_REQUIRES_ACK = 0XFFFF Index: shared/scripts/dialin/common/prs_defs.py =================================================================== diff -u --- shared/scripts/dialin/common/prs_defs.py (revision 0) +++ shared/scripts/dialin/common/prs_defs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,70 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 prs_defs.py +# +# @author (last) Quang Nguyen +# @date (last) 06-Jul-2021 +# @author (original) Behrouz NematiPour +# @date (original) 05-Nov-2020 +# +############################################################################ +class Ranges: + PRESSURE_STEPS = 10 + + ARTERIAL_PRESSURE_MINIMUM = -400 + ARTERIAL_PRESSURE_MAXIMUM = +600 + ARTERIAL_PRESSURE_LOW_MIN = -300 + ARTERIAL_PRESSURE_LOW_DEF = -300 + ARTERIAL_PRESSURE_LOW_MAX = +200 + ARTERIAL_PRESSURE_HIGH_MIN = -300 + ARTERIAL_PRESSURE_HIGH_DEF = +100 + ARTERIAL_PRESSURE_HIGH_MAX = +200 + + VENOUS_PRESSURE_MINIMUM = -100 + VENOUS_PRESSURE_MAXIMUM = +700 + VENOUS_PRESSURE_LOW_MIN = -100 + VENOUS_PRESSURE_LOW_DEF = -100 + VENOUS_PRESSURE_LOW_MAX = +600 + VENOUS_PRESSURE_HIGH_MIN = +100 + VENOUS_PRESSURE_HIGH_DEF = +400 + VENOUS_PRESSURE_HIGH_MAX = +600 + + +class AlarmPriority: + ALARM_HIGH = 3 + ALARM_MED = 2 + ALARM_LOW = 1 + ALARM_NONE = 0 + + +class AlarmFlags: + ALARM_STATE_FLAG_BIT_POS_SYSTEM_FAULT = 0 + ALARM_STATE_FLAG_BIT_POS_STOP = 1 + ALARM_STATE_FLAG_BIT_POS_NO_CLEAR = 2 + ALARM_STATE_FLAG_BIT_POS_NO_RESUME = 3 + ALARM_STATE_FLAG_BIT_POS_NO_RINSEBACK = 4 + ALARM_STATE_FLAG_BIT_POS_NO_END_TREATMENT = 5 + ALARM_STATE_FLAG_BIT_POS_NO_NEW_TREATMENT = 6 + ALARM_STATE_FLAG_BIT_POS_USER_MUST_ACK = 7 + ALARM_STATE_FLAG_BIT_POS_ALARMS_TO_ESCALATE = 8 + ALARM_STATE_FLAG_BIT_POS_ALARMS_SILENCED = 9 + ALARM_STATE_FLAG_BIT_POS_LAMP_ON = 10 + ALARM_STATE_FLAG_BIT_POS_UNUSED_1 = 11 + ALARM_STATE_FLAG_BIT_POS_UNUSED_2 = 12 + ALARM_STATE_FLAG_BIT_POS_UNUSED_3 = 13 + ALARM_STATE_FLAG_BIT_POS_UNUSED_4 = 14 + ALARM_STATE_FLAG_BIT_POS_TOP_CONDITION = 15 + + +class AlarmDataTypes: + ALARM_DATA_TYPE_NONE = 0 # No data given. + ALARM_DATA_TYPE_U32 = 1 # Alarm data is unsigned 32-bit integer type. + ALARM_DATA_TYPE_S32 = 2 # Alarm data is signed 32-bit integer type. + ALARM_DATA_TYPE_F32 = 3 # Alarm data is 32-bit floating point type. + ALARM_DATA_TYPE_BOOL = 4 # Alarm data is 32-bit boolean type. + NUM_OF_ALARM_DATA_TYPES = 5 # Total number of alarm data types. Index: shared/scripts/dialin/common/ui_defs.py =================================================================== diff -u --- shared/scripts/dialin/common/ui_defs.py (revision 0) +++ shared/scripts/dialin/common/ui_defs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,173 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 ui_defs.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 06-Apr-2021 +# +############################################################################ + +from . import RequestRejectReasons + + +class EResponse: + Rejected = 0 + Accepted = 1 + + +class GuiActionType: + Unknown = 0 + PowerOff = 1 + KeepAlive = 7 + BloodFlow = 5 + DialysateInletFlow = 8 + DialysateOutletFlow = 11 + TreatmentTime = 13 + PowerOffBroadcast = 14 + + AlarmStatus = 2 + AlarmTriggered = 3 + AlarmCleared = 4 + + PressureOcclusion = 9 + + DGDrainPumpData = 36 + DGHeatersData = 44 + LoadCellReadings = 12 + DGPressuresData = 32 + TemperatureSensors = 45 + + CanBUSFaultCount = 2457 + HDDebugText = 0xFFF1 + DGDebugText = 0xFFF2 + + AdjustBloodDialysateReq = 23 + AdjustBloodDialysateRsp = 24 + + AdjustDurationReq = 22 + AdjustDurationRsp = 27 + + AdjustUltrafiltrationStateReq = 16 + AdjustUltrafiltrationStateRsp = 65 + AdjustUltrafiltrationEditReq = 17 + AdjustUltrafiltrationEditRsp = 19 + AdjustUltrafiltrationConfirmReq = 21 + AdjustUltrafiltrationConfirmRsp = 46 + + AdjustPressuresLimitsReq = 70 + AdjustPressuresLimitsRsp = 71 + + TreatmentRanges = 26 + + String = 65279 + Acknow = 65535 + # Generic Acknowledgment is not a unique message ID and + # inherits its Id from the actual message. Zero is a placeholder + AcknowGeneric = 0 + + +class GuiActionTypeLength: + TREATMENT_LOG_LENGTH = 33 + + +class TXStates: + # Sub Mode + TREATMENT_START_STATE = 0 # Start treatment - initialize treatment and go to blood prime state + TREATMENT_BLOOD_PRIME_STATE = 1 # Prime blood-side of dialyzer with gradual ramp for 1 min. + TREATMENT_DIALYSIS_STATE = 2 # Perform dialysis. + TREATMENT_STOP_STATE = 3 # Treatment stopped. All pumps off. Dializer bypassed + TREATMENT_RINSEBACK_STATE = 4 # Perform rinseback with saline. Dialyzer bypassed. Dialysate recirculating + TREATMENT_RECIRC_STATE = 5 # Recirculate saline and dialysate while patient disconnected + TREATMENT_END_STATE = 6 # Dialysis has ended + + # Saline states + SALINE_BOLUS_STATE_IDLE = 0 # No saline bolus delivery is in progress + SALINE_BOLUS_STATE_WAIT_FOR_PUMPS_STOP = 1 # Wait for pumps to stop before starting bolus + SALINE_BOLUS_STATE_IN_PROGRESS = 2 # A saline bolus delivery is in progress + SALINE_BOLUS_STATE_MAX_DELIVERED = 3 # Maximum saline bolus volume reached + + # UF states + UF_START_STATE = 0 # Start state of the ultrafiltration state machine + UF_PAUSED_STATE = 1 # Paused state of the ultrafiltration state machine + UF_RUNNING_STATE = 2 # Running state of the ultrafiltration state machine + UF_OFF_STATE = 3 # Completed/off state of the ultrafiltration state machine + UF_COMPLETED_STATE = 4 # Completed state of ultrafiltration state machine + + # Heparin states + HEPARIN_STATE_OFF = 0 # No heparin delivery is in progress + HEPARIN_STATE_PAUSED = 1 # Heparin delivery paused + HEPARIN_STATE_INITIAL_BOLUS = 2 # Initial heparin bolus delivery in progress + HEPARIN_STATE_DISPENSING = 3 # Gradual heparin dispensing in progress + HEPARIN_STATE_COMPLETED = 4 # Heparin delivery stopped due to the set stop time before treatment end + HEPARIN_STATE_EMPTY = 5 # Heparin Syringe empty + + # Rinseback states + RINSEBACK_STOP_INIT_STATE = 0 # Start state (stopped) of the rinseback sub-mode state machine + RINSEBACK_RUN_STATE = 1 # Rinseback running state of the rinseback sub-mode state machine + RINSEBACK_PAUSED_STATE = 2 # Rinseback paused state of the rinseback sub-mode state machine + RINSEBACK_STOP_STATE = 3 # Rinseback stopped (done) state of the rinseback sub-mode state machine + RINSEBACK_RUN_ADDITIONAL_STATE = 4 # Additional rinseback volume (10 mL) state + + # Recirculate + TREATMENT_RECIRC_RECIRC_STATE = 0 # Re-circulate state of the treatment re-circulate sub-mode state machine + TREATMENT_RECIRC_STOPPED_STATE = 1 # Stopped state of the treatment re-circulate sub-mode state machine + + # Blood Prime + BLOOD_PRIME_RAMP_STATE = 0 # Ramp state of the blood prime sub-mode state machine + + # Treatment End + TREATMENT_END_WAIT_FOR_RINSEBACK_STATE = 0 # Wait for rinseback state of the treatment end sub-mode state machine + TREATMENT_END_PAUSED_STATE = 1 # Paused state of the treatment end sub-mode state machine + + # Treatment Stop + TREATMENT_STOP_RECIRC_STATE = 0 # Dialysate re-circulation state + TREATMENT_STOP_NO_RECIRC_STATE = 1 # No dialysate re-circulation state + + +class TreatmentParameterRejections: + def __init__(self): + self.param_request_valid = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_blood_flow_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_dialysate_flow_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_duration = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_heparin_stop_time = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_saline_bolus = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_acid_concentrate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_bicarbonate_concentrate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_dialyzer_type = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_blood_pressure_measure_interval = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_rinseback_flow_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_arterial_pressure_limit_low = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_arterial_pressure_limit_high = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_venous_pressure_limit_low = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_venous_pressure_limit_high = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_heparin_dispensing_rate = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_heparin_bolus_volume = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + self.param_dialysate_temp = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + + def set_all_valid(self): + """ + Sets all parameters as valid + + @return: None + """ + for attr in dir(self): + if not callable(getattr(self, attr)) and attr.startswith("param_"): + self.__dict__[attr] = RequestRejectReasons.REQUEST_REJECT_REASON_NONE + + def set_all_invalid(self): + """ + Sets all treatment parameters to be invalid + + @return: None + """ + for attr in dir(self): + if not callable(getattr(self, attr)) and attr.startswith("param_"): + self.__dict__[attr] = RequestRejectReasons.REQUEST_REJECT_REASON_NOT_ALLOWED_IN_CURRENT_MODE Index: shared/scripts/dialin/configuration/utility.py =================================================================== diff -u --- shared/scripts/dialin/configuration/utility.py (revision 0) +++ shared/scripts/dialin/configuration/utility.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,108 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 utils.py +# +# @author (last) Joseph varghese +# @date (last) 15-Jan-2022 +# +############################################################################ + + +import sys +import os +import test +import squish +from dialin.ui import utils + + +Application_name = "denaliSquish" + +COMMON_PATH = f"{os.environ['HOME']}/Projects" + + +def start_application(app_name): + """ + Function to start application and verify application status [running] + If application does not start or running status is false, test stops + Argument: + @param app_name : (str) - Name of the application + @param app_executable : (str) - Actual application + @return: handle for the application if the application is in running state, + or error (exist the application) + """ + counter = 0 + while True: + try: + counter += 1 + test.log("Starting {}".format(app_name)) + squish.startApplication(Application_name) + if counter == 1: + test.log(f"Application launched at the {counter}'st try.") + elif counter == 2: + test.log(f"Application launched at the {counter}'nd try.") + elif counter == 3: + test.log(f"Application launched at the {counter}'rd try.") + else: + test.log(f"Application launched at the {counter}'th try.") + squish.snooze(20) + break + except RuntimeError: + if counter == 1: + test.log(f"Application failed to launch after {counter} try - Please refer logs") + elif counter == 20: + test.log(f"Exiting after {counter} tries..") + sys.exit(1) + else: + test.log(f"Application failed to launch after {counter} tries - Please refer logs") + except: + logErrorDetails("Failed to start the application") + sys.exit(1) + + +def launch_application(test_name): + """ + Method to enables simulator and launch application + @param test_name: (str) name of the test case + @return: None, print out in the console + """ + + test.log("Launching a new instance of Firmware Simulator") + #cleanup() + try: + os.chdir(f"{COMMON_PATH}/dialin/ui") + #out = os.system("sudo chmod 777 sim_setup.sh") + res = os.system("./sim_setup.sh") + utils.waitForGUI(delay_s = 5) + if (res != 0) and (res != 256): + raise Exception("Wrong path to the simulator executable was given. Script not executed") + except Exception as msg: + test.log(str(msg)) + test.log("Launched Simulator......") + + start_application("Denali Application") + + +def cleanup(): + """ + Method to cleanup for simulator + @param test_name: (str) name of the test case + @return: None, print out in the console + """ + # cleanup for simulator + os.chdir(f"{COMMON_PATH}/dialin/ui") + try: + res = os.system("sudo ./cleanup.sh") + utils.waitForGUI(delay_s = 1) + if (res != 0) and (res != 50): + raise Exception("Incorrect command to kill the running simulator processes. " + \ + "Please check the cleanup.sh and provide required permissions") + except Exception as msg: + test.log(pyStr(msg)) + test.log("Cleaned up previously running simulator") + + Index: shared/scripts/dialin/dg/__init__.py =================================================================== diff -u --- shared/scripts/dialin/dg/__init__.py (revision 0) +++ shared/scripts/dialin/dg/__init__.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,26 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 __init__.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +from .constants import RESET, NO_RESET +from .dialysate_generator import DG +from .drain_pump import DGDrainPump +from .hd_proxy import DGHDProxy +from .heaters import Heaters +from .load_cells import DGLoadCells +from .pressures import DGPressures +from .reservoirs import DGReservoirs +from .ro_pump import DGROPump +from .temperatures import TemperatureSensors +from .valves import DGValves Index: shared/scripts/dialin/dg/accelerometer.py =================================================================== diff -u --- shared/scripts/dialin/dg/accelerometer.py (revision 0) +++ shared/scripts/dialin/dg/accelerometer.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,249 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 accelerometer.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Sean Nash +# @date (original) 29-Jul-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DGAccelerometer(AbstractSubSystem): + """ + Hemodialysis Delivery (DG) Dialin API sub-class for accelerometer related commands. + """ + + # Vector axes + class AccelerometerVector: + def __init__(self, x=0.0, y=0.0, z=0.0): + """ + DGAccelerometer constructor + + """ + self.x = x + self.y = y + self.z = z + + def __repr__(self): + return "{0}: ({1},{2},{3})".format(self.__class__.__name__, self.x, self.y, self.z) + + # Vector axes + VECTOR_AXIS_X = 0 + VECTOR_AXIS_Y = 1 + VECTOR_AXIS_Z = 2 + + def __init__(self, can_interface, logger: Logger): + """ + HDAccelerometer constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_ACCELEROMETER_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_accelerometer_sync) + + self.vector = self.AccelerometerVector() + self.vector_max = self.AccelerometerVector() + self.tilts = self.AccelerometerVector() + + def get_accel_vector(self): + """ + Gets the accelerometer vector. + @return: (vector) The vector from the accelerometer + """ + return self.vector + + def get_accel_max_vector(self): + """ + Gets the accelerometer maximum vector. + @return: (vector) The max. vector from the accelerometer + """ + return self.vector_max + + def get_accel_tilts(self): + """ + Gets the tilt angles from the accelerometer. + @return: (vector) The X, Y, and Z tilt angles. + """ + return self.tilts + + @publish([ + "vector", + "vector_max", + "tilts" + ]) + def _handler_accelerometer_sync(self, message): + """ + Handles published accelerometer data messages. Accelerometer data are captured + for reference. + + @param message: published accelerometer data message + @return: none + """ + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + self.vector = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + self.vector_max = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + self.tilts = self.AccelerometerVector(x[0], y[0], z[0]) + + def cmd_accel_vector_override(self, axis: int, mag: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer vector override command + Constraints: + Must be logged into DG. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCEL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG accelerometer axis magnitude") + + # 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(mag) + " g. " + self.logger.debug("Accelerometer axis " + str(axis) + " 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.debug("Timeout!!!!") + return False + + def cmd_accel_max_vector_override(self, axis: int, mag: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer maximum vector override command + Constraints: + Must be logged into DG. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCEL_MAX_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG accelerometer axis maximum magnitude") + + # 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(mag) + " g. " + self.logger.debug("Accelerometer max. axis " + str(axis) + " 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.debug("Timeout!!!!") + return False + + def cmd_accel_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCEL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG accelerometer 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("Accelerometer 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/dg/alarms.py =================================================================== diff -u --- shared/scripts/dialin/dg/alarms.py (revision 0) +++ shared/scripts/dialin/dg/alarms.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,252 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 alarms.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Quang Nguyen +# @date (original) 02-Sep-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..utils.checks import check_broadcast_interval_override_ms +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class DGAlarms(AbstractSubSystem): + """ + DG interface containing alarm related commands. + """ + + START_POS_ALARM_ID = DenaliMessage.PAYLOAD_START_INDEX + END_POS_ALARM_ID = START_POS_ALARM_ID + 2 + + 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.dg_alarm_broadcast_ch_id + msg_id = MsgIds.MSG_ID_ALARM_TRIGGERED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_activate) + + msg_id = MsgIds.MSG_ID_ALARM_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_clear) + + msg_id = MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_condition_clear) + + msg_id = MsgIds.MSG_ID_DG_ALARM_INFO.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_alarm_information_sync) + + # alarm states based on received DG alarm activation and alarm clear messages + self.alarm_states = [False] * 500 + # alarm condition states based on received DG alarm activation and clear condition messages + self.alarm_conditions = [False] * 500 + self.safety_shutdown_active = False + + def get_alarm_states(self): + """ + Gets all states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_states + + def get_alarm_conditions(self): + """ + Gets all alarm condition states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_conditions + + def get_alarm_state(self, alarm_id): + """ + Gets alarm state for given alarm + + @return: Alarm state + """ + return self.alarm_states[alarm_id] + + def get_safety_shutdown_activated(self): + """ + Gets the state of the DG safety shutdown signal. + + @return: (bool) safety shutdown line is activated (T/F) + """ + return self.safety_shutdown_active + + @publish(["alarm_states"]) + def _handler_alarm_activate(self, message): + """ + Handles published DG alarm activation messages. + + @param message: published DG alarm activation message + @return: none + """ + + alarm_id = struct.unpack(' int: + """ + Constructs and sends the alarm state override command + Constraints: + Must be logged into DG. + Given alarm must be valid. + If inactivating alarm, given alarm must be recoverable (clearable). + + @param alarm: integer - ID of alarm to override + @param state: integer - 1 for alarm active, 0 for alarm inactive + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + alm = integer_to_bytearray(alarm) + payload = rst + sta + alm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ALARM_STATE_OVERRIDE.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("active" if state != 0 else "inactive") + self.logger.debug("Alarm " + str(alarm) + " " + str_res + ": " + str( + received_message['message'][DenaliMessage.PAYLOAD_START_INDEX])) + # response payload is OK or not OK + return 1 == received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + return False + + def cmd_clear_all_alarms(self) -> int: + """ + Constructs and sends the clear all active alarms command to the DG. + This will clear even non-recoverable alarms. + Constraints: + Must be logged into DG. + + @return: 1 if successful, zero otherwise + """ + + key = integer_to_bytearray(-758926171) # 0xD2C3B4A5 + payload = key + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SUPER_CLEAR_ALARMS_CMD.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("All DG alarms cleared.") + # response payload is OK or not OK + return 1 == received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + return False + + def cmd_alarm_info_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET): + """ + Constructs and sends the alarm information 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_ALARM_INFO_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG alarm information 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("DG alarm information 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/dg/calibration_record.py =================================================================== diff -u --- shared/scripts/dialin/dg/calibration_record.py (revision 0) +++ shared/scripts/dialin/dg/calibration_record.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,575 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 calibration_record.py +# +# @author (last) Dara Navaei +# @date (last) 08-Nov-2021 +# @author (original) Dara Navaei +# @date (original) 12-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from logging import Logger + +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 + + +class DGCalibrationNVRecord(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for calibration commands. + """ + + _RECORD_START_INDEX = 6 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_HIGH_ORDER_GAIN_VALUE = 0 + _DEFAULT_GAIN_VALUE = 1 + _DEFAULT_OFFSET_VALUE = 0 + _DEFAULT_RATIO_VALUE = 1 + _DEFAULT_VOLUME_VALUE = 0 + _DEFAULT_ACID_MIX_RATIO = (2.35618 / 100) + _DEFAULT_BICARB_MIX_RATIO = (4.06812 / 100) + _DEFAULT_CALIBRATION_VALUE = 1 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + _DEFAULT_FLUSH_LINES_VOLUME = 0.01 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + 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._is_getting_cal_in_progress = False + self.cal_data = 0 + self._raw_cal_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + # DG Calibration_record main record + self.dg_calibration_record = self._prepare_dg_calibration_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_CALIBRATION_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_calibration_sync) + + def cmd_reset_dg_calibration_record(self) -> bool: + """ + Handles resetting DG calibration record. + + @return: True if successful, False otherwise + """ + self.dg_calibration_record = self._prepare_dg_calibration_record() + self.dg_calibration_record = self._utilities.reset_fw_record(self.dg_calibration_record) + status = self.cmd_set_dg_calibration_record(self.dg_calibration_record) + + return status + + def cmd_request_dg_calibration_record(self) -> bool: + """ + Handles getting DG calibration_record record from firmware. + + @return: True if successful, False otherwise + """ + self.logger.debug("Requesting a dg calibration record...") + + if not self._is_getting_cal_in_progress: + + self._is_getting_cal_in_progress = True + self._raw_cal_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_CALIBRATION_RECORD.value) + + received_message = self.can_interface.send(message, time_out=5) + + # 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] + + self.logger.debug("Timeout!!!!") + + self.logger.warning("Request cancelled: an existing request is in progress.") + return False + + def _handler_dg_calibration_sync(self, message): + """ + Handles published DG calibration_record record messages. DG calibration records are captured for + processing and updating the DG calibration_record record. + + @param message: published DG calibration_record record data message + + @return: None + """ + self.logger.debug("DG calibration sync handler...") + 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 + # 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 = self._RECORD_START_INDEX + self._RECORD_SPECS_BYTES + length + + # Get the calibration_record data only + self.cal_data = message['message'][self._RECORD_START_INDEX: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_cal_record += (message['message'][self._RECORD_START_INDEX + + self._RECORD_SPECS_BYTES:end_of_data_index]) + + if self.current_message == self.total_messages: + # Check if the requested read was just for comparing the results before writing to firmware back + self._is_getting_cal_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_calibration_record, self._raw_cal_record) + self._handler_received_complete_dg_calibration_record() + + @publish(["dg_calibration_record"]) + def _handler_received_complete_dg_calibration_record(self): + """ + Publishes the received calibration record + + @return: None + """ + self.logger.debug("Received a complete 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 + sends it to FW. + + @param previous_record: (OrderedDict) the dg calibration record to be sent + @return: True upon success, False otherwise + """ + # Pass the new changes as well as the previous calibration record + record_packets = self._utilities.prepare_record_to_send_to_fw(previous_record) + self.logger.debug('Setting DG calibration started') + + # 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_CALIBRATION_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 calibration record.") + return True + + def _prepare_dg_calibration_record(self): + """ + Handles assembling the sub dictionaries of each hardware group to make a DG calibration record. + + @return: (OrderedDict) the assembled record + """ + result = OrderedDict() + + groups_byte_size = 0 + # Call the other functions to get the dictionaries of each hardware group. All the dictionaries are + # ordered dictionaries to maintain the order in which they are inserted. The results are a tuple, the first + # element is the dictionary that was built and the second element is the byte size of the dictionary. + records_with_sizes = [self._prepare_pressure_sensors_cal_record(), self._prepare_flow_sensors_cal_record(), + self._prepare_load_cells_record(), self._prepare_temperature_sensors_record(), + 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()] + + for record, byte_size in records_with_sizes: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += byte_size + # Update the calibration record + result.update(record) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' int: + """ + Constructs and sends the concentrate pump state change request command + + @param pump_id: unsigned int - concentrate pump ID + @param on: bool - 1 to turn on, 0 to turn off + @return: 1 if successful, zero otherwise + + Concentrate pump IDs: \n + 0 = CP1 \n + 1 = CP2 \n + """ + payload = integer_to_bytearray(0) + integer_to_bytearray(on) + integer_to_bytearray(pump_id) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_CONCENTRATE_PUMP_STATE_CHANGE_REQUEST.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + if on: + self.logger.debug("Requested to turn on concentrate pump: CP" + str(pump_id)) + else: + self.logger.debug("Requested to turn off concentrate pump: CP" + str(pump_id)) + + # 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_dg_ch_id, + message_id=MsgIds.MSG_ID_CONCENTRATE_PUMP_TARGET_SPEED_OVERRIDE.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_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_CONCENTRATE_PUMP_MEASURED_SPEED_OVERRIDE.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_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_CONCENTRATE_PUMP_PUBLISH_INTERVAL_OVERRIDE.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 Index: shared/scripts/dialin/dg/conductivity_sensors.py =================================================================== diff -u --- shared/scripts/dialin/dg/conductivity_sensors.py (revision 0) +++ shared/scripts/dialin/dg/conductivity_sensors.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,191 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Quang Nguyen +# @date (original) 20-Jul-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class ConductivitySensorsEnum(DialinEnum): + CPI = 0 + CPO = 1 + CD1 = 2 + CD2 = 3 + + +class ConductivitySensors(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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_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 + + def get_conductivity_sensors(self): + """ + Gets the current conductivity value + + @return: List containing conductivity values: [ conductivity_sensor_cpi, conductivity_sensor_cpo, + conductivity_sensor_cd1, conductivity_sensor_cd2 ] + """ + return [self.conductivity_sensor_cpi, self.conductivity_sensor_cpo, + self.conductivity_sensor_cd1, self.conductivity_sensor_cd2] + + def get_ro_rejection_ratio(self): + """ + Gets the current RO rejection ratio value + + @return: ro_rejection_ratio + """ + return self.ro_rejection_ratio + + @publish(["ro_rejection_ratio", "conductivity_sensor_cpi", "conductivity_sensor_cpo", "conductivity_sensor_cd1", + "conductivity_sensor_cd2"]) + def _handler_conductivity_sensors_sync(self, message): + """ + Handles published conductivity sensor data messages. Conductivity sensor data are captured + for reference. + + @param message: published conductivity sensor data message + @return: None + """ + + ro_rejection_ratio = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cpi = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + cpo = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + cd1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + cd2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + self.ro_rejection_ratio = ro_rejection_ratio[0] + self.conductivity_sensor_cpi = cpi[0] + self.conductivity_sensor_cpo = cpo[0] + self.conductivity_sensor_cd1 = cd1[0] + self.conductivity_sensor_cd2 = cd2[0] + + def cmd_conductivity_sensor_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_dg_ch_id, + message_id=MsgIds.MSG_ID_CONDUCTIVITY_OVERRIDE.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_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_dg_ch_id, + message_id=MsgIds.MSG_ID_CONDUCTIVITY_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG 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 Index: shared/scripts/dialin/dg/constants.py =================================================================== diff -u --- shared/scripts/dialin/dg/constants.py (revision 0) +++ shared/scripts/dialin/dg/constants.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,18 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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) Sean +# @date (last) 14-Apr-2020 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ + +RESET = 1 +NO_RESET = 0 Index: shared/scripts/dialin/dg/dialysate_flow_sensor.py =================================================================== diff -u --- shared/scripts/dialin/dg/dialysate_flow_sensor.py (revision 0) +++ shared/scripts/dialin/dg/dialysate_flow_sensor.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,146 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 dialysate_flow_sensor.py +# +# @author (last) Sean Nash +# @date (last) 11-Nov-2021 +# @author (original) Hung Nguyen +# @date (original) 28-Oct-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DialysateFlowSensor(AbstractSubSystem): + """ + Dialysate Generation (DG) interface for dialysate flow sensor related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + Initialize CAN interface and logger + @param can_interface: Denali CAN Messenger object + @param: logger: (Logger) object + """ + + super().__init__() + + self.can_interface = can_interface + self.logger = logger + # The flow rate of the sensor mL/min + self.flow_rate = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_DIALYSATE_FLOW_METER_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_flow_sensor_sync) + + def get_flow_rate(self): + """ + Gets a flow value from the sensor + + @param: flow sensor: (int) sensor index + @return: The dialysate flow rate mL/min of a flow sensor + """ + return self.flow_rate + + @publish(['flow_rate']) + def _handler_flow_sensor_sync(self, message): + """ + Handles published flow sensor message. + + @param message: published flow sensor message + @return: none + """ + flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + + self.flow_rate = flow[0] + + def cmd_flow_sensor_value_override(self, rate: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the flow sensor value override command + Constraints: + Must be logged into DG. + + @param rate: (float) the sensor flow rate to be set + @param reset: (int) 1 to reset a previous override, 0 to override + @return 1 if successful, zero otherwise + """ + ml_per_liter: float = 1000.0 + reset_value = integer_to_bytearray(reset) + vlu = float_to_bytearray(rate / ml_per_liter) # DG expects the rate in mL/min + payload = reset_value + vlu + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DIALYSATE_MEASURED_FLOW_OVERRIDE.value, + payload=payload) + self.logger.debug("Overriding dialysate flow sensor broadcast 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("Dialysate flow sensor value override Timeout!!!") + return False + + def cmd_flow_sensor_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the flow sensor data publish interval command. + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG priority task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + msg_id = MsgIds.MSG_ID_DIALYSATE_FLOW_SEND_INTERVAL_OVERRIDE.value + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, message_id=msg_id, + payload=payload) + + self.logger.debug("Overriding dialysate flow sensor broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Dialysate flow sensor data broadcast interval overridden to " + str_res + " ms: " + + 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: shared/scripts/dialin/dg/dialysate_generator.py =================================================================== diff -u --- shared/scripts/dialin/dg/dialysate_generator.py (revision 0) +++ shared/scripts/dialin/dg/dialysate_generator.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,479 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 dialysate_generator.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ + +from .accelerometer import DGAccelerometer +from .alarms import DGAlarms +from .calibration_record import DGCalibrationNVRecord +from .chemical_disinfect import ChemicalDisinfect +from .concentrate_pumps import ConcentratePumps +from .conductivity_sensors import ConductivitySensors +from .constants import RESET +from .drain_pump import DGDrainPump +from .fans import Fans +from .dialysate_flow_sensor import DialysateFlowSensor +from .fluid_leak import DGFluidLeak +from .flush import FlushMode +from .hd_proxy import DGHDProxy +from .heat_disinfect import HeatDisinfect +from .heaters import Heaters +from .load_cells import DGLoadCells +from .pressures import DGPressures +from .reservoirs import DGReservoirs +from .ro_pump import DGROPump +from .rtc import DGRTC +from .samplewater import DGSampleWater +from .scheduled_runs_record import DGScheduledRunsNVRecord +from .service_record import DGServiceNVRecord +from .switches import DGSwitches +from .system_record import DGSystemNVRecord +from .temperatures import TemperatureSensors +from .thermistors import Thermistors +from .uv_reactors import UVReactors +from .valves import DGValves +from .voltages import DGVoltages +from .events import DGEvents +from ..common.msg_defs import MsgIds, MsgFieldPositions +from enum import unique +from .constants import NO_RESET +from ..protocols.CAN import DenaliCanMessenger, DenaliMessage, DenaliChannels +from ..utils import * +from ..utils.base import AbstractSubSystem, publish, LogManager, DialinEnum +from ..utils.conversions import integer_to_bytearray, unsigned_short_to_bytearray + +@unique +class DGOperationModes(DialinEnum): + # DG operation modes + DG_OP_MODE_FAULT = 0 + DG_OP_MODE_SERVICE = 1 + DG_OP_MODE_INIT_POST = 2 + DG_OP_MODE_STANDBY = 3 + DG_OP_MODE_STANDBY_SOLO = 4 + DG_OP_MODE_GEN_IDLE = 5 + DG_OP_MODE_FILL = 6 + DG_OP_MODE_DRAIN = 7 + DG_OP_MODE_FLUSH = 8 + DG_OP_MODE_DISINFECT = 9 + DG_OP_MODE_CHEMICAL_DISINFECT = 10 + + +class DG(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin object API. It provides the basic interface to communicate with + the DG firmware. + """ + + # HD login password + DG_LOGIN_PASSWORD = '123' + + # DG 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 + + # FPGA + START_POS_FPGA_ID = END_POS_BUILD + END_POS_FPGA_ID = START_POS_FPGA_ID + 1 + START_POS_FPGA_MAJOR = END_POS_FPGA_ID + END_POS_FPGA_MAJOR = START_POS_FPGA_MAJOR + 1 + START_POS_FPGA_MINOR = END_POS_FPGA_MAJOR + END_POS_FPGA_MINOR = START_POS_FPGA_MINOR + 1 + START_POS_FPGA_LAB = END_POS_FPGA_MINOR + END_POS_FPGA_LAB = START_POS_FPGA_LAB + 1 + + # DG sub_modes + DG_POST_STATE_START = 0 # Start initialize & POST mode state + DG_POST_STATE_FPGA = 1 # FPGA POST test state + DG_POST_STATE_WATCHDOG = 2 # Watchdog POST test state + DG_POST_STATE_TEMPERATURE_SENSORS = 3 # Temperature Sensors POST state + DG_POST_STATE_HEATERS = 4 # Heaters POST state + DG_POST_STATE_COMPLETED = 5 # POST completed successfully state + DG_POST_STATE_FAILED = 6 # POST failed state + NUM_OF_DG_POST_STATES = 7 # Number of initialize & POST mode states + + def __init__(self, can_interface="can0", log_level=None): + """ + Initializes the DG object + + + For example: + dg_object = DG(can_interface='can0') or + dg_object = DG(can_interface="can0", log_level="DEBUG") + + Possible log levels: + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "CAN_ONLY", "PRINT_ONLY"] + + @param can_interface: string with can bus name, e.g. "can0" + """ + 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, + log_can=self._log_manager.log_level == "CAN_ONLY") + self.can_interface.start() + + # register handler for HD operation mode broadcast messages + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_OP_MODE.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_op_mode_sync) + self.can_interface.register_receiving_publication_function(DenaliChannels.dg_sync_broadcast_ch_id, + MsgIds.MSG_ID_DG_VERSION.value, + self._handler_dg_version) + + # initialize variables that will be populated by DG version response + self.dg_version = None + self.fpga_version = None + # create properties + self.dg_operation_mode = 0 # self.DG_OP_MODE_INIT_POST + self.dg_operation_sub_mode = 0 + self.dg_logged_in = False + self.dg_set_logged_in_status(False) + self.dg_no_transmit_msg_list = [0,0,0,0,0,0,0,0] + + # Create command groups + self.accel = DGAccelerometer(self.can_interface, self.logger) + self.alarms = DGAlarms(self.can_interface, self.logger) + self.calibration_record = DGCalibrationNVRecord(self.can_interface, self.logger) + self.chemical_disinfect = ChemicalDisinfect(self.can_interface, self.logger) + self.concentrate_pumps = ConcentratePumps(self.can_interface, self.logger) + self.conductivity_sensors = ConductivitySensors(self.can_interface, self.logger) + self.dialysate_flow_sensor = DialysateFlowSensor(self.can_interface, self.logger) + self.drain_pump = DGDrainPump(self.can_interface, self.logger) + self.fans = Fans(self.can_interface, self.logger) + self.fluid_leak = DGFluidLeak(self.can_interface, self.logger) + self.flush = FlushMode(self.can_interface, self.logger) + self.hd_proxy = DGHDProxy(self.can_interface, self.logger) + self.heat_disinfect = HeatDisinfect(self.can_interface, self.logger) + self.heaters = Heaters(self.can_interface, self.logger) + self.load_cells = DGLoadCells(self.can_interface, self.logger) + self.pressures = DGPressures(self.can_interface, self.logger) + self.reservoirs = DGReservoirs(self.can_interface, self.logger) + self.ro_pump = DGROPump(self.can_interface, self.logger) + self.rtc = DGRTC(self.can_interface, self.logger) + self.samplewater = DGSampleWater(self.can_interface, self.logger) + self.scheduled_runs_record = DGScheduledRunsNVRecord(self.can_interface, self.logger) + self.service_record = DGServiceNVRecord(self.can_interface, self.logger) + self.switches = DGSwitches(self.can_interface, self.logger) + self.system_record = DGSystemNVRecord(self.can_interface, self.logger) + self.temperatures = TemperatureSensors(self.can_interface, self.logger) + self.thermistors = Thermistors(self.can_interface, self.logger) + self.uv_reactors = UVReactors(self.can_interface, self.logger) + self.valves = DGValves(self.can_interface, self.logger) + self.voltages = DGVoltages(self.can_interface, self.logger) + self.events = DGEvents(self.can_interface, self.logger) + + def get_version(self): + """ + Gets the DG version. Assumes DG version has already been requested. + + @return: The hd version string + """ + return self.dg_version + + def get_fpga_version(self): + """ + Gets the fpga version from the DG + + @return: The FPGA version + """ + return self.fpga_version + + def get_operation_mode(self): + """ + Gets the operation mode + + @return: The operation mode + """ + return self.dg_operation_mode + + def get_operation_sub_mode(self): + """ + Gets the operation sub mode + + @return: The operation sub mode + """ + return self.dg_operation_sub_mode + + def get_dg_logged_in(self): + """ + Gets the logged in status of the DG + + @return: True if DG is logged in, False if not + """ + return self.dg_logged_in + + def get_dg_blocked_msg_list(self): + """ + Gets the current list of message IDs that HD will prevent transmission of. + + @return: List of message IDs blocked from transmission + """ + return self.dg_no_transmit_msg_list + + @publish(["dg_logged_in"]) + def dg_set_logged_in_status(self, logged_in: bool = False): + """ + Callback for DG logged in status change. + @param logged_in boolean logged in status for DG + @return: none + """ + self.dg_logged_in = logged_in + + @publish(["dg_version", "fpga_version"]) + def _handler_dg_version(self, message): + """ + Handler for response from DG 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 + + @return: None if unsuccessful, the version string if unpacked successfully + """ + major = struct.unpack('B', bytearray(message['message'][self.START_POS_MAJOR:self.END_POS_MAJOR])) + minor = struct.unpack('B', bytearray(message['message'][self.START_POS_MINOR:self.END_POS_MINOR])) + micro = struct.unpack('B', bytearray(message['message'][self.START_POS_MICRO:self.END_POS_MICRO])) + build = struct.unpack('H', bytearray(message['message'][self.START_POS_BUILD:self.END_POS_BUILD])) + + fpga_id = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_ID:self.END_POS_FPGA_ID])) + fpga_major = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MAJOR:self.END_POS_FPGA_MAJOR])) + fpga_minor = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MINOR:self.END_POS_FPGA_MINOR])) + fpga_lab = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_LAB:self.END_POS_FPGA_LAB])) + + if all([len(each) > 0 for each in [major, minor, micro, build]]): + self.dg_version = f"v{major[0]}.{minor[0]}.{micro[0]}-{build[0]}" + self.logger.debug(f"DG VERSION: {self.dg_version}") + + if all([len(each) > 0 for each in [fpga_major, fpga_minor, fpga_id, fpga_lab]]): + self.fpga_version = f"ID: {fpga_id[0]} v{fpga_major[0]}.{fpga_minor[0]}.{fpga_lab[0]}" + self.logger.debug(f"DG FPGA VERSION: {self.fpga_version}") + + @publish(["dg_operation_mode", "dg_operation_sub_mode"]) + def _handler_dg_op_mode_sync(self, message): + """ + Handles published DG operation mode messages. Current DG operation mode + is captured for reference. + + @param message: published DG 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.dg_operation_mode = mode[0] + self.dg_operation_sub_mode = smode[0] + + def cmd_log_in_to_dg(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 DG. + + @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_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_TESTER_LOGIN_REQUEST.value, + payload=list(map(int, map(ord, self.DG_LOGIN_PASSWORD)))) + + self.logger.info("Logging in to the DG...") + + # 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.info("Successfully logged in to the DG.") + self.dg_set_logged_in_status(True) + else: + self.logger.error("Log In Failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.error("Timeout!!!!") + return False + + def cmd_ui_request_dg_version(self) -> None: + """ + Constructs and sends the ui request for version message + + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_REQUEST_FW_VERSIONS.value) + + self.logger.debug("Sending Dialin request for version to DG") + + self.can_interface.send(message, 0) + + def cmd_dg_safety_shutdown_override(self) -> int: + """ + Constructs and sends an DG safety shutdown override command via CAN bus. + + @return: 1 if successful, zero otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SAFETY_SHUTDOWN_OVERRIDE.value) + + self.logger.debug("overriding DG safety shutdown") + + # 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("Safety shutdown signal overridden") + else: + self.logger.debug("Safety shutdown signal override failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_dg_software_reset_request(self) -> None: + """ + Constructs and sends an DG software reset request via CAN bus. + Constraints: + Must be logged into DG. + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SOFTWARE_RESET_REQUEST.value) + + self.logger.debug("requesting DG software reset") + + # Send message + self.can_interface.send(message, 0) + self.logger.debug("Sent request to DG to reset...") + self.dg_set_logged_in_status(False) + + def cmd_dg_op_mode_broadcast_interval_override(self, ms:int=250, reset:int=NO_RESET) -> int: + """ + Constructs and sends the DG op mode 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_OP_MODE_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG op. mode 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("DG op. mode 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.debug("Timeout!!!!") + return False + + def cmd_block_dg_message_transmissions(self, msg1: int = 0, msg2: int = 0, msg3: int = 0, msg4: int = 0, + msg5: int = 0, msg6: int = 0, msg7: int = 0, msg8: int = 0): + """ + Constructs and sends a block dg message transmission request + Constraints: + Must be logged into DG. + + @param msg1: integer - 1st message ID to block DG from transmitting + @param msg2: integer - 2nd message ID to block DG from transmitting + @param msg3: integer - 3rd message ID to block DG from transmitting + @param msg4: integer - 4th message ID to block DG from transmitting + @param msg5: integer - 5th message ID to block DG from transmitting + @param msg6: integer - 6th message ID to block DG from transmitting + @param msg7: integer - 7th message ID to block DG from transmitting + @param msg8: integer - 8th message ID to block DG from transmitting + @return: 1 if successful, zero otherwise + """ + # Save blocked message(s) list + self.dg_no_transmit_msg_list[0] = msg1 + self.dg_no_transmit_msg_list[1] = msg2 + self.dg_no_transmit_msg_list[2] = msg3 + self.dg_no_transmit_msg_list[3] = msg4 + self.dg_no_transmit_msg_list[4] = msg5 + self.dg_no_transmit_msg_list[5] = msg6 + self.dg_no_transmit_msg_list[6] = msg7 + self.dg_no_transmit_msg_list[7] = msg8 + # Build message payload + m1 = unsigned_short_to_bytearray(msg1) + m2 = unsigned_short_to_bytearray(msg2) + m3 = unsigned_short_to_bytearray(msg3) + m4 = unsigned_short_to_bytearray(msg4) + m5 = unsigned_short_to_bytearray(msg5) + m6 = unsigned_short_to_bytearray(msg6) + m7 = unsigned_short_to_bytearray(msg7) + m8 = unsigned_short_to_bytearray(msg8) + payload = m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_BLOCK_MESSAGE_TRANSMISSION.value, + payload=payload) + + self.logger.debug("request DG block transmission of message(s)") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Given messages blocked." + + 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: shared/scripts/dialin/dg/drain_pump.py =================================================================== diff -u --- shared/scripts/dialin/dg/drain_pump.py (revision 0) +++ shared/scripts/dialin/dg/drain_pump.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,262 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 drain_pump.py +# +# @author (last) Dara Navaei +# @date (last) 10-Nov-2021 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class DrainPumpStates(DialinEnum): + DRAIN_PUMP_OFF_STATE = 0 + DRAIN_PUMP_CONTROL_TO_TARGET_STATE = 1 + DRAIN_PUMP_OPEN_LOOP_STATE = 2 + + +class DGDrainPump(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for drain pump related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGDrainPump constructor + + """ + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DRAIN_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_drain_pump_sync) + + self.target_drain_pump_rpm = 0 + self.dac_value = 0 + self.drain_pump_state = 0 + self.current_drain_pump_rpm = 0 + self.target_drain_pump_outlet_pressure = 0.0 + + def get_target_drain_pump_rpm(self): + """ + Gets the target drain pump RPM + + @return: The target drain pump RPM + """ + return self.target_drain_pump_rpm + + def get_dac_value(self): + """ + Gets the dac value + + @return: The dac value (int) + """ + return self.dac_value + + def get_drain_pump_state(self): + """ + Gets the drain pump state + + @return: Drain pump state + """ + return self.drain_pump_state + + def get_drain_pump_current_rpm(self): + """ + Gets the drain pump current RPM + + @return: Drain pump current RPM + """ + return self.current_drain_pump_rpm + + @publish(["target_drain_pump_rpm", "dac_value", "drain_pump_state", "current_drain_pump_rpm"]) + def _handler_drain_pump_sync(self, message): + """ + Handles published drain pump data messages. Drain pump data are captured + for reference. + + @param message: published drain pump data message + @return: none + """ + self.target_drain_pump_rpm = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.dac_value = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.drain_pump_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.current_drain_pump_rpm = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.target_drain_pump_outlet_pressure = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + def cmd_drain_pump_set_outlet_target_pressure(self, pressure: float) -> int: + """ + Constructs and sends the drain pump target outlet pressure command + Constraints: + Must be logged into DG. + + @param pressure: (float) target outlet pressure + @return: 1 if successful, zero otherwise + """ + + prssr = float_to_bytearray(pressure) + payload = prssr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_TARGET_OUTLET_PRESSURE.value, + payload=payload) + + self.logger.debug("Setting drain pump target pressure") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Drain pump outlet pressure set to " + str(pressure) + " psi" + + 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_drain_pump_set_rpm(self, rpm: int) -> int: + """ + Constructs and sends the drain pump speed set command + Constraints: + Must be logged into DG. + + @param rpm: (int) speed set point (in RPM) to override with + @return: 1 if successful, zero otherwise + """ + + spd = integer_to_bytearray(rpm) + payload = spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_SET_RPM.value, + payload=payload) + + self.logger.debug("Setting the drain pump RPM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Drain pump RPM set to " + str(rpm) + " RPM: " + + 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_drain_pump_measured_rpm_override(self, rpm: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the drain pump measured RPM override command. + Constraints: + Must be logged into DG. + Given RPM must be within 300 <= RPM <= 3000 + + @param rpm: (int) rpm to override with + @param reset: (int) 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + r = integer_to_bytearray(rpm) + payload = rst + r + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_PUMP_MEASURED_RPM_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override drain pump measured RPM") + + # 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(rpm) + self.logger.debug( + "Drain pump measured RPM overridden to " + str_res + " RPM " + + 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_drain_pump_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the drain pump data publication 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: (int) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override drain pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Drain pump data broadcast interval overridden to " + str_res + " ms: " + + 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: shared/scripts/dialin/dg/events.py =================================================================== diff -u --- shared/scripts/dialin/dg/events.py (revision 0) +++ shared/scripts/dialin/dg/events.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,227 @@ +########################################################################### +# +# Copyright (c) 2019-2021 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file events.py +# +# @author (last) Dara Navaei +# @date (last) 12-Nov-2021 +# @author (original) Dara Navaei +# @date (original) 12-Oct-2021 +# +############################################################################ + +import struct +from logging import Logger +from ..common import * +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from datetime import datetime + + +class DGEvents(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for events 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.dg_to_ui_ch_id + msg_id = MsgIds.MSG_ID_DG_EVENT.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_events_sync) + + # Define the dictionaries + self._dg_event_dictionary = dict() + self._dg_event_data_type = dict() + + # Dictionary of the the mode as key and the sub mode states enum class as the value + self._dg_op_mode_2_sub_mode = {DGOpModes.DG_MODE_FAUL.name: DGFaultStates, + DGOpModes.DG_MODE_INIT.name: DGInitStates, + DGOpModes.DG_MODE_STAN.name: DGStandByModeStates, + DGOpModes.DG_MODE_GENE.name: DGGenIdleModeStates, + DGOpModes.DG_MODE_FILL.name: DGFillModeStates, + DGOpModes.DG_MODE_DRAI.name: DGDrainModeStates, + DGOpModes.DG_MODE_FLUS.name: DGFlushStates, + DGOpModes.DG_MODE_HEAT.name: DGHeatDisinfectStates, + DGOpModes.DG_MODE_CHEM.name: DGChemicalDisinfectStates} + + # Loop through the list of the DG events enums and initial the event dictionary. Each event is a key in the + # dictionary and the value is a list. + for event in DGEventList: + self._dg_event_dictionary[DGEventList(event).name] = [] + + # Loop through the list of the event data type enum and update the dictionary + for data_type in DGEventDataType: + event_data_type = DGEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self._dg_event_data_type[event_data_type] = struct_unpack_type + + def get_dg_nth_event(self, event_id, event_number=0): + """ + Returns the nth requested DG event + + @param event_id the ID of the DG event types (i.e. DG_EVENT_STARTUP) + @param event_number the event number that is requested. The default is 0 meaning the last occurred event + + @returns the requested DG event number + """ + list_length = len(self._dg_event_dictionary[DGEventList(event_id).name]) + + if list_length == 0: + event = [] + elif event_number > list_length: + event = self._dg_event_dictionary[DGEventList(event_id).name][list_length - 1] + else: + event = self._dg_event_dictionary[DGEventList(event_id).name][list_length - event_number - 1] + + return event + + def get_dg_events(self, event_id, number_of_events=1): + """ + Returns the requested number of a certain DG event ID + + @param event_id the ID of the DG event types (i.e. DG_EVENT_STARTUP) + @param number_of_events the last number of messages of a certain event type + + @returns a list of the requested DG event type + """ + list_of_events = [] + + # If there are not enough event lists send all the events that are available + if len(self._dg_event_dictionary[DGEventList(event_id).name]) <= number_of_events: + list_of_events = self._dg_event_dictionary[DGEventList(event_id).name] + else: + # Get the all the events + complete_list = self._dg_event_dictionary[DGEventList(event_id).name] + # Since the last are located at the end of the list, iterate backwards for the defined + # event messages + for i in range(len(complete_list) - 1, len(complete_list) - number_of_events - 1, -1): + list_of_events.append(complete_list[i]) + + if number_of_events == 0: + list_of_events = self._dg_event_dictionary[DGEventList(event_id).name] + + return list_of_events + + @publish(['_dg_event_dictionary']) + def _handler_events_sync(self, message): + """ + Handles published events message + + @param message: published DG events data message + @returns none + """ + event_id = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + # Get the data type + event_data_type_1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + # Get the corresponding structure format + struct_data_type = self._dg_event_data_type[DGEventDataType(event_data_type_1).name] + # Get the data value by unpacking the data type + event_data_1 = struct.unpack(struct_data_type, bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + event_data_type_2 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + struct_data_type = self._dg_event_data_type[DGEventDataType(event_data_type_2).name] + event_data_2 = struct.unpack(struct_data_type, bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + # Convert the event ID to enum + event_state_name = DGEventList(event_id).name + + # Check if the event state name is operation mode change. If it is, get the name of the operation modes + # from the op modes enum class + if event_state_name == DGEventList.DG_EVENT_OP_MODE_CHANGE.name: + event_data_1 = DGOpModes(event_data_1).name + event_data_2 = DGOpModes(event_data_2).name + # Check if the event state name is sub mode change. + elif event_state_name == DGEventList.DG_EVENT_SUB_MODE_CHANGE.name: + # Get the length of the list of the sub mode list + op_list_len = len(self._dg_event_dictionary[DGEventList.DG_EVENT_OP_MODE_CHANGE.name]) + # Get the last tuple of the sub mode + # It is a list of tuples that each tuple is (timestamp, event type, prev op mode, current op mode) + last_op_tuple = self._dg_event_dictionary[DGEventList.DG_EVENT_OP_MODE_CHANGE.name][op_list_len - 1] + + # Get the current and previous operation modes of the last tuple in the list of the sub modes + # i.e. (timestamp, event type, prev, current) + current_op_mode = last_op_tuple[len(last_op_tuple) - 1] + current_op_mode_timestamp = datetime.strptime(last_op_tuple[0], '%Y-%m-%d %H:%M:%S.%f') + + sub_mode_list_len = len(self._dg_event_dictionary[DGEventList.DG_EVENT_SUB_MODE_CHANGE.name]) + + if sub_mode_list_len != 0: + # Get the tuple prior to the last tuple and get its previous and current operation modes + current_sub_tuple = self._dg_event_dictionary[DGEventList.DG_EVENT_SUB_MODE_CHANGE.name][ + sub_mode_list_len - 1] + + current_sub_mode_timestamp = datetime.strptime(current_sub_tuple[0], '%Y-%m-%d %H:%M:%S.%f') + else: + current_sub_mode_timestamp = 0 + + # Get the class of the states enums of the current operation mode that is running + current_sub_mode_enum_class = self._dg_op_mode_2_sub_mode[current_op_mode] + + # Check if the operation modes of the two tuples match + # i.e. last = (timestamp, event type, prev, current) and one before = (timestamp, event type, prev, current) + # If the prev and current match respectively, it means the current operation mode has not changed so the + # operation mode states can be converted from the current sub mode enum class + if current_sub_mode_timestamp != 0: + if current_op_mode_timestamp <= current_sub_mode_timestamp: + + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + + elif current_op_mode_timestamp > current_sub_mode_timestamp: + # If the previous and current of the last two tuples do not match, then an operation mode transition + # has occurred and the previous state is converted from the previous class and the current op mode + # is converted from current operation states enum class. + # i.e last = (timestamp, event type, 3, 8) and one before = (timestamp, event type, 8, 3) + # previous and current do not match so in the last type (timestamp, event type, 8, 3) the prev state + # should be from op mode 8 and the current state should be from op mode 3 + previous_op_mode = last_op_tuple[len(last_op_tuple) - 2] + previous_sub_mode_enum_class = self._dg_op_mode_2_sub_mode[previous_op_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + + if event_data_2 != 0: + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + previous_sub_mode = current_sub_tuple[len(current_sub_tuple) - 2] + previous_sub_mode_enum_class = self._dg_op_mode_2_sub_mode[previous_sub_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + + # Get the current timestamp and create a tuple of the current events + event_tuple = (datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'), event_state_name, event_data_1, event_data_2) + + # Update event dictionary + self._dg_event_dictionary[event_state_name].append(event_tuple) Index: shared/scripts/dialin/dg/fans.py =================================================================== diff -u --- shared/scripts/dialin/dg/fans.py (revision 0) +++ shared/scripts/dialin/dg/fans.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,272 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 fans.py +# +# @author (last) Dara Navaei +# @date (last) 10-Nov-2021 +# @author (original) Dara Navaei +# @date (original) 18-Nov-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET, RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class DGFansNames(DialinEnum): + FAN_INLET_1 = 0 + FAN_INLET_2 = 1 + FAN_INLET_3 = 2 + FAN_OUTLET_1 = 3 + FAN_OUTLET_2 = 4 + FAN_OUTLET_3 = 5 + + +class Fans(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for fans 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_FANS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_fans_sync) + + # Publish variables + self.dg_fans_duty_cycle = 0.0 + self.dg_fans_target_rpm = 0.0 + self.inlet_1_rpm = 0.0 + self.inlet_2_rpm = 0.0 + self.inlet_3_rpm = 0.0 + self.outlet_1_rpm = 0.0 + self.outlet_2_rpm = 0.0 + self.outlet_3_rpm = 0.0 + + def get_fans_target_duty_cycle(self): + """ + Gets the fans target duty cycle + + @return: Fans target duty cycle + """ + return self.dg_fans_duty_cycle + + def get_fan_inlet_1_rpm(self): + """ + Gets the inlet 1 fan RPM + + @return: Fan inlet 1 RPM + """ + return self.inlet_1_rpm + + def get_fan_inlet_2_rpm(self): + """ + Gets the inlet 2 fan RPM + + @return: Fan inlet 2 RPM + """ + return self.inlet_2_rpm + + def get_fan_inlet_3_rpm(self): + """ + Gets the inlet 3 fan RPM + + @return: Fan inlet 3 RPM + """ + return self.inlet_3_rpm + + def get_fan_outlet_1_rpm(self): + """ + Gets the outlet 1 fan RPM + + @return: Fan outlet 1 RPM + """ + return self.outlet_1_rpm + + def get_fan_outlet_2_rpm(self): + """ + Gets the outlet 2 fan RPM + + @return: Fan outlet 2 RPM + """ + return self.outlet_2_rpm + + def get_fan_outlet_3_rpm(self): + """ + Gets the outlet 3 fan RPM + + @return: Fan outlet 3 RPM + """ + return self.outlet_3_rpm + + def get_dg_fans_target_rpm(self): + """ + Gets the fans target RPM + + @return: Fans target RPM + """ + return self.dg_fans_target_rpm + + @publish(['target_duty_cycle', 'inlet_1_rpm', 'inlet_2_rpm', 'inlet_3_rpm', 'outlet_1_rpm', 'outlet_2_rpm', + 'outlet_3_rpm']) + def _handler_fans_sync(self, message): + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + self.dg_fans_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.dg_fans_target_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.inlet_1_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.inlet_2_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.inlet_3_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.outlet_1_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.outlet_2_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.outlet_3_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + + def cmd_fans_rpm_override(self, fan: int, rpm: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the dg fan RPM override command + Constraints: + Must be logged into DG. + + @param fan: (int) fan ID that is status is overridden + @param rpm: (int) RPM that the fan will 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) + f = integer_to_bytearray(fan) + r = float_to_bytearray(rpm) + payload = reset_value + r + f + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FANS_RPM_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fan RPM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Fan " + str(DGFansNames(fan).name) + " 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 + + def cmd_fans_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fans data publish 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) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FANS_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans data publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "Reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Fans data broadcast interval overridden to " + str_res + " ms: " + + 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_rpm_alarm_start_time_offset_override(self, hours: int, minutes: 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 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 + + 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, + payload=payload) + + self.logger.debug("Override fan RPM alarm start time") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Set RPM alarm start time 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: shared/scripts/dialin/dg/fluid_leak.py =================================================================== diff -u --- shared/scripts/dialin/dg/fluid_leak.py (revision 0) +++ shared/scripts/dialin/dg/fluid_leak.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,151 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 fluid_leak.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peman Montazemi +# @date (original) 11-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +class DGFluidLeak(AbstractSubSystem): + """ + DGFluidLeak + + Dialysate Generator (DG) Dialin API sub-class for fluid leak related commands. + """ + + # Fluid leak detector state + FLUID_LEAK_DETECTED_STATE = 0 # Wet + NO_FLUID_LEAK_DETECTED_STATE = 1 # Dry + + 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_FLUID_LEAK_STATE.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_fluid_leak_sync) + + self.fluid_leak_state = self.NO_FLUID_LEAK_DETECTED_STATE + + def get_fluid_leak_state(self): + """ + Gets the current fluid leak state + + @return: List containing fluid leak states: [detected, undetected] + """ + return self.fluid_leak_state + + @publish(["fluid_leak_state"]) + def _handler_fluid_leak_sync(self, message: dict) -> None: + """ + Handles published fluid leak state messages. Fluid leak state is captured + for reference. + + @param message: published fluid leak state message + @return: None + """ + + state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + + self.fluid_leak_state = state[0] + + def cmd_fluid_leak_detector_override(self, detected: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak detector state override command + Constraints: + Must be logged into DG. + Given detector must be one of the detectors listed below. + + @param detected: unsigned int - detected (0=wet, 1=dry) to override detector with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + det = integer_to_bytearray(detected) + payload = rst + det + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FLUID_LEAK_STATE_DETECTOR_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fluid leak detector state value") + + # 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_fluid_leak_state_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak state broadcast interval override command + Constraints: + Must be logged into DG. + Given interval must be non-zero and a multiple of the DG priority task interval (10 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FLUID_LEAK_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD fluid leak state 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("Fluid leak state 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/dg/flush.py =================================================================== diff -u --- shared/scripts/dialin/dg/flush.py (revision 0) +++ shared/scripts/dialin/dg/flush.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,67 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 flush.py +# +# @author (last) Dara Navaei +# @date (last) 18-Oct-2021 +# @author (original) Dara Navaei +# @date (original) 16-Apr-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..common.dg_defs import DGFlushStates + + +class FlushMode(AbstractSubSystem): + """ + Flush Mode class with APIs to set the timing of each of the stages. + """ + + def __init__(self, can_interface, logger: Logger): + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.flush_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.flush_drain_line_volume_l = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_FLUSH_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_flush_sync) + + @publish(["flush_state", "overall_elapsed_time", "state_elapsed_time", "flush_drain_line_volume"]) + def _handler_flush_sync(self, message: dict) -> None: + """ + Handles published flush message + + @param message: published flush data message + @returns none + """ + state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + elapsed_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + state_elapsed_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + drain_line_volume = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + self.flush_state = DGFlushStates(state).name + self.overall_elapsed_time = int(elapsed_time / 1000) + self.state_elapsed_time = int(state_elapsed_time / 1000) + self.flush_drain_line_volume_l = drain_line_volume Index: shared/scripts/dialin/dg/hd_proxy.py =================================================================== diff -u --- shared/scripts/dialin/dg/hd_proxy.py (revision 0) +++ shared/scripts/dialin/dg/hd_proxy.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,335 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 hd_proxy.py +# +# @author (last) Dara Navaei +# @date (last) 31-Oct-2021 +# @author (original) Sean +# @date (original) 15-Apr-2020 +# +############################################################################ +from logging import Logger + +from ..common.msg_defs import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DGHDProxy(AbstractSubSystem): + """ + Dialysate Generator (DG) Dialin API sub-class for HD proxy commands. + """ + + # Reservoir IDs + RESERVOIR1 = 0 + RESERVOIR2 = 1 + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali CAN Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + def cmd_switch_reservoirs(self, reservoir_id: int = RESERVOIR1) -> int: + """ + Constructs and sends the switch reservoirs command. + Constraints: + DG must be in re-circulate mode. + Given reservoirID must be in the reservoir list below. + + @param reservoir_id: unsigned int - reservoir to set as active (HD will draw from this reservoir). + @return: 1 if successful, zero otherwise + + @details Reservoir IDs: \n + 0 = RESERVOIR 1 \n + 1 = RESERVOIR 2 \n + """ + + res = integer_to_bytearray(reservoir_id) + payload = res + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCH_RESERVOIR_CMD.value, + payload=payload) + + self.logger.debug("switch reservoirs cmd sent to DG") + + # 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_fill(self, volume: int = 1500, tgt_fill_flow_lpm: float = 0.8, start: int = 1) -> int: + """ + Constructs and sends the fill command. + Constraints: + DG must be in re-circulate water state of re-circulate mode. + Given fill to volume must be between 0 and 1950 mL. + + @param volume: unsigned int - volume (in mL) to fill inactive reservoir to. + @param tgt_fill_flow_lpm: float - target fill flow rate in L/min. + @param start: unsigned int - 1 = start fill, 0 = stop fill. + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(volume) + integer_to_bytearray(start) + float_to_bytearray(tgt_fill_flow_lpm) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_FILL_CMD.value, + payload=payload) + + self.logger.debug("fill cmd sent to DG") + + # 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_drain(self, volume: int = 0, tare_load_cell: bool = False, rinse_conc_lines: bool = False, + start: int = 1) -> int: + """ + Constructs and sends the drain command. + Constraints: + DG must be in re-circulate mode. + Given drain to volume must be between 0 and 1950 mL. + + @param volume: unsigned int - volume (in mL) to drain the inactive reservoir to. + @param tare_load_cell: bool - flag indicates to tare load cell. + @param rinse_conc_lines: bool - flag indicates to whether rinse the concentrate lines or not. + @param start: int - start/stop drain command. The default is start = 1 + @return: 1 if successful, zero otherwise + """ + + vol = integer_to_bytearray(volume) + tare = integer_to_bytearray(tare_load_cell) + rinse = integer_to_bytearray(rinse_conc_lines) + st = integer_to_bytearray(start) + payload = vol + tare + rinse + st + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_DRAIN_CMD.value, + payload=payload) + + self.logger.debug("drain cmd sent to DG") + + # 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_start_stop_dg(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG command + Constraints: + DG must be in idle state of standby mode if start command given. + DG must be in re-circulate mode if stop command given. + + @param start: boolean - True = start DG, False = stop DG. + @return: 1 if successful, zero otherwise + """ + + if start: + cmd = 1 + cmd_str = "start " + else: + cmd = 0 + cmd_str = "stop " + payload = integer_to_bytearray(cmd) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_STARTING_STOPPING_TREATMENT_CMD.value, + payload=payload) + + self.logger.debug(cmd_str + "DG cmd sent to DG") + + # 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_start_stop_trimmer_heater(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG trimmer heater command + + @param start: boolean - True = start heater, False = stop heater. + @return: non-zero integer if successful, False otherwise + """ + + if start: + cmd = 1 + cmd_str = "start " + else: + cmd = 0 + cmd_str = "stop " + payload = integer_to_bytearray(cmd) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_TRIMMER_HEATER_CMD.value, + payload=payload) + + self.logger.debug(cmd_str + "DG trimmer heater cmd sent to DG") + + # 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_stop_primary_heater(self) -> None: + """ + Constructs and sends stop heat disinfect command + + @returns none + """ + # 0 is to stop + payload = integer_to_bytearray(0) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_START_STOP_PRIMARY_HEATER.value, + payload=payload) + self.logger.debug("Stopping primary heater") + self.can_interface.send(message, 0) + + def cmd_sample_water(self, cmd: int) -> None: + """ + Constructs and sends sample water command + + @param cmd: int - 0 = stop, 1 = start, 2 = flush, 3 = end. + @returns none + """ + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SAMPLE_WATER_CMD.value, + payload=payload) + self.logger.debug("Sending sample water command") + self.can_interface.send(message, 0) + + def cmd_start_stop_heat_disinfect(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG heat disinfect command + + @param start: (bool) True = start heat disinfect, False = stop heat disinfect. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + cmd_str = "Starting" + else: + cmd = 0 + cmd_str = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_HEAT_DISINFECT.value, + payload=payload) + + self.logger.debug(cmd_str + " DG heat disinfect") + + 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_start_stop_dg_flush(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG flush command + + @param start: (bool) True = start flush, False = stop flush. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + cmd_str = "Starting" + else: + cmd = 0 + cmd_str = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_FLUSH.value, + payload=payload) + + self.logger.debug(cmd_str + " DG flush") + + 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_start_stop_dg_chemical_disinfect(self, start: bool = True) -> int: + """ + Constructs and sends the start/stop DG chemical disinfect command + + @param start: (bool) True = start chemical disinfect, False = stop chemical disinfect. + @return: non-zero integer if successful, False otherwise + """ + # 1 is to start + if start: + cmd = 1 + str_text = "Starting" + else: + cmd = 0 + str_text = "Stopping" + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_CHEM_DISINFECT.value, + payload=payload) + + self.logger.debug(str_text + " DG chemical disinfect") + + 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 Index: shared/scripts/dialin/dg/heat_disinfect.py =================================================================== diff -u --- shared/scripts/dialin/dg/heat_disinfect.py (revision 0) +++ shared/scripts/dialin/dg/heat_disinfect.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,138 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 heat_disinfect.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Dara Navaei +# @date (original) 27-Feb-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum + + +@unique +class HeatDisinfectStates(DialinEnum): + DG_HEAT_DISINFECT_STATE_START = 0 + DG_HEAT_DISINFECT_STATE_DRAIN_R1 = 1 + DG_HEAT_DISINFECT_STATE_DRAIN_R2 = 2 + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN = 3 + DG_HEAT_DISINFECT_STATE_FLUSH_CIRCULATION = 4 + DG_HEAT_DISINFECT_STATE_FLUSH_R1_AND_R2 = 5 + DG_HEAT_DISINFECT_STATE_FLUSH_R2_AND_DRAIN_R1 = 6 + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R2 = 7 + DG_HEAT_DISINFECT_STATE_FLUSH_DRAIN_R1 = 8 + DG_HEAT_DISINFECT_STATE_FILL_WITH_WATER = 9 + DG_HEAT_DISINFECT_STATE_DISINFECT_R1_TO_R2 = 10 + DG_HEAT_DISINFECT_STATE_FILL_R2_WITH_HOT_WATER = 11 + DG_HEAT_DISINFECT_STATE_DISINFECT_R2_TO_R1 = 12 + DG_HEAT_DISINFECT_STATE_COOL_DOWN_HEATERS = 13 + DG_HEAT_DISINFECT_STATE_COOL_DOWN_RO_FILTER = 14 + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R1 = 15 + DG_HEAT_DISINFECT_STATE_MIX_DRAIN_R2 = 16 + DG_HEAT_DISINFECT_STATE_RINSE_R1_TO_R2 = 17 + DG_HEAT_DISINFECT_STATE_RINSE_R2_TO_R1_AND_DRAIN_R1 = 18 + DG_HEAT_DISINFECT_STATE_RINSE_CIRCULATION = 19 + DG_HEAT_DISINFECT_STATE_CANCEL_BASIC_PATH = 20 + DG_HEAT_DISINFECT_STATE_CANCEL_WATER_PATH = 21 + DG_HEAT_DISINFECT_STATE_COMPLETE = 22 + + +@unique +class HeatCancellationModes(DialinEnum): + CANCELLATION_MODE_NONE = 0 + CANCELLATION_MODE_BASIC = 1 + CANCELLATION_MODE_HOT = 2 + CANCELLATION_MODE_COLD = 3 + + +class HeatDisinfect(AbstractSubSystem): + """ + Heat Disinfection class with APIs to set the timing of each of the stages. + """ + + def __init__(self, can_interface, logger: Logger): + super().__init__() + + self.can_interface = can_interface + self.logger = logger + + self.heat_disinfect_state = 0 + self.heat_disinfect_ui_state = 0 + self.overall_elapsed_time = 0 + self.state_elapsed_time = 0 + self.heat_disinfect_target_time = 0 + self.heat_disinfect_count_down_time = 0 + self.cancellation_mode = 0 + self.r1_level = 0 + self.r2_level = 0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_HEAT_DISINFECT_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_heat_disinfect_sync) + + channel_id = DenaliChannels.dg_to_ui_ch_id + msg_id = MsgIds.MSG_ID_DG_HEAT_DISINFECT_TIME_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_heat_disinfect_to_ui_sync) + + @publish(["heat_disinfect_target_time", "heat_disinfect_count_down_time"]) + def _handler_heat_disinfect_to_ui_sync(self, message): + """ + Handles published heat disinfect message + + @param message: published heat disinfect UI data message + @returns none + """ + disinfect_target_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + disinfect_count_down_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + self.heat_disinfect_target_time = int(disinfect_target_time / 1000) + self.heat_disinfect_count_down_time = disinfect_count_down_time + + @publish(["heat_disinfect_state", "overall_elapsed_time", "state_elapsed_time", "cancellation_mode", "r1_level", + "r2_level", "heat_disinfect_ui_state"]) + def _handler_heat_disinfect_sync(self, message): + """ + Handles published heat disinfect message + + @param message: published heat disinfect data message + @returns none + """ + state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + elapsed_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + state_elapsed_time = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + cancellation_mode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + r1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + r2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + ui = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + + self.heat_disinfect_state = state + self.overall_elapsed_time = int(elapsed_time / 1000) + self.state_elapsed_time = int(state_elapsed_time / 1000) + self.cancellation_mode = cancellation_mode + self.r1_level = r1 + self.r2_level = r2 + self.heat_disinfect_ui_state = ui Index: shared/scripts/dialin/dg/heaters.py =================================================================== diff -u --- shared/scripts/dialin/dg/heaters.py (revision 0) +++ shared/scripts/dialin/dg/heaters.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,250 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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) Dara Navaei +# @date (last) 10-Nov-2021 +# @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 ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..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_RAMP_UP_TO_TARGET = 1 + HEATER_EXEC_STATE_CONTROL_TO_TARGET = 2 + + +class Heaters(AbstractSubSystem): + """ + + Dialysate Generator (DG) 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.small_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_efficiency = 0.0 + + self.temporary_remove_flow = 0.0 + self.temporary_internal_target = 0.0 + self.temporary_target_ro_flow = 0.0 + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_HEATERS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_heaters_sync) + + def get_main_primary_heater_duty_cycle(self): + """ + Gets the main primary heater duty cycle + + @return: Main primary heater duty cycle + """ + return self.main_primary_heater_duty_cycle + + def get_small_primary_heater_duty_cycle(self): + """ + Gets the small primary heater duty cycle + + @return: Small primary heater duty cycle + """ + return self.small_primary_heater_duty_cycle + + def get_trimmer_heater_duty_cycle(self): + """ + Gets the trimmer heater duty cycle + + @return: Trimmer heater duty cycle + """ + return self.trimmer_heater_duty_cycle + + def get_primary_heater_target_temperature(self): + """ + Gets the primary heater target temperature + + @return: Primary heater target temperature + """ + return self.primary_heaters_target_temperature + + def get_trimmer_heater_target_temperature(self): + """ + Gets the trimmer heater target temperature + + @return: Trimmer heater target temperature + """ + return self.trimmer_heater_target_temperature + + @publish(["main_primary_heater_duty_cycle", "small_primary_heater_duty_cycle", "trimmer_heater_duty_cycle", + "trimmer_heater_state", "primary_heaters_target_temperature", "trimmer_heater_target_temperature", + "primary_efficiency"]) + def _handler_heaters_sync(self, message): + """ + 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.small_primary_heater_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.trimmer_heater_duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.primary_heaters_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.trimmer_heater_target_temperature = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.primary_heater_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.trimmer_heater_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.primary_efficiency = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + + self.temporary_remove_flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.temporary_internal_target = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.temporary_target_ro_flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + + def cmd_start_stop_primary_heater(self, state: int = HeatersStartStop.STOP.name) -> None: + """ + Constructs and sends a start/stop primary heater command + Constraints: + A target temperature for primary heater between 10 and 90 deg C must have been given to DG previously. + + @param state: (int) start/stop state of the primary heater. The default is stop. + @returns none + """ + if state == HeatersStartStop.START.name: + payload = integer_to_bytearray(1) + operation = 'Turning on ' + else: + payload = integer_to_bytearray(0) + operation = 'Turning off ' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_START_STOP_PRIMARY_HEATER.value, + payload=payload) + + self.logger.debug(operation + " the Primary heater") + self.can_interface.send(message, 0) + + def cmd_start_stop_trimmer_heater(self, state: int = HeatersStartStop.STOP.name) -> None: + """ + Constructs and sends start trimmer heater command + Constraints: + A target temperature for trimmer heater between 10 and 90 deg C must have been given to DG previously. + + @param state: (int) start/stop state of the trimmer heater. The default is stop. + @returns none + """ + if state == HeatersStartStop.START.name: + payload = integer_to_bytearray(1) + operation = 'Turning on ' + else: + payload = integer_to_bytearray(0) + operation = 'Turning off' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_TRIMMER_HEATER_CMD.value, + payload=payload) + + self.logger.debug(operation + " the Primary heater") + self.can_interface.send(message, 0) + + def cmd_set_dialysate_target_temperature(self, + primary_target_temp: float = 37.0, + trimmer_target_temp: float = 38.0) -> None: + """ + Constructs and sends primary and trimmer heater target temperature + + @param primary_target_temp: (float) Primary heater target temperature + @param trimmer_target_temp: (float) Trimmer heater target temperature + @returns none + """ + primary_target = float_to_bytearray(primary_target_temp) + trimmer_target = float_to_bytearray(trimmer_target_temp) + payload = primary_target + trimmer_target + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_SET_DG_DIALYSATE_TEMP_TARGETS.value, + payload=payload) + self.logger.debug("Setting Primary Heater to {} C and Trimmer Heater to {} C".format(primary_target_temp, + trimmer_target_temp)) + self.can_interface.send(message, 0) + + 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_dg_ch_id, + message_id=MsgIds.MSG_ID_HEATERS_PUBLISH_INTERVAL_ORVERRIDE.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 Index: shared/scripts/dialin/dg/load_cells.py =================================================================== diff -u --- shared/scripts/dialin/dg/load_cells.py (revision 0) +++ shared/scripts/dialin/dg/load_cells.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,172 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 load_cells.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DGLoadCells(AbstractSubSystem): + """ + DGLoadCells + + Dialysate Generator (DG) Dialin API sub-class for load cell related commands. + """ + + # Load Cell IDs + LOAD_CELL_A1 = 0 + LOAD_CELL_A2 = 1 + LOAD_CELL_B1 = 2 + LOAD_CELL_B2 = 3 + + 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_LOAD_CELL_READINGS.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_load_cells_sync) + self.load_cell_A1 = 0.0 + self.load_cell_A2 = 0.0 + self.load_cell_B1 = 0.0 + self.load_cell_B2 = 0.0 + + def get_load_cells(self): + """ + Gets the current load cell weights + + @return: List containing load cell values: [A1, A2, B1, B2] + """ + return [self.load_cell_A1, self.load_cell_A2, self.load_cell_B1, self.load_cell_B2] + + @publish(["load_cell_A1", "load_cell_A2", "load_cell_B1", "load_cell_B2"]) + def _handler_load_cells_sync(self, message): + """ + Handles published load cell data messages. Load cell data are captured + for reference. + + @param message: published load cell data message + @return: None + """ + + a1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + a2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + b1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + b2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + + self.load_cell_A1 = a1[0] + self.load_cell_A2 = a2[0] + self.load_cell_B1 = b1[0] + self.load_cell_B2 = b2[0] + + def cmd_load_cell_override(self, sensor: int, grams: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the load cell override command + Constraints: + Must be logged into DG. + Given sensor must be one of the sensors listed below. + + @param sensor: unsigned int - sensor ID + @param grams: float - weight (in grams) to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Load Cell sensor IDs: \n + 0 = A1 \n + 1 = A2 \n + 2 = B1 \n + 3 = B2 \n + """ + + rst = integer_to_bytearray(reset) + grm = float_to_bytearray(grams) + idx = integer_to_bytearray(sensor) + payload = rst + grm + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override load cell weight value for sensor " + str(sensor)) + + # 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_load_cell_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the load cell 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG load cell 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("Load cell 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/dg/pressures.py =================================================================== diff -u --- shared/scripts/dialin/dg/pressures.py (revision 0) +++ shared/scripts/dialin/dg/pressures.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,175 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class DGPressures(AbstractSubSystem): + """ + DG interface containing pressure related commands. + """ + + # Pressure Sensor IDs + PRESSURE_SENSOR_RO_PUMP_INLET = 0 + PRESSURE_SENSOR_RO_PUMP_OUTLET = 1 + PRESSURE_SENSOR_DRAIN_PUMP_INLET = 2 + PRESSURE_SENSOR_DRAIN_PUMP_OUTLET = 3 + + 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_PRESSURES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_pressures_sync) + + self.ro_pump_inlet_pressure = 0.0 + self.ro_pump_outlet_pressure = 0.0 + self.drain_pump_inlet_pressure = 0.0 + self.drain_pump_outlet_pressure = 0.0 + + def get_pressures(self): + """ + Gets the pressure values + + @return: [ro pump inlet, ro pump outlet, drain pump inlet, drain pump outlet] + """ + return [self.ro_pump_inlet_pressure, + self.ro_pump_outlet_pressure, + self.drain_pump_inlet_pressure, + self.drain_pump_outlet_pressure] + + @publish([ + "ro_pump_inlet_pressure", + "ro_pump_outlet_pressure", + "drain_pump_inlet_pressure", + "drain_pump_outlet_pressure" + ]) + def _handler_pressures_sync(self, message): + """ + Handles published pressure data messages. Pressure data are captured + for reference. + + @param message: published pressure data message + @return: none + """ + + roi = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + roo = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + dri = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + dro = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + + self.ro_pump_inlet_pressure = roi[0] + self.ro_pump_outlet_pressure = roo[0] + self.drain_pump_inlet_pressure = dri[0] + self.drain_pump_outlet_pressure = dro[0] + + def cmd_pressure_override(self, sensor: int, pressure: int, 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 + + @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_dg_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override pressure sensor") + + # 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_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_dg_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_SEND_INTERVAL_OVERRIDE.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 Index: shared/scripts/dialin/dg/reservoirs.py =================================================================== diff -u --- shared/scripts/dialin/dg/reservoirs.py (revision 0) +++ shared/scripts/dialin/dg/reservoirs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,148 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 reservoirs.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from logging import Logger +from enum import unique + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray + + +@unique +class DGReservoirsNames(DialinEnum): + # Reservoir IDs + RESERVOIR1 = 0 + RESERVOIR2 = 1 + + +class DGReservoirs(AbstractSubSystem): + """ + DG interface containing reservoir 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_RESERVOIRS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_reservoirs_sync) + + self.active_reservoir = 0 + self.fill_to_vol_ml = 0 + self.drain_to_vol_ml = 0 + + self.time_reservoir_cycle = 0 + self.time_reservoir_fill_2_switch = 0 + self.time_uf_decay = 0.0 + self.temp_uf_fill = 0.0 + self.temp_reservoir_use_actual = 0.0 + self.temp_reservoir_end_fill = 0.0 + self.temp_avg_fill = 0.0 + self.temp_last_fill = 0.0 + self.time_rsrvr_fill = 0.0 + + def get_active_reservoir(self): + """ + Gets the active reservoir + RESERVOIR1 = 0 \n + RESERVOIR2 = 1 \n + + @return: 0 or 1 + """ + return self.active_reservoir + + def get_fill_to_vol(self): + """ + Gets the fill to volume + + @return: The fill to volume (mL) + """ + return self.fill_to_vol_ml + + def get_drain_to_vol(self): + """ + Gets the drain to volume + @return: The drain to volume (mL) + """ + return self.drain_to_vol_ml + + def cmd_switch_reservoirs(self, reservoir: int) -> bool: + """ + Sends a command to the DG to switch reservoirs + @param reservoir: (int) the new reservoir number + @return: True if command sent, False if invalid reservoir provided + """ + + if reservoir not in [0, 1]: + return False + + payload = integer_to_bytearray(reservoir) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCH_RESERVOIR_CMD.value, + payload=payload) + + self.logger.debug("Sending command to switch DG reservoir to {0}".format(reservoir)) + + self.can_interface.send(message, 0) + + return True + + @publish(["active_reservoir", "fill_to_vol_ml", "drain_to_vol_ml"]) + def _handler_reservoirs_sync(self, message): + """ + Handles published reservoir data messages. Reservoir data are captured + for reference. + + @param message: published reservoir data message + @return: none + """ + + self.active_reservoir = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.fill_to_vol_ml = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.drain_to_vol_ml = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.time_reservoir_cycle = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.time_reservoir_fill_2_switch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.time_uf_decay = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.temp_uf_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.temp_reservoir_use_actual = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.temp_reservoir_end_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.temp_avg_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + self.temp_last_fill = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11]))[0] + self.time_rsrvr_fill= struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12]))[0] Index: shared/scripts/dialin/dg/ro_pump.py =================================================================== diff -u --- shared/scripts/dialin/dg/ro_pump.py (revision 0) +++ shared/scripts/dialin/dg/ro_pump.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,273 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 ro_pump.py +# +# @author (last) Dara Navaei +# @date (last) 10-Nov-2021 +# @author (original) Sean +# @date (original) 14-Apr-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ROPumpStates(DialinEnum): + RO_PUMP_OFF_STATE = 0 + RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE = 1 + RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE = 2 + RO_PUMP_CONTROL_TO_MAX_PRESSURE_STATE = 3 + RO_PUMP_OPEN_LOOP_STATE = 4 + + +class DGROPump(AbstractSubSystem): + """ + DGROPump + Dialysate Generator (DG) Dialin API sub-class for RO pump related commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGROPump constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + + msg_id = MsgIds.MSG_ID_RO_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_ro_pump_sync) + + self.target_pressure_psi = 0.0 + self.measured_flow_rate_lpm = 0.0 + self.pwm_duty_cycle_pct = 0.0 + self.ro_pump_state = 0 + self.target_flow_lpm = 0.0 + self.feedback_duty_cycle_pct = 0.0 + + def get_target_pressure(self): + """ + Gets the target pressure + + @return: the target pressure (PSI) + """ + return self.target_pressure_psi + + def get_measured_flow_rate(self): + """ + Gets the measured flow rate (lpm) + + @return: The measured flow rate (float) + """ + return self.measured_flow_rate_lpm + + def get_pwm_duty_cycle_pct(self): + """ + Gets the PWM duty cycle pct + + @return: The PWM duty cycle pct (float) + """ + return self.pwm_duty_cycle_pct + + def get_ro_pump_state(self): + """ + Gets the RO pump state + + @return: The state of the RO pump + """ + return self.ro_pump_state + + @publish(["target_pressure_psi", "measured_flow_rate_lpm", "pwm_duty_cycle_pct", "ro_pump_state", + "feedback_duty_cycle_pct"]) + def _handler_ro_pump_sync(self, message): + """ + Handles published ro pump data messages. RO pump data are captured + for reference. + + @param message: published RO pump data message + + @return: None + """ + self.target_pressure_psi = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.measured_flow_rate_lpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.pwm_duty_cycle_pct = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + ro_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.target_flow_lpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.feedback_duty_cycle_pct = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + + self.ro_pump_state = ROPumpStates(ro_state).name + + def cmd_ro_pump_duty_cycle_pct(self, duty: float) -> int: + """ + Constructs and sends the set RO pump duty cycle message + Constraints: + Must be logged into DG. + + @param duty: integer - 1 percentage for duty cycle between 0 and 100 + @return: 1 if successful, zero otherwise + """ + dc = float_to_bytearray(duty/100) + payload = dc + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RO_PUMP_DUTY_CYCLE_OVERRIDE.value, + payload=payload) + + self.logger.debug("RO pump duty cycle set") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug( + "RO pump duty cycle set to " + str(duty) + " %" + + 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_ro_pump_measured_flow_rate_override(self, flow: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the RO flow rate override command. + Constraints: + Must be logged into DG. + + @param flow: float - flow rate (in L/min) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = float_to_bytearray(flow) + payload = rst + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_RO_FLOW_RATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override RO pump measured flow rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "Reset back to normal" + else: + str_res = str(flo) + self.logger.debug( + "Measured RO flow rate overridden to " + str_res + " L/min: " + + 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_set_ro_flow_rate(self, flow: float) -> int: + """ + Constructs and sends the RO rate set command. + Constraints: + Must be logged into DG. + + @param flow: float - flow rate (in L/min) to set + @return: 1 if successful, zero otherwise + """ + + flo = float_to_bytearray(flow) + payload = flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_RO_PUMP_TARGET_FLOW.value, + payload=payload) + + self.logger.debug("Set RO pump target flow rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + str_res = str(flo) + self.logger.debug( + "Target RO flow rate set to " + str_res + " L/min: " + + 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_ro_pump_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the RO pump set point 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 - time interval (in ms) to broadcast RO pump data + @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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_RO_PUMP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override RO pump data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "RO pump data broadcast interval overridden to " + str_res + " ms: " + + 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: shared/scripts/dialin/dg/rtc.py =================================================================== diff -u --- shared/scripts/dialin/dg/rtc.py (revision 0) +++ shared/scripts/dialin/dg/rtc.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,137 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 rtc.py +# +# @author (last) Dara Navaei +# @date (last) 10-Nov-2021 +# @author (original) Quang Nguyen +# @date (original) 13-May-2021 +# +############################################################################ +import struct +from ..common.msg_defs import MsgIds, MsgFieldPositions +from logging import Logger + +from ..common.msg_defs import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class DGRTC(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for rtc 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_RTC_EPOCH.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_rtc_epoch) + self.rtc_epoch = 0 + + def get_rtc_epoch(self): + """ + Gets the rtc epoch + + @return: The rtc epoch + """ + return self.rtc_epoch + + @publish(["rtc_epoch"]) + def _handler_rtc_epoch(self, message): + """ + Publishes the rtc time in epoch + + @param message: published rtc epoch message + @return: None + """ + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.rtc_epoch = epoch + + def cmd_stop_rtc(self): + """ + Stops the DG RTC clock + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_STOP_RTC_CLOCK.value) + + self.logger.debug("Stopping the DG RTC") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + self.logger.debug("RTC stop command was sent" + + 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_set_rtc_time_and_date(self, second: int, minute: int, hour: int, day: int, month: int, year: int) -> int: + """ + Sets the RTC time and date with the provided date and time + + @param second: (int) Second + @param minute: (int) Minute + @param hour: (int) Hour + @param day: (int) Day + @param month: (int) Month + @param year: (int) Year + @return: 1 if Successful, False otherwise + """ + sec = bytes([second]) + mint = bytes([minute]) + hour = bytes([hour]) + day = bytes([day]) + month = bytes([month]) + year = integer_to_bytearray(year) + payload = sec + mint + hour + day + month + year + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SET_RTC_DATE_TIME.value, + payload=payload) + + self.logger.debug("Setting time and date to rtc") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + self.logger.debug( + "Time and Date in rtc set to seconds: " + str(sec) + " minutes: " + str(min) + " hours: " + + str(hour) + " days: " + str(day) + " months: " + str(month) + " years: " + str(year) + + 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: shared/scripts/dialin/dg/samplewater.py =================================================================== diff -u --- shared/scripts/dialin/dg/samplewater.py (revision 0) +++ shared/scripts/dialin/dg/samplewater.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,109 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 samplewater.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Quang Nguyen +# @date (original) 02-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class DGSampleWater(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for sample water related commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGSampleWater constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + self.can_interface.register_receiving_publication_function(DenaliChannels.dg_sync_broadcast_ch_id, + MsgIds.MSG_ID_DG_FILTER_FLUSH_PROGRESS.value, + self._handler_filter_flush_progress_sync) + + self.filter_flush_timeout = 0 + self.filter_flush_time_countdown = 0 + + def get_filter_flush_timeout(self): + """ + Gets the filter flush timeout + + @return: The filter flush timeout + """ + return self.filter_flush_timeout + + def get_filter_flush_time_countdown(self): + """ + Gets the filter flush time countdown + + @return: The filter flush time countdown + """ + return self.filter_flush_time_countdown + + @publish([ + "filter_flush_timeout", + "filter_flush_time_countdown" + ]) + def _handler_filter_flush_progress_sync(self, message: dict) -> None: + """ + Handles published filter flush progress data messages. Filter flush progress data are captured for reference. + + @param message: published filter flush progress data message + @return: None + """ + + self.filter_flush_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.filter_flush_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + def cmd_filter_flush_time_period_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the filter flush time periodoverride command + + @param ms: integer - time period (in ms) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(reset) + integer_to_bytearray(ms) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_FILTER_FLUSH_TIME_PERIOD_OVERRIDE.value, + payload=payload) + + self.logger.debug("override DG filter flush time period") + + # 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: shared/scripts/dialin/dg/scheduled_runs_record.py =================================================================== diff -u --- shared/scripts/dialin/dg/scheduled_runs_record.py (revision 0) +++ shared/scripts/dialin/dg/scheduled_runs_record.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,364 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 scheduled_runs_record.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Dara Navaei +# @date (original) 12-Feb-2021 +# +############################################################################ +import struct +import time +import math +from collections import OrderedDict +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 logging import Logger + + +class DGScheduledRunsNVRecord(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for scheduled runs commands. + """ + + SCHEDULED_RUNS_RECORD_START_INDEX = 6 + SCHEDULED_RECORD_SPECS_BYTES = 12 + SCHEDULED_RECORDS_SPECS_BYTE_ARRAY = 3 + + MAX_PART_NUMBER_BYTES = 10 + MAX_SERIAL_NUMBER_BYTES = 15 + + DEFAULT_SCHEDULED_RUNS_DATE_VALUE = 0 + DEFAULT_SCHEDULED_RUNS_CRC_VALUE = 0 + DEFAULT_SCHEDULED_RUNS_PADDING_VALUE = 0 + + CURRENT_MESSAGE_NUM_INDEX = 0 + TOTAL_MESSAGES_NUM_INDEX = 4 + PAYLOAD_LENGTH_INDEX = 8 + PAYLOAD_START_INDEX = 12 + + SCHEDULED_RUNS_DATA_TYPE_INDEX = 0 + SCHEDULED_RUNS_VALUE_INDEX = 1 + + TARGET_BYTES_TO_SEND_TO_FW = 150 + MIN_PAYLOAD_BYTES_SPACE = 4 + + RTC_RAM_MAX_BYTES_TO_WRITE = 64 + + PAYLOAD_CURRENT_MSG_INDEX = 0 + PAYLOAD_TOTAL_MSG_INDEX = 1 + PAYLOAD_SCHEDULED_RUNS_BYTES_INDEX = 2 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + 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._is_getting_runs_in_progress = False + self._write_fw_data_to_excel = True + self.cal_data = 0 + self.raw_scheduled_runs_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + + # DG scheduled runs main record as an ordered dictionary + self.dg_scheduled_runs_record = self._prepare_dg_scheduled_runs_record() + + if self.can_interface is not None: + + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_SCHEDULED_RUNS_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_scheduled_runs_record) + + def cmd_reset_dg_calibration_record(self) -> bool: + """ + Handles resetting DG scheduled services record. + + @return: True if successful, False otherwise + """ + self._prepare_dg_scheduled_runs_record() + print(self.dg_scheduled_runs_record) + self.dg_scheduled_runs_record = self._utilities.reset_fw_record(self.dg_scheduled_runs_record) + status = self.cmd_set_dg_scheduled_runs_record(self.dg_scheduled_runs_record) + + return status + + def cmd_request_dg_scheduled_runs_record(self) -> int: + """ + Handles getting DG scheduled runs record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_runs_in_progress is not True: + self._is_getting_runs_in_progress = True + # Clear the list for the next call + self.raw_scheduled_runs_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_SCHEDULED_RUNS_RECORD.value) + + self.logger.debug('Getting DG scheduled runs record') + + 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 + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_dg_scheduled_runs_record(self, message): + """ + Handles published DG scheduled runs record messages. DG scheduled runs records are captured for processing and + updating the DG scheduled runs record. + + @param message: published DG scheduled runs 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 scheduled runs record payload is from the start index + 12 bytes for the current message + total + # messages + the length of scheduled runs record. The rest is the CAN messaging CRC and is not + # needed. + end_of_data_index = self.SCHEDULED_RUNS_RECORD_START_INDEX + self.SCHEDULED_RECORD_SPECS_BYTES + \ + self.received_msg_length + + # Get the scheduled runs data only + self.cal_data = message['message'][self.SCHEDULED_RUNS_RECORD_START_INDEX:end_of_data_index] + + # Continue getting calibration_record records until the all the messages are received. Concatenate the + # calibration_record records to each other + if self.current_message <= self.total_messages: + self.raw_scheduled_runs_record += (message['message'][self.SCHEDULED_RUNS_RECORD_START_INDEX + + self.SCHEDULED_RECORD_SPECS_BYTES:end_of_data_index]) + if self.current_message == self.total_messages: + # Done receiving the messages + self._is_getting_runs_in_progress = False + # If all the messages have been received, call another function to process the raw data + self._update_dg_scheduled_record_from_fw() + self._handler_received_complete_dg_scheduled_runs_record() + + @publish(["dg_scheduled_runs_record"]) + def _handler_received_complete_dg_scheduled_runs_record(self): + """ + Publishes the received scheduled runs record + + @return: None + """ + self.logger.debug("Received a complete dg scheduled runs record.") + + def _update_dg_scheduled_record_from_fw(self): + """ + Handles parsing the scheduled runs messages that were received from DG firmware. + + @return: None + """ + raw_payload_temp_start_index = 0 + # Convert the concatenated raw data into a byte array since the struct library requires byte arrays. + self.raw_scheduled_runs_record = bytearray(self.raw_scheduled_runs_record) + + # Loop through the keys for the main calibration_record dictionary + # DG_Calibration : {pressure_sensors : { ppi : { gain: [' bool: + """ + Handles updating the DG system record with the newest calibration_record data of a hardware and sends it to FW. + + @return: True upon success, False otherwise + """ + + record_packets = self._utilities.prepare_record_to_send_to_fw(dg_scheduled_runs_record) + + self.logger.debug('Setting DG scheduled runs started') + + # 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_SCHEDULED_RUNS_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 scheduled runs record.") + return True Index: shared/scripts/dialin/dg/service_record.py =================================================================== diff -u --- shared/scripts/dialin/dg/service_record.py (revision 0) +++ shared/scripts/dialin/dg/service_record.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,252 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 service_record.py +# +# @author (last) Dara Navaei +# @date (last) 12-Oct-2021 +# @author (original) Dara Navaei +# @date (original) 12-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, DialinEnum, publish +from ..utils.nv_ops_utils import NVOpsUtils + + +@unique +class ServiceLocation(DialinEnum): + SERVICE_LOCATION_FACTORY = 0 + SERVICE_LOCATION_FIELD = 1 + + +class DGServiceNVRecord(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for service record commands. + """ + + # The default service time interval is 6 months in seconds + _DEFAULT_SERVICE_INTERVAL_S = 15768000 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_SERVICE_LOCATION = ServiceLocation.SERVICE_LOCATION_FACTORY.value + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + + 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.service_data = 0 + self._is_getting_service_in_progress = False + self._raw_service_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.dg_service_record = self._prepare_dg_service_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_SERVICE_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dg_service_sync) + + def cmd_reset_dg_service_record(self) -> bool: + """ + Handles resetting DG service record. + + @return: True if successful, False otherwise + """ + self.dg_service_record = self._prepare_dg_service_record() + self.dg_service_record = self._utilities.reset_fw_system_service_record(self.dg_service_record) + status = self.cmd_set_dg_service_record(self.dg_service_record) + + return status + + def cmd_request_dg_service_record(self) -> int: + """ + Handles getting DG service record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_service_in_progress is not True: + self._is_getting_service_in_progress = True + # Clear the list for the next call + self._raw_service_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_SERVICE_RECORD.value) + + self.logger.debug('Getting DG service 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 service 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_service_sync(self, message): + """ + Handles published DG service record messages. DG service records are captured for + processing and updating the DG service record. + + @param message: published DG service 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.service_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_service_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_service_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_service_record, self._raw_service_record) + self._handler_received_complete_dg_service_record() + + @publish(["dg_service_record"]) + def _handler_received_complete_dg_service_record(self): + """ + Publishes the received service record + + @return: None + """ + self.logger.debug("Received a complete dg service record.") + + def cmd_set_dg_service_record(self, dg_service_record: OrderedDict) -> bool: + """ + Handles updating the DG service record and sends it to FW. + + @param dg_service_record: (OrderedDict) the dg service record to be sent + @return: True upon success, False otherwise + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(dg_service_record) + + self.logger.debug('Setting DG service 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_SERVICE_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 service record.") + return True + + def _prepare_dg_service_record(self): + """ + Handles assembling the sub dictionaries of each group to make a blank dg service record. + + @return: (OrderedDict) the assembled dg service record + """ + record = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_service_record()] + + for function in functions: + # Update the groups bytes size so far to be use 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': [' dict: + """ + Gets the status of a switch + + @return: The status of all of the switches in a dictionary + """ + return self.dg_switches_status + + @publish(["dg_switches_status"]) + def _handler_switches_sync(self, message): + """ + Handles published drain pump data messages. Switches data are captured + for reference. + + @param message: published drain pump data message + @return: none + """ + conc_cap = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + dialysate_cap = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + fluid_door = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self.dg_switches_status[DGSwitchesName.CONCENTRATE_CAP.name] = DGSwitchStatus(conc_cap).value + self.dg_switches_status[DGSwitchesName.DIALYSATE_CAP.name] = DGSwitchStatus(dialysate_cap).value + self.dg_switches_status[DGSwitchesName.FLUID_DOOR.name] = DGSwitchStatus(fluid_door).value + + def cmd_dg_switch_status_override(self, switch: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the dg switch status override command + Constraints: + Must be logged into DG. + + @param switch: (int) switch ID that is status is overridden + @param status: (int) status that the switch will 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) + sw = integer_to_bytearray(switch) + st = integer_to_bytearray(status) + payload = reset_value + st + sw + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCHES_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override switch status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Switch " + str(DGSwitchesName(switch).name) + " 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 + + def cmd_dg_switches_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG switch data publication 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: (int) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_SWITCHES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override DG switches data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "DG Switches data broadcast interval overridden to " + str_res + " ms: " + + 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: shared/scripts/dialin/dg/system_record.py =================================================================== diff -u --- shared/scripts/dialin/dg/system_record.py (revision 0) +++ shared/scripts/dialin/dg/system_record.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,266 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 system_record.py +# +# @author (last) Dara Navaei +# @date (last) 28-Oct-2021 +# @author (original) Dara Navaei +# @date (original) 10-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, DialinEnum, publish +from ..utils.nv_ops_utils import NVOpsUtils + + +@unique +class MFGLocation(DialinEnum): + MFG_LOCATION_FACTORY = 0 + + +class DGSystemNVRecord(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for system record commands. + """ + + _RECORD_SPECS_BYTES = 12 + _DEFAULT_MFG_LOCATION = MFGLocation.MFG_LOCATION_FACTORY.value + _MAX_PN_BYTES = 10 + _MAX_SN_BYTES = 15 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + 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._is_getting_sys_in_progress = False + self.sys_data = 0 + self._raw_system_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + # System main record + self.dg_system_record = self._prepare_dg_system_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_DG_SEND_SYSTEM_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_dg_system_sync) + + def cmd_reset_dg_system_record(self) -> bool: + """ + Handles resetting DG system record. + + @return: True if successful, False otherwise + """ + self.dg_system_record = self._prepare_dg_system_record() + self.dg_system_record = self._utilities.reset_fw_system_service_record(self.dg_system_record) + status = self.cmd_set_dg_system_record(self.dg_system_record) + + return status + + def get_dg_system_record(self) -> dict: + """ + Handles getting DG system record per user's request. + NOTE: In order to get the latest system record, use cmd_request_dg_system_record first + to fetch the system record from the firmware. + + @return: DG system record dictionary + """ + return self.dg_system_record['system_record'] + + def cmd_request_dg_system_record(self) -> int: + """ + Handles getting DG system record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_sys_in_progress is not True: + # Receiving the system record is in progress + self._is_getting_sys_in_progress = True + # Clear the list for the next call + self._raw_system_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_GET_SYSTEM_RECORD.value) + + self.logger.debug('Getting DG system record') + + 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 + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_dg_system_sync(self, message): + """ + Handles published DG system record messages. HD system records are captured for + processing and updating the DG system record. + + @param message: published DG system 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.sys_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_system_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_sys_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_system_record, self._raw_system_record) + self._handler_received_complete_dg_system_record() + + @publish(["dg_system_record"]) + def _handler_received_complete_dg_system_record(self): + """ + Publishes the received system record + + @return: None + """ + self.logger.debug("Received a complete dg system record.") + + def cmd_set_dg_system_record(self, dg_system_record: OrderedDict) -> bool: + """ + Handles updating the DG system and sends it to FW. + + @param dg_system_record: (OrderedDict) the dg system record to be sent + @return: True upon success, False otherwise + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(dg_system_record) + + self.logger.debug('Setting DG system 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_SYSTEM_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 service record.") + return True + + def _prepare_dg_system_record(self): + """ + Handles assembling the sub dictionaries of each group to make the main DG system record. + + @return: (OrderedDict) the assembled dg system record + """ + result = OrderedDict() + + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_system_record()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' 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_dg_ch_id, + message_id=MsgIds.MSG_ID_TEMPERATURE_SENSORS_PUBLISH_INTERVAL_OVERRIDE.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 DG. + 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 + + @details temperature sensor indexes: \n + 0 = Primary Heater Inlet + 1 = Heat Disinfect + 2 = Primary Heater Outlet + 3 = Conductivity Sensor 1 + 4 = Conductivity Sensor 2 + 5 = Dialysate (Redundant) + 6 = Dialysate + 7 = Primary Heater Thermocouple + 8 = Trimmer Heater Thermocouple + 9 = Primary Heater Cold Junction + 10 = Trimmer Heater Cold Junction + 11 = Primary Heater Internal + 12 = Trimmer Heater Internal + 13 = FPGA board + 14 = Load cell A1/B1 + 15 = Load cell A2/B2 + 16 = Internal THDO RTD + 17 = Internal TDI RTD + 18 = Internal conductivity temp sensor + """ + 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_dg_ch_id, + message_id=MsgIds.MSG_ID_TEMPERATURE_SENSORS_VALUE_OVERRIDE.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 Index: shared/scripts/dialin/dg/thermistors.py =================================================================== diff -u --- shared/scripts/dialin/dg/thermistors.py (revision 0) +++ shared/scripts/dialin/dg/thermistors.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,160 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 thermistors.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Dara Navaei +# @date (original) 18-Nov-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ThermistorsNames(DialinEnum): + THERMISTOR_ONBOARD_NTC = 0 + THERMISTOR_POWER_SUPPLY_1 = 1 + THERMISTOR_POWER_SUPPLY_2 = 2 + + +class Thermistors(AbstractSubSystem): + """ + Dialysate Generation (DG) interface for thermistors and board temperature 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 + # Dictionary of the thermistors + self.thermistors = {ThermistorsNames.THERMISTOR_ONBOARD_NTC.name: {}, + ThermistorsNames.THERMISTOR_POWER_SUPPLY_1.name: {}, + ThermistorsNames.THERMISTOR_POWER_SUPPLY_2.name: {}} + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_THERMISTORS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_thermistors_sync) + + def get_temperature(self, thermistor): + """ + Gets a thermistor's value + + @param: thermistor: (int) thermistor index + @return: The temperature of a thermistor + """ + return self.thermistors[ThermistorsNames(thermistor).name] + + @publish(['thermistors']) + def _handler_thermistors_sync(self, message): + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + onboard_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + pwr_supply_1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + pwr_supply_2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + self.thermistors[ThermistorsNames.THERMISTOR_ONBOARD_NTC.name] = onboard_temp + self.thermistors[ThermistorsNames.THERMISTOR_POWER_SUPPLY_1.name] = pwr_supply_1 + self.thermistors[ThermistorsNames.THERMISTOR_POWER_SUPPLY_2.name] = pwr_supply_2 + + def cmd_thermistors_value_override(self, thermistor: int, value: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the thermistors value override command + Constraints: + Must be logged into DG. + + @param value: float the temperature to be set + @param thermistor: (int) thermistor index + @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(value) + thr = integer_to_bytearray(thermistor) + payload = reset_value + vlu + thr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_THERMISTORS_VALUE_OVERRIDE.value, + payload=payload) + # 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("Themristors value override Timeout!!!") + return False + + def cmd_thermistors_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the thermistors data publish interval 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: (int) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + msg_id = MsgIds.MSG_ID_DG_THERMISTORS_DATA_PUBLISH_INTERVAL_OVERRIDE.value + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, message_id=msg_id, + payload=payload) + + self.logger.debug("Overriding themistors broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Thermistors data broadcast interval overridden to " + str_res + " ms: " + + 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: shared/scripts/dialin/dg/uv_reactors.py =================================================================== diff -u --- shared/scripts/dialin/dg/uv_reactors.py (revision 0) +++ shared/scripts/dialin/dg/uv_reactors.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,266 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 uv_reactors.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Dara Navaei +# @date (original) 18-Nov-2020 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET, RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +@unique +class ReactorsNames(DialinEnum): + INLET_UV_REACTOR = 0 + OUTLET_UV_REACTOR = 1 + + +@unique +class ReactorsStates(DialinEnum): + UV_REACTOR_STATE_OFF = 0 + UV_REACTOR_STATE_ON = 1 + + +@unique +class ReactorsHealthStatus(DialinEnum): + UV_REACTOR_NOT_HEALTHY = 0 + UV_REACTOR_HEALTHY = 1 + UV_REACTOR_OFF = 2 + + +class UVReactors(AbstractSubSystem): + """ + + Dialysate Generator (DG) Dialin API sub-class for UV reactors 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_UV_REACTORS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_uv_reactors_sync) + + self.inlet_uv_reactor_health = 0 + self.outlet_uv_reactor_health = 0 + self.inlet_uv_reactor_state = 0 + self.outlet_uv_reactor_state = 0 + + def get_inlet_uv_reactor_health(self) -> int: + """ + Gets the inlet UV reactor health + + @return: Inlet UV reactor health + """ + return self.inlet_uv_reactor_health + + def get_outlet_uv_reactor_health(self) -> int: + """ + Gets the outlet UV reactor health + + @return: Outlet UV reactor health + """ + return self.outlet_uv_reactor_health + + def get_inlet_uv_reactor_state(self) -> int: + """ + Gets the inlet UV reactor state + + @return: Inlet UV reactor state + """ + return self.inlet_uv_reactor_state + + def get_outlet_uv_reactor_state(self) -> int: + """ + Gets the outlet UV reactor state + + @return: Outlet UV reactor state + """ + return self.outlet_uv_reactor_state + + @publish(['inlet_uv_reactor_health', 'outlet_uv_reactor_health', 'inlet_uv_reactor_state', + 'outlet_uv_reactor_state']) + def _handler_uv_reactors_sync(self, message: dict) -> None: + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + inlet_health = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + outlet_health = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + inlet_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + outlet_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + self.inlet_uv_reactor_health = inlet_health + self.outlet_uv_reactor_health = outlet_health + + # Check if the received state from firmware is in range otherwise, the state will be unknown + self.inlet_uv_reactor_state = inlet_state + self.outlet_uv_reactor_state = outlet_state + + def cmd_uv_reactors_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the UV reactors data publish 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) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_UV_REACTORS_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override UV reactors data publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "Reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "UV reactors data broadcast interval overridden to " + str_res + " ms: " + + 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_uv_reactors_health_override(self, reactor: int, health: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the UV reactors' health override command. + Constraints: + Must be logged into DG. + + @param reactor: (int) UV reactor index + @param health: (int) 0 for unhealthy and 1 for healthy + @param reset: (int) 0 for no reset and 1 for reset + @return 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + rctr = integer_to_bytearray(reactor) + hlth = integer_to_bytearray(health) + payload = reset_value + hlth + rctr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_UV_REACTORS_HEALTH_OVERRIDE.value, + payload=payload) + # 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("UV reactors health override Timeout!!!") + return False + + def cmd_start_stop_inlet_uv_reactor(self, state: int = ReactorsStates.UV_REACTOR_STATE_OFF.value) -> int: + """ + Constructs and sends a start/stop command to the DG inlet UV reactor + + @param: state: (int) the state of the inlet UV reactor. 0 for Off (default) and 1 for On. + @return: 1 if successful, zero otherwise + """ + rst = integer_to_bytearray(0) + inlet_uv_reactor_index = integer_to_bytearray(0) + if state == ReactorsStates.UV_REACTOR_STATE_ON.value: + payload = rst + integer_to_bytearray(1) + inlet_uv_reactor_index + operation = 'Turning on ' + else: + payload = rst + integer_to_bytearray(0) + inlet_uv_reactor_index + operation = 'Turning off ' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_UV_REACTORS.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug(operation + "inlet UV reactor") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug(operation + "inlet UV reactor timeout!!") + return False + + def cmd_start_stop_outlet_uv_reactor(self, state: int = ReactorsStates.UV_REACTOR_STATE_ON.value) -> int: + """ + Constructs and sends a start/stop command to the DG outlet UV reactor + + @param: state: (int) the state of the outlet UV reactor. 0 for Off (default) and 1 for On. + @return: 1 if successful, zero otherwise + """ + rst = integer_to_bytearray(0) + outlet_uv_reactor_index = integer_to_bytearray(1) + if state == ReactorsStates.UV_REACTOR_STATE_ON.value: + payload = rst + integer_to_bytearray(1) + outlet_uv_reactor_index + operation = 'Turning on ' + else: + payload = rst + integer_to_bytearray(0) + outlet_uv_reactor_index + operation = 'Turning off ' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_START_STOP_UV_REACTORS.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug(operation + "outlet UV reactor") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug(operation + "outlet UV reactor timeout!!") + return False Index: shared/scripts/dialin/dg/valves.py =================================================================== diff -u --- shared/scripts/dialin/dg/valves.py (revision 0) +++ shared/scripts/dialin/dg/valves.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,368 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Peman Montazemi +# @date (original) 19-May-2020 +# +############################################################################ + +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from ..common.msg_defs import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + +# Valve states +ENERGIZED = True +DEENERGIZED = False + + +@unique +class VPiVSPVBfVRD1VRD2States(DialinEnum): + VALVE_STATE_CLOSED = 0 + VALVE_STATE_OPEN = 1 + + +@unique +class VPdStates(DialinEnum): + VALVE_STATE_DRAIN_C_TO_NO = 0 + VALVE_STATE_OPEN_C_TO_NC = 1 + + +@unique +class VPoStates(DialinEnum): + VALVE_STATE_NOFILL_C_TO_NO = 0 + VALVE_STATE_FILL_C_TO_NC = 1 + + +@unique +class VDrVRcStates(DialinEnum): + VALVE_STATE_DRAIN_C_TO_NO = 0 + VALVE_STATE_RECIRC_C_TO_NC = 1 + + +@unique +class VRoVRiStates(DialinEnum): + VALVE_STATE_R1_C_TO_NO = 0 + VALVE_STATE_R2_C_TO_NC = 1 + + +@unique +class VRdVRfStates(DialinEnum): + VALVE_STATE_R2_C_TO_NO = 0 + VALVE_STATE_R1_C_TO_NC = 1 + + +class DGValves(AbstractSubSystem): + """ + Dialysate Generation (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) + + # Valve IDs + VALVE_RESERVOIR_FILL = 0 # VRF + VALVE_RESERVOIR_INLET = 1 # VRI + VALVE_RESERVOIR_DRAIN = 2 # VRD + VALVE_RESERVOIR_OUTLET = 3 # VRO + VALVE_PRESSURE_OUTLET = 4 # VPO + VALVE_BYPASS_FILTER = 5 # VBF + VALVE_RECIRCULATE = 6 # VRC + VALVE_DRAIN = 7 # VDR + VALVE_PRESSURE_INLET = 8 # VPI + VALVE_SAMPLING_PORT = 9 # VSP + VALVE_RESERVOIR_DRAIN_1 = 10 # VRD1 + VALVE_RESERVOIR_DRAIN_2 = 11 # VRD2 + VALVE_PRODUCTION_DRAIN = 12 # VPD + NUM_OF_VALVES = 13 # Number of valves + + 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.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_VALVES_STATES.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_valves_sync) + + self.valve_states_all = 0x0000 + self.valve_state_VRF = {"id": self.VALVE_RESERVOIR_FILL, "state": DEENERGIZED} + self.valve_state_VRI = {"id": self.VALVE_RESERVOIR_INLET, "state": DEENERGIZED} + self.valve_state_VRD = {"id": self.VALVE_RESERVOIR_DRAIN, "state": DEENERGIZED} + self.valve_state_VRO = {"id": self.VALVE_RESERVOIR_OUTLET, "state": DEENERGIZED} + self.valve_state_VPO = {"id": self.VALVE_PRESSURE_OUTLET, "state": DEENERGIZED} + self.valve_state_VBF = {"id": self.VALVE_BYPASS_FILTER, "state": DEENERGIZED} + self.valve_state_VRC = {"id": self.VALVE_RECIRCULATE, "state": DEENERGIZED} + self.valve_state_VDR = {"id": self.VALVE_DRAIN, "state": DEENERGIZED} + self.valve_state_VPI = {"id": self.VALVE_PRESSURE_INLET, "state": DEENERGIZED} + self.valve_state_VSP = {"id": self.VALVE_SAMPLING_PORT, "state": DEENERGIZED} + self.valve_state_VRD1 = {"id": self.VALVE_RESERVOIR_DRAIN_1, "state": DEENERGIZED} + self.valve_state_VRD2 = {"id": self.VALVE_RESERVOIR_DRAIN_2, "state": DEENERGIZED} + self.valve_state_VPD = {"id": self.VALVE_PRODUCTION_DRAIN, "state": DEENERGIZED} + + self.valve_states_enum = [0 for _ in range(self.NUM_OF_VALVES)] + + def get_valve_states(self): + """ + Gets the valve states + + @return: All valve states: \n + [\n + Valve Reservoir Fill \n + Valve Reservoir Inlet \n + Valve Reservoir Drain \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_VRD.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_VRD': {'id': 2, '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_VRD', 2, 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([ + "valve_states_all", + "valve_state_VRF", + "valve_state_VRI", + "valve_state_VRD", + "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): + """ + 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_VRD["state"] = self._binary_to_valve_state(vst[0] & 4) + 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[self.VALVE_RESERVOIR_FILL] = VRdVRfStates(self._binary_to_valve_state(vst[0] & 1)).name # VRF + self.valve_states_enum[self.VALVE_RESERVOIR_INLET] = VRoVRiStates(self._binary_to_valve_state(vst[0] & 2)).name # VRI + self.valve_states_enum[self.VALVE_RESERVOIR_DRAIN] = VRdVRfStates(self._binary_to_valve_state(vst[0] & 4)).name # VRD + self.valve_states_enum[self.VALVE_RESERVOIR_OUTLET] = VRoVRiStates(self._binary_to_valve_state(vst[0] & 8)).name # VRO + self.valve_states_enum[self.VALVE_PRESSURE_OUTLET] = VPoStates(self._binary_to_valve_state(vst[0] & 16)).name # VPO + self.valve_states_enum[self.VALVE_BYPASS_FILTER] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 32)).name # VBF + self.valve_states_enum[self.VALVE_RECIRCULATE] = VDrVRcStates(self._binary_to_valve_state(vst[0] & 64)).name # VRC + self.valve_states_enum[self.VALVE_DRAIN] = VDrVRcStates(self._binary_to_valve_state(vst[0] & 128)).name # VDR + self.valve_states_enum[self.VALVE_PRESSURE_INLET] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 256)).name # VPI + self.valve_states_enum[self.VALVE_SAMPLING_PORT] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 512)).name # VSP + self.valve_states_enum[self.VALVE_RESERVOIR_DRAIN_1] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 1024)).name # VRD1 + self.valve_states_enum[self.VALVE_RESERVOIR_DRAIN_2] = VPiVSPVBfVRD1VRD2States(self._binary_to_valve_state(vst[0] & 2048)).name # VRD2 + self.valve_states_enum[self.VALVE_PRODUCTION_DRAIN] = VPdStates(self._binary_to_valve_state(vst[0] & 4096)).name # VPD + + 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 = Valve Reservoir Drain \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_dg_ch_id, + message_id=MsgIds.MSG_ID_VALVE_STATE_OVERRIDE.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_dg_ch_id, + message_id=MsgIds.MSG_ID_VALVES_STATES_PUBLISH_INTERVAL_OVERRIDE.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 Index: shared/scripts/dialin/dg/voltages.py =================================================================== diff -u --- shared/scripts/dialin/dg/voltages.py (revision 0) +++ shared/scripts/dialin/dg/voltages.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,219 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 voltages.py +# +# @author (last) Quang Nguyen +# @date (last) 30-Aug-2021 +# @author (original) Sean Nash +# @date (original) 22-Apr-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.base import DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +# Monitored voltages +@unique +class DGMonitoredVoltages(DialinEnum): + MONITORED_LINE_24V_MAIN = 0 # Main voltage (24V) + MONITORED_LINE_1_8V_FPGA = 1 # FPGA logic voltage (1.8V) + MONITORED_LINE_1V_FPGA = 2 # FPGA reference voltage (1V) + MONITORED_LINE_3_3V_SENSORS = 3 # Sensors voltage (3.3V) + MONITORED_LINE_1_8V_PROC = 4 # Processor voltage (1.8V) + MONITORED_LINE_5V_SENSORS = 5 # Sensors voltage (5V) + MONITORED_LINE_5V_LOGIC = 6 # Logic voltage (5V) + MONITORED_LINE_3_3V = 7 # Logic voltage (3.3V) + MONITORED_LINE_1_2V_PROC = 8 # Processor voltage (1.2V) + MONITORED_LINE_V_REF = 9 # Reference voltage (3V) + MONITORED_LINE_EXT_ADC_1_REF_V = 10 # External ADC 1 reference voltage (3V) + MONITORED_LINE_EXT_ADC_2_REF_V = 11 # External ADC 2 reference voltage (3V) + MONITORED_LINE_PS_GATE_DRIVER_V = 12 # P/S gate driver (5V) + MONITORED_LINE_24V_PRIM_HTR_V = 13 # Primary heater, secondary element voltage (0..24V) + MONITORED_LINE_24V_TRIM_HTR_V = 14 # Trimmer heater voltage (0..24V) + NUM_OF_MONITORED_LINES = 15 # Number of monitored voltages + + +class DGVoltages(AbstractSubSystem): + """ + Hemodialysis Delivery (DG) Dialin API sub-class for voltage monitor related commands and data. + """ + + def __init__(self, can_interface, logger: Logger): + """ + DGVoltages constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.dg_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DG_VOLTAGES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_monitored_voltages_sync) + self.monitored_voltages = [0.0] * DGMonitoredVoltages.NUM_OF_MONITORED_LINES.value + + def get_monitored_voltages(self): + """ + Gets all DG monitored voltages + + @return: List of voltages of size NUM_OF_MONITORED_VOLTAGE_LINES + """ + return self.monitored_voltages + + @publish([ + "monitored_voltages" + ]) + def _handler_monitored_voltages_sync(self, message): + """ + Handles published DG monitored voltages data messages. Voltage data are captured + for reference. + + @param message: published monitored voltages data message + @return: none + """ + + v1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + v12 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + v18p = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + v18f = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + v3r = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + ve1 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + ve2 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + v33 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + v33s = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + v5l = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + v5s = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + v5g = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + v24 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13])) + v24p = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14])) + v24t = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15])) + + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1V_FPGA.value] = v1[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1_2V_PROC.value] = v12[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1_8V_PROC.value] = v18p[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_1_8V_FPGA.value] = v18f[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_V_REF.value] = v3r[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_EXT_ADC_1_REF_V.value] = ve1[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_EXT_ADC_2_REF_V.value] = ve2[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_3_3V.value] = v33[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_3_3V_SENSORS.value] = v33s[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_5V_LOGIC.value] = v5l[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_5V_SENSORS.value] = v5s[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_PS_GATE_DRIVER_V.value] = v5g[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_MAIN.value] = v24[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_PRIM_HTR_V.value] = v24p[0] + self.monitored_voltages[DGMonitoredVoltages.MONITORED_LINE_24V_TRIM_HTR_V.value] = v24t[0] + + def cmd_monitored_voltage_override(self, signal: int = 0, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the DG monitored voltage override command + Constraints: + Must be logged into DG. + Given signal must be valid member of DGMonitoredVoltages enum + + @param signal: integer - ID of signal to override + @param volts: float - value (in volts) to override signal with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vlt = float_to_bytearray(volts) + idx = integer_to_bytearray(signal) + payload = rst + vlt + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DG_MONITORED_VOLTAGES_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored DG voltage for signal " + str(signal)) + + # 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(volts) + " V. " + self.logger.debug("Monitored DG voltage 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.debug("Timeout!!!!") + return False + + def cmd_monitored_voltages_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET) -> int: + """ + Constructs and sends the monitored DG voltages 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_dg_ch_id, + message_id=MsgIds.MSG_ID_DG_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored DG voltages 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("DG monitored voltages 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/__init__.py =================================================================== diff -u --- shared/scripts/dialin/hd/__init__.py (revision 0) +++ shared/scripts/dialin/hd/__init__.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,12 @@ +from .alarms import HDAlarms +from .blood_flow import HDBloodFlow +from .buttons import HDButtons +from .constants import RESET, NO_RESET, BUTTON_PRESSED, BUTTON_RELEASED +from .dialysate_inlet_flow import HDDialysateInletFlow +from .dialysate_outlet_flow import HDDialysateOutletFlow +from .hemodialysis_device import HD +from .pressure_occlusion import HDPressureOcclusion +from .rtc import HDRTC +from .treatment import HDTreatment +from .ui_proxy import HDUIProxy +from .watchdog import HDWatchdog Index: shared/scripts/dialin/hd/accelerometer.py =================================================================== diff -u --- shared/scripts/dialin/hd/accelerometer.py (revision 0) +++ shared/scripts/dialin/hd/accelerometer.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,249 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 accelerometer.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Sean Nash +# @date (original) 29-Jul-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDAccelerometer(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for accelerometer related commands. + """ + + # Vector axes + class AccelerometerVector: + def __init__(self, x=0.0, y=0.0, z=0.0): + """ + HDAccelerometer constructor + + """ + self.x = x + self.y = y + self.z = z + + def __repr__(self): + return "{0}: ({1},{2},{3})".format(self.__class__.__name__, self.x, self.y, self.z) + + # Vector axes + VECTOR_AXIS_X = 0 + VECTOR_AXIS_Y = 1 + VECTOR_AXIS_Z = 2 + + def __init__(self, can_interface, logger: Logger): + """ + HDAccelerometer constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_ACCELEROMETER_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_accelerometer_sync) + + self.vector = self.AccelerometerVector() + self.vector_max = self.AccelerometerVector() + self.tilts = self.AccelerometerVector() + + def get_accel_vector(self): + """ + Gets the accelerometer vector. + @return: (vector) The vector from the accelerometer + """ + return self.vector + + def get_accel_max_vector(self): + """ + Gets the accelerometer maximum vector. + @return: (vector) The max. vector from the accelerometer + """ + return self.vector_max + + def get_accel_tilts(self): + """ + Gets the tilt angles from the accelerometer. + @return: (vector) The X, Y, and Z tilt angles. + """ + return self.tilts + + @publish([ + "vector", + "vector_max", + "tilts" + ]) + def _handler_accelerometer_sync(self, message): + """ + Handles published accelerometer data messages. Accelerometer data are captured + for reference. + + @param message: published accelerometer data message + @return: none + """ + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + self.vector = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + self.vector_max = self.AccelerometerVector(x[0], y[0], z[0]) + + x = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + y = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + z = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + self.tilts = self.AccelerometerVector(x[0], y[0], z[0]) + + def cmd_accel_vector_override(self, axis: int, mag: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer vector override command + Constraints: + Must be logged into HD. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCEL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD accelerometer axis magnitude") + + # 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(mag) + " g. " + self.logger.debug("Accelerometer axis " + str(axis) + " 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.debug("Timeout!!!!") + return False + + def cmd_accel_max_vector_override(self, axis: int, mag: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer maximum vector override command + Constraints: + Must be logged into HD. + + @param axis: integer - accelerometer axis to override + @param mag: float - axis magnitude (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = float_to_bytearray(mag) + idx = integer_to_bytearray(axis) + payload = rst + sta + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCEL_MAX_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD accelerometer axis maximum magnitude") + + # 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(mag) + " g. " + self.logger.debug("Accelerometer max. axis " + str(axis) + " 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.debug("Timeout!!!!") + return False + + def cmd_accel_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the accelerometer broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCEL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD accelerometer 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("Accelerometer 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/air_bubbles.py =================================================================== diff -u --- shared/scripts/dialin/hd/air_bubbles.py (revision 0) +++ shared/scripts/dialin/hd/air_bubbles.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,218 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 air_bubbles.py +# +# @author (last) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Peman Montazemi +# @date (original) 18-May-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class HDAirBubbles(AbstractSubSystem): + """ + HDAirBubbles + + Hemodialysis Delivery (HD) Dialin API sub-class for air bubbles related commands. + ADA: Air bubble Detector Arterial + ADV: Air bubble Detector Venous + """ + + # Air bubble detectors + ADV = 0 # Air bubble Detector Venous + + # Air bubble detectors status + BUBBLE_DETECTED_STATUS = 0 # Air bubble detected + FLUID_DETECTED_STATUS = 1 # Fluid (no air bubble) detected + + # Air bubble detectors state machine states + AIR_BUBBLE_NORMAL_STATE = 0 # Normal state + AIR_BUBBLE_SELF_TEST_STATE = 1 # Self-test state + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_BUBBLES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_air_bubbles_data_sync) + + # Initialize status of ADV air bubble detectors to fluid (no air bubble) detected + self.air_bubbles_status = [self.FLUID_DETECTED_STATUS] + + # Initialize state of ADV air bubble detectors state machine to normal + self.air_bubbles_state = [self.AIR_BUBBLE_NORMAL_STATE] + + def get_air_bubble_status(self, index: int) -> int: + """ + Gets the given air bubble status using index 0 for ADV. + + @param index: integer - 0 for getting ADV detector status + @return: Air bubble status (air bubble or fluid) for given detector + """ + + return self.air_bubbles_status[index] + + def get_air_bubble_state(self, index: int) -> int: + """ + Gets the given air bubble state using index 0 for ADV. + + @param index: integer - 0 for getting ADV detector state + @return: integer - air bubble state (0: init, 1: self-test, 2: normal) for given detector + """ + + return self.air_bubbles_state[index] + + @publish("air_bubbles_data") + def _handler_air_bubbles_data_sync(self, message): + """ + Handles published air bubbles data messages. Air bubble status and state are captured + for ADV detector. + + @param message: published air bubbles data message as: ADV status, ADV state + @return: None + """ + + adv_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + adv_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.air_bubbles_status = [adv_status[0]] + self.air_bubbles_state = [adv_state[0]] + + def cmd_air_bubble_status_override(self, status: int, index: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air bubble detector status override command + Constraints: + Must be logged into HD. + Given detector must be one of the detectors listed below. + + @param status: unsigned int - status (0=air bubble, 1=fluid) to override detector with + @param index: integer - 0 for ADV status override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + i = integer_to_bytearray(index) + stat = integer_to_bytearray(status) + payload = rst + stat + i + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BUBBLE_STATUS_OVERRIDE.value, + payload=payload) + + if index == self.ADV: + self.logger.debug("Override air bubble detector ADV status value") + + # 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_air_bubble_self_test_request(self, index: int) -> int: + """ + Request air bubble self-test for a given detector (ADV) + Constraints: + Must be logged into HD. + + @param index: integer - 0 for ADV status override + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(index) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BUBBLE_SELF_TEST_REQUEST.value, + payload=payload) + + if index == self.ADV: + self.logger.debug("Request air bubble self-test for detector ADV") + + # 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_air_bubbles_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air bubbles data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be positive non-zero and a multiple of the HD priority 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 ms > 0 and ms % 50 == 0: + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BUBBLES_DATA_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD air bubbles data broadcast interval to"+str(ms)+"ms") + + # 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("Air bubbles 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.debug("Timeout!!!!") + return False + elif ms <= 0: + self.logger.debug("ms must be positive non-zero.") + return False + elif ms % 50 != 0: + self.logger.debug("ms must be a multiple of 50.") + return False + else: + self.logger.debug("ms must be an integer.") + return False Index: shared/scripts/dialin/hd/air_trap.py =================================================================== diff -u --- shared/scripts/dialin/hd/air_trap.py (revision 0) +++ shared/scripts/dialin/hd/air_trap.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,181 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 air_trap.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Sean Nash +# @date (original) 21-Sep-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDAirTrap(AbstractSubSystem): + """ + HDAirTrap + + Hemodialysis Delivery (HD) Dialin API sub-class for air trap related commands. + """ + + # Air trap level sensor IDs + LOWER_LEVEL_SENSOR = 0 + UPPER_LEVEL_SENSOR = 1 + + # Air trap level sensor levels + AIR_DETECTED_AT_LEVEL = 0 + FLUID_DETECTED_AT_LEVEL = 1 + + 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.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_AIR_TRAP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_air_trap_sync) + + self.lower_level = self.AIR_DETECTED_AT_LEVEL + self.upper_level = self.AIR_DETECTED_AT_LEVEL + + def get_air_trap_levels(self): + """ + Gets the current air trap levels + + @return: List containing air trap levels: [Lower, Upper] + """ + return [self.lower_level, self.upper_level] + + def get_air_trap_lower_level(self): + """ + Gets the current air trap lower level reading + + @return: 0 for air, 1 for fluid at lower level + """ + return self.lower_level + + def get_air_trap_upper_level(self): + """ + Gets the current air trap upper level reading + + @return: 0 for air, 1 for fluid at upper level + """ + return self.upper_level + + @publish(["lower_level", "upper_level"]) + def _handler_air_trap_sync(self, message): + """ + Handles published air trap data messages. Air trap data are captured + for reference. + + @param message: published air trap data message + @return: None + """ + + lower = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + upper = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.lower_level = lower[0] + self.upper_level = upper[0] + + def cmd_air_trap_level_sensor_override(self, sensor: int, detected: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air trap level sensor override command + Constraints: + Must be logged into HD. + Given sensor must be one of the sensors listed below. + + @param sensor: unsigned int - sensor ID + @param detected: unsigned int - detected (0=air, 1=fluid) to override sensor with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Air trap sensor IDs: \n + 0 = Lower level \n + 1 = Upper level \n + """ + + rst = integer_to_bytearray(reset) + det = float_to_bytearray(detected) + idx = integer_to_bytearray(sensor) + payload = rst + det + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_AIR_TRAP_LEVEL_SENSOR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override air trap level sensor detection value for sensor " + str(sensor)) + + # 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_air_trap_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the air trap data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_AIR_TRAP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD air trap data 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("Air trap 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/alarms.py =================================================================== diff -u --- shared/scripts/dialin/hd/alarms.py (revision 0) +++ shared/scripts/dialin/hd/alarms.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,819 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 alarms.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDAlarms(AbstractSubSystem): + """ + HD interface containing alarm related commands. + """ + + # Alarm lamp patterns + HD_ALARM_LAMP_PATTERN_OFF = 0 + HD_ALARM_LAMP_PATTERN_OK = 1 + HD_ALARM_LAMP_PATTERN_FAULT = 2 + HD_ALARM_LAMP_PATTERN_HIGH = 3 + HD_ALARM_LAMP_PATTERN_MEDIUM = 4 + HD_ALARM_LAMP_PATTERN_LOW = 5 + HD_ALARM_LAMP_PATTERN_MANUAL = 6 + + # Alarm states + HD_ALARM_STATE_NONE = 0 + HD_ALARM_STATE_LOW = 1 + HD_ALARM_STATE_MEDIUM = 2 + HD_ALARM_STATE_HIGH = 3 + + # Alarm status message field positions + START_POS_ALARM_STATE = DenaliMessage.PAYLOAD_START_INDEX + END_POS_ALARM_STATE = START_POS_ALARM_STATE + 4 + START_POS_ALARM_TOP = END_POS_ALARM_STATE + END_POS_ALARM_TOP = START_POS_ALARM_TOP + 4 + START_POS_ALARM_ESCALATES_IN = END_POS_ALARM_TOP + END_POS_ALARM_ESCALATES_IN = START_POS_ALARM_ESCALATES_IN + 4 + START_POS_ALARM_SILENCE_EXPIRES_IN = END_POS_ALARM_ESCALATES_IN + END_POS_ALARM_SILENCE_EXPIRES_IN = START_POS_ALARM_SILENCE_EXPIRES_IN + 4 + START_POS_ALARMS_FLAGS = END_POS_ALARM_SILENCE_EXPIRES_IN + END_POS_ALARMS_FLAGS = START_POS_ALARMS_FLAGS + 2 + + START_POS_ALARM_ID = DenaliMessage.PAYLOAD_START_INDEX + END_POS_ALARM_ID = START_POS_ALARM_ID + 2 + + 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.hd_alarm_broadcast_ch_id + msg_id = MsgIds.MSG_ID_ALARM_STATUS.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarms_status_sync) + + channel_id = DenaliChannels.hd_alarm_broadcast_ch_id + msg_id = MsgIds.MSG_ID_ALARM_TRIGGERED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_activate) + msg_id = MsgIds.MSG_ID_ALARM_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_clear) + msg_id = MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_condition_clear) + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_ALARM_INFORMATION.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_alarm_information_sync) + + # composite alarm status based on latest HD alarm status broadcast message + self.alarms_state = 0 + self.alarm_top = 0 + self.alarms_silence_expires_in = 0 + self.alarms_escalates_in = 0 + self.alarms_flags = 0 + + # alarm states based on received HD alarm activation and alarm clear messages + self.alarm_states = [False] * 500 + # alarm condition states based on received HD alarm activation and clear condition messages + self.alarm_conditions = [False] * 500 + + # alarm information + self.alarm_volume = 0 + self.alarm_audio_curr_hg = 0.0 + self.alarm_audio_curr_lg = 0.0 + self.alarm_backup_audio_curr = 0.0 + self.safety_shutdown_active = False + + def get_current_alarms_state(self): + """ + Gets the current alarms state. + None, Low, Medium, or High + + @return: (str) the current alarms state in text form. + """ + result = "" + if self.alarms_state == self.HD_ALARM_STATE_NONE: + result = "None" + elif self.alarms_state == self.HD_ALARM_STATE_LOW: + result = "Low" + elif self.alarms_state == self.HD_ALARM_STATE_MEDIUM: + result = "Medium" + elif self.alarms_state == self.HD_ALARM_STATE_HIGH: + result = "High" + + return result + + def get_alarm_states(self): + """ + Gets all states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_states + + def get_alarm_conditions(self): + """ + Gets all alarm condition states for all alarms + + @return: List of booleans of size 500 + """ + return self.alarm_conditions + + def get_alarm_state(self, alarm_id): + """ + Gets alarm state for given alarm + + @return: Alarm state + """ + return self.alarm_states[alarm_id] + + def get_alarms_top(self): + """ + Gets the top alarm + + @return: (int) the top alarm + """ + return self.alarm_top + + def get_alarms_silence_expires_in(self): + """ + Gets the remaining time the alarms will be silenced (s) + + @return: (int) how long until the alarm silence expires + """ + return self.alarms_silence_expires_in + + def get_alarms_escalates_in(self): + """ + Gets the alarms escalates in time (s) + + @return: (int) how long until the alarm escalates + """ + return self.alarms_escalates_in + + def get_alarms_flags(self): # TODO - update flags to latest + """ + Gets the alarms flags + + Extract each flag from the flags int using bit-masking. E.g. + + System Fault = result & 1 + Stop = result & 2 + No Clear = result & 4 + No Resume = result & 8 + No Rinseback = result & 16 + No End Treatment = result & 32 + No New Treatment = result & 64 + User Must ACK = result & 128 + Alarms to Escalate = result & 256 + Alarms Silenced = result & 512 + Alarm Lamp On = result & 1024 + TBD = result & 2048 + TBD = result & 4096 + TBD = result & 8192 + No Minimize = result & 16384 + Condition Detected = result & 32768 + + @return: (int) The alarms flags value + """ + return self.alarms_flags + + def get_alarm_volume(self): + """ + Gets the alarm audio volume level. + + @return: (int) current alarm audio volume (1..5) + """ + return self.alarm_volume + + def get_alarm_audio_current_hg(self): + """ + Gets the alarm audio current - high gain. + + @return: (float) alarm audio current - high gain (in mA) + """ + return self.alarm_audio_curr_hg + + def get_alarm_audio_current_lg(self): + """ + Gets the alarm audio current - low gain. + + @return: (float) alarm audio current - low gain (in mA) + """ + return self.alarm_audio_curr_lg + + def get_alarm_backup_audio_current(self): + """ + Gets the alarm backup audio current. + + @return: (float) alarm backup audio current (in mA) + """ + return self.alarm_backup_audio_curr + + def get_safety_shutdown_activated(self): + """ + Gets the state of the HD safety shutdown signal. + + @return: (bool) safety shutdown line is activated (T/F) + """ + return self.safety_shutdown_active + + def get_alarm_flag_system_fault(self) -> bool: + """ + Gets the alarm flag system fault. + + @return: (bool) Alarm flag system fault (T/F) + """ + return (self.alarms_flags & 1) > 0 + + def get_alarm_flag_stop(self) -> bool: + """ + Gets the alarm flag no clear. + + @return: (bool) Alarm flag no clear (T/F) + """ + return (self.alarms_flags & 2) > 0 + + def get_alarm_flag_no_clear(self) -> bool: + """ + Gets the alarm flag no clear. + + @return: (bool) Alarm flag no clear (T/F) + """ + return (self.alarms_flags & 4) > 0 + + def get_alarm_flag_no_resume(self) -> bool: + """ + Gets the alarm flag no resume. + + @return: (bool) Alarm flag no resume (T/F) + """ + return (self.alarms_flags & 8) > 0 + + def get_alarm_flag_no_rinseback(self) -> bool: + """ + Gets the alarm flag no rinseback. + + @return: (bool) Alarm flag no rinseback (T/F) + """ + return (self.alarms_flags & 16) > 0 + + def get_alarm_flag_no_end_treatment(self) -> bool: + """ + Gets the alarm flag no end treatment. + + @return: (bool) Alarm flag no end treatment (T/F) + """ + return (self.alarms_flags & 32) > 0 + + def get_alarm_flag_no_new_treatment(self) -> bool: + """ + Gets the alarm flag no new treatment. + + @return: (bool) Alarm flag no new treatment (T/F) + """ + return (self.alarms_flags & 64) > 0 + + def get_alarm_flag_lamp_on(self) -> bool: + """ + Gets the alarm flag lamp on. + + @return: (bool) Alarm lamp on (T/F) + """ + return (self.alarms_flags & 1024) > 0 + + def get_alarm_flag_no_minimize(self) -> bool: + """ + Gets the alarm flag no minimize. + + @return: (bool) Alarm cannot be minimized (T/F) + """ + return (self.alarms_flags & 16384) > 0 + + @publish(["alarms_state", "alarm_top", "alarms_silence_expires_in", "alarms_escalates_in", "alarms_flags"]) + def _handler_alarms_status_sync(self, message): + """ + Handles published alarms status messages. alarms status data are captured + for reference. + + @param message: published blood flow data message + @return: none + """ + + self.alarms_state = int.from_bytes(bytearray( + message['message'][self.START_POS_ALARM_STATE:self.END_POS_ALARM_STATE]), + byteorder=DenaliMessage.BYTE_ORDER) + self.alarm_top = int.from_bytes(bytearray( + message['message'][self.START_POS_ALARM_TOP:self.END_POS_ALARM_TOP]), + byteorder=DenaliMessage.BYTE_ORDER) + self.alarms_escalates_in = int.from_bytes(bytearray( + message['message'][self.START_POS_ALARM_ESCALATES_IN:self.END_POS_ALARM_ESCALATES_IN]), + byteorder=DenaliMessage.BYTE_ORDER) + self.alarms_silence_expires_in = int.from_bytes(bytearray( + message['message'][self.START_POS_ALARM_SILENCE_EXPIRES_IN:self.END_POS_ALARM_SILENCE_EXPIRES_IN]), + byteorder=DenaliMessage.BYTE_ORDER) + self.alarms_flags = int.from_bytes(bytearray( + message['message'][self.START_POS_ALARMS_FLAGS:self.END_POS_ALARMS_FLAGS]), + byteorder=DenaliMessage.BYTE_ORDER) + + # TODO this clears the alarm state even if the state is not cleared yet. + # TODO investigate this code + # if no active alarms from HD, set all alarms (on Dialin side) to False in case we got out of sync + #if self.alarm_top == 0: + # for x in range(500): + # self.alarm_states[x] = False + + @publish(["alarm_states", "alarm_conditions"]) + def _handler_alarm_activate(self, message): + """ + Handles published HD alarm activation messages. + + @param message: published HD alarm activation message + @return: none + """ + self.logger.debug("Alarm activated!") + + alarm_id = struct.unpack(' int: + """ + Constructs and sends the clear all active alarms command. + This will clear even non-recoverable alarms. + Constraints: + Must be logged into HD. + + @return: 1 if successful, zero otherwise + """ + + key = integer_to_bytearray(-758926171) # 0xD2C3B4A5 + payload = key + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_SUPER_CLEAR_ALARMS_CMD.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("All alarms cleared.") + # response payload is OK or not OK + return 1 == received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + return False + + def cmd_alarm_state_override(self, alarm: int, state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the alarm state override command + Constraints: + Must be logged into HD. + Given alarm must be valid. + If inactivating alarm, given alarm must be recoverable (clearable). + + @param alarm: integer - ID of alarm to override + @param state: integer - 1 for alarm active, 0 for alarm inactive + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + alm = integer_to_bytearray(alarm) + payload = rst + sta + alm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATE_OVERRIDE.value, + payload=payload) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("active" if state != 0 else "inactive") + self.logger.debug("Alarm " + str(alarm) + " " + 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.debug("Timeout!!!!") + return False + + def cmd_alarm_time_override(self, alarm: int, time_ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the alarm time override command + Constraints: + Must be logged into HD. + Given alarm must be valid. + + @param alarm: integer - ID of alarm to override + @param time_ms: integer - time (in ms) since alarm was activated + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + ms = integer_to_bytearray(time_ms) + alm = integer_to_bytearray(alarm) + payload = rst + ms + alm + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_ALARM_TIME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm time since activated") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(time_ms) + self.logger.debug("Alarm time since activated overridden to " + str_res + " ms: " + + 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_alarm_lamp_pattern_override(self, pattern: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the alarm lamp pattern override command. + Constraints: + Must be logged into HD. + Given pattern must be one of the patterns listed below. + + @param pattern: integer - ID of alarm lamp pattern to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Patterns: \n + 0 = off \n + 1 = ok \n + 2 = fault \n + 3 = high \n + 4 = medium \n + 5 = low \n + 6 = manual + """ + rst = integer_to_bytearray(reset) + pat = integer_to_bytearray(pattern) + payload = rst + pat + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_ALARM_LAMP_PATTERN_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override Alarm Lamp Pattern") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug(received_message) + if reset == RESET: + str_pat = "reset back to normal" + elif pattern == self.HD_ALARM_LAMP_PATTERN_OFF: + str_pat = "off" + elif pattern == self.HD_ALARM_LAMP_PATTERN_OK: + str_pat = "ok" + elif pattern == self.HD_ALARM_LAMP_PATTERN_FAULT: + str_pat = "fault" + elif pattern == self.HD_ALARM_LAMP_PATTERN_HIGH: + str_pat = "high" + elif pattern == self.HD_ALARM_LAMP_PATTERN_MEDIUM: + str_pat = "medium" + elif pattern == self.HD_ALARM_LAMP_PATTERN_LOW: + str_pat = "low" + else: + str_pat = "manual" + self.logger.debug("Alarm lamp pattern overridden to " + str_pat + ":" + + 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_alarm_info_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET): + """ + Constructs and sends the alarm information broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_INFO_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm information 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("Alarm information 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.debug("Timeout!!!!") + return False + + def cmd_alarm_status_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the alarm status broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_STATUS_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm status 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("Alarm status 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.debug("Timeout!!!!") + return False + + def cmd_alarm_audio_volume_override(self, volume: int = 5, reset: int = NO_RESET): + """ + Constructs and sends the alarm audio volume override command + Constraints: + Must be logged into HD. + Given volume must be an integer between 1 and 5. + + @param volume: integer - alarm audio volume level (1..5) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = integer_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_VOLUME_LEVEL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm audio volume level") + + # 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(volume) + ": " + self.logger.debug("Alarm audio volume level 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.debug("Timeout!!!!") + return False + + def cmd_alarm_audio_current_hg_override(self, current: float = 0.0, reset: int = NO_RESET): + """ + Constructs and sends the alarm audio current (high gain) override command + Constraints: + Must be logged into HD. + + @param current: float - current (in mA) for high gain alarm audio + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(current) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_CURRENT_HG_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm audio high gain current") + + # 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(current) + " mA: " + self.logger.debug("Alarm audio high gain current 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.debug("Timeout!!!!") + return False + + def cmd_alarm_audio_current_lg_override(self, current: float = 0.0, reset: int = NO_RESET): + """ + Constructs and sends the alarm audio current (low gain) override command + Constraints: + Must be logged into HD. + + @param current: float - current (in mA) for low gain alarm audio + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(current) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_CURRENT_LG_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm audio high gain current") + + # 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(current) + " mA: " + self.logger.debug("Alarm audio low gain current 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.debug("Timeout!!!!") + return False + + def cmd_alarm_backup_audio_current_override(self, current: float = 0.0, reset: int = NO_RESET): + """ + Constructs and sends the backup alarm audio current override command + Constraints: + Must be logged into HD. + + @param current: float - current (in mA) for backup alarm audio + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(current) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_BACKUP_AUDIO_CURRENT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override alarm backup audio current") + + # 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(current) + " mA: " + self.logger.debug("Alarm backup audio current 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/blood_flow.py =================================================================== diff -u --- shared/scripts/dialin/hd/blood_flow.py (revision 0) +++ shared/scripts/dialin/hd/blood_flow.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,515 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 blood_flow.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import PUMP_CONTROL_MODE_CLOSED_LOOP, PUMP_CONTROL_MODE_OPEN_LOOP +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDBloodFlow(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for blood-flow related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HD_BloodFlow constructor + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_BLOOD_FLOW_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_blood_flow_sync) + + self.target_blood_flow_rate = 0 + self.measured_blood_flow_rate = 0.0 + self.measured_blood_pump_rotor_speed = 0.0 + self.measured_blood_pump_speed = 0.0 + self.measured_blood_pump_mc_speed = 0.0 + self.measured_blood_pump_mc_current = 0.0 + self.pwm_duty_cycle_pct = 0.0 + self.rotor_count = 0 + + def get_target_blood_flow_rate(self): + """ + Gets the target blood flow rate + + @return: The target blood flow rate + """ + return self.target_blood_flow_rate + + def get_measured_blood_flow_rate(self): + """ + Gets the measured blood flow rate + + @return: the measured blood flow rate + """ + return self.measured_blood_flow_rate + + def get_measured_blood_pump_rotor_speed(self): + """ + Gets the measured blood pump rotor speed + + @return: The measured blood pump rotor speed + """ + return self.measured_blood_pump_rotor_speed + + def get_measured_blood_pump_speed(self): + """ + Gets the measured blood pump speed + + @return: the measured blood pump speed + """ + return self.measured_blood_pump_speed + + def get_measured_blood_pump_motor_controller_speed(self): + """ + Gets the measured blood pump motor controller speed + + @return: The measured blood pump motor controller speed + """ + return self.measured_blood_pump_mc_speed + + def get_measured_blood_pump_motor_controller_current(self): + """ + Gets the measured blood pump motor controller current + + @return: the measured blood pump motor controller current + """ + return self.measured_blood_pump_mc_current + + def get_pwm_duty_cycle_pct(self): + """ + Gets the pwm duty cycle pct + + @return: the pwm duty cycle pct (0..100) + """ + return self.pwm_duty_cycle_pct + + def get_rotor_count(self): + """ + Gets the blood pump rotor count (since cartridge installed) + + @return: the blood pump rotor count + """ + return self.rotor_count + + @publish(["target_blood_flow_rate", "measured_blood_flow_rate", "measured_blood_pump_rotor_speed", + "measured_blood_pump_speed", "measured_blood_pump_mc_speed", "measured_blood_pump_mc_current", + "pwm_duty_cycle_pct", "flow_signal_strength"]) + def _handler_blood_flow_sync(self, message): + """ + Handles published blood flow data messages. Blood flow data are captured + for reference. + + @param message: published blood flow data message + @return: none + """ + + tgt = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + rotor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + mcspeed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + mccurr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + pwm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + rot = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + + self.target_blood_flow_rate = tgt[0] + self.measured_blood_flow_rate = flow[0] + self.measured_blood_pump_rotor_speed = rotor[0] + self.measured_blood_pump_speed = speed[0] + self.measured_blood_pump_mc_speed = mcspeed[0] + self.measured_blood_pump_mc_current = mccurr[0] + self.pwm_duty_cycle_pct = pwm[0] + self.rotor_count = rot[0] + + def cmd_blood_flow_set_point_override(self, flow: int, mode: int = PUMP_CONTROL_MODE_CLOSED_LOOP, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the blood flow set point override command + Constraints: + Must be logged into HD. + Given flow rate must be valid (<= +/-500 mL/min). + + @param flow: integer - flow set point (in mL/min) to override with (negative for reverse direction) + @param mode: integer - 0 for closed loop control mode or 1 for open loop control mode + @param reset: integer - N/A + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = integer_to_bytearray(flow) + mod = integer_to_bytearray(mode) + payload = rst + flo + mod + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_SET_PT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override blood flow set point") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + + if mode == PUMP_CONTROL_MODE_OPEN_LOOP: + str_mode = " (open loop): " + else: + str_mode = " (closed loop): " + self.logger.debug( + "Blood flow set point overridden to " + str_res + " mL/min" + str_mode + + + 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_blood_flow_measured_override(self, flow: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood flow override command + Constraints: + Must be logged into HD. + + @param flow: integer - measured flow (in mL/min) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = float_to_bytearray(flow) + payload = rst + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_MEAS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood flow") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + self.logger.debug("Blood flow (measured)) overridden to " + str_res + " mL/min: " + + 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_blood_pump_measured_motor_controller_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump motor controller speed \n + override command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MC_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump motor controller speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Blood pump MC speed (measured) overridden to " + str_res + " RPM: " + + 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_blood_pump_measured_motor_controller_current_override(self, curr: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump motor controller current override command + Constraints: + Must be logged into HD. + + @param curr: float - current (in mA) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(curr) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MC_MEAS_CURR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump motor controller current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + self.logger.debug("Blood pump MC current (measured) overridden to " + str_res + " mA: " + + 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_blood_pump_measured_motor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump motor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Blood pump speed (measured) overridden to " + str_res + " RPM: " + + 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_blood_pump_measured_rotor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump rotor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_MEAS_ROTOR_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump rotor speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Blood pump rotor speed (measured) overridden to " + str_res + " RPM: " + + 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_blood_flow_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood flow broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override blood flow broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(ms) + " ms: " + self.logger.debug("Blood flow 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.debug("Timeout!!!!") + return False + + def cmd_home_blood_pump(self) -> int: + """ + Constructs and sends a blood pump home request message to the HD. + Constraints: + Must be logged into HD. + Blood pump must be stopped (off) prior to requesting home position. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_HOME_CMD.value) + + self.logger.debug("Homing blood pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Blood pump homed : " + + 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_blood_pump_rotor_count_override(self, rot_count: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the blood pump rotor count override command + Constraints: + Must be logged into HD. + Given count must be zero or positive integer. + + @param rot_count: integer - rotor count to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cnt = integer_to_bytearray(rot_count) + payload = rst + cnt + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_PUMP_ROTOR_COUNT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override blood pump rotor count") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal: " + else: + str_res = str(rot_count) + ": " + self.logger.debug("Blood pump rotor count 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.debug("Timeout!!!!") + return False + Index: shared/scripts/dialin/hd/blood_leak.py =================================================================== diff -u --- shared/scripts/dialin/hd/blood_leak.py (revision 0) +++ shared/scripts/dialin/hd/blood_leak.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,273 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 blood_leak.py +# +# @author (last) Quang Nguyen +# @date (last) 11-Aug-2021 +# @author (original) Peman Montazemi +# @date (original) 15-Apr-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDBloodLeak(AbstractSubSystem): + """ + HDBloodLeak + + Hemodialysis Delivery (HD) Dialin API sub-class for blood leak related commands. + """ + + # Blood leak detector status + BLOOD_LEAK_DETECTED = 0 # Blood detected + NO_BLOOD_LEAK_DETECTED = 1 # No blood detected + + # Blood leak detector state machine states + BLOOD_LEAK_INIT_STATE = 0 # Initial state + BLOOD_LEAK_ZERO_STATE = 1 # Zero state + BLOOD_LEAK_SELF_TEST_STATE = 2 # Self-test state + BLOOD_LEAK_NORMAL_STATE = 3 # Normal state + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: Denali Can Messenger object + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_BLOOD_LEAK_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_blood_leak_sync) + + self.blood_leak_status = self.NO_BLOOD_LEAK_DETECTED + self.blood_leak_state = self.BLOOD_LEAK_INIT_STATE + self.blood_leak_zeroed_status_counter = 0 + self.blood_leak_counter = 0 + self.blood_leak_zeroed_status = 0 + self.blood_leak_detect_set_point = 0 + self.blood_leak_detect_level = 0 + self.blood_leak_st_count = 0 + self.blood_leak_led_intensity = 0 + self.blood_leak_register_counter = 0 + + def get_blood_leak_status(self): + """ + Gets the current blood leak status + + @return: List containing blood leak status: [detected, undetected] + """ + return self.blood_leak_status + + def get_blood_leak_state(self): + """ + Gets the current blood leak state + + @return: integer - blood leak state (0: init, 1: zeroing, 2: self-test, 3: normal) + """ + return self.blood_leak_state + + def get_blood_leak_zero_status_counter(self): + """ + Gets the current blood leak zero status counter + + @return: integer - blood leak zero status counter + """ + return self.blood_leak_zeroed_status_counter + + def get_blood_leak_counter(self): + """ + Gets the current blood leak counter + + @return: integer - blood leak counter + """ + return self.blood_leak_counter + + def get_blood_leak_zeroed_status(self): + """ + Gets the current blood leak zeroed status + + @return: integer - blood leak zeroed status + """ + return self.blood_leak_zeroed_status + + def get_blood_leak_detect_set_point(self): + """ + Gets the current blood leak detect set point + + @return: integer - blood leak detect set point + """ + return self.blood_leak_detect_set_point + + def get_blood_leak_detect_level(self): + """ + Gets the current blood leak detect level + + @return: integer - blood leak detect level + """ + return self.blood_leak_detect_level + + def get_blood_leak_st_count(self): + """ + Gets the current blood leak st count + + @return: integer - blood leak st count + """ + return self.blood_leak_st_count + + def get_blood_leak_led_intensity(self): + """ + Gets the current blood leak LED intensity + + @return: integer - blood leak LED intensity + """ + return self.blood_leak_led_intensity + + def get_blood_leak_register_counter(self): + """ + Gets the current blood leak register counter + + @return: integer - blood leak register counter + """ + return self.blood_leak_register_counter + + @publish("blood_leak_status") + def _handler_blood_leak_sync(self, message): + """ + Handles published blood leak status messages. Blood leak status is captured + for reference. + + @param message: published blood leak status message + @return: None + """ + + self.blood_leak_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.blood_leak_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.blood_leak_zeroed_status_counter = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.blood_leak_counter = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.blood_leak_zeroed_status = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.blood_leak_detect_set_point = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.blood_leak_detect_level = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + self.blood_leak_st_count = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.blood_leak_led_intensity = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + self.blood_leak_register_counter = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10]))[0] + + def cmd_blood_leak_detector_override(self, detected, reset=NO_RESET): + """ + Constructs and sends the blood leak detector state override command + Constraints: + Must be logged into HD. + + @param detected: unsigned int - detected (0=detected, 1=undetected) to override detector with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + det = float_to_bytearray(detected) + payload = rst + det + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_LEAK_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override blood leak detector state value") + + # 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_blood_leak_zero_request(self): + """ + Request blood leak zeroing + 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_BLOOD_LEAK_ZERO_REQUEST.value) + + self.logger.debug("Request blood leak zeroing") + + # 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_blood_leak_data_broadcast_interval_override(self, ms, reset=NO_RESET): + """ + Constructs and sends the blood leak data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + """ + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_LEAK_DATA_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD blood leak data 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("Blood leak 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/buttons.py =================================================================== diff -u --- shared/scripts/dialin/hd/buttons.py (revision 0) +++ shared/scripts/dialin/hd/buttons.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,164 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 buttons.py +# +# @author (last) Quang Nguyen +# @date (last) 11-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class HDButtons(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for button related commands. + """ + + def __init__(self, can_interface: DenaliCanMessenger, logger: Logger): + """ + HD_Buttons constructor + + @param can_interface: the Denali CAN interface object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + self.poweroff_timeout_expired = False + + if self.can_interface is not None: + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, + MsgIds.MSG_ID_OFF_BUTTON_PRESS.value, + self._handler_poweroff_timeout_occurred) + + def get_power_timeout_expired(self): + """ + Gets the poweroff timeout expired status + + @return: True if expired, False otherwise + """ + + return self.poweroff_timeout_expired + + def reset_poweroff_timeout_expired(self): + """ + Resets the dialin poweroff timeout flag to False + + @return: None + """ + + self.poweroff_timeout_expired = False + + @publish(["poweroff_timeout_expired"]) + def _handler_poweroff_timeout_occurred(self, message): + """ + Poweroff timeout occurred handler + + @param message: The poweroff timeout occurred message string + @return: None + """ + if len(message["message"]) < 16: + self.logger.debug("Poweroff message id detected, but was the wrong length.") + return + + mode = struct.unpack('h', bytearray( + message["message"][MsgFieldPositions.START_POS_FIELD_1: MsgFieldPositions.START_POS_FIELD_1 + 2])) + + if len(mode) > 0: + self.poweroff_timeout_expired = bool(mode[0]) + + def cmd_off_button_override(self, state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the Off button override command + See HD/App/Controllers/Buttons.c + Constraints: + Must be logged into HD. + Given state must be a 0 or 1. + + @param state: integer - 1 to override off button to "pressed", 0 to "released" + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + payload = rst + sta + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override off button") + + # 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 = ("pressed" if state != 0 else "released") + + self.logger.debug("Off button 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.debug("Timeout!!!!") + return False + + def cmd_stop_button_override(self, state: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the Stop button override command + See HD/App/Controllers/Buttons.c + Constraints: + Must be logged into HD. + Given state must be a 0 or 1. + + @param state: integer - 1 to override stop button to "pressed", 0 to "released" + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + payload = rst + sta + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_STOP_BUTTON_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override stop button") + + # 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 = ("pressed" if state != 0 else "released") + self.logger.debug("Stop button 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/calibration_record.py =================================================================== diff -u --- shared/scripts/dialin/hd/calibration_record.py (revision 0) +++ shared/scripts/dialin/hd/calibration_record.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,436 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 calibration_record.py +# +# @author (last) Dara Navaei +# @date (last) 12-Oct-2021 +# @author (original) Dara Navaei +# @date (original) 14-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from logging import Logger + +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 + + +class HDCalibrationNVRecord(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for calibration_record commands. + """ + + _RECORD_START_INDEX = 6 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_HIGH_ORDER_GAIN_VALUE = 0 + _DEFAULT_GAIN_VALUE = 1 + _DEFAULT_OFFSET_VALUE = 0 + _DEFAULT_RATIO_VALUE = 1 + _DEFAULT_VOLUME_VALUE = 0 + _DEFAULT_CONCENTRATE_VALUE = 1 + _DEFAULT_CALIBRATION_VALUE = 1 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + 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._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) + # HD Calibration main record + self.hd_calibration_record = self._prepare_hd_calibration_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_CALIBRATION_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_calibration_sync) + + def cmd_reset_hd_calibration_record(self) -> bool: + """ + Handles resetting HD calibration record. + + @return: True if successful, False otherwise + """ + self.hd_calibration_record = self._prepare_hd_calibration_record() + self.hd_calibration_record = self._utilities.reset_fw_record(self.hd_calibration_record) + status = self.cmd_set_hd_calibration_record(self.hd_calibration_record) + + return status + + def cmd_request_hd_calibration_record(self) -> int: + """ + Handles getting HD calibration_record record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_cal_in_progress is not True: + self._is_getting_cal_in_progress = True + # Clear the list for the next call + self._raw_cal_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_CALIBRATION_RECORD.value) + + self.logger.debug('Getting HD calibration record') + + 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 + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_hd_calibration_sync(self, message): + """ + Handles published HD calibration_record record messages. DG calibration_record records are captured for + processing and updating the HD calibration_record record. + + @param message: published HD calibration_record 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 = self._RECORD_START_INDEX + self._RECORD_SPECS_BYTES + self.received_msg_length + + # Get the calibration_record data only + self.cal_data = message['message'][self._RECORD_START_INDEX: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_cal_record += (message['message'][self._RECORD_START_INDEX + + self._RECORD_SPECS_BYTES:end_of_data_index]) + if self.current_message == self.total_messages: + # Done with receiving the messages + self._is_getting_cal_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_calibration_record, self._raw_cal_record) + self._handler_received_complete_hd_calibration_record() + + @publish(["hd_calibration_record"]) + def _handler_received_complete_hd_calibration_record(self): + """ + Publishes the received system record + + @return: None + """ + self.logger.debug("Received a complete 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 + @return: none + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(hd_calibration_record) + + self.logger.debug("Setting HD calibration 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_CALIBRATION_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 calibration record.") + return True + + def _prepare_hd_calibration_record(self): + """ + Handles assembling the sub dictionaries of each hardware group to make the main HD calibration_record record. + + @return: (OrderedDict) the assembled hd calibration record + """ + result = OrderedDict() + + 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()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' int: + """ + Constructs and sends the dialysate flow set point override command + Constraints: + Must be logged into HD. + Given flow must be valid (<= +/-600 mL/min). + + @param flow: integer - flow set point (in mL/min) to override with + @param mode: integer - 0 for closed loop control mode or 1 for open loop control mode + @param reset: integer - N/A + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = integer_to_bytearray(flow) + mod = integer_to_bytearray(mode) + payload = rst + flo + mod + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_FLOW_SET_PT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate flow set point") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + + if mode == PUMP_CONTROL_MODE_OPEN_LOOP: + str_mode = " (open loop): " + else: + str_mode = " (closed loop): " + self.logger.debug( + "Dialysate flow set point overridden to " + str_res + " mL/min" + str_mode + + + 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_dialysate_inlet_flow_measured_override(self, flow: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate flow override command + Constraints: + Must be logged into HD. + + @param flow: integer - measured flow (in mL/min) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = float_to_bytearray(flow) + payload = rst + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_FLOW_MEAS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate flow") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(flow) + self.logger.debug("Dialysate flow (measured)) overridden to " + str_res + " mL/min: " + + 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_dialysate_inlet_pump_measured_motor_controller_speed_override(self, speed: int, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump motor controller speed \n + override command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MC_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump motor controller speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Dialysate pump MC speed (measured) overridden to " + str_res + " RPM: " + + 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_dialysate_inlet_pump_measured_motor_controller_current_override(self, curr: int, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump motor current override command + Constraints: + Must be logged into HD. + + @param curr: integer - current (in mA) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(curr) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MC_MEAS_CURR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump motor controller current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + self.logger.debug("Dialysate inlet pump MC current (measured) overridden to " + str_res + " mA: " + + 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_dialysate_inlet_pump_measured_motor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump motor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Dialysate inlet pump speed (measured) overridden to " + str_res + " RPM: " + + 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_dialysate_inlet_pump_measured_rotor_speed_override(self, speed: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet pump rotor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: integer - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_MEAS_ROTOR_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate inlet pump rotor speed") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(speed) + self.logger.debug("Dialysate inlet pump rotor speed (measured) overridden to " + str_res + " RPM: " + + 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_dialysate_inlet_flow_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate inlet flow broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_FLOW_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate inlet flow 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("Dialysate inlet flow 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.debug("Timeout!!!!") + return False + + def cmd_home_dialysate_inlet_pump(self) -> int: + """ + Constructs and sends a dialysate inlet pump home request message to the HD. + Constraints: + Must be logged into HD. + Dialysate inlet pump must be stopped (off) prior to requesting home position. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_IN_PUMP_HOME_CMD.value) + + self.logger.debug("Homing dialysate inlet pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Dialysate inlet pump homed : " + + 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: shared/scripts/dialin/hd/dialysate_outlet_flow.py =================================================================== diff -u --- shared/scripts/dialin/hd/dialysate_outlet_flow.py (revision 0) +++ shared/scripts/dialin/hd/dialysate_outlet_flow.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,588 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 dialysate_outlet_flow.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import PUMP_CONTROL_MODE_CLOSED_LOOP, PUMP_CONTROL_MODE_OPEN_LOOP +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDDialysateOutletFlow(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for dialysate outlet pump related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDDialysateFlow constructor + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_DIALYSATE_OUT_FLOW_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_dialysate_outlet_flow_sync) + + self.reference_dialysate_outlet_uf_volume = 0.0 + self.measured_dialysate_outlet_uf_volume = 0.0 + self.measured_dialysate_outlet_pump_rotor_speed = 0.0 + self.measured_dialysate_outlet_pump_speed = 0.0 + self.measured_dialysate_outlet_pump_mc_speed = 0.0 + self.measured_dialysate_outlet_pump_mc_current = 0.0 + self.pwm_duty_cycle_pct = 0.0 + + def get_reference_dialysate_outlet_uf_volume(self): + """ + Gets the reference dialysate outlet uf volume + + @return: the reference dialysate outlet uf volume + """ + return self.reference_dialysate_outlet_uf_volume + + def get_measured_dialysate_outlet_uf_volume(self): + """ + Gets the measured dialysate outlet uf volume + + @return: the measured dialysate outlet uf volume + """ + return self.measured_dialysate_outlet_uf_volume + + def get_measured_dialysate_outlet_pump_rotor_speed(self): + """ + Gets the measured dialysate outlet pump rotor speed + @return: the measured dialysate outlet pump rotor speed + """ + return self.measured_dialysate_outlet_pump_rotor_speed + + def get_measured_dialysate_outlet_pump_speed(self): + """ + Gets the measured dialysate outlet pump speed + @return: the measured dialysate outlet pump speed + """ + return self.measured_dialysate_outlet_pump_speed + + def get_measured_dialysate_outlet_pump_motor_controller_speed(self): + """ + Gets the measured dialysate outlet pump motor controller speed + + @return: the measured dialysate outlet pump motor controller speed + """ + return self.measured_dialysate_outlet_pump_mc_speed + + def get_measured_dialysate_outlet_pump_motor_controller_current(self): + """ + Gets the measured dialysate outlet pump motor controller current + + @return: Gets the measured dialysate outlet pump motor controller current + """ + return self.measured_dialysate_outlet_pump_mc_current + + def get_pwm_duty_cycle_pct(self): + """ + Gets the pwm duty cycle percent + + @return: the pwm duty cycle percent + """ + return self.pwm_duty_cycle_pct + + @publish([ + "reference_dialysate_outlet_uf_volume", + "measured_dialysate_outlet_uf_volume", + "measured_dialysate_outlet_pump_rotor_speed", + "measured_dialysate_outlet_pump_speed", + "measured_dialysate_outlet_pump_mc_speed", + "measured_dialysate_outlet_pump_mc_current", + "pwm_duty_cycle_pct" + ]) + def _handler_dialysate_outlet_flow_sync(self, message): + """ + Handles published dialysate outlet flow data messages. Dialysate flow data are captured + for reference. + + @param message: published dialysate outlet flow data message + @return: none + """ + + refvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + measvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + rotor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + speed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + mcspeed = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + mccurr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + pwm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + + self.reference_dialysate_outlet_uf_volume = refvol[0] + self.measured_dialysate_outlet_uf_volume = measvol[0] + self.measured_dialysate_outlet_pump_rotor_speed = rotor[0] + self.measured_dialysate_outlet_pump_speed = speed[0] + self.measured_dialysate_outlet_pump_mc_speed = mcspeed[0] + self.measured_dialysate_outlet_pump_mc_current = mccurr[0] + self.pwm_duty_cycle_pct = pwm[0] + + def cmd_dialysate_outlet_flow_set_point_override(self, + flow: int, + mode: int = PUMP_CONTROL_MODE_CLOSED_LOOP, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the dialysate outlet pump set point override command + Constraints: + Must be logged into HD. + Given flow must be valid (<= +/-600 mL/min). + + @param flow: integer - flow set point (in mL/min) to override with + @param mode: integer - 0 for closed loop control mode or 1 for open loop control mode + @param reset: integer - N/A + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + flo = integer_to_bytearray(flow) + mod = integer_to_bytearray(mode) + payload = rst + flo + mod + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_FLOW_SET_PT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate outlet pump set point") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # print(received_message) + if reset == RESET: + str_res = "reset back to stopped" + else: + str_res = str(flow) + if mode == PUMP_CONTROL_MODE_OPEN_LOOP: + str_mode = " (open loop): " + else: + str_mode = " (closed loop): " + self.logger.debug("Dialysate outlet pump set point overridden to " + + str_res + + " mL/min" + + str_mode + + 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_dialysate_outlet_reference_uf_volume_override(self, refvol: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the UF reference volume override command + Constraints: + Must be logged into HD. + + @param refvol: float - reference UF volume (in mL) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(refvol) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_UF_REF_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override UF reference volume with " + str(refvol) + "mL.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(refvol) + " mL. " + self.logger.debug( + "UF reference volume " + 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.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_measured_uf_volume_override(self, measvol: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured UF volume override command + Constraints: + Must be logged into HD. + + @param measvol: float - measured UF volume (in mL) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(measvol) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_UF_MEAS_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured UF volume with " + str(measvol) + " mL.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(measvol) + " mL. " + self.logger.debug("UF measured volume " + 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.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_motor_controller_speed_override(self, speed: float, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump motor controller measured speed \n + override command. + Constraints: + Must be logged into HD. + + @param speed: float - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MC_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump motor controller speed to " + str(spd) + " RPM.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(speed) + " RPM. " + self.logger.debug("Dialysate outlet pump MC speed (measured) " + 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.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_motor_controller_current_override(self, curr: float, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump motor current override command + Constraints: + Must be logged into HD. + + @param curr: float - current (in mA) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(curr) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MC_MEAS_CURR_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump motor controller current") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(curr) + " mA. " + self.logger.debug("Dialysate outlet pump MC current (measured) " + 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.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_motor_speed_override(self, speed: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump motor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: float - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MEAS_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump speed to " + str(speed) + " RPM.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(speed) + " RPM. " + self.logger.debug("Dialysate outlet pump speed (measured) " + 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.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_rotor_speed_override(self, speed: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet pump rotor speed override \n + command. + Constraints: + Must be logged into HD. + + @param speed: float - speed (in RPM) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(speed) + payload = rst + spd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_MEAS_ROTOR_SPEED_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured dialysate outlet pump rotor speed to " + str(speed) + " RPM.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(speed) + " RPM. " + self.logger.debug("Dialysate outlet pump rotor speed (measured) " + 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.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_flow_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured dialysate outlet flow broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_FLOW_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override dialysate outlet flow broadcast interval to " + str(ms) + " ms.") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(ms) + " ms. " + self.logger.debug("Dialysate outlet flow broadcast interval " + 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.debug("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_load_cell_weight_override(self, sensor: int, weight: float, + reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured load cell weight override command. + Constraints: + Must be logged into HD. + Given sensor must be one of the sensors listed below. + + @param sensor: integer - ID of load cell to override + @param weight: float - weight (in g) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + Load Cells: \n + 0 = reservoir 1 primary \n + 1 = reservoir 1 backup \n + 2 = reservoir 2 primary \n + 3 = reservoir 2 backup \n + """ + + rst = integer_to_bytearray(reset) + spd = float_to_bytearray(weight) + sen = integer_to_bytearray(sensor) + payload = rst + spd + sen + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_LOAD_CELL_WEIGHT_OVERRIDE.value, + payload=payload) + + self.logger.debug( + "override measured load cell weight to " + str(weight) + " grams for load cell # " + str(sensor)) + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal. " + else: + str_res = "overridden to " + str(weight) + " grams. " + self.logger.debug("Load cell # " + str(sensor) + " " + 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.debug("Timeout!!!!") + return False + + def cmd_set_load_cell_weights(self, r1p: float, r1b: float, r2p: float, r2b: float) -> int: + """ + Constructs and sends the set load cell weights command. + + @param r1p: float - weight (in g) for reservoir 1 primary load cell + @param r1b: float - weight (in g) for reservoir 1 backup load cell + @param r2p: float - weight (in g) for reservoir 2 primary load cell + @param r2b: float - weight (in g) for reservoir 2 backup load cell + @return: 0 - no response will come from HD for this message + """ + + r1pb = float_to_bytearray(r1p) + r1bb = float_to_bytearray(r1b) + r2pb = float_to_bytearray(r2p) + r2bb = float_to_bytearray(r2b) + payload = r1pb + r1bb + r2pb + r2bb + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_READINGS.value, + payload=payload) + + self.logger.debug("measured load cell weights set.") + + # Send message + self.can_interface.send(message, 0) + + return 0 + + def cmd_home_dialysate_outlet_pump(self) -> int: + """ + Constructs and sends a dialysate outlet pump home request message to the HD. + Constraints: + Must be logged into HD. + Dialysate outlet pump must be stopped (off) prior to requesting home position. + + @return: 1 if successful, zero otherwise + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_DIAL_OUT_PUMP_HOME_CMD.value) + + self.logger.debug("Homing dialysate outlet pump") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Dialysate outlet pump homed : " + + 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: shared/scripts/dialin/hd/fans.py =================================================================== diff -u --- shared/scripts/dialin/hd/fans.py (revision 0) +++ shared/scripts/dialin/hd/fans.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,216 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 fans.py +# +# @author (last) Dara Navaei +# @date (last) 12-Oct-2021 +# @author (original) Dara Navaei +# @date (original) 04-Aug-2021 +# +############################################################################ + + +import struct +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..utils.checks import check_broadcast_interval_override_ms +from .constants import NO_RESET, RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import (DenaliMessage, DenaliChannels) +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from logging import Logger +from enum import unique + + +@unique +class HDFansNames(DialinEnum): + + FAN_INLET_1 = 0 + + +class HDFans(AbstractSubSystem): + """ + @brief Hemodialysis Device (HD) Dialin API sub-class for fans 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.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_FANS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_fans_sync) + + # Publish variables + self.duty_cycle = 0.0 + self.target_rpm = 0.0 + self.inlet_1_rpm = 0.0 + self.rpm_alarm_time_offset = 0 + + def get_fans_target_duty_cycle(self): + """ + Gets the fans target duty cycle + + @return: Fans target duty cycle + """ + return self.duty_cycle + + def get_fan_inlet_1_rpm(self): + """ + Gets the inlet 1 fan RPM + + @return: Fan inlet 1 RPM + """ + return self.inlet_1_rpm + + def get_fan_target_rpm(self): + """ + Gets the fans target RPM + + @return: target RPM + """ + return self.target_rpm + + @publish(['duty_cycle', 'target_rpm', 'inlet_1_rpm']) + def _handler_fans_sync(self, message): + """ + Handles published thermistors message. + + @param message: published thermistors message + @return: none + """ + self.duty_cycle = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.target_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.inlet_1_rpm = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.rpm_alarm_time_offset = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + + def cmd_fans_rpm_override(self, fan: int, rpm: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD fan RPM override command + Constraints: + Must be logged into HD. + + @param fan: (int) fan ID that is status is overridden + @param rpm: (int) RPM that the fan will 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) + f = integer_to_bytearray(fan) + r = float_to_bytearray(rpm) + payload = reset_value + r + f + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FANS_RPM_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fan RPM") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Fan " + str(HDFansNames(fan).name) + " 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 + + def cmd_fans_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fans data publish interval. + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FANS_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fans data publish interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "Reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "Fans data broadcast interval overridden to " + str_res + " ms: " + + 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_rpm_alarm_start_time_offset_override(self, hours: int, minutes: 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 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 + + 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, + payload=payload) + + self.logger.debug("Override fan RPM alarm start time") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Set RPM alarm start time 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: shared/scripts/dialin/hd/fluid_leak.py =================================================================== diff -u --- shared/scripts/dialin/hd/fluid_leak.py (revision 0) +++ shared/scripts/dialin/hd/fluid_leak.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,151 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 fluid_leak.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peman Montazemi +# @date (original) 10-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray + + +class HDFluidLeak(AbstractSubSystem): + """ + HDFluidLeak + + Hemodialysis Delivery (HD) Dialin API sub-class for fluid leak related commands. + """ + + # Fluid leak detector state + FLUID_LEAK_DETECTED_STATE = 0 # Wet + NO_FLUID_LEAK_DETECTED_STATE = 1 # Dry + + 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.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_FLUID_LEAK_STATE.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_fluid_leak_sync) + + self.fluid_leak_state = self.NO_FLUID_LEAK_DETECTED_STATE + + def get_fluid_leak_state(self): + """ + Gets the current fluid leak state + + @return: List containing fluid leak states: [detected, undetected] + """ + return self.fluid_leak_state + + @publish(["fluid_leak_state"]) + def _handler_fluid_leak_sync(self, message): + """ + Handles published fluid leak state messages. Fluid leak state is captured + for reference. + + @param message: published fluid leak state message + @return: None + """ + + state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + + self.fluid_leak_state = state[0] + + def cmd_fluid_leak_detector_override(self, detected: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak detector state override command + Constraints: + Must be logged into HD. + Given detector must be one of the detectors listed below. + + @param detected: unsigned int - detected (0=wet, 1=dry) to override detector with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + det = integer_to_bytearray(detected) + payload = rst + det + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FLUID_LEAK_STATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override fluid leak detector state value") + + # 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_fluid_leak_state_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the fluid leak state broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD priority task interval (10 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_FLUID_LEAK_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD fluid leak state 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("Fluid leak state 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/hd_events.py =================================================================== diff -u --- shared/scripts/dialin/hd/hd_events.py (revision 0) +++ shared/scripts/dialin/hd/hd_events.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,224 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 hd_events.py +# +# @author (last) Dara Navaei +# @date (last) 12-Nov-2021 +# @author (original) Dara Navaei +# @date (original) 12-Nov-2021 +# +############################################################################ + +import struct +from logging import Logger +from ..common import * +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from datetime import datetime + + +class HDEvents(AbstractSubSystem): + """ + Hemodialysis (HD) Dialin API sub-class for events 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.hd_to_ui_ch_id + msg_id = MsgIds.MSG_ID_HD_EVENT.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_events_sync) + + # Define the dictionaries + self._hd_event_dictionary = dict() + self._hd_event_data_type = dict() + + # Dictionary of the the mode as key and the sub mode states enum class as the value + self._hd_op_mode_2_sub_mode = {HDOpModes.MODE_FAUL.name: HDFaultStates, + HDOpModes.MODE_INIT.name: HDInitStates, + HDOpModes.MODE_STAN.name: StandbyStates, + HDOpModes.MODE_TPAR.name: TreatmentParametersStates, + HDOpModes.MODE_PRET.name: PreTreatmentSubModes, + HDOpModes.MODE_TREA.name: TreatmentStates, + HDOpModes.MODE_POST.name: PostTreatmentStates} + + # Loop through the list of the HD events enums and initial the event dictionary. Each event is a key in the + # dictionary and the value is a list. + for event in HDEventList: + self._hd_event_dictionary[HDEventList(event).name] = [] + + # Loop through the list of the event data type enum and update the dictionary + for data_type in HDEventDataType: + event_data_type = HDEventDataType(data_type).name + struct_unpack_type = None + + # If U32 is in the data type enum (i.e. EVENT_DATA_TYPE_U32), then the key is the enum and the value is + # the corresponding format in the python struct + if 'U32' in event_data_type or 'BOOL' in event_data_type: + struct_unpack_type = 'I' + elif 'S32' in event_data_type: + struct_unpack_type = 'i' + elif 'F32' in event_data_type: + struct_unpack_type = 'f' + + self._hd_event_data_type[event_data_type] = struct_unpack_type + + def get_hd_nth_event(self, event_id, event_number=0): + """ + Returns the nth requested HD event + + @param event_id the ID of the HD event types (i.e. HD_EVENT_STARTUP) + @param event_number the event number that is requested. The default is 0 meaning the last occurred event + + @returns the requested HD event number + """ + list_length = len(self._hd_event_dictionary[HDEventList(event_id).name]) + + if list_length == 0: + event = [] + elif event_number > list_length: + event = self._hd_event_dictionary[HDEventList(event_id).name][list_length - 1] + else: + event = self._hd_event_dictionary[HDEventList(event_id).name][list_length - event_number - 1] + + return event + + def get_hd_events(self, event_id, number_of_events=1): + """ + Returns the requested number of a certain HD event ID + + @param event_id the ID of the HD event types (i.e. HD_EVENT_STARTUP) + @param number_of_events the last number of messages of a certain event type + + @returns a list of the requested HD event type + """ + list_of_events = [] + + # If there are not enough event lists send all the events that are available + if len(self._hd_event_dictionary[HDEventList(event_id).name]) <= number_of_events: + list_of_events = self._hd_event_dictionary[HDEventList(event_id).name] + else: + # Get the all the events + complete_list = self._hd_event_dictionary[HDEventList(event_id).name] + # Since the last are located at the end of the list, iterate backwards for the defined + # event messages + for i in range(len(complete_list) - 1, len(complete_list) - number_of_events - 1, -1): + list_of_events.append(complete_list[i]) + + if number_of_events == 0: + list_of_events = self._hd_event_dictionary[HDEventList(event_id).name] + + return list_of_events + + @publish(['_hd_event_dictionary']) + def _handler_events_sync(self, message): + """ + Handles published events message + + @param message: published HD events data message + @returns none + """ + event_id = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + # Get the data type + event_data_type_1 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + # Get the corresponding structure format + struct_data_type = self._hd_event_data_type[HDEventDataType(event_data_type_1).name] + # Get the data value by unpacking the data type + event_data_1 = struct.unpack(struct_data_type, bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + + event_data_type_2 = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + struct_data_type = self._hd_event_data_type[HDEventDataType(event_data_type_2).name] + event_data_2 = struct.unpack(struct_data_type, bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + # Convert the event ID to enum + event_state_name = HDEventList(event_id).name + + # Check if the event state name is operation mode change. If it is, get the name of the operation modes + # from the op modes enum class + if event_state_name == HDEventList.HD_EVENT_OP_MODE_CHANGE.name: + event_data_1 = HDOpModes(event_data_1).name + event_data_2 = HDOpModes(event_data_2).name + # Check if the event state name is sub mode change. + elif event_state_name == HDEventList.HD_EVENT_SUB_MODE_CHANGE.name: + # Get the length of the list of the sub mode list + op_list_len = len(self._hd_event_dictionary[HDEventList.HD_EVENT_OP_MODE_CHANGE.name]) + # Get the last tuple of the sub mode + # It is a list of tuples that each tuple is (timestamp, event type, prev op mode, current op mode) + last_op_tuple = self._hd_event_dictionary[HDEventList.HD_EVENT_OP_MODE_CHANGE.name][op_list_len - 1] + + # Get the current and previous operation modes of the last tuple in the list of the sub modes + # i.e. (timestamp, event type, prev, current) + current_op_mode = last_op_tuple[len(last_op_tuple) - 1] + current_op_mode_timestamp = datetime.strptime(last_op_tuple[0], '%Y-%m-%d %H:%M:%S.%f') + + sub_mode_list_len = len(self._hd_event_dictionary[HDEventList.HD_EVENT_SUB_MODE_CHANGE.name]) + + if sub_mode_list_len != 0: + # Get the tuple prior to the last tuple and get its previous and current operation modes + current_sub_tuple = self._hd_event_dictionary[HDEventList.HD_EVENT_SUB_MODE_CHANGE.name][ + sub_mode_list_len - 1] + + current_sub_mode_timestamp = datetime.strptime(current_sub_tuple[0], '%Y-%m-%d %H:%M:%S.%f') + else: + current_sub_mode_timestamp = 0 + + # Get the class of the states enums of the current operation mode that is running + current_sub_mode_enum_class = self._hd_op_mode_2_sub_mode[current_op_mode] + + # Check if the operation modes of the two tuples match + # i.e. last = (timestamp, event type, prev, current) and one before = (timestamp, event type, prev, current) + # If the prev and current match respectively, it means the current operation mode has not changed so the + # operation mode states can be converted from the current sub mode enum class + if current_sub_mode_timestamp != 0: + if current_op_mode_timestamp <= current_sub_mode_timestamp: + + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + + elif current_op_mode_timestamp > current_sub_mode_timestamp: + # If the previous and current of the last two tuples do not match, then an operation mode transition + # has occurred and the previous state is converted from the previous class and the current op mode + # is converted from current operation states enum class. + # i.e last = (timestamp, event type, 3, 8) and one before = (timestamp, event type, 8, 3) + # previous and current do not match so in the last type (timestamp, event type, 8, 3) the prev state + # should be from op mode 8 and the current state should be from op mode 3 + previous_op_mode = last_op_tuple[len(last_op_tuple) - 2] + previous_sub_mode_enum_class = self._hd_op_mode_2_sub_mode[previous_op_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + + if event_data_2 != 0: + event_data_1 = current_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + else: + previous_sub_mode = current_sub_tuple[len(current_sub_tuple) - 2] + previous_sub_mode_enum_class = self._hd_op_mode_2_sub_mode[previous_sub_mode] + event_data_1 = previous_sub_mode_enum_class(event_data_1).name + event_data_2 = current_sub_mode_enum_class(event_data_2).name + + # Get the current timestamp and create a tuple of the current events + event_tuple = (datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'), event_state_name, event_data_1, event_data_2) + + # Update event dictionary + self._hd_event_dictionary[event_state_name].append(event_tuple) Index: shared/scripts/dialin/hd/hemodialysis_device.py =================================================================== diff -u --- shared/scripts/dialin/hd/hemodialysis_device.py (revision 0) +++ shared/scripts/dialin/hd/hemodialysis_device.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,608 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct + +from .accelerometer import HDAccelerometer +from .air_bubbles import HDAirBubbles +from .air_trap import HDAirTrap +from .alarms import HDAlarms +from .blood_flow import HDBloodFlow +from .blood_leak import HDBloodLeak +from .buttons import HDButtons +from .calibration_record import HDCalibrationNVRecord +from .dialysate_inlet_flow import HDDialysateInletFlow +from .dialysate_outlet_flow import HDDialysateOutletFlow +from .fluid_leak import HDFluidLeak +from .pressure_occlusion import HDPressureOcclusion +from .pretreatment import HDPreTreatment +from .rtc import HDRTC +from .service_record import HDServiceNVRecords +from .switches import HDSwitches +from .temperatures import HDTemperatures +from .fans import HDFans +from .constants import NO_RESET, RESET +from .syringe_pump import HDSyringePump +from .system_record import HDSystemNVRecords +from .treatment import HDTreatment +from .ui_proxy import HDUIProxy +from .valves import HDValves +from .voltages import HDVoltages +from .watchdog import HDWatchdog +from ..common.hd_defs import HDOpModes +from .hd_events import HDEvents +from .reservoirs import HDReservoirs +from ..common.msg_defs import MsgIds, MsgFieldPositions +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 + + +class HD(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin object API. + It provides the basic interface to communicate with the HD firmware. + """ + + # HD login password + HD_LOGIN_PASSWORD = '123' + + def __init__(self, can_interface="can0", log_level=None): + """ + HD object provides test/service commands for the HD sub-system. + + >> hd_object = HD('can0') + >> hd_object = HD(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, + log_can=self._log_manager.log_level == "CAN_ONLY") + self.can_interface.start() + + # register handler for HD operation mode broadcast messages + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_OP_MODE.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_op_mode_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] + + # Create command groups + self.accel = HDAccelerometer(self.can_interface, self.logger) + self.air_bubbles = HDAirBubbles(self.can_interface, self.logger) + self.air_trap = HDAirTrap(self.can_interface, self.logger) + self.alarms = HDAlarms(self.can_interface, self.logger) + self.blood_leak = HDBloodLeak(self.can_interface, self.logger) + self.bloodflow = HDBloodFlow(self.can_interface, self.logger) + self.buttons = HDButtons(self.can_interface, self.logger) + self.calibration_record = HDCalibrationNVRecord(self.can_interface, self.logger) + self.dialysate_inlet_flow = HDDialysateInletFlow(self.can_interface, self.logger) + self.dialysate_outlet_flow = HDDialysateOutletFlow(self.can_interface, self.logger) + self.fluid_leak = HDFluidLeak(self.can_interface, self.logger) + self.pressure_occlusion = HDPressureOcclusion(self.can_interface, self.logger) + self.pretreatment = HDPreTreatment(self.can_interface, self.logger) + self.rtc = HDRTC(self.can_interface, self.logger) + self.service_record = HDServiceNVRecords(self.can_interface, self.logger) + self.switches = HDSwitches(self.can_interface, self.logger) + self.syringe_pump = HDSyringePump(self.can_interface, self.logger) + self.system_record = HDSystemNVRecords(self.can_interface, self.logger) + self.treatment = HDTreatment(self.can_interface, self.logger) + self.ui = HDUIProxy(self.can_interface, self.logger) + self.valves = HDValves(self.can_interface, self.logger) + self.voltages = HDVoltages(self.can_interface, self.logger) + self.calibration_record = HDCalibrationNVRecord(self.can_interface, self.logger) + self.system_record = HDSystemNVRecords(self.can_interface, self.logger) + self.service_record = HDServiceNVRecords(self.can_interface, self.logger) + self.switches = HDSwitches(self.can_interface, self.logger) + self.temperatures = HDTemperatures(self.can_interface, self.logger) + self.fans = HDFans(self.can_interface, self.logger) + self.watchdog = HDWatchdog(self.can_interface, self.logger) + self.hd_events = HDEvents(self.can_interface, self.logger) + self.hd_reservoirs = HDReservoirs(self.can_interface, self.logger) + + def get_operation_mode(self): + """ + Gets the HD operation mode + + @return: The hd operation mode + """ + return self.hd_operation_mode + + def get_hd_logged_in(self): + """ + Gets the logged in status of the HD + + @return: True if HD is logged in, False if not + """ + return self.hd_logged_in + + def get_hd_blocked_msg_list(self): + """ + Gets the current list of message IDs that HD will prevent transmission of. + + @return: List of message IDs blocked from transmission + """ + return self.hd_no_transmit_msg_list + + @publish(["hd_logged_in"]) + def hd_set_logged_in_status(self, logged_in: bool = False): + """ + Callback for HD logged in status change. + @param logged_in boolean logged in status for HD + @return: none + """ + self.hd_logged_in = logged_in + + @publish(["hd_operation_mode"]) + def _handler_hd_op_mode_sync(self, message): + """ + Handles published HD operation mode messages. Current HD operation mode + is captured for reference. + + @param message: published HD 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.hd_operation_mode = mode[0] + self.hd_operation_sub_mode = smode[0] + + def cmd_log_in_to_hd(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 HD. + + @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_hd_ch_id, + message_id=MsgIds.MSG_ID_TESTER_LOGIN_REQUEST.value, + payload=list(map(int, map(ord, self.HD_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.hd_set_logged_in_status(True) + 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_hd_request_calibration_data(self) -> int: + """ + Constructs and sends an HD calibration data request command via CAN bus. + Constraints: + Must be logged into HD. + + \returns response message if received, False if no response received + + @return: 1 if successful, zero otherwise + + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_CALIBRATION_RECORD.value) + + self.logger.debug("requesting HD calibration data.") + + # 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("HD calibration data request accepted.") + else: + self.logger.debug("HD calibration data request failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_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 HD. + Transition from current to requested op mode must be legal. + + @param new_mode: ID of operation mode to transition to + HD_OP_MODE_FAULT = 0 + HD_OP_MODE_SERVICE = 1 + HD_OP_MODE_INIT_POST = 2 + HD_OP_MODE_STANDBY = 3 + HD_OP_MODE_TREATMENT_PARAMS = 4 + HD_OP_MODE_PRE_TREATMENT = 6 + HD_OP_MODE_TREATMENT = 7 + HD_OP_MODE_POST_TREATMENT = 8 + + @return: 1 if successful, zero otherwise + + """ + + payload = integer_to_bytearray(new_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_OP_MODE_REQUEST.value, + payload=payload) + + self.logger.debug("Requesting HD 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("HD mode change request Timeout!!!!") + return False + + def cmd_hd_safety_shutdown_override(self, active: bool = True, reset: int = NO_RESET) -> int: + """ + Constructs and sends an HD safety shutdown override command via CAN bus. + Constraints: + Must be logged into HD. + + \returns response message if received, False if no response received + + @param active: boolean - True to activate safety shutdown, False to deactivate + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + + """ + + if active: + sft = 1 + else: + sft = 0 + rst = integer_to_bytearray(reset) + saf = integer_to_bytearray(sft) + payload = rst + saf + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SAFETY_SHUTDOWN_OVERRIDE.value, + payload=payload) + + self.logger.debug("overriding HD safety shutdown") + + # 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("Safety shutdown signal overridden") + else: + self.logger.debug("Safety shutdown signal override failed.") + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Timeout!!!!") + return False + + def cmd_hd_software_reset_request(self) -> None: + """ + Constructs and sends an HD software reset request via CAN bus. + Constraints: + Must be logged into HD. + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SOFTWARE_RESET_REQUEST.value) + + self.logger.debug("requesting HD software reset") + + # Send message + self.can_interface.send(message, 0) + self.logger.debug("Sent request to HD to reset...") + self.hd_set_logged_in_status(False) + + def cmd_op_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the HD operation mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_OP_MODE_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override operation mode data 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("Operation mode 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.debug("Timeout!!!!") + return False + + def cmd_standby_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the standby mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_STANDBY_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override standby mode data 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("Standby mode 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.debug("Timeout!!!!") + return False + + def cmd_pre_treatment_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the pre-treatment mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_PRE_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override pre-treatment mode 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("Pre-treatment mode 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.debug("Timeout!!!!") + return False + + def cmd_treatment_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment mode data 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("Treatment mode 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.debug("Timeout!!!!") + return False + + def cmd_post_treatment_mode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the post-treatment mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_POST_TREATMENT_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override post-treatment mode data 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("Post-treatment mode 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.debug("Timeout!!!!") + return False + + def cmd_block_hd_message_transmissions(self, msg1: int = 0, msg2: int = 0, msg3: int = 0, msg4: int = 0, + msg5: int = 0, msg6: int = 0, msg7: int = 0, msg8: int = 0): + """ + Constructs and sends a block hd message transmission request + Constraints: + Must be logged into HD. + + @param msg1: integer - 1st message ID to block HD from transmitting + @param msg2: integer - 2nd message ID to block HD from transmitting + @param msg3: integer - 3rd message ID to block HD from transmitting + @param msg4: integer - 4th message ID to block HD from transmitting + @param msg5: integer - 5th message ID to block HD from transmitting + @param msg6: integer - 6th message ID to block HD from transmitting + @param msg7: integer - 7th message ID to block HD from transmitting + @param msg8: integer - 8th message ID to block HD from transmitting + @return: 1 if successful, zero otherwise + """ + # Save blocked message(s) list + self.hd_no_transmit_msg_list[0] = msg1 + self.hd_no_transmit_msg_list[1] = msg2 + self.hd_no_transmit_msg_list[2] = msg3 + self.hd_no_transmit_msg_list[3] = msg4 + self.hd_no_transmit_msg_list[4] = msg5 + self.hd_no_transmit_msg_list[5] = msg6 + self.hd_no_transmit_msg_list[6] = msg7 + self.hd_no_transmit_msg_list[7] = msg8 + # Build message payload + m1 = unsigned_short_to_bytearray(msg1) + m2 = unsigned_short_to_bytearray(msg2) + m3 = unsigned_short_to_bytearray(msg3) + m4 = unsigned_short_to_bytearray(msg4) + m5 = unsigned_short_to_bytearray(msg5) + m6 = unsigned_short_to_bytearray(msg6) + m7 = unsigned_short_to_bytearray(msg7) + m8 = unsigned_short_to_bytearray(msg8) + payload = m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOCK_MESSAGE_TRANSMISSION.value, + payload=payload) + + self.logger.debug("request HD block transmission of message(s)") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Given messages blocked." + + 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: shared/scripts/dialin/hd/pressure_occlusion.py =================================================================== diff -u --- shared/scripts/dialin/hd/pressure_occlusion.py (revision 0) +++ shared/scripts/dialin/hd/pressure_occlusion.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,300 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 pressure_occlusion.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDPressureOcclusion(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for pressure related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDPressureOcclusion constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_PRESSURE_OCCLUSION_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_pressure_occlusion_sync) + + self.arterial_pressure = 0.0 + self.venous_pressure = 0.0 + self.blood_pump_occlusion = 0 + + def get_arterial_pressure(self): + """ + Gets the arterial pressure. + @return: (float) The arterial pressure + """ + return self.arterial_pressure + + def get_venous_pressure(self): + """ + Gets the venous pressure + + @return: (float) The venous pressure + """ + return self.venous_pressure + + def get_blood_pump_occlusion(self): + """ + Gets the blood pump occlusion + + @return: (int) The blood pump occlusion + """ + return self.blood_pump_occlusion + + @publish([ + "arterial_pressure", + "venous_pressure", + "blood_pump_occlusion", + "dialysate_inlet_pump_occlusion", + "dialysate_outlet_pump_occlusion" + ]) + def _handler_pressure_occlusion_sync(self, message): + """ + Handles published pressure & occlusion data messages. Pressure data are captured + for reference. + + @param message: published pressure & occlusion data message + @return: none + """ + + art = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + ven = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + bp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + dpi = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + dpo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + self.arterial_pressure = art[0] + self.venous_pressure = ven[0] + self.blood_pump_occlusion = bp[0] + self.dialysate_inlet_pump_occlusion = dpi[0] + self.dialysate_outlet_pump_occlusion = dpo[0] + + def cmd_arterial_pressure_measured_override(self, pres: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured arterial pressure override command + Constraints: + Must be logged into HD. + + @param pres: float - measured arterial pressure (in mmHg) to override with + @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(pres) + payload = rst + prs + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_ARTERIAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured arterial pressure") + + # 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(pres) + " mmHg. " + self.logger.debug("Arterial pressure (measured)) 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.debug("Timeout!!!!") + return False + + def cmd_venous_pressure_measured_override(self, pres: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured venous pressure \n + override command. + Constraints: + Must be logged into HD. + + @param pres: float - venous pressure (in mmHg) to override with + @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(pres) + payload = rst + prs + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_VENOUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured venous pressure") + + # 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(pres) + " mmHg. " + self.logger.debug("Venous pressure (measured) 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.debug("Timeout!!!!") + return False + + def cmd_blood_pump_measured_occlusion_override(self, occl: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the measured blood pump occlusion pressure override command + Constraints: + Must be logged into HD. + + @param occl: integer - pressure (in counts) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + occ = integer_to_bytearray(occl) + payload = rst + occ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_OCCLUSION_BLOOD_PUMP_OVERRIDE.value, + payload=payload) + + self.logger.debug("override measured blood pump occlusion pressure") + + # 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(occl) + " mmHg. " + self.logger.debug("Blood pump occlusion pressure (measured) 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.debug("Timeout!!!!") + return False + + def cmd_pressure_occlusion_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the pressure/occlusion broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_PRES_OCCL_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override pressure/occlusion 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("Pressure/occlusion 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.debug("Timeout!!!!") + return False + + def cmd_arterial_pressure_offset_override(self, offset: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the arterial pressure offset override command + Constraints: + Must be logged into HD. + + @param offset: float - offset (in mmHg) for arterial pressure sensor + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + off = float_to_bytearray(offset) + payload = rst + off + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_SET_ARTERIAL_PRESSURE_OFFSET.value, + payload=payload) + + self.logger.debug("override arterial pressure offset") + + # 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(offset) + " mmHg: " + self.logger.debug("Arterial pressure offset 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/pretreatment.py =================================================================== diff -u --- shared/scripts/dialin/hd/pretreatment.py (revision 0) +++ shared/scripts/dialin/hd/pretreatment.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,323 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 pretreatment.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Quang Nguyen +# @date (original) 02-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from ..common import (PreTreatmentSubModes, + PreTreatmentSampleWaterStates, + PreTreatmentConsumableSelfTestStates, + PreTreatmentRecircStates, + PreTreatmentNoCartSelfTestStates, + PreTreatmentDrySelfTestStates, + PreTreatmentPrimeStates) +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliChannels +from ..utils.base import AbstractSubSystem, publish + + +class HDPreTreatment(AbstractSubSystem): + """ + + Hemodialysis Delivery (HD) Dialin API sub-class for pretreatment related commands. + + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDPreTreatment constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_PRE_TREATMENT_STATE.value, + self._handler_pre_treatment_state_sync) + + msg_id = MsgIds.MSG_ID_HD_NO_CART_SELF_TEST_PROGRESS.value + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, msg_id, + self._handler_no_cart_self_test_progress_sync) + + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_HD_DRY_SELF_TEST_PROGRESS.value, + self._handler_dry_self_test_progress_sync) + + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_HD_PRIMING_STATUS_DATA.value, + self._handler_prime_progress_sync) + + self.pre_treatment_submode = 0 + self.pre_treatment_sample_water_state = 0 + self.pre_treatment_consumable_self_test_state = 0 + self.pre_treatment_no_cart_self_test_state = 0 + self.pre_treatment_installation_state = 0 + self.pre_treatment_dry_self_test_state = 0 + self.pre_treatment_prime_state = 0 + self.pre_treatment_recirc_state = 0 + self.pre_treatment_patient_connection_state = 0 + + self.no_cart_self_test_timeout = 0 + self.no_cart_self_test_time_countdown = 0 + + self.dry_self_test_timeout = 0 + self.dry_self_test_time_countdown = 0 + + self.prime_timeout = 0 + self.prime_time_countdown = 0 + + def get_pre_treatment_submode(self): + """ + Gets the pre-treatment state + + @return: The pre-treatment state + """ + return self.pre_treatment_submode + + def get_pre_treatment_sample_water_state(self): + """ + Gets the pre-treatment sample water state + + @return: The pre-treatment sample water state + """ + return self.pre_treatment_sample_water_state + + def get_pre_treatment_consumable_self_test_state(self): + """ + Gets the pre-treatment consumable self-test state + + @return: The pre-treatment consumable self-test state + """ + return self.pre_treatment_consumable_self_test_state + + def get_pre_treatment_no_cart_self_test_state(self): + """ + Gets the pre-treatment no cartridge self-test state + + @return: The pre-treatment no cartridge self-test state + """ + return self.pre_treatment_no_cart_self_test_state + + def get_pre_treatment_installation_state(self): + """ + Gets the pre-treatment installation state + + @return: The pre-treatment installation state + """ + return self.pre_treatment_installation_state + + def get_pre_treatment_dry_self_test_state(self): + """ + Gets the pre-treatment dry self-test state + + @return: The pre-treatment dry self-test state + """ + return self.pre_treatment_dry_self_test_state + + def get_pre_treatment_prime_state(self): + """ + Gets the pre-treatment primt state + + @return: The pre-treatment prime state + """ + return self.pre_treatment_prime_state + + def get_pre_treatment_recirc_state(self): + """ + Gets the pre-treatment re-circulate state + + @return: The pre-treatment re-circulate state + """ + return self.pre_treatment_recirc_state + + def get_pre_treatment_patient_connection_state(self): + """ + Gets the pre-treatment patient connection state + + @return: The pre-treatment patient connection state + """ + return self.pre_treatment_patient_connection_state + + def get_no_cart_self_test_timeout(self): + """ + Gets the pre-treatment no cartridge self-test timeout + + @return: The pre-treatment no cartridge self-test timeout + """ + return self.no_cart_self_test_timeout + + def get_no_cart_self_test_time_countdown(self): + """ + Gets the pre-treatment no cartridge self-test time countdown + + @return: The pre-treatment no cartridge self-test time countdown + """ + return self.no_cart_self_test_time_countdown + + def get_dry_self_test_timeout(self): + """ + Gets the pre-treatment dry self-test timeout + + @return: The pre-treatment dry self-test timeout + """ + return self.dry_self_test_timeout + + def get_dry_self_test_time_countdown(self): + """ + Gets the pre-treatment dry self-test time countdown + + @return: The pre-treatment dry self-test time countdown + """ + return self.dry_self_test_time_countdown + + def get_prime_timeout(self): + """ + Gets the pre-treatment prime timeout + + @return: The pre-treatment prime timeout + """ + return self.prime_timeout + + def get_prime_time_countdown(self): + """ + Gets the pre-treatment prime time countdown + + @return: The pre-treatment prime time countdown + """ + return self.prime_time_countdown + + @publish([ + "pre_treatment_submode", + "pre_treatment_sample_water_state", + "pre_treatment_consumable_self_test_state", + "pre_treatment_no_cart_self_test_state", + "pre_treatment_installation_state", + "pre_treatment_dry_self_test_state", + "pre_treatment_prime_state", + "pre_treatment_recirc_state", + "pre_treatment_patient_connection_state", + ]) + def _handler_pre_treatment_state_sync(self, message): + """ + Handles published pre-treatment state data messages. + + @param message: published pre-treatment state data message + @return: none + """ + + pt_submode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + pt_sw_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + pt_consumable_st_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + pt_no_cart_st_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.pre_treatment_installation_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + pt_dry_st_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + pt_prime_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + pt_recirc_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + self.pre_treatment_patient_connection_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9]))[0] + + self.pre_treatment_submode = 'Unknown SubMode' + if PreTreatmentSubModes.has_value(pt_submode): + self.pre_treatment_submode = PreTreatmentSubModes(pt_submode).name + + self.pre_treatment_sample_water_state = 'Unknown State' + if PreTreatmentSampleWaterStates.has_value(pt_sw_state): + self.pre_treatment_sample_water_state = PreTreatmentSampleWaterStates(pt_sw_state).name + + self.pre_treatment_consumable_self_test_state = 'Unknown State' + if PreTreatmentConsumableSelfTestStates.has_value(pt_consumable_st_state): + self.pre_treatment_consumable_self_test_state = PreTreatmentConsumableSelfTestStates( + pt_consumable_st_state).name + + self.pre_treatment_no_cart_self_test_state = 'Unknown State' + if PreTreatmentNoCartSelfTestStates.has_value(pt_no_cart_st_state): + self.pre_treatment_no_cart_self_test_state = PreTreatmentNoCartSelfTestStates(pt_no_cart_st_state).name + + self.pre_treatment_dry_self_test_state = 'Unknown State' + if PreTreatmentDrySelfTestStates.has_value(pt_dry_st_state): + self.pre_treatment_dry_self_test_state = PreTreatmentDrySelfTestStates(pt_dry_st_state).name + + self.pre_treatment_dry_self_test_state = 'Unknown State' + if PreTreatmentDrySelfTestStates.has_value(pt_dry_st_state): + self.pre_treatment_dry_self_test_state = PreTreatmentDrySelfTestStates(pt_dry_st_state).name + + self.pre_treatment_prime_state = 'Unknown State' + if PreTreatmentPrimeStates.has_value(pt_prime_state): + self.pre_treatment_prime_state = PreTreatmentPrimeStates(pt_prime_state).name + + self.pre_treatment_recirc_state = 'Unknown State' + if PreTreatmentRecircStates.has_value(pt_recirc_state): + self.pre_treatment_recirc_state = PreTreatmentRecircStates(pt_recirc_state).name + + @publish([ + "no_cart_self_test_timeout", + "no_cart_self_test_time_countdown" + ]) + def _handler_no_cart_self_test_progress_sync(self, message): + """ + Handles published no cartridge self-test progress data messages. + + @param message: published no cartridge self-test progress data message + @return: None + """ + + self.no_cart_self_test_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.no_cart_self_test_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + @publish([ + "dry_self_test_timeout", + "dry_self_test_time_countdown" + ]) + def _handler_dry_self_test_progress_sync(self, message): + """ + Handles published dry self-test progress data messages. + + @param message: published dry self-test progress data message + @return: None + """ + + self.dry_self_test_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.dry_self_test_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + @publish([ + "prime_timeout", + "prime_time_countdown" + ]) + def _handler_prime_progress_sync(self, message): + """ + Handles published prime progress data messages. + + @param message: published prime progress data message + @return: None + """ + + self.prime_timeout = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.prime_time_countdown = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] Index: shared/scripts/dialin/hd/reservoirs.py =================================================================== diff -u --- shared/scripts/dialin/hd/reservoirs.py (revision 0) +++ shared/scripts/dialin/hd/reservoirs.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,81 @@ + + +import struct +from logging import Logger +from enum import unique + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray + + +@unique +class HDReservoirStates(DialinEnum): + + TREATMENT_RESERVOIR_MGMT_START_STATE = 0 + TREATMENT_RESERVOIR_MGMT_DRAIN_RESERVOIR_STATE = 1 + TREATMENT_RESERVOIR_MGMT_WAIT_TO_FILL_STATE = 2 + TREATMENT_RESERVOIR_MGMT_FILL_RESERVOIR_STATE = 3 + TREATMENT_RESERVOIR_MGMT_WAIT_FOR_FILL_SETTLE_STATE = 4 + TREATMENT_RESERVOIR_MGMT_WAIT_FOR_SWITCH_SETTLE_STATE = 5 + NUM_OF_TREATMENT_RESERVOIR_MGMT_STATES = 6 + + +class HDReservoirs(AbstractSubSystem): + """ + HD interface containing reservoir related commands. + """ + + def __init__(self, can_interface, logger: Logger): + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_RESERVOIRS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_reservoirs_sync) + + self.reservoir_state = 0 + self.active_reservoir_uf_ml = 0.0 + self.active_reservoir_spent_vol_ml = 0.0 + self.dilution_level_pct = 0.0 + self.recirculation_level_pct = 0.0 + self.time_depletion = 0 + self.time_wait_to_fill = 0 + + self.temp_remove_fill_flow = 0.0 + + @publish(['reservoir_state', 'active_reservoir_uf_ml', 'active_reservoir_spent_vol_ml', 'dilution_level_pct', + 'recirculation_level_pct', 'time_depletion', 'time_wait_to_fill', 'temp_remove_fill_flow']) + def _handler_reservoirs_sync(self, message): + """ + Handles published reservoir data messages. Reservoir data are captured + for reference. + + @param message: published reservoir data message + @return: none + """ + + self.reservoir_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.active_reservoir_uf_ml = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + self.active_reservoir_spent_vol_ml = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + self.dilution_level_pct = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + self.recirculation_level_pct = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.time_depletion = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6]))[0] + self.time_wait_to_fill = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7]))[0] + + + self.temp_remove_fill_flow = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8]))[0] + Index: shared/scripts/dialin/hd/rtc.py =================================================================== diff -u --- shared/scripts/dialin/hd/rtc.py (revision 0) +++ shared/scripts/dialin/hd/rtc.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,138 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 rtc.py +# +# @author (last) Dara Navaei +# @date (last) 11-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ + +import struct +from ..common.msg_defs import MsgIds, MsgFieldPositions +from logging import Logger + +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray + + +class HDRTC(AbstractSubSystem): + """ + + Hemodialysis Delivery (HD) Dialin API sub-class for rtc 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.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_RTC_EPOCH.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_rtc_epoch) + self.rtc_epoch = 0 + + def get_rtc_epoch(self): + """ + Gets the rtc epoch + + @return: The rtc epoch + """ + return self.rtc_epoch + + @publish(["rtc_epoch"]) + def _handler_rtc_epoch(self, message): + """ + Publishes the rtc time in epoch + + @param message: published rtc epoch message + @return: None + """ + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + self.rtc_epoch = epoch + + def cmd_stop_rtc(self): + """ + Stops the HD RTC clock + + @return: 1 if Successful, False otherwise + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_STOP_RTC_CLOCK.value) + + self.logger.debug("Stopping the HD RTC") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + self.logger.debug("RTC stop command was sent" + + 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_set_rtc_time_and_date(self, second: int, minute: int, hour: int, day: int, month: int, year: int) -> int: + """ + Sets the RTC time and date from the provided + + @param second: (int) Second + @param minute: (int) Minute + @param hour: (int) Hour + @param day: (int) Day + @param month: (int) Month + @param year: (int) Year + @return: 1 if Successful, False otherwise + """ + sec = bytes([second]) + mint = bytes([minute]) + hour = bytes([hour]) + day = bytes([day]) + month = bytes([month]) + year = integer_to_bytearray(year) + payload = sec + mint + hour + day + month + year + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_SET_RTC_DATE_TIME.value, + payload=payload) + + self.logger.debug("Setting time and date to rtc") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug(received_message) + # str_res = str(flow) + self.logger.debug( + "Time and Date in rtc set to seconds: " + str(sec) + " minutes: " + str(min) + " hours: " + + str(hour) + " days: " + str(day) + " months: " + str(month) + " years: " + str(year) + + 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: shared/scripts/dialin/hd/service_record.py =================================================================== diff -u --- shared/scripts/dialin/hd/service_record.py (revision 0) +++ shared/scripts/dialin/hd/service_record.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,254 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 service_record.py +# +# @author (last) Dara Navaei +# @date (last) 12-Oct-2021 +# @author (original) Dara Navaei +# @date (original) 14-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.nv_ops_utils import NVOpsUtils + + +@unique +class ServiceLocation(DialinEnum): + SERVICE_LOCATION_FACTORY = 0 + SERVICE_LOCATION_FIELD = 1 + + +class HDServiceNVRecords(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for service record commands. + """ + + # The default service time interval is 6 months in seconds + _DEFAULT_SERVICE_INTERVAL_S = 15768000 + _RECORD_SPECS_BYTES = 12 + _DEFAULT_SERVICE_LOCATION = ServiceLocation.SERVICE_LOCATION_FACTORY.value + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes to be written to RTC RAM + _RTC_RAM_MAX_BYTES_TO_WRITE = 64 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + 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._is_getting_service_in_progress = False + self._write_fw_data_to_excel = True + self.service_data = 0 + self._raw_service_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + self.hd_service_record = self._prepare_hd_service_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_SERVICE_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_service_sync) + + def cmd_reset_hd_service_record(self) -> bool: + """ + Handles resetting HD service record. + + @return: True if successful, False otherwise + """ + self.hd_service_record = self._prepare_hd_service_record() + self.hd_service_record = self._utilities.reset_fw_system_service_record(self.hd_service_record) + status = self.cmd_set_hd_service_record(self.hd_service_record) + + return status + + def cmd_request_hd_service_record(self) -> int: + """ + Handles getting HD service record from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_service_in_progress is not True: + self._is_getting_service_in_progress = True + # Clear the list for the next call + self._raw_service_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_SERVICE_RECORD.value) + + self.logger.debug('Getting HD service record') + + 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 + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_hd_service_sync(self, message): + """ + Handles published HD system record messages. HD system records are captured for + processing and updating the HD system record. + + @param message: published HD system 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.service_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_service_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_service_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_service_record, self._raw_service_record) + self._handler_received_complete_hd_service_record() + + @publish(["hd_service_record"]) + def _handler_received_complete_hd_service_record(self): + """ + Publishes the received service record + + @return: None + """ + self.logger.debug("Received a complete hd service record.") + + def cmd_set_hd_service_record(self, hd_service_record: OrderedDict) -> bool: + """ + Handles updating the HD system and sends it to FW. + + @param hd_service_record: (OrderedDict) the hd service record to be sent + @return: True upon success, False otherwise + """ + + self.logger.debug('Setting HD service record') + + record_packets = self._utilities.prepare_record_to_send_to_fw(hd_service_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_SERVICE_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 service record.") + return True + + def _prepare_hd_service_record(self): + """ + Handles assembling the sub dictionaries of each group to make the main HD service record. + + @return: (OrderedDict) an assembled hd service record + """ + result = OrderedDict() + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_service_record()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' dict: + """ + Gets the status of a switch + + @return: The status of all of the switches in a dictionary + """ + return self.hd_switches_status + + @publish(["hd_switches_status"]) + def _handler_switches_sync(self, message): + """ + Handles published HD switches data messages. Switches data are captured for reference. + + @param message: published switches data message + @return: none + """ + front_door = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + pump_track = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + self.hd_switches_status[HDSwitchesNames.FRONT_DOOR.name] = HDSwitchStatus(front_door).value + self.hd_switches_status[HDSwitchesNames.PUMP_TRACK_SWITCH.name] = HDSwitchStatus(pump_track).value + + def cmd_hd_switch_status_override(self, switch: int, status: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD switch status override command + Constraints: + Must be logged into HD. + + @param switch: (int) switch ID that is status is overridden + @param status: (int) status that the switch will 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) + sw = integer_to_bytearray(switch) + st = integer_to_bytearray(status) + payload = reset_value + st + sw + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SWITCHES_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override switch status") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Switch " + str(HDSwitchesNames(switch).name) + " 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 + + def cmd_hd_switches_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD switch data publication override command. + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SWITCHES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD switches data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "HD Switches data broadcast interval overridden to " + str_res + " ms: " + + 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: shared/scripts/dialin/hd/syringe_pump.py =================================================================== diff -u --- shared/scripts/dialin/hd/syringe_pump.py (revision 0) +++ shared/scripts/dialin/hd/syringe_pump.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,747 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 (original) Sean Nash +# @date (original) 12-Mar-2021 +# +############################################################################ +import struct +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common.hd_defs import HeparinStates, SyringePumpStates, SyringePumpOperations +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDSyringePump(AbstractSubSystem): + """ + HDSyringePump + + Hemodialysis Delivery (HD) Dialin API sub-class for syringe pump 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.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_SYRINGE_PUMP_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_syringe_pump_data) + + self.syringe_pump_state = SyringePumpStates.SYRINGE_PUMP_INIT_STATE.value + self.heparin_state = HeparinStates.HEPARIN_STATE_OFF.value + self.syringe_pump_set_rate_ml_hr = 0.0 + self.syringe_pump_meas_rate_ml_hr = 0.0 + self.syringe_pump_position = 0 + self.syringe_pump_volume_ml = 0.0 + self.syringe_pump_safety_volume_ml = 0.0 + self.syringe_pump_home_v = 0.0 + self.syringe_pump_switch_v = 0.0 + self.syringe_pump_force_v = 0.0 + self.syringe_pump_status = 0 + self.syringe_pump_encoder_status = 0 + self.syringe_pump_adc_dac_status = 0 + self.syringe_pump_adc_read_counter = 0 + + def get_syringe_pump_state(self): + """ + Gets the current syringe pump state. + + @return: latest published syringe pump state. + SYRINGE_PUMP_INIT_STATE = 0 + SYRINGE_PUMP_OFF_STATE = 1 + SYRINGE_PUMP_RETRACT_STATE = 2 + SYRINGE_PUMP_SEEK_STATE = 3 + SYRINGE_PUMP_PRIME_STATE = 4 + SYRINGE_PUMP_HEP_BOLUS_STATE = 5 + SYRINGE_PUMP_HEP_CONTINUOUS_STATE = 6 + SYRINGE_PUMP_CONFIG_FORCE_SENSOR_STATE = 7 + """ + return self.syringe_pump_set_rate_ml_hr + + def get_heparin_state(self): + """ + Gets the current Heparin state. + + @return: latest published Heparin state. + HEPARIN_STATE_OFF = 0 + HEPARIN_STATE_STOPPED = 1 + HEPARIN_STATE_PAUSED = 2 + HEPARIN_STATE_INITIAL_BOLUS = 3 + HEPARIN_STATE_DISPENSING = 4 + HEPARIN_STATE_COMPLETED = 5 + HEPARIN_STATE_EMPTY = 6 + """ + return self.heparin_state + + def get_syringe_pump_set_rate(self): + """ + Gets the current set syringe pump rate. + + @return: latest published syringe pump set rate (in mL/hr). + """ + return self.syringe_pump_set_rate_ml_hr + + def get_syringe_pump_meas_rate(self): + """ + Gets the current measured syringe pump rate. + + @return: latest published syringe pump measured rate (in mL/hr). + """ + return self.syringe_pump_meas_rate_ml_hr + + def get_syringe_pump_position(self): + """ + Gets the current syringe pump position. + + @return: latest published syringe pump position (in encoder counts). + """ + return self.syringe_pump_position + + def get_syringe_pump_volume_delivered_ml(self): + """ + Gets the current syringe pump volume delivered. + + @return: latest published syringe pump volume delivered (in mL). + """ + return self.syringe_pump_volume_ml + + def get_syringe_pump_home_v(self): + """ + Gets the current syringe pump home voltage reading + + @return: latest published voltage read from the home optical sensor + """ + return self.syringe_pump_home_v + + def get_syringe_pump_switch_v(self): + """ + Gets the current syringe pump switch voltage reading + + @return: latest published voltage read from the syringe detection switch + """ + return self.syringe_pump_switch_v + + def get_syringe_pump_force_v(self): + """ + Gets the current syringe pump force voltage reading + + @return: latest published voltage read from the force sensor + """ + return self.syringe_pump_force_v + + def get_syringe_pump_safety_volume(self): + """ + Gets the current syringe pump safety volume reading + + @return: latest published safety volume calculated by HD firmware + """ + return self.syringe_pump_safety_volume_ml + + def get_syringe_pump_status(self): + """ + Gets the current syringe pump status + + @return: latest published syringe pump status by HD firmware + """ + return self.syringe_pump_status + + def get_syringe_pump_encoder_status(self): + """ + Gets the current syringe pump encoder status + + @return: latest published syringe pump encoder status by HD firmware + """ + return self.syringe_pump_encoder_status + + def get_syringe_pump_adc_dac_status(self): + """ + Gets the current syringe pump ADC & DAC status + + @return: latest published syringe pump ADC & DAC status by HD firmware + """ + return self.syringe_pump_adc_dac_status + + def get_syringe_pump_adc_read_counter(self): + """ + Gets the current syringe pump ADC read counter + + @return: latest published ADC read counter by HD firmware + """ + return self.syringe_pump_adc_read_counter + + @publish(["syringe_pump_state", "syringe_pump_set_rate_ml_hr", + "syringe_pump_meas_rate_ml_hr", "syringe_pump_position", + "syringe_pump_volume_ml", "syringe_pump_home_v", + "syringe_pump_switch_v", "syringe_pump_force_v", + "heparin_state", "syringe_pump_safety_volume_ml", + "syringe_pump_status", "syringe_pump_encoder_status", + "syringe_pump_adc_dac_status", "syringe_pump_adc_read_counter"]) + def _handler_syringe_pump_data(self, message): + """ + Handles published syringe pump data messages. Syringe pump data are captured + for reference. + + @param message: published syringe pump data message + @return: None + """ + + sta = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + hep = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + srt = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + mrt = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + pos = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + hom = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + det = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + frc = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + saf = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + sts = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + + self.syringe_pump_state = sta[0] + self.heparin_state = hep[0] + self.syringe_pump_set_rate_ml_hr = srt[0] + self.syringe_pump_meas_rate_ml_hr = mrt[0] + self.syringe_pump_position = pos[0] + self.syringe_pump_volume_ml = vol[0] + self.syringe_pump_home_v = hom[0] + self.syringe_pump_switch_v = det[0] + self.syringe_pump_force_v = frc[0] + self.syringe_pump_safety_volume_ml = saf[0] + self.syringe_pump_status = (sts[0] & 0xFF000000) >> 24 + self.syringe_pump_encoder_status = (sts[0] & 0x00FF0000) >> 16 + self.syringe_pump_adc_dac_status = (sts[0] & 0x0000FF00) >> 8 + self.syringe_pump_adc_read_counter = (sts[0] & 0x000000FF) + + def cmd_syringe_pump_operation(self, operation: int = SyringePumpOperations.SYRINGE_PUMP_OP_STOP.value, + rate: float = 0.0, + volume: float = 0.0) -> int: + """ + Constructs and sends the syringe pump operation command + Constraints: + Must be logged into HD. + Syringe pump must be in appropriate state for the given operation. + Given rate/volume (when applicable) must be within valid range for the given operation. + + @param operation: unsigned int - ID of operation being requested + @param rate: float - target rate for given operation (if applicable) + @param volume: float - target volume for given operation (if applicable) + @return: 1 if successful, zero otherwise + + Syringe pump operation IDs: + SYRINGE_PUMP_OP_STOP = 0 + SYRINGE_PUMP_OP_RETRACT = 1 + SYRINGE_PUMP_OP_SEEK = 2 + SYRINGE_PUMP_OP_PRIME = 3 + SYRINGE_PUMP_OP_BOLUS = 4 + SYRINGE_PUMP_OP_CONTINUOUS = 5 + """ + + op = integer_to_bytearray(operation) + rat = float_to_bytearray(rate) + vol = float_to_bytearray(volume) + payload = op + rat + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_OPERATION_REQUEST.value, + payload=payload) + + self.logger.debug("requesting syringe pump operation " + str(operation) + + ", rate=" + str(rate) + + ", volume=" + str(volume)) + + # 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_syringe_pump_data_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump data 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("Syringe pump 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_position_override(self, position: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured position override command + Constraints: + Must be logged into HD. + + @param position: integer - position (in encoder counts) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + pos = integer_to_bytearray(position) + payload = rst + pos + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_POSITION_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured position") + + # 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(position) + " encoder counts: " + self.logger.debug("Syringe pump measured position 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_rate_override(self, rate: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured rate override command + Constraints: + Must be logged into HD. + + @param rate: float - rate (in mL/hr) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + rat = float_to_bytearray(rate) + payload = rst + rat + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_RATE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured rate") + + # 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(rate) + " mL/hr: " + self.logger.debug("Syringe pump measured rate 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_force_override(self, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured force override command + Constraints: + Must be logged into HD. + + @param volts: float - volts to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volts) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_FORCE_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured force") + + # 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(volts) + " volts: " + self.logger.debug("Syringe pump measured force 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_detect_override(self, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured syringe detect override command + Constraints: + Must be logged into HD. + + @param volts: float - volts to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volts) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_SYRINGE_DETECT_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured syringe detection") + + # 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(volts) + " volts: " + self.logger.debug("Syringe pump measured syringe detection 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_home_override(self, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured home override command + Constraints: + Must be logged into HD. + + @param volts: float - volts to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volts) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_HOME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured home") + + # 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(volts) + " volts: " + self.logger.debug("Syringe pump measured home 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_meas_volume_override(self, volume: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump measured volume override command + Constraints: + Must be logged into HD. + + @param volume: float - volume (in mL) to override with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_MEASURED_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump measured volume delivered") + + # 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(volume) + " mL: " + self.logger.debug("Syringe pump measured volume 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump status override command + Constraints: + Must be logged into HD. + + @param status: integer - status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x000000FF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump status") + + # 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(status) + self.logger.debug("Syringe pump status 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_encoder_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump encoder status override command + Constraints: + Must be logged into HD. + + @param status: integer - encoder status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x000000FF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_ENCODER_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump encoder status") + + # 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(status) + self.logger.debug("Syringe pump encoder status 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_adc_dac_status_override(self, status: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump ADC & DAC status override command + Constraints: + Must be logged into HD. + + @param status: integer - ADC and DAC status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sts = integer_to_bytearray(status & 0x000000FF) + payload = rst + sts + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_ADC_DAC_STATUS_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump ADC & DAC status") + + # 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(status) + self.logger.debug("Syringe pump ADC & DAC status 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.debug("Timeout!!!!") + return False + + def cmd_syringe_pump_adc_read_counter_override(self, counter: int = 0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the syringe pump ADC read counter override command + Constraints: + Must be logged into HD. + + @param counter: integer - status (0..255) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + ctr = integer_to_bytearray(counter & 0x000000FF) + payload = rst + ctr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_ADC_READ_COUNTER_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD syringe pump ADC read counter") + + # 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(counter) + self.logger.debug("Syringe pump ADC read counter 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.debug("Timeout!!!!") + return False + + def cmd_set_syringe_pump_dac_ref_voltage(self, dac_v_ref: float = 0.15) -> int: + """ + Constructs and sends the set syringe pump DAC pressure sensor reference voltag command + Constraints: + Must be logged into HD. + + @param dac_v_ref: float - status (0.0..3.3) + @return: 1 if successful, zero otherwise + """ + + payload = float_to_bytearray(dac_v_ref) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SYRINGE_PUMP_FORCE_SENSOR_DAC_CALIBRATE.value, + payload=payload) + + self.logger.debug("set HD syringe pump DAC reference voltage") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + str_res = str(dac_v_ref) + self.logger.debug("Syringe pump DAC reference voltage set to " + str_res + "V. " + + 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: shared/scripts/dialin/hd/system_record.py =================================================================== diff -u --- shared/scripts/dialin/hd/system_record.py (revision 0) +++ shared/scripts/dialin/hd/system_record.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,267 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 system_record.py +# +# @author (last) Dara Navaei +# @date (last) 28-Oct-2021 +# @author (original) Dara Navaei +# @date (original) 14-Feb-2021 +# +############################################################################ +import struct +import time +from collections import OrderedDict +from enum import unique +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.nv_ops_utils import NVOpsUtils + + +@unique +class MFGLocation(DialinEnum): + MFG_LOCATION_FACTORY = 0 + + +class HDSystemNVRecords(AbstractSubSystem): + """ + + Hemodialysis Device (HD) Dialin API sub-class for system record commands. + """ + + _RECORD_SPECS_BYTES = 12 + _DEFAULT_MFG_LOCATION = MFGLocation.MFG_LOCATION_FACTORY.value + _MAX_PN_BYTES = 10 + _MAX_SN_BYTES = 15 + _DEFAULT_TIME_VALUE = 0 + _DEFAULT_CRC_VALUE = 0 + + # Maximum allowed bytes that are allowed to be written to EEPROM in firmware + # The padding size then is calculated to be divisions of 16 + _EEPROM_MAX_BYTES_TO_WRITE = 16 + + # Delay in between each payload transfer + _PAYLOAD_TRANSFER_DELAY_S = 0.2 + _DIALIN_RECORD_UPDATE_DELAY_S = 0.2 + + 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._is_getting_system_in_progress = False + self._write_fw_data_to_excel = True + self.sys_data = 0 + self._raw_system_record = [] + self._utilities = NVOpsUtils(logger=self.logger) + # System main record + self.hd_system_record = self._prepare_hd_system_record() + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_dialin_ch_id + msg_id = MsgIds.MSG_ID_HD_SEND_SYSTEM_RECORD.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, self._handler_hd_system_sync) + + def cmd_reset_hd_system_record(self) -> bool: + """ + Handles resetting HD system record. + + @return: True if successful, False otherwise + """ + self.hd_system_record = self._prepare_hd_system_record() + self.hd_system_record = self._utilities.reset_fw_system_service_record(self.hd_system_record) + status = self.cmd_set_hd_system_record(self.hd_system_record) + + return status + + def get_hd_system_record(self) -> dict: + """ + Handles getting HD system record per user's request. + NOTE: In order to get the latest system record, use cmd_request_hd_system_record first + to fetch the system record from the firmware. + + @return: HD system record dictionary + """ + return self.hd_system_record['system_record'] + + def cmd_request_hd_system_record(self) -> int: + """ + Handles getting HD calibration_record data from firmware. + + @return: 1 upon success, False otherwise + """ + if self._is_getting_system_in_progress is not True: + self._is_getting_system_in_progress = True + # Clear the list for the next call + self._raw_system_record.clear() + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_GET_SYSTEM_RECORD.value) + + self.logger.debug('Getting HD system record') + + 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 + + self.logger.debug("Request cancelled: an existing request is in progress.") + return False + + def _handler_hd_system_sync(self, message): + """ + Handles published HD system record messages. HD system records are captured for + processing and updating the HD system record. + + @param message: published HD system 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.sys_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_system_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_system_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_system_record, self._raw_system_record) + self._handler_received_complete_hd_system_record() + + @publish(["hd_system_record"]) + def _handler_received_complete_hd_system_record(self): + """ + Publishes the received system record + + @return: None + """ + self.logger.debug("Received a complete hd system record.") + + def cmd_set_hd_system_record(self, hd_system_record: OrderedDict) -> bool: + """ + Handles updating the HD system and sends it to FW. + + @param hd_system_record: (OrderedDict) the hd system record to be sent + @return: True upon success, False otherwise + """ + record_packets = self._utilities.prepare_record_to_send_to_fw(hd_system_record) + + self.logger.debug('Setting HD system 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_SYSTEM_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 system record.") + return True + + def _prepare_hd_system_record(self): + """ + Handles assembling the sub dictionaries of each group to make the main HD system record. + + @return: (OrderedDict) the assembled hd system record + """ + result = OrderedDict() + groups_byte_size = 0 + # create a list of the functions of the sub dictionaries + functions = [self._prepare_hd_system_group()] + + for function in functions: + # Update the groups bytes size so far to be use to padding later + groups_byte_size += function[1] + # Update the calibration record + result.update(function[0]) + + # Build the CRC of the main calibration_record record + record_crc = OrderedDict({'crc': [' dict: + """ + Gets the status of HD temperature sensors + + @return: The HD temperatures values in a dictionary + """ + return self.hd_temperatures + + @publish(["hd_temperatures"]) + def _handler_temperatures_sync(self, message): + """ + Handles published HD temperatures data messages. Temperatures data are captured for reference. + + @param message: published temperatures data message + @return: none + """ + onboard_thermistor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + power_supply_thermistor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + fpga_board_sensor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + venous_pressure_sensor_temp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + pba_adc_sensor = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + + self.hd_temperatures[HDTemperaturesNames.THERMISTOR_ONBOARD_NTC.name] = onboard_thermistor + self.hd_temperatures[HDTemperaturesNames.THERMISTOR_POWER_SUPPLY_1.name] = power_supply_thermistor + self.hd_temperatures[HDTemperaturesNames.TEMPSENSOR_FPGA_BOARD.name] = fpga_board_sensor + self.hd_temperatures[HDTemperaturesNames.TEMPSENSOR_VENOUS_PRESS_TEMP.name] = venous_pressure_sensor_temp + self.hd_temperatures[HDTemperaturesNames.TEMPSENSOR_PBA_ADC_SENSOR.name] = pba_adc_sensor + + def cmd_temperatures_value_override(self, sensor: int, value: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD temperatures value override command + Constraints: + Must be logged into HD. + + @param sensor: (int) sensor ID that is status is overridden + @param value: (int) value that the temperature sensor will 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) + snsr = integer_to_bytearray(sensor) + vl = float_to_bytearray(value) + payload = reset_value + vl + snsr + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TEMPERATURES_VALUE_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override temperature sensor value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is no content... + if received_message is not None: + + self.logger.debug("Switch " + str(HDTemperaturesNames(sensor).name) + " 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 + + def cmd_temperatures_data_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD temperatures data publication override command. + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: (int) interval (in ms) to override with + @param reset: (int) 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) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TEMPERATURES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Override HD temperatures data broadcast interval") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = str(mis) + self.logger.debug( + "HD temperatures data broadcast interval overridden to " + str_res + " ms: " + + 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: shared/scripts/dialin/hd/treatment.py =================================================================== diff -u --- shared/scripts/dialin/hd/treatment.py (revision 0) +++ shared/scripts/dialin/hd/treatment.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,1551 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from logging import Logger + +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..utils.checks import check_broadcast_interval_override_ms +from .constants import RESET, NO_RESET + + +class HDTreatment(AbstractSubSystem): + """ + + Hemodialysis Delivery (HD) Dialin API sub-class for treatment related commands. + + """ + + # Treatment Parameter IDs + HD_TREATMENT_PARAMETER_BLOOD_FLOW = 0 + HD_TREATMENT_PARAMETER_DIALYSATE_FLOW = 1 + HD_TREATMENT_PARAMETER_TREATMENT_DURATION = 2 + HD_TREATMENT_PARAMETER_HEPARIN_PRE_STOP_TIME = 3 + HD_TREATMENT_PARAMETER_SALINE_BOLUS_VOLUME = 4 + HD_TREATMENT_PARAMETER_ACID_CONCENTRATE = 5 + HD_TREATMENT_PARAMETER_BICARB_CONCENTRATE = 6 + HD_TREATMENT_PARAMETER_DIALYZER_TYPE = 7 + HD_TREATMENT_PARAMETER_BP_MEAS_INTERVAL = 8 + HD_TREATMENT_PARAMETER_RINSEBACK_FLOW_RATE = 9 + HD_TREATMENT_PARAMETER_ART_PRESSURE_LOW_LIMIT = 10 + HD_TREATMENT_PARAMETER_ART_PRESSURE_HIGH_LIMIT = 11 + HD_TREATMENT_PARAMETER_VEN_PRESSURE_LOW_LIMIT = 12 + HD_TREATMENT_PARAMETER_VEN_PRESSURE_HIGH_LIMIT = 13 + HD_TREATMENT_PARAMETER_HEPARIN_DISPENSE_RATE = 14 + HD_TREATMENT_PARAMETER_HEPARIN_BOLUS_VOLUME = 15 + HD_TREATMENT_PARAMETER_DIALYSATE_TEMPERATURE = 16 + HD_TREATMENT_PARAMETER_UF_VOLUME = 17 + + # Dialyzer Type IDs + DIALYZER_TYPE_NIPRO_ELISIO_H_17 = 0 + DIALYZER_TYPE_NIPRO_ELISIO_H_19 = 1 + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F160NRE = 2 + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F180NRE = 3 + + # Acid Concentrate IDs + ACID_CONC_TYPE_FRESENIUS_08_1251_1 = 0 + ACID_CONC_TYPE_FRESENIUS_08_2251_0 = 1 + ACID_CONC_TYPE_FRESENIUS_08_3251_9 = 2 + + # Bicarbonate Concentrate IDs + BICARB_CONC_TYPE_FRESENIUS_CENTRISOL = 0 + + # UF states + UF_START_STATE = 0 # Start state of the ultrafiltration state machine + UF_PAUSED_STATE = 1 # Paused state of the ultrafiltration state machine + UF_RUNNING_STATE = 2 # Running state of the ultrafiltration state machine + + # Saline bolus states + SALINE_BOLUS_STATE_IDLE = 0 # No saline bolus delivery is in progress + SALINE_BOLUS_STATE_WAIT_FOR_PUMPS_STOP = 1 # Wait for pumps to stop before starting bolus + SALINE_BOLUS_STATE_IN_PROGRESS = 2 # A saline bolus delivery is in progress + SALINE_BOLUS_STATE_MAX_DELIVERED = 3 # Maximum saline bolus volume reached + + def __init__(self, can_interface, logger: Logger): + """ + HDTreatment constructor + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_TREATMENT_TIME.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_treatment_time_sync) + msg_id = MsgIds.MSG_ID_TREATMENT_STATE.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_treatment_state_sync) + msg_id = MsgIds.MSG_ID_SALINE_BOLUS_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_saline_bolus_data_sync) + msg_id = MsgIds.MSG_ID_HD_RINSEBACK_PROGRESS.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_rinseback_data_sync) + msg_id = MsgIds.MSG_ID_HD_BLOOD_PRIME_PROGRESS.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_blood_prime_data_sync) + msg_id = MsgIds.MSG_ID_HD_RECIRC_PROGRESS.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_recirculate_data_sync) + msg_id = MsgIds.MSG_ID_HD_TREATMENT_STOP_TIMER_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_treatment_stop_timer_data_sync) + + # treatment duration data + self.treatment_time_prescribed = 0 + self.treatment_time_elapsed = 0 + self.treatment_time_remaining = 0 + # treatment state data + self.treatment_state = 0 + self.treatment_uf_state = 0 + self.saline_bolus_state = 0 + self.heparin_state = 0 + self.rinseback_state = 0 + self.treatment_recirculate_state = 0 + self.blood_prime_state = 0 + self.treatment_end_state = 0 + self.treatment_stop_state = 0 + # saline bolus status + self.saline_bolus_max_vol = 0 + self.saline_bolus_cum_vol = 0.0 + self.saline_bolus_bol_vol = 0.0 + # blood prime status + self.blood_prime_tgt_vol = 0.0 + self.blood_prime_cum_vol = 0.0 + # rinseback status + self.rinseback_tgt_vol = 0.0 + self.rinseback_cum_vol = 0.0 + self.rinseback_cur_rate = 0 + self.rinseback_timeout_secs = 0 + self.rinseback_countdown_secs = 0 + # re-circulation status + self.recirc_timeout_secs = 0 + self.recirc_countdown_secs = 0 + # treatment stop status + self.treatment_stop_timeout_secs = 0 + self.treatment_stop_timeout_coundown_secs = 0 + + def reset(self) -> None: + """ + Reset all treatment variables + + @return: None + """ + # treatment duration data + self.treatment_time_prescribed = 0 + self.treatment_time_elapsed = 0 + self.treatment_time_remaining = 0 + # treatment state data + self.treatment_state = 0 + self.treatment_uf_state = 0 + self.saline_bolus_state = 0 + self.heparin_state = 0 + self.rinseback_state = 0 + self.treatment_recirculate_state = 0 + self.blood_prime_state = 0 + self.treatment_end_state = 0 + self.treatment_stop_state = 0 + self.dialysis_state = 0 + # saline bolus status + self.saline_bolus_max_vol = 0 + self.saline_bolus_cum_vol = 0.0 + self.saline_bolus_bol_vol = 0.0 + # blood prime status + self.blood_prime_tgt_vol = 0.0 + self.blood_prime_cum_vol = 0.0 + self.blood_prime_ind_cum_vol = 0.0 + # rinseback status + self.rinseback_tgt_vol = 0.0 + self.rinseback_cum_vol = 0.0 + self.rinseback_cur_rate = 0 + self.rinseback_timeout_secs = 0 + self.rinseback_countdown_secs = 0 + # re-circulation status + self.recirc_timeout_secs = 0 + self.recirc_countdown_secs = 0 + + def get_treatment_time_prescribed(self) -> int: + """ + Gets the prescribed treatment time + + @return: The prescribed treatment time + """ + return self.treatment_time_prescribed + + def get_treatment_time_elapsed(self) -> int: + """ + Gets the elapsed treatment time + + @return: The elapsed treatment time + """ + return self.treatment_time_elapsed + + def get_treatment_time_remaining(self) -> int: + """ + Gets the remaining treatment time + + @return: The remaining treatment time + """ + return self.treatment_time_remaining + + def get_treatment_state(self) -> int: + """ + Gets the current treatment state + + @return: The current treatment state ID + """ + return self.treatment_state + + def get_treatment_UF_state(self) -> int: + """ + Gets the current treatment ultrafiltration state + + @return: The current treatment ultrafiltration state ID + """ + return self.treatment_uf_state + + def get_treatment_saline_bolus_state(self) -> int: + """ + Gets the current treatment saline bolus state + + @return: The current treatment saline bolus state ID + """ + return self.saline_bolus_state + + def get_treatment_heparin_state(self) -> int: + """ + Gets the current treatment Heparin state + + @return: The current treatment Heparin state ID + """ + return self.heparin_state + + def get_treatment_rinseback_state(self) -> int: + """ + Gets the current treatment rinseback state + + @return: The current treatment rinseback state ID + """ + return self.rinseback_state + + def get_treatment_recirculate_state(self) -> int: + """ + Gets the current treatment recirculate state + + @return: The current treatment recirculate state ID + """ + return self.treatment_recirculate_state + + def get_treatment_blood_prime_state(self) -> int: + """ + Gets the current treatment blood prime state + + @return: The current treatment blood prime state ID + """ + return self.blood_prime_state + + def get_treatment_end_state(self) -> int: + """ + Gets the current treatment end state + + @return: The current treatment end state ID + """ + return self.treatment_end_state + + def get_treatment_stop_state(self) -> int: + """ + Gets the current treatment stop state + + @return: The current treatment stop state ID + """ + return self.treatment_stop_state + + def get_dialysis_state(self) -> int: + """ + Gets the current treatment dialysis state + + @return: The current treatment dialysis state ID + """ + return self.dialysis_state + + def get_saline_bolus_max_volume(self) -> int: + """ + Returns maximum volume (in mL) saline that can be delivered to a patient + + @return: The maximum saline bolus volume + """ + return self.saline_bolus_max_vol + + def get_saline_bolus_cumulative_volume_delivered(self) -> float: + """ + Returns cumulative volume (in mL) of saline delivered + + @return: The cumulative saline volume delivered + """ + return self.saline_bolus_cum_vol + + def get_saline_bolus_volume_delivered(self) -> float: + """ + Returns bolus volume (in mL) of saline delivered + + @return: The bolus saline volume delivered + """ + return self.saline_bolus_bol_vol + + def get_blood_prime_target_volume(self) -> float: + """ + Returns blood prime target volume (in mL) + + @return: The blood prime target volume + """ + return self.blood_prime_tgt_vol + + def get_blood_prime_volume_delivered(self) -> float: + """ + Returns blood prime volume (in mL) delivered + + @return: The blood prime volume delivered + """ + return self.blood_prime_cum_vol + + def get_rinseback_target_volume(self) -> float: + """ + Returns rinseback target volume (in mL) + + @return: The rinseback target volume + """ + return self.rinseback_tgt_vol + + def get_rinseback_volume_delivered(self) -> float: + """ + Returns rinseback volume (in mL) delivered + + @return: The rinseback volume delivered + """ + return self.rinseback_cum_vol + + def get_rinseback_current_rate(self) -> int: + """ + Returns rinseback current rate (in mL/min) + + @return: The rinseback current rate + """ + return self.rinseback_cur_rate + + def get_rinseback_timeout(self) -> int: + """ + Returns rinseback timeout period (in seconds) + + @return: The rinseback timeout period + """ + return self.rinseback_timeout_secs + + def get_rinseback_timeout_countdown(self) -> int: + """ + Returns rinseback timeout countdown (in seconds) + + @return: The rinseback timeout countdown + """ + return self.rinseback_countdown_secs + + def get_recirc_timeout(self) -> int: + """ + Returns recirc timeout (in seconds) + + @return: The recirc timeout + """ + return self.recirc_timeout_secs + + def get_recirc_timeout_countdown(self) -> int: + """ + Returns recirc timeout countdown (in seconds) + + @return: The recirc timeout countdown + """ + return self.recirc_countdown_secs + + @publish([ + "treatment_time_prescribed", + "treatment_time_elapsed", + "treatment_time_remaining" + ]) + def _handler_treatment_time_sync(self, message): + """ + Handles published treatment time data messages. treatment time data are captured + for reference. + + @param message: published treatment time data message + @return: None + """ + + tot = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + ela = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + rem = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + + self.treatment_time_prescribed = tot[0] + self.treatment_time_elapsed = ela[0] + self.treatment_time_remaining = rem[0] + + @publish([ + "treatment_state", + "treatment_uf_state", + "saline_bolus_state", + "heparin_state", + "rinseback_state", + "treatment_recirculate_state", + "blood_prime_state", + "treatment_end_state", + "treatment_stop_state", + "dialysis_state" + ]) + def _handler_treatment_state_sync(self, message): + """ + Handles published treatment state data messages. treatment state data are captured + for reference. + + @param message: published treatment state data message + @return: none + """ + + tst = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + ufs = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + bol = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + hep = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + rin = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + rec = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + bpr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + txe = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + txs = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + dia = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + + self.treatment_state = tst[0] + self.treatment_uf_state = ufs[0] + self.saline_bolus_state = bol[0] + self.heparin_state = hep[0] + self.rinseback_state = rin[0] + self.treatment_recirculate_state = rec[0] + self.blood_prime_state = bpr[0] + self.treatment_end_state = txe[0] + self.treatment_stop_state = txs[0] + self.dialysis_state = dia[0] + + @publish([ + "saline_bolus_max_vol", + "saline_bolus_cum_vol", + "saline_bolus_bol_vol" + ]) + def _handler_saline_bolus_data_sync(self, message): + """ + Handles published saline bolus data messages. Saline bolus data are captured + for reference. + + @param message: published saline bolus data message + @return: none + """ + + mxm = struct.unpack('i', bytearray( + 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])) + bol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + + self.saline_bolus_max_vol = mxm[0] + self.saline_bolus_cum_vol = cum[0] + self.saline_bolus_bol_vol = bol[0] + + @publish([ + "rinseback_tgt_vol", + "rinseback_cum_vol", + "rinseback_cur_rate", + "rinseback_timeout_secs", + "rinseback_countdown_secs" + ]) + def _handler_rinseback_data_sync(self, message): + """ + Handles published rinseback data (progress) messages. Rinseback data are captured + for reference. + + @param message: published rinseback data message + @return: none + """ + + tgt = struct.unpack('f', bytearray( + 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])) + rat = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + tmo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + cdn = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + self.rinseback_tgt_vol = tgt[0] + self.rinseback_cum_vol = cum[0] + self.rinseback_cur_rate = rat[0] + self.rinseback_timeout_secs = tmo[0] + self.rinseback_countdown_secs = cdn[0] + + @publish([ + "blood_prime_tgt_vol", + "blood_prime_cum_vol", + "blood_prime_ind_cum_vol" + ]) + def _handler_blood_prime_data_sync(self, message): + """ + Handles published blood prime data (progress) messages. Blood prime data are captured + for reference. + + @param message: published blood prime data message + @return: none + """ + + tgt = struct.unpack('f', bytearray( + 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", + "recirc_countdown_secs" + ]) + def _handler_recirculate_data_sync(self, message): + """ + Handles published recirculate data (progress) messages. Recirculate data are captured + for reference. + + @param message: published recirculate data message + @return: none + """ + + tmo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cdn = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.recirc_timeout_secs = tmo[0] + self.recirc_countdown_secs = cdn[0] + + @publish(["treatment_stop_timeout_secs", "treatment_stop_timeout_coundown_secs"]) + def _handler_treatment_stop_timer_data_sync(self, message) -> None: + """ + Handles published treatment stop progress data messages. + + @param message: published treatment stop progress data message + @return: None + """ + + tmo = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + cnd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + self.treatment_stop_timeout_secs = tmo[0] + self.treatment_stop_timeout_coundown_secs = cnd[0] + + def cmd_set_treatment_param_blood_flow_rate(self, flow: int) -> int: + """ + Constructs and sends the set blood flow rate treatment parameter command. + This will only set the treatment parameter setting. It will not immediately + set the blood pump on with this set point. + Constraints: + Must be logged into HD. + Flow must be positive integer and should be between 100 and 500 mL/min + + @param flow: integer - set blood flow rate (in mL/min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_BLOOD_FLOW) + flo = integer_to_bytearray(flow) + payload = par + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting blood flow rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + str_flo = str(flow) + self.logger.debug("Blood flow rate parameter set to " + str_flo + " mL/min: " + + 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_set_treatment_param_dialysate_flow_rate(self, flow: int) -> int: + """ + Constructs and sends the set dialysate flow rate treatment parameter command. + This will only set the treatment parameter setting. It will not immediately + set the dialysate inlet pump on with this set point. + Constraints: + Must be logged into HD. + Flow must be positive integer and should be between 100 and 600 mL/min + + @param flow: integer - set blood flow rate (in mL/min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_DIALYSATE_FLOW) + flo = integer_to_bytearray(flow) + payload = par + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting dialysate flow rate") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_flo = str(flow) + self.logger.debug("Dialysate flow rate parameter set to " + str_flo + " mL/min: " + + 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_set_treatment_param_duration(self, duration: int) -> int: + """ + Constructs and sends the set treatment duration parameter command. + Constraints: + Must be logged into HD. + Duration must be positive integer and should be between 60 and 480 min + + @param duration: integer - set treatment duration (in min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_TREATMENT_DURATION) + dur = integer_to_bytearray(duration) + payload = par + dur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting treatment duration") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_dur = str(duration) + self.logger.debug("Treatment duration parameter set to " + str_dur + " min: " + + 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_set_heparin_pre_stop_time(self, pre_stop: int) -> int: + """ + Constructs and sends the set Heparin pre-stop time parameter command. + Constraints: + Must be logged into HD. + Pre-stop time for Heparin must be positive integer and should be between 0 and 120 min + + @param pre_stop: integer - set Heparin pre-stop time (in min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_HEPARIN_PRE_STOP_TIME) + sto = integer_to_bytearray(pre_stop) + payload = par + sto + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting Heparin pre-stop time") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_sto = str(pre_stop) + self.logger.debug("Heparin pre-stop time parameter set to " + str_sto + " min: " + + 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_set_saline_bolus_volume(self, volume: int) -> int: + """ + Constructs and sends the set saline bolus volume parameter command. + Constraints: + Must be logged into HD. + Volume must be positive integer and should be between 0 and 300 mL + + @param volume: integer - set saline bolus volume (in mL) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_SALINE_BOLUS_VOLUME) + vol = integer_to_bytearray(volume) + payload = par + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting saline bolus volume") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_vol = str(volume) + self.logger.debug("Saline bolus volume parameter set to " + str_vol + " mL: " + + 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_set_acid_concentrate(self, acid: int) -> int: + """ + Constructs and sends the set acid concentrate parameter command. + Constraints: + Must be logged into HD. + Acid ID must be positive integer and should be between 0 and 2 (see below) + ACID_CONC_TYPE_FRESENIUS_08_1251_1 = 0 + ACID_CONC_TYPE_FRESENIUS_08_2251_0 = 1 + ACID_CONC_TYPE_FRESENIUS_08_3251_9 = 2 + + @param acid: integer - set acid concentrate type + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_ACID_CONCENTRATE) + acd = integer_to_bytearray(acid) + payload = par + acd + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting acid concentrate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_acd = str(acid) + self.logger.debug("Acid concentrate parameter set to " + str_acd + ": " + + 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_set_bicarb_concentrate(self, bicarb: int) -> int: + """ + Constructs and sends the set bicarbonate concentrate parameter command. + Constraints: + Must be logged into HD. + Bicarb ID must be positive integer and should be between 0 and 0 (see below) + BICARB_CONC_TYPE_FRESENIUS_CENTRISOL = 0 + + @param bicarb: integer - set bicarbonate concentrate type + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_BICARB_CONCENTRATE) + bic = integer_to_bytearray(bicarb) + payload = par + bic + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting bicarbonate concentrate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_bic = str(bicarb) + self.logger.debug("Bicarbonate concentrate parameter set to " + str_bic + ": " + + 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_set_dialyzer_type(self, dialyzer: int) -> int: + """ + Constructs and sends the set dialyzer type parameter command. + Constraints: + Must be logged into HD. + Dialyzer ID must be positive integer and should be between 0 and 3 (see below) + DIALYZER_TYPE_NIPRO_ELISIO_H_17 = 0 + DIALYZER_TYPE_NIPRO_ELISIO_H_19 = 1 + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F160NRE = 2 + DIALYZER_TYPE_FRESENIUS_OPTIFLUX_F180NRE = 3 + + @param dialyzer: integer - set dialyzer type + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_DIALYZER_TYPE) + dia = integer_to_bytearray(dialyzer) + payload = par + dia + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting dialyzer type parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_dia = str(dialyzer) + self.logger.debug("Dialyzer type parameter set to " + str_dia + ": " + + 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_set_bp_measurement_interval(self, intvl: int) -> int: + """ + Constructs and sends the set blood pressure measurement interval parameter command. + Constraints: + Must be logged into HD. + Interval must be positive integer and should be between 0 and 60 min + + @param intvl: integer - set blood pressure measurement interval (in min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_BP_MEAS_INTERVAL) + bpi = integer_to_bytearray(intvl) + payload = par + bpi + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting BP measurement interval parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_bpi = str(intvl) + self.logger.debug("BP measurement interval parameter set to " + str_bpi + ": " + + 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_set_rinseback_flow_rate(self, flow: int) -> int: + """ + Constructs and sends the set rinseback flow rate parameter command. + Constraints: + Must be logged into HD. + Flow must be positive integer and should be between 50 and 175 mL/min + + @param flow: integer - set rinseback flow rate (in mL/min) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_RINSEBACK_FLOW_RATE) + flo = integer_to_bytearray(flow) + payload = par + flo + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting rinseback flow rate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_flo = str(flow) + self.logger.debug("Rinseback flow rate parameter set to " + str_flo + ": " + + 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_set_arterial_pressure_low_alarm_limit(self, pres: int) -> int: + """ + Constructs and sends the set arterial pressure lower alarm limit parameter command. + Constraints: + Must be logged into HD. + Pressure must be integer and should be between -300 and +0 mmHg + + @param pres: integer - set arterial pressure lower alarm limit (in mmHg) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_ART_PRESSURE_LOW_LIMIT) + pre = integer_to_bytearray(pres) + payload = par + pre + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting arterial pressure lower alarm limit parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_pre = str(pres) + self.logger.debug("Arterial pressure lower alarm limit parameter set to " + str_pre + ": " + + 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_set_arterial_pressure_high_alarm_limit(self, pres: int) -> int: + """ + Constructs and sends the set arterial pressure upper alarm limit parameter command. + Constraints: + Must be logged into HD. + Pressure must be integer and should be between -300 and +0 mmHg + + @param pres: integer - set arterial pressure upper alarm limit (in mmHg) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_ART_PRESSURE_HIGH_LIMIT) + pre = integer_to_bytearray(pres) + payload = par + pre + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting arterial pressure upper alarm limit parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_pre = str(pres) + self.logger.debug("Arterial pressure upper alarm limit parameter set to " + str_pre + ": " + + 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_set_venous_pressure_low_alarm_limit(self, pres: int) -> int: + """ + Constructs and sends the set venous pressure lower alarm limit parameter command. + Constraints: + Must be logged into HD. + Pressure must be integer and should be between +20 and +600 mmHg + + @param pres: integer - set venous pressure lower alarm limit (in mmHg) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_VEN_PRESSURE_LOW_LIMIT) + pre = integer_to_bytearray(pres) + payload = par + pre + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting venous pressure lower alarm limit parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_pre = str(pres) + self.logger.debug("Venous pressure lower alarm limit parameter set to " + str_pre + ": " + + 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_set_venous_pressure_high_alarm_limit(self, pres: int) -> int: + """ + Constructs and sends the set venous pressure upper alarm limit parameter command. + Constraints: + Must be logged into HD. + Pressure must be integer and should be between +20 and +600 mmHg + + @param pres: integer - set venous pressure upper alarm limit (in mmHg) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_VEN_PRESSURE_HIGH_LIMIT) + pre = integer_to_bytearray(pres) + payload = par + pre + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting venous pressure upper alarm limit parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_pre = str(pres) + self.logger.debug("Venous pressure upper alarm limit parameter set to " + str_pre + ": " + + 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_set_heparin_dispense_rate(self, rate: float) -> int: + """ + Constructs and sends the set Heparin dispense rate parameter command. + Constraints: + Must be logged into HD. + Rate must be floating point value and should be between 0 and 1.0 mL/hr + + @param rate: float - set Heparin dispense rate (in mL/hr) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_HEPARIN_DISPENSE_RATE) + rat = float_to_bytearray(rate) + payload = par + rat + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting Heparin dispense rate parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_rat = str(rate) + self.logger.debug("Heparin dispense rate parameter set to " + str_rat + ": " + + 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_set_heparin_bolus_volume(self, volume: float) -> int: + """ + Constructs and sends the set Heparin bolus volume parameter command. + Constraints: + Must be logged into HD. + Volume must be floating point value and should be between 0 and 2.0 mL + + @param volume: float - set Heparin bolus volume (in mL) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_HEPARIN_BOLUS_VOLUME) + vol = float_to_bytearray(volume) + payload = par + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting Heparin bolus volume parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_vol = str(volume) + self.logger.debug("Heparin bolus volume parameter set to " + str_vol + ": " + + 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_set_dialysate_temperature(self, temp: float) -> int: + """ + Constructs and sends the set dialysate temperature parameter command. + Constraints: + Must be logged into HD. + Temperature must be floating point value and should be between 35.0 and 38.0 deg C + + @param temp: float - set dialysate temperature (in deg C) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_DIALYSATE_TEMPERATURE) + tmp = float_to_bytearray(temp) + payload = par + tmp + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting dialysate temperature parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_tmp = str(temp) + self.logger.debug("Dialysate temperature parameter set to " + str_tmp + ": " + + 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_set_ultrafiltration_volume(self, volume: float) -> int: + """ + Constructs and sends the set ultrafiltration volume parameter command. + Constraints: + Must be logged into HD. + Volume must be floating point value and should be between 0.0 and 8.0L + + @param volume: float - set ultrafiltration volume (in L) + @return: 1 if successful, zero otherwise + """ + + par = integer_to_bytearray(self.HD_TREATMENT_PARAMETER_UF_VOLUME) + vol = float_to_bytearray(volume) + payload = par + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + + self.logger.debug("setting ultrafiltration volume parameter") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + str_vol = str(volume) + self.logger.debug("Ultrafiltration volume parameter set to " + str_vol + ": " + + 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_treatment_time_remaining_override(self, secs_remaining: int) -> int: + """ + Constructs and sends the treatment time remaining override command + Constraints: + Must be logged into HD. + + @param secs_remaining: integer - number of seconds remaining (must be positive) + @return: 1 if successful, zero otherwise + """ + + payload = integer_to_bytearray(secs_remaining) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_TIME_REMAINING_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD treatment time remaining (in seconds).") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Treatment time remaining overridden to " + str(secs_remaining) + " seconds. " + + 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_blood_prime_volume_delivered_override(self, volume: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the blood prime volume delivered override command + Constraints: + Must be logged into HD. + + @param volume: float - volume (in mL) of blood delivered during blood prime (must be positive) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PRIME_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD blood prime volume delivered (in mL).") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Blood prime volume delivered overridden to " + str(volume) + " mL. " + + 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_rinseback_volume_delivered_override(self, volume: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the rinseback volume delivered override command + Constraints: + Must be logged into HD. + + @param volume: float - volume (in mL) of blood returned during rinseback (must be positive) + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vol = float_to_bytearray(volume) + payload = rst + vol + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_VOLUME_OVERRIDE.value, + payload=payload) + + self.logger.debug("override HD rinseback volume delivered (in mL).") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Rinseback volume delivered overridden to " + str(volume) + " mL. " + + 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_blood_prime_submode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment blood prime sub-mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PRIME_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment blood prime sub-mode data 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("Treatment blood prime sub-mode 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.debug("Timeout!!!!") + return False + + def cmd_treatment_stop_submode_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment stop sub-mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_STOP_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment stop sub-mode data 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("Treatment stop sub-mode 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.debug("Timeout!!!!") + return False + + def cmd_rinseback_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment rinseback sub-mode broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment rinseback sub-mode data 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("Treatment rinseback sub-mode 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.debug("Timeout!!!!") + return False + + def cmd_treatment_param_ranges_broadcast_interval_override(self, ms: int = 60000, reset: int = NO_RESET): + """ + Constructs and sends the treatment parameter ranges broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_RANGES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment parameter ranges data 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("Treatment parameter ranges 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.debug("Timeout!!!!") + return False + + def cmd_treatment_time_broadcast_interval_override(self, ms: int = 250, reset: int = NO_RESET): + """ + Constructs and sends the treatment time data broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_TIME_DATA_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override treatment time data 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("Treatment time 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.debug("Timeout!!!!") + return False + Index: shared/scripts/dialin/hd/ui_proxy.py =================================================================== diff -u --- shared/scripts/dialin/hd/ui_proxy.py (revision 0) +++ shared/scripts/dialin/hd/ui_proxy.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,1700 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 ui_proxy.py +# +# @author (last) Dara Navaei +# @date (last) 15-Oct-2021 +# @author (original) Sean +# @date (original) 15-Apr-2020 +# +############################################################################ +import enum +import struct +from collections import OrderedDict +from logging import Logger + +from ..common.msg_defs import MsgIds, RequestRejectReasons, MsgFieldPositions +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +class HDUIProxy(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for ui commands. + """ + + # misc definitions + LITER_TO_ML_CONVERSION_FACTOR = 1000.0 + MAX_ALARM_VOLUME_LEVEL = 7 + + # UF pause/resume command IDs + UF_CMD_PAUSE = 0 + UF_CMD_RESUME = 1 + + # UF change option IDs + UF_CMD_CHANGE_TIME_TO_ADJUST = 0 + UF_CMD_CHANGE_RATE_TO_ADJUST = 1 + + # in-treatment change response codes + RESPONSE_REJECTED = 0 + RESPONSE_ACCEPTED = 1 + + # HD 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 + + # FPGA + START_POS_FPGA_ID = END_POS_BUILD + END_POS_FPGA_ID = START_POS_FPGA_ID + 1 + START_POS_FPGA_MAJOR = END_POS_FPGA_ID + END_POS_FPGA_MAJOR = START_POS_FPGA_MAJOR + 1 + START_POS_FPGA_MINOR = END_POS_FPGA_MAJOR + END_POS_FPGA_MINOR = START_POS_FPGA_MINOR + 1 + START_POS_FPGA_LAB = END_POS_FPGA_MINOR + END_POS_FPGA_LAB = START_POS_FPGA_LAB + 1 + + class TreatmentParameters(enum.Enum): + + TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN = 0 + TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN = 1 + TREATMENT_PARAM_TREATMENT_DURATION_MIN = 2 + TREATMENT_PARAM_HEPARIN_PRESTOP_MIN = 3 + TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML = 4 + TREATMENT_PARAM_ACID_CONCENTRATE = 5 + TREATMENT_PARAM_BICARB_CONCENTRATE = 6 + TREATMENT_PARAM_DIALYZER_TYPE = 7 + TREATMENT_PARAM_HEPARIN_TYPE = 8 + TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN = 9 + TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN = 10 + TREATMENT_PARAM_ARTERIAL_PRESSURE_LOW_LIMIT_MMHG = 11 + TREATMENT_PARAM_ARTERIAL_PRESSURE_HIGH_LIMIT_MMHG = 12 + TREATMENT_PARAM_VENOUS_PRESSURE_LOW_LIMIT_MMHG = 13 + TREATMENT_PARAM_VENOUS_PRESSURE_HIGH_LIMIT_MMHG = 14 + TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR = 15 + TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML = 16 + TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C = 17 + TREATMENT_PARAM_UF_VOLUME_L = 18 + NUM_OF_TREATMENT_PARAMS = 19 + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + class AlarmUserOptions(enum.Enum): + + ALARM_USER_ACTION_RESUME = 0 + ALARM_USER_ACTION_RINSEBACK = 1 + ALARM_USER_ACTION_END_TREATMENT = 2 + ALARM_USER_ACTION_ACK = 3 + NUMBER_OF_ALARM_USER_ACTIONS = 4 + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + class RinsebackUserActions(enum.Enum): + + REQUESTED_USER_ACTION_RINSEBACK_CONFIRM_START = 0 + REQUESTED_USER_ACTION_RINSEBACK_INCREASE_RATE = 1 + REQUESTED_USER_ACTION_RINSEBACK_DECREASE_RATE = 2 + REQUESTED_USER_ACTION_RINSEBACK_PAUSE = 3 + REQUESTED_USER_ACTION_RINSEBACK_RESUME = 4 + REQUESTED_USER_ACTION_RINSEBACK_END = 5 + REQUESTED_USER_ACTION_RINSEBACK_ADDITIONAL = 6 + REQUESTED_USER_ACTION_RINSEBACK_CONFIRM_DISCONNECT = 7 + REQUESTED_USER_ACTION_RINSEBACK_END_TREATMENT = 8 + REQUESTED_USER_ACTION_RINSEBACK_BACK_TO_TREATMENT = 9 + NUM_OF_REQUESTED_RINSEBACK_USER_ACTIONS = 10 + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + class RecircUserActions(enum.Enum): + + REQUESTED_USER_ACTION_TX_RECIRC_RECONNECT = 0 + REQUESTED_USER_ACTION_TX_RECIRC_CONFIRM_RECONNECT = 1 + REQUESTED_USER_ACTION_TX_RECIRC_RESUME_RC = 2 + REQUESTED_USER_ACTION_TX_RECIRC_END_TREATMENT = 3 + NUM_OF_REQUESTED_TX_RECIRC_USER_ACTIONS = 4 + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + class TreatmentEndUserActions(enum.Enum): + + REQUESTED_USER_ACTION_TX_END_RINSEBACK_START = 0 + NUM_OF_REQUESTED_TX_END_USER_ACTIONS = 1 + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: the Denali CAN interface object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + # register function to handle HD response to UF change requests + if self.can_interface is not None: + channel_id = DenaliChannels.hd_to_ui_ch_id + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_RESPONSE.value, + self._handler_uf_change_response) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_CONFIRMATION_RESPONSE.value, + self._handler_uf_change_confirm_response) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_TREATMENT_TIME_CHANGE_RESPONSE.value, + self._handler_treatment_duration_change_response) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_RESPONSE.value, + self._handler_blood_and_dialysate_change_response) + self.can_interface.register_receiving_publication_function(channel_id, MsgIds.MSG_ID_TREATMENT_PARAM_CHANGE_RANGES.value, + self._handler_treatment_param_ranges) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_VERSION.value, + self._handler_hd_version) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, MsgIds.MSG_ID_UI_NEW_TREATMENT_PARAMS.value, + self._handler_treatment_param_settings) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE.value, + self._handler_treatment_param_settings_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_USER_SALINE_BOLUS_RESPONSE.value, + self._handler_saline_bolus_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, MsgIds.MSG_ID_UI_SET_UF_VOLUME_PARAMETER.value, + self._handler_uf_volume_setting_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_RINSEBACK_CMD_RESPONSE.value, + self._handler_rinseback_cmd_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_RECIRC_CMD_RESPONSE.value, + self._handler_recirc_cmd_response) + self.can_interface.register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, MsgIds.MSG_ID_HD_TX_END_CMD_RESPONSE.value, + self._handler_treatment_end_cmd_response) + self.can_interface.\ + register_receiving_publication_function(DenaliChannels.hd_to_ui_ch_id, + MsgIds.MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_RESPONSE.value, + self._handler_treatment_end_cmd_response) + self.can_interface.\ + register_receiving_publication_function(DenaliChannels.hd_sync_broadcast_ch_id, + MsgIds.MSG_ID_HD_DISINFECT_STANDBY_DATA.value, + self._handler_disinfects_data_publish) + + # initialize variables that will be populated by HD version response + self.hd_version = None + self.fpga_version = None + + # initialize treatment parameters that are seen from UI or that Dialin user sets + self.treatment_parameters = [0.0] * self.TreatmentParameters.NUM_OF_TREATMENT_PARAMS.value + # initialize variables that will be populated by treatment parameters response message + self.treatment_parameters_valid = False + self.treatment_parameters_reject_reasons = [0] * self.TreatmentParameters.NUM_OF_TREATMENT_PARAMS.value + # initialize variables that will be populated by treatment parameter ranges message + self.min_treatment_duration_min = 0 + self.max_treatment_duration_min = 0 + self.min_uf_volume_ml = 0.0 + self.max_uf_volume_ml = 0.0 + self.min_dialysate_flow_rate_ml_min = 0 + self.max_dialysate_flow_rate_ml_min = 0 + # initialize variables that will be populated by response from HD to treatment duration change request + self.duration_change_succeeded = False + self.duration_change_reject_reason = 0 + self.duration_change_time_min = 0 + self.duration_change_uf_vol_ml = 0 + # initialize variables that will be populated by response from HD to UF change request + self.uf_change_succeeded = False + self.uf_change_reject_reason = 0 + self.uf_change_volume_ml = 0.0 + self.uf_change_time_min = 0 + self.uf_change_rate_ml_min = 0.0 + self.uf_change_time_diff = 0 + self.uf_change_rate_diff = 0.0 + self.uf_old_rate_ml_min = 0.0 + # initialize variables that will be populated by response from HD to blood & dialysate flow rate change request + self.blood_and_dialysate_flow_rate_change_succeeded = False + self.blood_and_dialysate_flow_rate_change_reject_reason = 0 + self.target_blood_flow_rate = 0 + self.target_dialysate_flow_rate = 0 + # initialize variables that will be populated by response to saline bolus request + self.saline_bolus_request_succeeded = False + self.saline_bolus_request_reject_reason = 0 + self.saline_bolus_request_bolus_volume = 0 + # initialize variables that will be populated by response to rinseback command + self.rinseback_cmd_succeeded = False + self.rinseback_cmd_reject_reason = 0 + # initialize variables that will be populated by response to recirculate command + self.recirc_cmd_succeeded = False + self.recirc_cmd_reject_reason = 0 + # initialize variables that will be populated by response to treatment end command + self.treatment_end_cmd_succeeded = False + self.treatment_end_cmd_reject_reason = 0 + + self.reject_reasons = OrderedDict() + for attr in RequestRejectReasons: + self.reject_reasons[RequestRejectReasons(attr).name] = RequestRejectReasons(attr).value + self.reject_reasons = OrderedDict(sorted(self.reject_reasons.items(), key=lambda key: key[1])) + + def get_hd_version(self): + """ + Gets the hd version + + @return: The hd version + """ + return self.hd_version + + def get_fpga_version(self): + """ + Gets the fpga version + + @return: the fpga version + """ + return self.fpga_version + + def get_reject_reasons(self): + """ + Gets all possible reject reasons + + @return: OrderedDict(), {"": ... } + """ + return self.reject_reasons + + def get_treatment_parameters(self): + """ + Gets the array of treatment parameters set by user or Dialin + + @return: the array of treatment parameters + """ + return self.treatment_parameters + + def get_treatment_parameters_reject_reasons(self): + """ + Gets the array of reject reasons for treatment parameters by HD firmware + + @return: the array of reject reasons + """ + return self.treatment_parameters_reject_reasons + + def get_treatment_parameters_valid(self): + """ + Gets the T/F flag for whether treatment parameters + are considered valid by HD. + + @return: True if treatment parameters are valid, False if not + """ + return self.treatment_parameters_valid + + def get_min_treatment_duration_min(self): + """ + Gets the min treatment duration + + @return: the min treatment duration (minutes) + """ + return self.min_treatment_duration_min + + def get_max_treatment_duration_min(self): + """ + Gets the max treatment duration + + @return: The max treatment duration (minutes) + """ + return self.max_treatment_duration_min + + def get_min_uf_volume_ml(self): + """ + Gets the min uf volume + + @return: the min uf volume (mL) + """ + return self.min_uf_volume_ml + + def get_max_uf_volume_ml(self): + """ + Gets the max uf volume + + @return: The max uf volume (mL) + """ + return self.max_uf_volume_ml + + def get_min_dialysate_flow_rate_ml_min(self): + """ + Gets the min dialysate flow rate + + @return: The min dialysate flow rate (mL/min) + """ + return self.min_dialysate_flow_rate_ml_min + + def get_max_dialysate_flow_rate_ml_min(self): + """ + Gets the max dialysate flow rate + + @return: The max dialysate flow rate (mL/min) + """ + return self.max_dialysate_flow_rate_ml_min + + def get_duration_change_succeeded(self): + """ + Gets the duration change succeeded status + + @return: (bool) The duration change succeeded status + """ + return self.duration_change_succeeded + + def get_duration_change_reject_reason(self): + """ + Gets the duration change reject reason + + @return: (int) The duration change reject reason + """ + return self.duration_change_reject_reason + + def get_duration_change_time_min(self): + """ + Gets the duration change time + + @return: the duration change time (min) + """ + return self.duration_change_time_min + + def get_duration_change_uf_vol_ml(self): + """ + Gets the duration change uf vol + + @return: the duration change uf vol (mL) + """ + return self.duration_change_uf_vol_ml + + def get_uf_change_succeeded(self): + """ + Gets the uf change succeeded status + + @return: True if succeeded, False otherwise + """ + return self.uf_change_succeeded + + def get_uf_change_reject_reason(self): + """ + Gets the uf change reject reason + @return: (int) The uf change reject reason + """ + return self.uf_change_reject_reason + + def get_uf_change_volume_ml(self): + """ + Gets the uf change volume + @return: The uf change volume (mL) + """ + return self.uf_change_volume_ml + + def get_uf_change_time_min(self): + """ + Gets the uf change time + + @return: The uf change time (min) + """ + return self.uf_change_time_min + + def get_uf_change_rate_ml_min(self): + """ + Gets the uf change rate + + @return: The uf change rate (mL/min) + """ + return self.uf_change_rate_ml_min + + def get_uf_change_time_diff(self): + """ + Gets the uf change time diff + + @return: The uf change time diff + """ + return self.uf_change_time_diff + + def get_uf_change_rate_diff(self): + """ + Gets the uf change rate diff + + @return: The uf change rate diff + """ + return self.uf_change_rate_diff + + def get_blood_and_dialysate_flow_rate_change_succeeded(self): + """ + Gets the blood and dialysate flow rate change succeeded status + + @return: True if successful, False otherwise + """ + return self.blood_and_dialysate_flow_rate_change_succeeded + + def get_blood_and_dialysate_flow_rate_change_reject_reason(self): + """ + Gets the blood and dialysate flow rate change reject reason + + @return: (int) The reason for the rejection + """ + return self.blood_and_dialysate_flow_rate_change_reject_reason + + def get_target_blood_flow_rate(self): + """ + Gets the target blood flow rate + + @return: The target blood flow rate + """ + return self.target_blood_flow_rate + + def get_target_dialysate_flow_rate(self): + """ + Gets the target dialysate flow rate + + @return: The target dialysate flow rate + """ + return self.target_dialysate_flow_rate + + def get_saline_bolus_reject_reason(self): + """ + Gets the reject reason code for the saline bolus request + + @return: The reject reason code for saline bolus request + """ + return self.saline_bolus_request_reject_reason + + def get_saline_bolus_volume(self): + """ + Gets the HD f/w saline bolus volume (in mL) + + @return: The saline bolus volume (in mL) + """ + return self.saline_bolus_request_bolus_volume + + @publish([ + "hd_version", + "fpga_version" + ]) + def _handler_hd_version(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 + + @return: None if not successful, the version string if unpacked successfully + """ + major = struct.unpack('B', bytearray( + message['message'][self.START_POS_MAJOR:self.END_POS_MAJOR])) + minor = struct.unpack('B', bytearray( + message['message'][self.START_POS_MINOR:self.END_POS_MINOR])) + micro = struct.unpack('B', bytearray( + message['message'][self.START_POS_MICRO:self.END_POS_MICRO])) + build = struct.unpack('H', bytearray( + message['message'][self.START_POS_BUILD:self.END_POS_BUILD])) + + fpga_id = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_ID:self.END_POS_FPGA_ID])) + fpga_major = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MAJOR:self.END_POS_FPGA_MAJOR])) + fpga_minor = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_MINOR:self.END_POS_FPGA_MINOR])) + fpga_lab = struct.unpack('B', bytearray( + message['message'][self.START_POS_FPGA_LAB:self.END_POS_FPGA_LAB])) + + if all([len(each) > 0 for each in [major, minor, micro, build]]): + self.hd_version = f"v{major[0]}.{minor[0]}.{micro[0]}-{build[0]}" + self.logger.debug(f"HD VERSION: {self.hd_version}") + + if all([len(each) > 0 for each in [fpga_major, fpga_minor, fpga_id, fpga_lab]]): + self.fpga_version = f"ID: {fpga_id[0]} v{fpga_major[0]}.{fpga_minor[0]}.{fpga_lab[0]}" + self.logger.debug(f"HD FPGA VERSION: {self.fpga_version}") + + @publish([ + "treatment_parameters" + ]) + def _handler_treatment_param_settings(self, message): + """ + Handler for UI msg containing user set treatment parameters. + + @param message: message from UI to HD containing user selected treatment parameters.\n + U32 blood flow rate. \n + U32 dialysate flow rate. \n + U32 treatment duration. \n + U32 Heparin pre-stop time. \n + U32 saline bolus volume. \n + U32 acid concentrate. \n + U32 bicarb concentrate. \n + U32 dialyzer type. \n + U32 heparin type. \n + U32 BP measurement interval. \n + U32 rinseback flow rate. \n + S32 arterial pressure low alarm limit. \n + S32 arterial pressure high alarm limit. \n + S32 venous pressure low alarm limit. \n + S32 venous pressure high alarm limit. \n + F32 Heparin dispense rate. \n + F32 Heparin bolus volume. \n + F32 dialysate temperature. \n + + @return: None + """ + bld = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + dia = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + dur = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + sto = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + sal = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + acd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + bic = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + dlz = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + hpr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + bpi = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + rbf = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + apl = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + aph = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13])) + vpl = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14])) + vph = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15])) + hdr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_16:MsgFieldPositions.END_POS_FIELD_16])) + hbv = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_17:MsgFieldPositions.END_POS_FIELD_17])) + tmp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_18:MsgFieldPositions.END_POS_FIELD_18])) + + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN.value] = bld[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN.value] = dia[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_TREATMENT_DURATION_MIN.value] = dur[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_PRESTOP_MIN.value] = sto[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML.value] = sal[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_ACID_CONCENTRATE.value] = acd[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_BICARB_CONCENTRATE.value] = bic[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_DIALYZER_TYPE.value] = dlz[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_TYPE.value] = hpr[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN.value] = bpi[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN.value] = rbf[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_ARTERIAL_PRESSURE_LOW_LIMIT_MMHG.value] = apl[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_ARTERIAL_PRESSURE_HIGH_LIMIT_MMHG.value] = aph[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_VENOUS_PRESSURE_LOW_LIMIT_MMHG.value] = vpl[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_VENOUS_PRESSURE_HIGH_LIMIT_MMHG.value] = vph[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value] = hdr[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value] = hbv[0] + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value] = tmp[0] + + @publish([ + "treatment_parameters" + ]) + def _handler_uf_volume_setting_response(self, message): + """ + Handler for UI msg containing user set ultrafiltration volume parameter. + + @param message: message from UI to HD containing user selected ultrafiltration volume.\n + F32 ultrafiltration volume (in mL). + + @return: None + """ + tmp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_UF_VOLUME_L.value] = tmp[0] / 1000.0 + + @publish([ + "treatment_parameters_valid", + "treatment_parameters_reject_reasons" + ]) + def _handler_treatment_param_settings_response(self, message): + """ + Handler for response from HD regarding validation of treatment parameters. + + @param message: response message from HD regarding validity of provided treatment parameters.\n + U32 0=Treatment parameters are valid, 1=Treatment parameters are invalid. \n + U32 Reject reason code for blood flow rate (0=valid). \n + U32 Reject reason code for dialysate flow rate (0=valid). \n + U32 Reject reason code for treatment duration (0=valid). \n + U32 Reject reason code for Heparin pre-stop time (0=valid). \n + U32 Reject reason code for saline bolus volume (0=valid). \n + U32 Reject reason code for acid concentrate (0=valid). \n + U32 Reject reason code for bicarb concentrate (0=valid). \n + U32 Reject reason code for dialyzer type (0=valid). \n + U32 Reject reason code for heparin type (0=valid). \n + U32 Reject reason code for BP measurement interval (0=valid). \n + U32 Reject reason code for rinseback flow rate (0=valid). \n + U32 Reject reason code for arterial pressure low alarm limit (0=valid). \n + U32 Reject reason code for arterial pressure high alarm limit (0=valid). \n + U32 Reject reason code for venous pressure low alarm limit (0=valid). \n + U32 Reject reason code for venous pressure high alarm limit (0=valid). \n + U32 Reject reason code for Heparin dispense rate (0=valid). \n + U32 Reject reason code for Heparin bolus volume (0=valid). \n + U32 Reject reason code for dialysate temperature (0=valid). \n + + @return: None + """ + val = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + bld = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + dia = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + dur = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + sto = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + sal = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + acd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + bic = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + dlz = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + hpr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + bpi = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + rbf = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_12:MsgFieldPositions.END_POS_FIELD_12])) + apl = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_13:MsgFieldPositions.END_POS_FIELD_13])) + aph = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_14:MsgFieldPositions.END_POS_FIELD_14])) + vpl = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_15:MsgFieldPositions.END_POS_FIELD_15])) + vph = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_16:MsgFieldPositions.END_POS_FIELD_16])) + hdr = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_17:MsgFieldPositions.END_POS_FIELD_17])) + hbv = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_18:MsgFieldPositions.END_POS_FIELD_18])) + tmp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_19:MsgFieldPositions.END_POS_FIELD_19])) + + if val[0] == 1: + self.treatment_parameters_valid = True + else: + self.treatment_parameters_valid = False + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN.value] = bld[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN.value] = dia[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_TREATMENT_DURATION_MIN.value] = dur[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_PRESTOP_MIN.value] = sto[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML.value] = sal[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_ACID_CONCENTRATE.value] = acd[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_BICARB_CONCENTRATE.value] = bic[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_DIALYZER_TYPE.value] = dlz[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_TYPE.value] = hpr[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN.value] = bpi[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN.value] = rbf[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_ARTERIAL_PRESSURE_HIGH_LIMIT_MMHG.value] = aph[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_ARTERIAL_PRESSURE_LOW_LIMIT_MMHG.value] = apl[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_VENOUS_PRESSURE_HIGH_LIMIT_MMHG.value] = vph[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_VENOUS_PRESSURE_LOW_LIMIT_MMHG.value] = vpl[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value] = hdr[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value] = hbv[0] + self.treatment_parameters_reject_reasons[self.TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value] = tmp[0] + + @publish([ + "min_treatment_duration_min", + "max_treatment_duration_min", + "min_uf_volume_ml", + "max_uf_volume_ml", + "min_dialysate_flow_rate_ml_min", + "max_dialysate_flow_rate_ml_min" + ]) + def _handler_treatment_param_ranges(self, message): + """ + Handler for response from HD regarding valid treatment parameter ranges. + + @param message: response message from HD regarding valid treatment parameter ranges.\n + U32 Minimum treatment duration setting (in min.). \n + U32 Maximum treatment duration setting (in min.). \n + F32 Minimum ultrafiltration volume (in mL). \n + F32 Maximum ultrafiltration volume (in mL). \n + U32 Minimum dialysate flow rate (in mL/min). \n + U32 Maximum dialysate flow rate (in mL/min). + + @return: None + """ + mintime = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + maxtime = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + minufvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + maxufvol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + mindialrt = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + maxdialrt = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + + self.min_treatment_duration_min = mintime[0] + self.max_treatment_duration_min = maxtime[0] + self.min_uf_volume_ml = minufvol[0] + self.max_uf_volume_ml = maxufvol[0] + self.min_dialysate_flow_rate_ml_min = mindialrt[0] + self.max_dialysate_flow_rate_ml_min = maxdialrt[0] + + @publish([ + "saline_bolus_request_succeeded", + "saline_bolus_request_reject_reason", + "saline_bolus_request_bolus_volume" + ]) + def _handler_saline_bolus_response(self, message): + """ + Handler for response from HD regarding saline bolus request. + + @param message: response message from HD regarding saline bolus request.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + U32 Saline bolus volume (mL) + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + vol = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + + if rsp[0] == 1: + self.saline_bolus_request_succeeded = True + else: + self.saline_bolus_request_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.saline_bolus_request_reject_reason = RequestRejectReasons(rea[0]) + self.saline_bolus_request_bolus_volume = vol[0] + + @publish([ + "duration_change_succeeded", + "duration_change_reject_reason", + "duration_change_time_min", + "duration_change_uf_vol_ml" + ]) + def _handler_treatment_duration_change_response(self, message): + """ + Handler for response from HD regarding treatment duration change request. + + @param message: response message from HD regarding treatment duration change.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + U32 treatment duration (min) \n + F32 UF volue (mL) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + tim = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + + self.duration_change_succeeded = rsp[0] + if RequestRejectReasons.has_value(rea[0]): + self.duration_change_reject_reason = RequestRejectReasons(rea[0]) + self.duration_change_time_min = tim[0] + self.duration_change_uf_vol_ml = vol[0] + + @publish([ + "blood_and_dialysate_flow_rate_change_succeeded", + "blood_and_dialysate_flow_rate_change_reject_reason", + "target_blood_flow_rate", + "target_dialysate_flow_rate" + ]) + def _handler_blood_and_dialysate_change_response(self, message): + """ + Handler for response from HD regarding blood & dialysate flow rate change request. + + @param message: response message from HD regarding requested blood & dialysate flow rate settings change.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + U32 Blood flow rate (mL/min) \n + U32 Dialysate flow rate (mL/min) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + bld = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + dil = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.blood_and_dialysate_flow_rate_change_succeeded = resp + if RequestRejectReasons.has_value(rea[0]): + self.blood_and_dialysate_flow_rate_change_reject_reason = RequestRejectReasons(rea[0]) + self.target_blood_flow_rate = bld[0] + self.target_dialysate_flow_rate = dil[0] + + @publish([ + "uf_change_succeeded", + "uf_change_reject_reason", + "uf_change_volume_ml", + "uf_change_time_min", + "uf_change_time_diff", + "uf_change_rate_ml_min", + "uf_change_rate_diff" + ]) + def _handler_uf_change_response(self, message): + """ + Handler for response from HD regarding UF change request. + + @param message: response message from HD regarding requested ultrafiltration settings change.\n + BOOL Accepted \n + U32 RejectReason (if not accepted) + F32 UF Volume (mL) - converted to Liters \n + U32 treatment Time (min) \n + S32 treatment Time Change (min) \n + F32 UF Rate (mL/min) \n + F32 UF Rate Change (mL/min) + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + tim = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + tmd = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + rat = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + rtd = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + ort = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.uf_change_succeeded = resp + if RequestRejectReasons.has_value(rea[0]): + self.uf_change_reject_reason = RequestRejectReasons(rea[0]) + + self.uf_change_volume_ml = vol[0] # / self.LITER_TO_ML_CONVERSION_FACTOR + + self.uf_change_time_min = tim[0] + self.uf_change_time_diff = tmd[0] + self.uf_change_rate_ml_min = rat[0] + self.uf_change_rate_diff = rtd[0] + + self.uf_old_rate_ml_min = ort[0] + + @publish(["uf_change_succeeded", + "uf_change_reject_reason", + "uf_change_volume_ml", + "uf_change_time_min", + "uf_change_rate_ml_min"]) + def _handler_uf_change_confirm_response(self, message): + """ + Handler for response from HD regarding UF change confirmation. + + @param message: response message from HD regarding confirmed ultrafiltration settings change.\n + BOOL Accepted \n + U32 RejectReason (if not accepted) + F32 UF Volume (mL) - converted to Liters \n + U32 treatment Time (min) \n + F32 UF Rate (mL/min) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + vol = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + tim = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + rat = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.uf_change_succeeded = resp + if RequestRejectReasons.has_value(rea[0]): + self.uf_change_reject_reason = RequestRejectReasons(rea[0]) + self.uf_change_volume_ml = vol[0] / self.LITER_TO_ML_CONVERSION_FACTOR + self.uf_change_time_min = tim[0] + self.uf_change_rate_ml_min = rat[0] + + @publish([ + "rinseback_cmd_succeeded", + "rinseback_cmd_reject_reason", + ]) + def _handler_rinseback_cmd_response(self, message): + """ + Handler for response from HD regarding rinseback user command. + + @param message: response message from HD regarding rinseback user command.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + if rsp[0] == 1: + self.rinseback_cmd_succeeded = True + else: + self.rinseback_cmd_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.rinseback_cmd_reject_reason = RequestRejectReasons(rea[0]) + + @publish([ + "recirc_cmd_succeeded", + "recirc_cmd_reject_reason", + ]) + def _handler_recirc_cmd_response(self, message): + """ + Handler for response from HD regarding recirculate user command. + + @param message: response message from HD regarding recirculate user command.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + if rsp[0] == 1: + self.recirc_cmd_succeeded = True + else: + self.recirc_cmd_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.recirc_cmd_reject_reason = RequestRejectReasons(rea[0]) + + @publish([ + "treatment_end_cmd_succeeded", + "treatment_end_cmd_reject_reason", + ]) + def _handler_treatment_end_cmd_response(self, message: dict) -> None: + """ + Handler for response from HD regarding treatment end user command. + + @param message: response message from HD regarding treatment end user command.\n + BOOL Accepted \n + U32 Reject reason (if not accepted) \n + + @return: None + """ + rsp = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + rea = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + + if rsp[0] == 1: + self.treatment_end_cmd_succeeded = True + else: + self.treatment_end_cmd_succeeded = False + if RequestRejectReasons.has_value(rea[0]): + self.treatment_end_cmd_reject_reason = RequestRejectReasons(rea[0]) + + def cmd_ui_checkin_with_hd(self) -> None: + """ + Constructs and sends the ui check-in message + + @return: None + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_CHECK_IN.value) + + self.logger.debug("Sending ui checkin w/ HD") + + self.can_interface.send(message, 0) + + def cmd_ui_request_hd_version(self) -> None: + """ + Constructs and sends the ui request for version message + + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_REQUEST_FW_VERSIONS.value) + + self.logger.debug("Sending ui request for version to HD") + + self.can_interface.send(message, 0) + + def cmd_ui_set_alarm_audio_volume_level(self, volume: int = MAX_ALARM_VOLUME_LEVEL) -> None: + """ + Constructs and sends the ui set alarm volume level message + + @param volume: alarm volume level (1..5) + + """ + + payload = integer_to_bytearray(volume) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SET_ALARM_AUDIO_VOLUME_LEVEL_CMD.value, + payload=payload) + + self.logger.debug("Sending ui request to set alarm audio volume to level " + str(volume) + " to HD") + self.can_interface.send(message, 0) + + def cmd_ui_uf_volume_set(self, uf_volume: float) -> None: + """ + Constructs and sends the ui set ultrafiltration volume parameter message + + @param uf_volume: ultrafiltration volume (in mL) + + """ + + payload = float_to_bytearray(uf_volume) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SET_UF_VOLUME_PARAMETER.value, + payload=payload) + + self.logger.debug("Sending ui request to set ultrafiltration volume parameter of " + str(uf_volume) + " to HD") + self.can_interface.send(message, 0) + + def cmd_ui_uf_pause_resume(self, cmd: int = UF_CMD_PAUSE) -> None: + """ + Constructs and sends a ui UF command message + + @param cmd: 0 for pause, 1 for resume + + @return: None + """ + + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_PAUSE_RESUME_REQUEST.value, + payload=payload) + + if cmd == self.UF_CMD_PAUSE: + str_cmd = "pause" + else: + str_cmd = "resume" + self.logger.debug("Sending UF " + str_cmd + " command.") + + self.can_interface.send(message, 0) + + def cmd_ui_uf_settings_change_request(self, vol: float = 0.0) -> None: + """ + Constructs and sends a ui UF change settings command message + + @param vol: (float) new ultrafiltration volume setting (in L) + + @return: None + """ + + # reset response to this command so we can tell when response is received + # self.UFChangeResponse = None + # build command message + volume = float_to_bytearray(vol * self.LITER_TO_ML_CONVERSION_FACTOR) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_REQUEST.value, + payload=volume) + + self.logger.debug("Sending UF settings change request.") + + self.can_interface.send(message, 0) + + def cmd_ui_uf_settings_change_confirm(self, vol: float = 0.0, adj: int = UF_CMD_CHANGE_TIME_TO_ADJUST) -> None: + """ + Constructs and sends a ui UF change settings command message + + @param vol: (float) new ultrafiltration volume setting (in L) + @param adj: (int) 0 for adjust time, 1 for adjust rate + + @return: None + """ + + # reset response to this command so we can tell when response is received + # self.UFChangeResponse = None + + # build command message + volume = float_to_bytearray(vol * self.LITER_TO_ML_CONVERSION_FACTOR) + adjust = integer_to_bytearray(adj) + payload = volume + adjust + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_CONFIRM_UF_SETTINGS_CHANGE.value, + payload=payload) + + self.logger.debug("Sending UF settings change request.") + + self.can_interface.send(message, 0) + + def cmd_ui_treatment_duration_setting_change_request(self, time_min: int = 0) -> None: + """ + Constructs and sends a ui UF change settings confirmed by user message + + @param time_min: (int) treatment time (in min). + @return: None + """ + + payload = integer_to_bytearray(time_min) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_TREATMENT_TIME_CHANGE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending treatment duration setting change request.") + + # Send message + self.can_interface.send(message, 0) + + def cmd_ui_blood_and_dialysate_flow_settings_change_request(self, blood_flow: int, dial_flow: int) -> None: + + """ + Constructs and sends a ui blood & dialysate flow settings change request by user message + + @param blood_flow: (int) blood flow rate set point (in mL/min). + @param dial_flow: (int) dialysate flow rate set point (in mL/min). + + @return: None + """ + + bld = integer_to_bytearray(blood_flow) + dial = integer_to_bytearray(dial_flow) + payload = bld + dial + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_REQUEST.value, + payload=payload) + + self.logger.debug("Sending blood & dialysate flow rate settings change request.") + + self.can_interface.send(message, 0) + + def cmd_ui_initiate_treatment_request(self, cmnd: int = 1) -> None: + """ + Constructs and sends a ui initiate treatment command + + @param cmnd: (int) start treatment command code (1 = initiate, 0 = cancel) + @return: None + """ + + cmd = integer_to_bytearray(cmnd) + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_INITIATE_TREATMENT_REQUEST.value, + payload=payload) + + self.logger.debug("Sending start treatment command request.") + + self.can_interface.send(message, 0) + + def set_a_treatment_parameter(self, param_idx: int, value: int) -> None: + """ + Constructs and sends a set treatment parameter message + Constraints: + Must be logged into HD. + + @param param_idx: (int) index/enum of parameter to set (see TreatmentParameters enum) + @param value: (int or float - depends on param_idx) value to set for given treatment parameter + + @return: None + """ + if self.TreatmentParameters.has_value(param_idx): + self.treatment_parameters[self.TreatmentParameters(param_idx)] = value + idx = integer_to_bytearray(param_idx) + if param_idx == self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value: + val = float_to_bytearray(value) + elif param_idx == self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value: + val = float_to_bytearray(value) + elif param_idx == self.TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value: + val = float_to_bytearray(value) + elif param_idx == self.TreatmentParameters.TREATMENT_PARAM_UF_VOLUME_L.value: + val = float_to_bytearray(value) + else: + val = integer_to_bytearray(value) + payload = idx + val + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_PARAMETER_TREATMENT_PARAMETER.value, + payload=payload) + self.logger.debug("Setting treatment parameter " + str(param_idx) + " to " + str(value) + ".") + self.can_interface.send(message, 0) + else: + self.logger.debug("Invalid param_idx given.") + + def cmd_set_treatment_parameters(self, bld_flow: int = 100, + dia_flow: int = 100, + duration: int = 240, + hep_rate: float = 0.0, + hep_bol: float = 0.0, + hep_stop: int = 0, + sal_bol: int = 100, + acid: int = 0, + bicarb: int = 0, + dialyzer: int = 0, + heparin: int = 0, + dia_temp: float = 37.0, + art_low: int = -300, + art_high: int = 0, + ven_low: int = 20, + ven_high: int = 400, + bp_intvl: int = 30, + rb_flow: int = 75) -> None: + """ + Constructs and sends a ui set treatment parameters message + Constraints: + HD must be in treatment parameters mode + + @param bld_flow: (int) blood flow rate (in mL/min) + @param dia_flow: (int) dialysate flow rate (in mL/min) + @param duration: (int) treatment duration (in min) + @param hep_rate: (float) Heparin dispense rate (in mL/hr) + @param hep_bol: (float) Heparin bolus volume (in mL) + @param hep_stop: (int) Heparin pre-stop time (in min) + @param sal_bol: (int) Saline bolus volume (in mL) + @param acid: (int) acid concentrate type + @param bicarb: (int) bicarbonate concentrate type + @param dialyzer: (int) dialyzer type + @param heparin: (int) heparin type + @param dia_temp: (float) dialysate temperature (in deg C) + @param art_low: (int) arterial pressure low alarm limit (in mmHg) + @param art_high: (int) arterial pressure high alarm limit (in mmHg) + @param ven_low: (int) venous pressure low alarm limit (in mmHg) + @param ven_high: (int) venous pressure high alarm limit (in mmHg) + @param bp_intvl: (int) blood pressure measurement interval (in min) + @param rb_flow: (int) rinseback flow rate (in mL/min) + + @return: None + """ + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_BLOOD_FLOW_RATE_ML_MIN.value] = bld_flow + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_DIALYSATE_FLOW_RATE_ML_MIN.value] = dia_flow + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_TREATMENT_DURATION_MIN.value] = duration + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_PRESTOP_MIN.value] = hep_stop + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_SALINE_BOLUS_VOLUME_ML.value] = sal_bol + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_ACID_CONCENTRATE.value] = acid + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_BICARB_CONCENTRATE.value] = bicarb + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_DIALYZER_TYPE.value] = dialyzer + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_TYPE.value] = heparin + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_BLOOD_PRESSURE_MEAS_INTERVAL_MIN.value] = bp_intvl + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_RINSEBACK_FLOW_RATE_ML_MIN.value] = rb_flow + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_ARTERIAL_PRESSURE_LOW_LIMIT_MMHG.value] = art_low + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_ARTERIAL_PRESSURE_HIGH_LIMIT_MMHG.value] = art_high + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_VENOUS_PRESSURE_LOW_LIMIT_MMHG.value] = ven_low + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_VENOUS_PRESSURE_HIGH_LIMIT_MMHG.value] = ven_high + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_DISPENSE_RATE_ML_HR.value] = hep_rate + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_HEPARIN_BOLUS_VOLUME_ML.value] = hep_bol + self.treatment_parameters[self.TreatmentParameters.TREATMENT_PARAM_DIALYSATE_TEMPERATURE_C.value] = dia_temp + + bld = integer_to_bytearray(bld_flow) + dia = integer_to_bytearray(dia_flow) + dur = integer_to_bytearray(duration) + hps = integer_to_bytearray(hep_stop) + sal = integer_to_bytearray(sal_bol) + acc = integer_to_bytearray(acid) + bic = integer_to_bytearray(bicarb) + dzr = integer_to_bytearray(dialyzer) + hpr = integer_to_bytearray(heparin) + bpi = integer_to_bytearray(bp_intvl) + rbf = integer_to_bytearray(rb_flow) + apl = integer_to_bytearray(art_low) + aph = integer_to_bytearray(art_high) + vpl = integer_to_bytearray(ven_low) + vph = integer_to_bytearray(ven_high) + hdr = float_to_bytearray(hep_rate) + hbv = float_to_bytearray(hep_bol) + tmp = float_to_bytearray(dia_temp) + + payload = bld + dia + dur + hps + sal + acc + bic + dzr + hpr + bpi + rbf + apl + aph + vpl + vph + hdr + hbv + tmp + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_NEW_TREATMENT_PARAMS.value, + payload=payload) + + self.logger.debug("Sending treatment parameters to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_confirm_treatment_parameters(self, cmd: int = 0) -> None: + """ + Constructs and sends a ui confirm treatment parameters message + Constraints: + Command must be one of the following: + REJECT = 0 (user rejects treatment parameters) + CONFIRM = 1 (uesr confirms treatment parameters) + + @param cmd: (int) confirm treatment parameters command code + + @return: None + """ + + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_USER_CONFIRM_TREATMENT_PARAMS.value, + payload=payload) + + self.logger.debug("Sending confirm treatment parameters message.") + + self.can_interface.send(message, 0) + + def cmd_ui_request_saline_bolus(self, start: bool = False) -> None: + """ + Constructs and sends a ui request for a saline bolus message + Constraints: + HD must be in treatment mode, dialysis sub-mode. + Will not succeed if saline bolus already in progress. + Will not succeed if max. saline volume has already been reached. + + @param start: (bool) True if we're requesting bolus start, False if bolus abort + + @return: none + """ + + if start: + sta = integer_to_bytearray(1) + request = "start" + else: + sta = integer_to_bytearray(0) + request = "abort" + + payload = sta + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_USER_SALINE_BOLUS_REQUEST.value, + payload=payload) + + self.logger.debug("Sending request to " + request + " a saline bolus.") + + self.can_interface.send(message, 0) + + def cmd_ui_rinseback_user_action( + self, action: int = RinsebackUserActions.REQUESTED_USER_ACTION_RINSEBACK_CONFIRM_START.value) -> None: + """ + Constructs and sends a UI rinseback user action message + Constraints: + HD must be in treatment mode, rinseback sub-mode. + + @param action: (enumerated int) User action + CONFIRM_START = 0 (start rinseback) + INCREASE_RATE = 1 (increase rinseback flow rate by 25 mL/min) + DECREASE_RATE = 2 (decrease rinseback flow rate by 25 mL/min) + PAUSE = 3 (pause rinseback) + RESUME = 4 (resume paused rinseback) + RINSEBACK_END = 5 (end rinseback now) + ADDITIONAL = 6 (10 mL more) + CONFIRM_DISCONNECT = 7 (go to re-circulate sub-mode) + END_TREATMENT = 8 (go to post-treatment mode) + BACK_TO_TREATMENT = 9 (ready to resume treatment) + + @return: none + """ + + cmd = integer_to_bytearray(action) + + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_RINSEBACK_CMD.value, + payload=payload) + + self.logger.debug("Sending rinseback command " + str(action) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_recirculate_user_action( + self, action: int = RecircUserActions.REQUESTED_USER_ACTION_TX_RECIRC_RECONNECT.value) -> None: + """ + Constructs and sends a UI recirculate user action message + Constraints: + HD must be in treatment mode, recirculate sub-mode. + + @param action: (enumerated int) User action + RECONNECT = 0 (ready to reconnect patient) + CONFIRM_RECONNECT = 1 (confirm patient is reconnected - ready to resume treatment) + RESUME_RC = 2 (resume recirculation - not ready to reconnect patient) + END_TREATMENT = 3 (go to post-treatment mode) + + @return: none + """ + + cmd = integer_to_bytearray(action) + + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_RECIRC_CMD.value, + payload=payload) + + self.logger.debug("Sending recirculate command " + str(action) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_treatment_end_user_action( + self, action: int = TreatmentEndUserActions.REQUESTED_USER_ACTION_TX_END_RINSEBACK_START.value) -> None: + """ + Constructs and sends a UI treatment end user action message + Constraints: + HD must be in treatment mode, treatment end sub-mode. + + @param action: (enumerated int) User action + RINSEBACK_START = 0 (start final rinseback) + + @return: none + """ + + cmd = integer_to_bytearray(action) + + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_TX_END_CMD.value, + payload=payload) + + self.logger.debug("Sending treatment end command " + str(action) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_user_alarm_response(self, option: int = AlarmUserOptions.ALARM_USER_ACTION_ACK.value) -> None: + """ + Constructs and sends a ui alarm response message. + Constraints: + An alarm must be active. + The selected user action must be enabled for the "top" active alarm and current op. mode. + + @param option: (enum - AlarmUserOptions) ID of user alarm response option \n + 0-RESUME \n + 1-RINSEBACK \n + 2-END TREATMENT \n + 3-ACK + + @return: none + """ + + opt = integer_to_bytearray(option) + payload = opt + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_ALARM_USER_ACTION.value, + payload=payload) + + self.logger.debug("Sending user alarm response option " + str(option) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_silence_alarm(self, toggle: int = 1) -> None: + """ + Constructs and sends a ui alarm response message + + @param toggle: (U32) alarm silence cmd \n + 0-Cancel alarm silence \n + 1-Silence alarms + + @return: none + """ + + cmd = integer_to_bytearray(toggle) + payload = cmd + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_ALARM_USER_ACTION.value, + payload=payload) + + self.logger.debug("Sending user alarm response option " + str(toggle) + " to HD.") + + self.can_interface.send(message, 0) + + def cmd_ui_sample_water(self, cmd: int = 0) -> None: + """ + Constructs and sends a ui water sample request message + + @param cmd: (U32) sample water cmd \n + 0-Stop sample water \n + 1-Start sample water + + @return: none + """ + + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SAMPLE_WATER_CMD.value, + payload=payload) + + self.logger.debug("Sending user sample water command " + str(cmd) + " to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_send_sample_water_result(self, result: int = 1) -> None: + """ + Constructs and sends a ui water sample result message + + @param result: (U32) sample water result \n + 0-Fail \n + 1-Pass + + @return: none + """ + + payload = integer_to_bytearray(result) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_SAMPLE_WATER_RESULT.value, + payload=payload) + + self.logger.debug("Sending user sample water result " + str(result) + " to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_consumable_installation_confirm(self) -> None: + """ + Constructs and sends a ui consumable installation confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_CONSUMABLE_INSTALL_CONFIRM.value) + + self.logger.debug("Sending user consumable installation confirm to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_disposable_installation_confirm(self) -> None: + """ + Constructs and sends a ui disposable installation confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_INSTALLATION_CONFIRM.value) + + self.logger.debug("Sending user disposable installation confirm to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_start_prime_request(self) -> None: + """ + Constructs and sends a ui start prime request message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_START_PRIME_REQUEST.value) + + self.logger.debug("Sending user start prime request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_continue_to_treatment_request(self) -> None: + """ + Constructs and sends a ui continue to treatment request message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_PATIENT_CONNECTION_BEGIN_REQUEST.value) + + self.logger.debug("Sending user continue to treatment request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_patient_connection_confirm(self) -> None: + """ + Constructs and sends a ui patient connection confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_PATIENT_CONNECTION_CONFIRM.value) + + self.logger.debug("Sending user continue to treatment request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_start_treatment_request(self) -> None: + """ + Constructs and sends a ui start treatment request message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_START_TREATMENT_REQUEST.value) + + self.logger.debug("Sending user start treatment request to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_patient_disconnection_confirm(self) -> None: + """ + Constructs and sends a ui patient disconnection confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_PATIENT_DISCONNECTION_CONFIRM.value) + + self.logger.debug("Sending user patient disconnection confirm msg to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_disposable_removal_confirm(self) -> None: + """ + Constructs and sends a ui disposable removal confirm message + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_UI_DISPOSABLE_REMOVAL_CONFIRM.value) + + self.logger.debug("Sending user disposable removal confirm msg to HD.") + self.can_interface.send(message, 0) + + def cmd_ui_set_standby_submode_to_disinfect(self) -> None: + """ + Constructs and sends a ui set standby submode to wait for disinfect + + @return: none + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_STANDBY_DISINFECT_SUB_MODE_REQUEST.value) + + self.logger.debug("Sending setting standby submode to wait for disinfect to HD.") + self.can_interface.send(message, 0) + + @publish(["disinfects_hd_submode", "disinfects_dg_mode"]) + def _handler_disinfects_data_publish(self, message: dict) -> None: + """ + Handles published disinfect mode and submode that is published to UI + + @param message: published RO pump data message + + @return: None + """ + dg_submode = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + hd_state = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + + self.disinfects_hd_submode = hd_state + self.disinfects_dg_mode = dg_submode Index: shared/scripts/dialin/hd/valves.py =================================================================== diff -u --- shared/scripts/dialin/hd/valves.py (revision 0) +++ shared/scripts/dialin/hd/valves.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,409 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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) Quang Nguyen +# @date (last) 05-Aug-2021 +# @author (original) Dara Navaei +# @date (original) 19-Aug-2020 +# +############################################################################ + + +import struct +from enum import unique +from logging import Logger + +from .constants import NO_RESET +from ..common import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.checks import check_broadcast_interval_override_ms +from ..utils.conversions import integer_to_bytearray, float_to_bytearray + + +@unique +class ValvesEnum(DialinEnum): + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + + +@unique +class ValvesPositions(DialinEnum): + VALVE_POSITION_A_INSERT_EJECT = 1 + VALVE_POSITION_B_OPEN = 2 + VALVE_POSITION_C_CLOSE = 3 + + +@unique +class ValvesStates(DialinEnum): + VALVE_STATE_WAIT_FOR_POST = 0 + VALVE_STATE_HOMING_NOT_STARTED = 1 + VALVE_STATE_HOMING_FIND_ENERGIZED_EDGE = 2 + VALVE_STATE_HOMING_FIND_DEENERGIZED_EDGE = 3 + VALVE_STATE_IDLE = 4 + VALVE_STATE_IN_TRANSITION = 5 + VALVE_STATE_IN_BYPASS_MODE = 6 + + +@unique +class AirTrapState(DialinEnum): + STATE_CLOSED = 0 + STATE_OPEN = 1 + + +class HDValves(AbstractSubSystem): + """ + Hemodialysis Device (HD) Dialin API sub-class for valves related commands. + """ + # Valves states publish message field positions + # Note the MsgFieldPosition was not used since some of the published data are S16 + START_POS_VALVES_ID = DenaliMessage.PAYLOAD_START_INDEX + END_POS_VALVES_ID = START_POS_VALVES_ID + 4 + + START_VALVES_STATE = END_POS_VALVES_ID + END_VALVES_STATE = START_VALVES_STATE + 4 + + START_POS_VALVES_CURR_POS = END_VALVES_STATE + END_POS_VALVES_CURR_POS = START_POS_VALVES_CURR_POS + 4 + + START_POS_VALVES_CURR_POS_CNT = END_POS_VALVES_CURR_POS + END_POS_VALVES_CURR_POS_CNT = START_POS_VALVES_CURR_POS_CNT + 2 + + START_POS_VALVES_NEXT_POS_CNT = END_POS_VALVES_CURR_POS_CNT + END_POS_VALVES_NEXT_POS_CNT = START_POS_VALVES_NEXT_POS_CNT + 2 + + START_POS_VALVES_CURRENT = END_POS_VALVES_NEXT_POS_CNT + END_POS_VALVES_CURRENT = START_POS_VALVES_CURRENT + 4 + + START_VALVES_POS_C = END_POS_VALVES_CURRENT + END_VALVES_POS_C = START_VALVES_POS_C + 2 + + START_VALVES_POS_A = END_VALVES_POS_C + END_VALVES_POS_A = START_VALVES_POS_A + 2 + + START_VALVES_POS_B = END_VALVES_POS_A + END_VALVES_POS_B = START_VALVES_POS_B + 2 + + START_VALVES_PWM = END_VALVES_POS_B + END_VALVES_PWM = START_VALVES_PWM + 4 + + START_AIR_TRAP_VALVE_STATUS = END_VALVES_PWM + END_AIR_TRAP_VALVE_STATUS = START_AIR_TRAP_VALVE_STATUS + 4 + + def __init__(self, can_interface, logger: Logger): + """ + DGDrainPump constructor + + @param can_interface: (DenaliCanMessenger) - Denali CAN messenger object. + @param logger: (Logger) - Dialin logger + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_VALVES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_hd_valves_sync) + + # A dictionary of the valves with the status + self.valves_status = {ValvesEnum.VDI.name: {}, ValvesEnum.VDO.name: {}, ValvesEnum.VBA.name: {}, + ValvesEnum.VBV.name: {}} + + self.hd_air_trap_status = 0 + + def get_hd_air_trap_status(self): + """ + Returns the hd air trap status + @return: (str) the HD air trap status + """ + return self.hd_air_trap_status + + def get_hd_valves_status(self): + """ + Gets the hd valves status + + @return: (dict) the hd valves status + """ + return self.valves_status + + def cmd_hd_valves_broadcast_interval_override(self, ms: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends broadcast time interval + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD general task interval (50 ms). + + @param ms: Publish time interval in ms + @param reset: integer - 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_hd_ch_id, + message_id=MsgIds.MSG_ID_VALVES_STATES_PUBLISH_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("Sending {} ms publish interval to the HD valves 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_set_hd_valve_position(self, valve: int, position: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves set position for a valve + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param position: integer - Position number: + VALVE_POSITION_A_INSERT_EJECT = 1 + VALVE_POSITION_B_OPEN = 2 + VALVE_POSITION_C_CLOSE = 3 + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + pos = integer_to_bytearray(position) + payload = reset_value + pos + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_POSITION_OVERRIDE.value, + payload=payload) + # 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("HD cmd_valve_override Timeout!!!") + return False + + def cmd_set_hd_valve_current_override(self, valve: int, current: float, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves set position for a valve + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param current: float value to override current + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + cur = float_to_bytearray(current) + payload = reset_value + cur + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_CURRENT_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug("Setting {} current to {:5.3f} A".format(str(ValvesEnum(valve).name), current)) + + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD current override Timeout!!!") + return False + + def cmd_set_hd_valve_position_count_override(self, valve: int, position_count: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves set position for a valve + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param position_count: integer value + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + pos = integer_to_bytearray(position_count) + payload = reset_value + pos + vlv + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_POSITION_COUNT_OVERRIDE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + + self.logger.debug("Setting {} position to {} ".format(str(ValvesEnum(valve).name), position_count)) + + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("HD current override Timeout!!!") + return False + + def cmd_set_hd_valve_pwm(self, valve: int, pwm: int, direction: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD valves PWM command + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @param pwm: integer - sets the pwm value + @param direction: integer - Direction number: + 0 = Clockwise + 1 = Counter clockwise + @param reset: integer - 1 to reset a previous override, 0 to override + @returns 1 if successful, zero otherwise + """ + reset_value = integer_to_bytearray(reset) + vlv = integer_to_bytearray(valve) + pwm = integer_to_bytearray(pwm) + dir_value = integer_to_bytearray(direction) + payload = reset_value + vlv + pwm + dir_value + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_SET_PWM_OVERRIDE.value, + payload=payload) + # 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("HD cmd_valve_override Timeout!!!") + return False + + def cmd_home_hd_valve(self, valve: int) -> int: + """ + Constructs and sends the HD valves home command + + @param valve: integer - Valve number: + VDI = 0 + VDO = 1 + VBA = 2 + VBV = 3 + @returns 1 if successful, zero otherwise + """ + payload = integer_to_bytearray(valve) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_HOME.value, + payload=payload) + # 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("HD Homing Valve Timeout!!!") + return False + + def cmd_set_hd_air_trap_valve(self, valve_state: int = AirTrapState.STATE_CLOSED.name) -> int: + """ + Constructs and sends an open/close command to the HD air trap valve + + @param valve_state: air trap valve state (open or close) + @returns 1 if successful, zero otherwise + """ + + if valve_state == AirTrapState.STATE_OPEN.value: + payload = integer_to_bytearray(1) + else: + payload = integer_to_bytearray(0) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_VALVES_SET_AIR_TRAP_VALVE.value, + payload=payload) + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + self.logger.debug("Opening air trap valve") + # response payload is OK or not OK + return received_message['message'][DenaliMessage.PAYLOAD_START_INDEX] + else: + self.logger.debug("Opening air trap valve timeout!!") + return False + + @publish(["valves_status", "hd_air_trap_status"]) + def _handler_hd_valves_sync(self, message: dict) -> None: + """ + Handles published HD valves data messages. HD valves data are captured + for reference. + + @param message: published HD valves data message + @returns none + """ + vlv_id = struct.unpack('i', bytearray( + message['message'][self.START_POS_VALVES_ID:self.END_POS_VALVES_ID]))[0] + state_id = struct.unpack('i', bytearray( + message['message'][self.START_VALVES_STATE:self.END_VALVES_STATE]))[0] + pos_id = struct.unpack('i', bytearray( + message['message'][self.START_POS_VALVES_CURR_POS:self.END_POS_VALVES_CURR_POS]))[0] + pos_cnt = struct.unpack('h', bytearray( + message['message'][self.START_POS_VALVES_CURR_POS_CNT:self.END_POS_VALVES_CURR_POS_CNT]))[0] + next_pos = struct.unpack('h', bytearray( + message['message'][self.START_POS_VALVES_NEXT_POS_CNT:self.END_POS_VALVES_NEXT_POS_CNT]))[0] + current = struct.unpack('f', bytearray( + message['message'][self.START_POS_VALVES_CURRENT:self.END_POS_VALVES_CURRENT]))[0] + pos_c = struct.unpack('h', bytearray( + message['message'][self.START_VALVES_POS_C:self.END_VALVES_POS_C]))[0] + pos_a = struct.unpack('h', bytearray( + message['message'][self.START_VALVES_POS_A:self.END_VALVES_POS_A]))[0] + pos_b = struct.unpack('h', bytearray( + message['message'][self.START_VALVES_POS_B:self.END_VALVES_POS_B]))[0] + pwm = struct.unpack('i', bytearray( + message['message'][self.START_VALVES_PWM:self.END_VALVES_PWM]))[0] + air_trap = struct.unpack('i', bytearray( + message['message'][self.START_AIR_TRAP_VALVE_STATUS:self.END_AIR_TRAP_VALVE_STATUS]))[0] + + # To make sure values of the enums are not out of range + if ValvesEnum.has_value(vlv_id) and ValvesPositions.has_value(pos_id) and ValvesStates.has_value(pos_id): + vlv_name = ValvesEnum(vlv_id).name + # Update the valves dictionary + self.valves_status[vlv_name] = {'Valve': vlv_name, 'PosID': ValvesPositions(pos_id).name, 'PosCnt': pos_cnt, + 'Cmd': next_pos, 'State': ValvesStates(state_id).name, 'Current': current, + 'PosA': pos_a, 'PosB': pos_b, 'PosC': pos_c, 'PWM': pwm} + # Update the air trap valve's status + self.hd_air_trap_status = air_trap Index: shared/scripts/dialin/hd/voltages.py =================================================================== diff -u --- shared/scripts/dialin/hd/voltages.py (revision 0) +++ shared/scripts/dialin/hd/voltages.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,199 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 voltages.py +# +# @author (last) Quang Nguyen +# @date (last) 30-Aug-2021 +# @author (original) Sean Nash +# @date (original) 15-Apr-2021 +# +############################################################################ +import struct +from enum import unique +from logging import Logger + +from .constants import RESET, NO_RESET +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem, publish, DialinEnum +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..utils.checks import check_broadcast_interval_override_ms +from ..common.msg_defs import MsgIds, MsgFieldPositions + + +# Monitored voltages +@unique +class HDMonitoredVoltages(DialinEnum): + MONITORED_LINE_1_2V = 0 # Processor voltage (1.2V) + MONITORED_LINE_3_3V = 1 # Logic voltage (3.3V) + MONITORED_LINE_5V_LOGIC = 2 # Logic voltage (5V) + MONITORED_LINE_5V_SENSORS = 3 # Sensors voltage (5V) + MONITORED_LINE_24V = 4 # Actuators voltage (24V) + MONITORED_LINE_24V_REGEN = 5 # Actuators regen voltage (24V) + MONITORED_LINE_FPGA_REF_V = 6 # FPGA ADC reference voltage (1V) + MONITORED_LINE_PBA_REF_V = 7 # PBA ADC reference voltage (3V) + MONITORED_LINE_FPGA_VCC_V = 8 # FPGA Vcc (3V) + MONITORED_LINE_FPGA_AUX_V = 9 # FPGA Vaux (3V) + MONITORED_LINE_FPGA_PVN_V = 10 # FPGA Vpvn (1V) + NUM_OF_MONITORED_VOLTAGE_LINES = 11 # Number of HD operation modes + +class HDVoltages(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for voltage monitor related commands and data. + """ + + def __init__(self, can_interface, logger: Logger): + """ + HDVoltages constructor + + """ + super().__init__() + self.can_interface = can_interface + self.logger = logger + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = MsgIds.MSG_ID_HD_VOLTAGES_DATA.value + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self._handler_monitored_voltages_sync) + self.monitored_voltages = [0.0] * HDMonitoredVoltages.NUM_OF_MONITORED_VOLTAGE_LINES.value + + def get_monitored_voltages(self): + """ + Gets all HD monitored voltages + + @return: List of voltages of size NUM_OF_MONITORED_VOLTAGE_LINES + """ + return self.monitored_voltages + + @publish(["monitored_voltages"]) + def _handler_monitored_voltages_sync(self, message): + """ + Handles published HD monitored voltages data messages. Voltage data are captured + for reference. + + @param message: published monitored voltages data message + @return: none + """ + + v12 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1])) + v33 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2])) + v5l = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3])) + v5s = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4])) + v24 = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5])) + v24g = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_6:MsgFieldPositions.END_POS_FIELD_6])) + vfr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_7:MsgFieldPositions.END_POS_FIELD_7])) + vpr = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_8:MsgFieldPositions.END_POS_FIELD_8])) + vfc = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_9:MsgFieldPositions.END_POS_FIELD_9])) + vfa = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_10:MsgFieldPositions.END_POS_FIELD_10])) + vfp = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_11:MsgFieldPositions.END_POS_FIELD_11])) + + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_1_2V.value] = v12[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_3_3V.value] = v33[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_5V_LOGIC.value] = v5l[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_5V_SENSORS.value] = v5s[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_24V.value] = v24[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_24V_REGEN.value] = v24g[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_REF_V.value] = vfr[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_PBA_REF_V.value] = vpr[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_VCC_V.value] = vfc[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_AUX_V.value] = vfa[0] + self.monitored_voltages[HDMonitoredVoltages.MONITORED_LINE_FPGA_PVN_V.value] = vfp[0] + + def cmd_monitored_voltage_override(self, signal: int = 0, volts: float = 0.0, reset: int = NO_RESET) -> int: + """ + Constructs and sends the HD monitored voltage override command + Constraints: + Must be logged into HD. + Given signal must be valid member of HDMonitoredVoltages enum + + @param signal: integer - ID of signal to override + @param volts: float - value (in volts) to override signal with + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + vlt = float_to_bytearray(volts) + idx = integer_to_bytearray(signal) + payload = rst + vlt + idx + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_MONITORED_VOLTAGES_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored HD voltage for signal " + str(signal)) + + # 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(volts) + " V. " + self.logger.debug("Monitored HD voltage 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.debug("Timeout!!!!") + return False + + def cmd_monitored_voltages_broadcast_interval_override(self, ms: int = 1000, reset: int = NO_RESET) -> int: + """ + Constructs and sends the monitored HD voltages broadcast interval override command + Constraints: + Must be logged into HD. + Given interval must be non-zero and a multiple of the HD 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 + + rst = integer_to_bytearray(reset) + mis = integer_to_bytearray(ms) + payload = rst + mis + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_HD_MONITORED_VOLTAGES_SEND_INTERVAL_OVERRIDE.value, + payload=payload) + + self.logger.debug("override monitored HD voltages 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("HD monitored voltages 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/hd/watchdog.py =================================================================== diff -u --- shared/scripts/dialin/hd/watchdog.py (revision 0) +++ shared/scripts/dialin/hd/watchdog.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,81 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 watchdog.py +# +# @author (last) Quang Nguyen +# @date (last) 11-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +from logging import Logger + +from .constants import RESET, NO_RESET +from ..common import MsgIds +from ..protocols.CAN import DenaliMessage, DenaliChannels +from ..utils.base import AbstractSubSystem +from ..utils.conversions import integer_to_bytearray + + +class HDWatchdog(AbstractSubSystem): + """ + Hemodialysis Delivery (HD) Dialin API sub-class for watchdog related commands. + """ + + def __init__(self, can_interface, logger: Logger): + """ + + @param can_interface: the Denali CAN interface object + """ + + super().__init__() + self.can_interface = can_interface + self.logger = logger + + def cmd_watchdog_task_check_in_override(self, state: int, task: int, reset: int = NO_RESET) -> int: + """ + Constructs and sends the watchdog task check-in override command + Constraints: + Must be logged into HD. + Given task must be valid. + Given state must be a 0 or 1. + + @param state: integer - 1 for task checked in, 0 for task not checked in + @param task: integer - ID of task to override + @param reset: integer - 1 to reset a previous override, 0 to override + @return: 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + sta = integer_to_bytearray(state) + tsk = integer_to_bytearray(task) + payload = rst + sta + tsk + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=MsgIds.MSG_ID_WATCHDOG_TASK_CHECKIN_OVERRIDE.value, + payload=payload) + + self.logger.debug("override watchdog task check-in state") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # self.logger.debug(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("checked in" if state != 0 else "not checked in") + self.logger.debug("watchdog task check-in 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.debug("Timeout!!!!") + return False Index: shared/scripts/dialin/protocols/CAN.py =================================================================== diff -u --- shared/scripts/dialin/protocols/CAN.py (revision 0) +++ shared/scripts/dialin/protocols/CAN.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,801 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 CAN.py +# +# @author (last) Quang Nguyen +# @date (last) 10-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ + +import threading +from collections import deque +import asyncio + +import can +from can.interfaces import socketcan +import math +from time import sleep +from datetime import datetime +import sys +from logging import Logger +import struct +from .. import common +from ..utils import SingletonMeta +from concurrent.futures import ThreadPoolExecutor + + +class DenaliMessage: + BYTE_ORDER = 'little' + START_BYTE = 0xA5 + START_INDEX = 0 + MSG_SEQ_INDEX = 1 + MSG_ID_INDEX = 3 + PAYLOAD_LENGTH_INDEX = 5 + PAYLOAD_START_INDEX = 6 + PAYLOAD_LENGTH_FIRST_PACKET = 1 + HEADER_LENGTH = 6 + PACKET_LENGTH = 8 + CRC_LENGTH = 1 + MAX_MSG_ID_NUMBER = 65535 + MAX_NUMBER_OF_PAYLOAD_BYTES = 254 + CRC_LIST = [ + 0, 49, 98, 83, 196, 245, 166, 151, 185, 136, 219, 234, 125, 76, 31, 46, + 67, 114, 33, 16, 135, 182, 229, 212, 250, 203, 152, 169, 62, 15, 92, 109, + 134, 183, 228, 213, 66, 115, 32, 17, 63, 14, 93, 108, 251, 202, 153, 168, + 197, 244, 167, 150, 1, 48, 99, 82, 124, 77, 30, 47, 184, 137, 218, 235, + 61, 12, 95, 110, 249, 200, 155, 170, 132, 181, 230, 215, 64, 113, 34, 19, + 126, 79, 28, 45, 186, 139, 216, 233, 199, 246, 165, 148, 3, 50, 97, 80, + 187, 138, 217, 232, 127, 78, 29, 44, 2, 51, 96, 81, 198, 247, 164, 149, + 248, 201, 154, 171, 60, 13, 94, 111, 65, 112, 35, 18, 133, 180, 231, 214, + 122, 75, 24, 41, 190, 143, 220, 237, 195, 242, 161, 144, 7, 54, 101, 84, + 57, 8, 91, 106, 253, 204, 159, 174, 128, 177, 226, 211, 68, 117, 38, 23, + 252, 205, 158, 175, 56, 9, 90, 107, 69, 116, 39, 22, 129, 176, 227, 210, + 191, 142, 221, 236, 123, 74, 25, 40, 6, 55, 100, 85, 194, 243, 160, 145, + 71, 118, 37, 20, 131, 178, 225, 208, 254, 207, 156, 173, 58, 11, 88, 105, + 4, 53, 102, 87, 192, 241, 162, 147, 189, 140, 223, 238, 121, 72, 27, 42, + 193, 240, 163, 146, 5, 52, 103, 86, 120, 73, 26, 43, 188, 141, 222, 239, + 130, 179, 224, 209, 70, 119, 36, 21, 59, 10, 89, 104, 255, 206, 157, 172 + ] + + _seq_num = 1 + + @staticmethod + def build_basic_message(channel_id=0, message=None): + """ + Builds a basic message dictionary containing the channel id and message + + @param channel_id: (int) indicates the channel + @param message: (list) integers forming the message + @return:: dictionary with channel_id and message keys + """ + if message is None: + message = [] + return {'channel_id': channel_id, 'message': message} + + @classmethod + def build_message(cls, channel_id=0, message_id=0, payload=None, seq=None): + """ + Builds a Denali message + + @param channel_id: (int) indicates the channel + @param message_id: (int) indicating the request type + @param payload: (list) contains the payload + @param seq: (int) Overrides current sequence number if set + @return:: dictionary with channel_id and 8-byte padded message + """ + + if payload is None: + payload = [] + + message_list = [DenaliCanMessenger.START_BYTE] + + if 0 <= message_id <= DenaliMessage.MAX_MSG_ID_NUMBER: + if seq is None: + # Wrap sequence number if it hits a max int16 + if cls._seq_num >= 32767: + cls._seq_num = 1 + + seq = cls._seq_num + + if message_id not in common.msg_defs.ACK_NOT_REQUIRED: + seq *= -1 + + message_seq_in_bytes = seq.to_bytes(2, byteorder=DenaliMessage.BYTE_ORDER, signed=True) + + message_list += [message_seq_in_bytes[0]] + message_list += [message_seq_in_bytes[1]] + + # Add message ID as unsigned 16-bit # + message_id_in_bytes = message_id.to_bytes(2, byteorder=DenaliMessage.BYTE_ORDER) + + message_list += [message_id_in_bytes[0]] + message_list += [message_id_in_bytes[1]] + + cls._seq_num += 1 + + else: + + return [] + + # Check payload length + payload_length = len(payload) + + # if payload is larger than 255 return nothing + if payload_length <= DenaliMessage.MAX_NUMBER_OF_PAYLOAD_BYTES: + # payload has to be a list + message_list += [payload_length] + else: + return [] + + message_list += payload + + # Because CRC does not include first byte, then we pass a list with out it + message_list += [DenaliMessage.crc8(message_list[1:])] + + message_list = DenaliMessage.pad_message_with_zeros(message_list) + + return DenaliMessage.build_basic_message(channel_id=channel_id, message=message_list) + + @staticmethod + def crc8(message_list): + """ + Returns the calculated crc from a message list + + @param message_list: is a list of integer numbers containing the message + @return:: integer containing a unsigned byte + """ + crc = 0 + for byte in message_list: + unsigned_byte = byte ^ crc + crc = DenaliMessage.CRC_LIST[unsigned_byte] + + return crc + + @staticmethod + def pad_message_with_zeros(message): + """ + Returns a packet padded with zeros that guarantees that the packet is a multiple of 8 bytes. + + @param message: packet that may or may not be multiple of 8 bytes + @return:: packet that is 8-byte multiple + + """ + message_length = len(message) + + # message must be multiple of 8 + if message_length % DenaliMessage.PACKET_LENGTH != 0: + # We need to pad the message with trailing zeros + add_these_many_zeros = math.ceil(message_length / DenaliMessage.PACKET_LENGTH) * \ + DenaliMessage.PACKET_LENGTH - message_length + + message += [0] * add_these_many_zeros + + return message + + @staticmethod + def get_crc(message): + """ + Gets the CRC in message + + @param message: Dialin complete message with CRC + + @return:: CRC in message + """ + + crc_index = DenaliMessage.PAYLOAD_START_INDEX + DenaliMessage.get_payload_length(message) + + return message['message'][crc_index] + + @staticmethod + def verify_crc(message): + """ + Verifies message CRC equals calculated message CRC + + @return:: TRUE if CRC matches, FALSE otherwise + """ + + if message is None: + return False + else: + message_list = message['message'] + + message_length = DenaliMessage.PAYLOAD_START_INDEX + DenaliMessage.get_payload_length(message) + calculated_crc = DenaliMessage.crc8(message_list[1:message_length]) + actual_crc = DenaliMessage.get_crc(message) + + return calculated_crc == actual_crc + + @staticmethod + def get_channel_id(message: dict) -> int: + """ + Returns request ID from message + + @param message: dictionary with channel_id and message keys + @return:: integer with channel id + + """ + + return message['channel_id'] + + @staticmethod + def get_sequence_number(message: dict) -> int: + """ + Returns sequence number from the message + + @param message: dictionary containing the message + @return:: (int) the sequence number + """ + seq = message['message'][DenaliMessage.MSG_SEQ_INDEX:DenaliMessage.MSG_ID_INDEX] + return int.from_bytes(seq, byteorder=DenaliMessage.BYTE_ORDER, signed=True) + + @staticmethod + def create_ack_message(message: dict, passive_mode: bool = True): + """ + Negates the sequence number and replaces the original message's sequence number with the + negated sequence number to create the ACK message. + @param message: (dict) a complete dialin message + @param passive_mode: (dict) true if in passive mode, false otherwise + @return: (dict) ACK message for the input message + """ + message = message.copy() + + seq = struct.unpack('h', bytearray( + message['message'][DenaliMessage.MSG_SEQ_INDEX:DenaliMessage.MSG_ID_INDEX]))[0] + + # send back empty payload since this is an ACK + payload = bytearray() + + channel_id_rx = DenaliMessage.get_channel_id(message) + + if passive_mode: + channel_rx_tx_pairs = { + DenaliChannels.hd_alarm_broadcast_ch_id: None, + DenaliChannels.dg_alarm_broadcast_ch_id: None, + DenaliChannels.ui_alarm_broadcast_ch_id: None, + DenaliChannels.hd_to_dg_ch_id: None, + DenaliChannels.dg_to_hd_ch_id: None, + DenaliChannels.hd_to_ui_ch_id: None, + DenaliChannels.hd_sync_broadcast_ch_id: None, + DenaliChannels.dg_to_ui_ch_id: None, + DenaliChannels.dg_sync_broadcast_ch_id: None, + DenaliChannels.ui_to_hd_ch_id: None, + DenaliChannels.ui_sync_broadcast_ch_id: None, + DenaliChannels.hd_to_dialin_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.dg_to_dialin_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.ui_to_dialin_ch_id: DenaliChannels.dialin_to_ui_ch_id + } + else: + channel_rx_tx_pairs = { + DenaliChannels.hd_alarm_broadcast_ch_id: None, + DenaliChannels.dg_alarm_broadcast_ch_id: None, + DenaliChannels.ui_alarm_broadcast_ch_id: None, + DenaliChannels.hd_to_dg_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.dg_to_hd_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.hd_to_ui_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.hd_sync_broadcast_ch_id: None, + DenaliChannels.dg_to_ui_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.dg_sync_broadcast_ch_id: None, + DenaliChannels.ui_to_hd_ch_id: DenaliChannels.hd_to_ui_ch_id, + DenaliChannels.ui_sync_broadcast_ch_id: DenaliChannels.hd_to_ui_ch_id, + DenaliChannels.hd_to_dialin_ch_id: DenaliChannels.dialin_to_hd_ch_id, + DenaliChannels.dg_to_dialin_ch_id: DenaliChannels.dialin_to_dg_ch_id, + DenaliChannels.ui_to_dialin_ch_id: DenaliChannels.dialin_to_ui_ch_id + } + + channel_id_tx = channel_rx_tx_pairs.get(channel_id_rx, None) + + if channel_id_tx is None: + return None + + message = DenaliMessage.build_message( + channel_id=channel_id_tx, + message_id=common.msg_defs.MsgIds.MSG_ID_ACK_MESSAGE_THAT_REQUIRES_ACK.value, + payload=payload, + seq=-seq) + + return message + + @staticmethod + def get_message_id(message): + """ + Returns request ID from packet + + @param message: complete Diality Packet + + @return:: integer with request ID + + """ + msg_id_array = message['message'][DenaliMessage.MSG_ID_INDEX: + DenaliMessage.PAYLOAD_LENGTH_INDEX] + + return int.from_bytes(msg_id_array, byteorder=DenaliMessage.BYTE_ORDER) + + @staticmethod + def get_payload_length(message): + """ + Returns payload length from message + + @param message: dictionary with channel_id and message keys + @return:: a unsigned payload length + """ + return message['message'][DenaliMessage.PAYLOAD_LENGTH_INDEX] + + @staticmethod + def get_payload(message): + """ + Returns payload array from message + + @param message: dictionary with channel_id and message keys + @return:: a payload array if exist + """ + + payload_length = DenaliMessage.get_payload_length(message) + + if payload_length == 0: + return None + else: + return message['message'][DenaliMessage.PAYLOAD_START_INDEX:] + + @staticmethod + def get_total_packets(message, is_array=True): + """ + Returns the number of packets needed to transmit Denali Message + + @param message: dictionary with channel_id and message keys or raw message + @param is_array: True if message is an array and not a dictionary + @return:: number of packets + """ + the_message = message if is_array else message['message'] + + return math.ceil((the_message[DenaliMessage.PAYLOAD_LENGTH_INDEX] + + DenaliMessage.HEADER_LENGTH + DenaliMessage.CRC_LENGTH) / + DenaliMessage.PACKET_LENGTH) + + +class DenaliChannels: + """ + Convenience class listing all the possible CAN channels used in the Denali system. + Updates made to the "Message List.xlsx" document found in the Diality Software Team SharePoint, + specifically in the CAN Channels sheet, should also be applied here. + """ + + hd_alarm_broadcast_ch_id = 0x001 + dg_alarm_broadcast_ch_id = 0x002 + ui_alarm_broadcast_ch_id = 0x004 + hd_to_dg_ch_id = 0x008 + dg_to_hd_ch_id = 0x010 + hd_to_ui_ch_id = 0x020 + hd_sync_broadcast_ch_id = 0x040 + dg_to_ui_ch_id = 0x070 + dg_sync_broadcast_ch_id = 0x080 + ui_to_hd_ch_id = 0x100 + ui_to_dg_ch_id = 0x110 + ui_sync_broadcast_ch_id = 0x200 + dialin_to_hd_ch_id = 0x400 + hd_to_dialin_ch_id = 0x401 + dialin_to_dg_ch_id = 0x402 + dg_to_dialin_ch_id = 0x403 + dialin_to_ui_ch_id = 0x404 + ui_to_dialin_ch_id = 0x405 + + +class LongDenaliMessageBuilder: + + def __init__(self, message: can.Message): + """ + LongDialityMessageBuilder is a utility object that helps construct a Denali message + that is longer than 8 bytes. It is only called when we don't yet have a long message + builder for the current channel. Basic principle is to construct an object with the + first 8 byte message which contains the length of the message, and the later push the + remaining messages. e.g., let's imagine a 3 message packet. + + obj = LongDialityMessageBuilder(msg1) + + message = obj.push(msg2), returns None, message is not complete + + message = obj.push(msg3), return the packet which is the concatenation of msg1, msg2 and msg3 + + @param message: a CAN message + """ + self.message_data = [b for b in message.data] + self.number_of_can_packets_needed = DenaliMessage.get_total_packets(self.message_data) + self.number_of_can_packets_up_to_now = 1 + + def push(self, message: can.Message, first_packet=False): + """ + push appends the CAN message to the current list of messages + + @param message: 8-byte message + + @param first_packet: True if it is the first packet received + + @return:: None if the packet is not completed, otherwise returns the complete packet + """ + message_data = [b for b in message.data] + if first_packet: + self.message_data = message_data + self.number_of_can_packets_needed = DenaliMessage.get_total_packets(message_data) + self.number_of_can_packets_up_to_now = 1 + + else: + self.message_data += message_data + self.number_of_can_packets_up_to_now += 1 + + if self.number_of_can_packets_up_to_now == self.number_of_can_packets_needed: + return_message = self.message_data + self.message_data = None + return return_message + + else: + return None + + +class DenaliCanMessenger(metaclass=SingletonMeta): + START_BYTE = DenaliMessage.START_BYTE + DIALIN_MSG_RESP_TO = 0.5 # number of seconds to wait for a response to a send command + + def __init__(self, can_interface: str, + logger: Logger, + log_can=False, + passive_mode=True, + console_out=False): + """ + DenaliCanMessenger constructor + + @param can_interface - string containing the can interface, e.g., 'can0" + @return: DialityCanMessenger object + + """ + + self.logger = logger + self.log_can = log_can + self.message_queue = deque() + self.callback_listener_complete_messages = None + self.callback_listener_invalid_messages = None + self.thread_pool_executor = ThreadPoolExecutor(max_workers=1) + + # try to setup can bus and exit if the can bus has not ben setup to use. + try: + self.bus = socketcan.SocketcanBus(channel=can_interface) + self.loop = asyncio.get_event_loop() + if self.bus is not None: + self.thread_canbus = threading.Thread(target=self.listener, daemon=True) + self.thread_message_queue = threading.Thread(target=self.handle_messages, daemon=True) + else: + self.thread_canbus = None + s = "Can connection is not valid" + self.logger.debug(s) + sys.exit(s) + self.listener_buffer = can.AsyncBufferedReader() + self.notifier = can.Notifier(bus=self.bus, listeners=[self.listener_buffer], loop=self.loop) + except Exception as e: + s = str(e) + self.logger.error(s) + print(s) + sys.exit(19) + + self.passive_mode = passive_mode + self.console_out = console_out + self.send_event = threading.Event() + self.long_message_builders = {} + self.long_msg_channel_id_set = set() + self.messages = None + self.command_response_message = None + self.response_channel_id = -1 + self.run = False + self.sync_response_dictionary = {} + self.pending_requests = {} + + def start(self): + """ + starts listening to the can interface. + + """ + + if self.bus is None: + self.logger.error("Cannot start can listener.") + return + else: + self.run = True + if self.thread_message_queue is not None and self.thread_canbus is not None: + if not self.thread_canbus.is_alive(): + self.thread_canbus.start() + self.logger.info("Canbus thread has started.") + if not self.thread_message_queue.is_alive(): + self.thread_message_queue.start() + self.logger.info("Message queue thread has started.") + else: + self.logger.error("Cannot start listener...") + + def stop(self): + """ + Stop listening the can interface + + """ + self.run = False + self.logger.debug("\nCan listener has stopped.") + + def listener(self): + """ + Listens for diality message on the can interface passed during construction. + """ + async def _listener(): + while True: + message = await self.listener_buffer.get_message() + + if message is not None: + self.message_queue.append(message) + + else: # no new packets in receive buffer + # Careful here, making this any shorter will start limiting CPU time for other threads + sleep(0.01) + + if not self.loop.is_running(): + self.loop.run_until_complete(_listener()) + else: + self.loop.create_task(_listener()) + + def handle_messages(self): + """ + Handles messages added to the dialin canbus message queue + @return: None + """ + + while True: + + if not self.message_queue: + # Careful here, making this any shorter will start limiting CPU time for other threads + sleep(0.01) + else: + message: can.Message = self.message_queue.popleft() + + if message.dlc == DenaliMessage.PACKET_LENGTH: + # We have received a legit CAN message of 8 bytes + can_data = [b for b in message.data] + channel_id = message.arbitration_id + if not DenaliMessage.PAYLOAD_LENGTH_INDEX < len(can_data): + self.logger.error("Invalid Denali message received: {0}".format(message)) + self.messages = None # Can't process this message, get the next one + continue + else: + message_length = can_data[DenaliMessage.PAYLOAD_LENGTH_INDEX] + + if self.log_can: + self.do_log_can(message, style="candump", channel=self.bus.channel, send=False) + + # if we are building a long message, then proceed to push it to the channel dictionary + if channel_id in self.long_msg_channel_id_set: + self.messages = self.long_message_builders[channel_id].push(message) + + elif can_data[0] == DenaliMessage.START_BYTE and \ + message_length <= DenaliMessage.PAYLOAD_LENGTH_FIRST_PACKET: # This is a short packet + # This is the first time that we are building a message + self.messages = can_data # deliver the packet + + elif can_data[0] == self.START_BYTE and \ + message_length > DenaliMessage.PAYLOAD_LENGTH_FIRST_PACKET: # Long packet start + # We are starting to build a long message, include it in the lonMsgChannelIDSet + self.long_msg_channel_id_set.add(channel_id) + + # if we don't have a long Denali message builder yet, create it + if channel_id not in self.long_message_builders.keys(): + self.long_message_builders[channel_id] = LongDenaliMessageBuilder(message) + self.messages = None + + else: # if we do have a builder. This is the first time + self.long_message_builders[channel_id].push(message, first_packet=True) + self.messages = None + + # Do we have a complete (long or short) Denali Message? + if self.messages is not None: + message_valid = True # assume true for now, set to false if CRC check fails below + complete_dialin_message = DenaliMessage.build_basic_message(channel_id=channel_id, + message=self.messages) + dialin_msg_id = DenaliMessage.get_message_id(complete_dialin_message) + dialin_ch_id = DenaliMessage.get_channel_id(complete_dialin_message) + + if dialin_ch_id in self.long_msg_channel_id_set: + # We need to remove channel ID from the long message set + self.long_msg_channel_id_set.remove(dialin_ch_id) + + # Need to verify CRC at this point + if not DenaliMessage.verify_crc(complete_dialin_message): + # if verify is False, let's drop (ignore) this message + message_valid = False + dialin_ch_id = None + dialin_msg_id = None + self.logger.critical( + "Incorrect CRC, received message: {}, crc: {}, calculated crc: {}\n".format( + self.messages, DenaliMessage.get_crc(complete_dialin_message), + DenaliMessage.crc8(self.messages))) + + if message_valid: + if self.console_out: + self.do_console_out(complete_dialin_message) + # Send an ack if required + if DenaliMessage.get_sequence_number(complete_dialin_message) < 0: + # ACK required. Send back the received message with the sequence sign bit flipped + msg = DenaliMessage.create_ack_message(message=complete_dialin_message, + passive_mode=False) + if msg is not None: + self.send(msg, 0, is_ack=True) + + # We first check if this is a response to a send request that is pending + if self.pending_requests and dialin_msg_id in self.pending_requests: + + self.pending_requests[dialin_msg_id] = complete_dialin_message + self.send_event.set() + + # If it is not, this is a publication message and we need to call it's register function + elif dialin_ch_id in self.sync_response_dictionary.keys() and \ + dialin_msg_id in self.sync_response_dictionary[channel_id].keys(): + self.thread_pool_executor.submit( + self.sync_response_dictionary[dialin_ch_id][dialin_msg_id], + complete_dialin_message + ) + else: + self.logger.critical("Invalid message: {}\n".format(self.messages)) + + # Done with this message, let's get the next one + self.messages = None + + def register_receiving_publication_function(self, channel_id, message_id, function): + """ + Assign a function with packet parameter to an sync request id, e.g., + def function(packet). + + @param channel_id: can channel number where messages are received + @param message_id: Diality request ID in message + @param function: function reference + """ + + # if the channel_id exist, we just update the dictionary for the channel_id + if channel_id in self.sync_response_dictionary.keys(): + self.sync_response_dictionary[channel_id].update({message_id: function}) + + # otherwise, we need to create the dictionary for the channel_id + else: + self.sync_response_dictionary[channel_id] = {message_id: function} + + def send(self, + built_message: dict, + time_out: float = DIALIN_MSG_RESP_TO, + resend: bool = False, + is_ack: bool = False): + """ + Sends a Denali message + + @param built_message: (dict) message built using DialinMessage class + @param time_out: (float) time it will wait for a response in seconds + @param resend: (bool) Allow resending the message when no response is received. Disabled by default + @param is_ack: (bool) If we're sending an ACK, False by default + @return: (dict) The Denali packet. If a timeout occurs returns None + """ + + msg_sent = False + msg_id = -1 + + # keep trying to send message until we get a response + while not msg_sent: + + channel_id = DenaliMessage.get_channel_id(built_message) + + padded_can_message_array = built_message['message'] + + msg_id = DenaliMessage.get_message_id(built_message) + if not is_ack and msg_id not in self.pending_requests: + self.pending_requests[msg_id] = None + + # A message can be longer than 8 bytes, so we need to split it + # into 8 bytes packets. + + number_of_packets = DenaliMessage.get_total_packets(padded_can_message_array) + + # We are sending one message at a time on CAN + for n in range(number_of_packets): + packet = padded_can_message_array[n * DenaliMessage.PACKET_LENGTH: + (n + 1) * DenaliMessage.PACKET_LENGTH] + + # Sending one packet at a time + packet = can.Message(arbitration_id=channel_id, + data=packet, + is_extended_id=False) + + if self.log_can: + self.do_log_can(packet, style="candump", channel=self.bus.channel, send=True) + self.bus.send(packet, 0) # 0.1) + + # After all messages have been sent, we clear a flag + self.send_event.clear() + + # Block until the timeout completes or until the threading event's flag is set in self.listener, + # indicating a response has been received + self.send_event.wait(time_out) + + # if we're sending an ack, nothing left to do + if is_ack: + return None + + # only resend the message if resend is enabled and we haven't received a response yet + if resend and self.pending_requests.get(msg_id, None) is None: + msg_sent = False + self.logger.debug("No response. Re-sending message.") + else: + msg_sent = True + + response = self.pending_requests.get(msg_id, None) + if response is not None: + del self.pending_requests[msg_id] + return response + + @staticmethod + def _format_message_candump_style(message: can.Message, channel: str, send: bool = True) -> str: + """ + Formats a packet + @param message: (can.Message) The packet to log + @param channel: (str) The channel send or received on + @param send: (bool) Whether we're sending or receiving this packet + @return: The styled message + """ + + tmp = str(message) + + data = tmp[-41:-18].upper() + if send: + data = tmp[-23:].upper() + + return " {0} {1} [{2}] {3}\n".format(channel, str(hex(message.arbitration_id)[2:]).zfill(3), message.dlc, data) + + def do_log_can(self, packet: can.Message, style="candump", channel="can0", send=True): + """ + Logs all packets sent or received by dialin in candump, or non-candump style format + + @param packet: (can.Message) The packet to log + @param style: (str) The style to log in (candump, non-candump) + @param channel: (str) The channel send or received on + @param send: (bool) Whether we're sending or receiving this packet + @return: None + """ + filename = "Dialin_CAN_Send.log" + if not send: + filename = "Dialin_CAN_Receive.log" + if style == "candump": + with open(filename, 'a') as f: + styled_message = self._format_message_candump_style(message=packet, channel=channel, send=send) + f.write(styled_message) + else: + with open(filename, 'a') as f: + f.write("{0}\n".format(packet)) + + def do_console_out(self, complete_dialin_message: any) -> None: + """ + prints out the message in hex format similar to the candump + @param complete_dialin_message: the compele can bus message + @return: None + """ + print( + datetime.now().time(), + " " # can id + f"{(complete_dialin_message['channel_id']):0{3}X}" + " " # head tag + f"{(complete_dialin_message['message'][0]):0{2}X}" + " " # 2 bytes : seq + f"{(complete_dialin_message['message'][1]):0{2}X}" + f"{(complete_dialin_message['message'][2]):0{2}X}" + " " # 2 bytes : msg id + f"{(complete_dialin_message['message'][3]):0{2}X}" + f"{(complete_dialin_message['message'][4]):0{2}X}" + " " # 1 byte : len + f"{(complete_dialin_message['message'][5]):0{2}X}" + # TODO : add the payload here + # the complete message bytes listp + # , complete_dialin_message['message'] + ) + Index: shared/scripts/dialin/protocols/__init__.py =================================================================== diff -u --- shared/scripts/dialin/protocols/__init__.py (revision 0) +++ shared/scripts/dialin/protocols/__init__.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,4 @@ +from .CAN import DenaliCanMessenger +from .CAN import DenaliChannels +from .CAN import LongDenaliMessageBuilder +from .CAN import DenaliMessage Index: shared/scripts/dialin/ui/__init__.py =================================================================== diff -u --- shared/scripts/dialin/ui/__init__.py (revision 0) +++ shared/scripts/dialin/ui/__init__.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,2 @@ +from .hd_simulator import HDSimulator +from .dg_simulator import DGSimulator Index: shared/scripts/dialin/ui/cleanup.sh =================================================================== diff -u --- shared/scripts/dialin/ui/cleanup.sh (revision 0) +++ shared/scripts/dialin/ui/cleanup.sh (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,6 @@ +#!/bin/bash + +cd "$HOME"/denali + +killall -9 simulator.sh + Index: shared/scripts/dialin/ui/crc.py =================================================================== diff -u --- shared/scripts/dialin/ui/crc.py (revision 0) +++ shared/scripts/dialin/ui/crc.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,63 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 crc.py +# +# @author (last) Quang Nguyen +# @date (last) 22-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ + +crc8_table = ( + 0, 49, 98, 83, 196, 245, 166, 151, 185, 136, 219, 234, 125, 76, 31, 46, + 67, 114, 33, 16, 135, 182, 229, 212, 250, 203, 152, 169, 62, 15, 92, 109, + 134, 183, 228, 213, 66, 115, 32, 17, 63, 14, 93, 108, 251, 202, 153, 168, + 197, 244, 167, 150, 1, 48, 99, 82, 124, 77, 30, 47, 184, 137, 218, 235, + 61, 12, 95, 110, 249, 200, 155, 170, 132, 181, 230, 215, 64, 113, 34, 19, + 126, 79, 28, 45, 186, 139, 216, 233, 199, 246, 165, 148, 3, 50, 97, 80, + 187, 138, 217, 232, 127, 78, 29, 44, 2, 51, 96, 81, 198, 247, 164, 149, + 248, 201, 154, 171, 60, 13, 94, 111, 65, 112, 35, 18, 133, 180, 231, 214, + 122, 75, 24, 41, 190, 143, 220, 237, 195, 242, 161, 144, 7, 54, 101, 84, + 57, 8, 91, 106, 253, 204, 159, 174, 128, 177, 226, 211, 68, 117, 38, 23, + 252, 205, 158, 175, 56, 9, 90, 107, 69, 116, 39, 22, 129, 176, 227, 210, + 191, 142, 221, 236, 123, 74, 25, 40, 6, 55, 100, 85, 194, 243, 160, 145, + 71, 118, 37, 20, 131, 178, 225, 208, 254, 207, 156, 173, 58, 11, 88, 105, + 4, 53, 102, 87, 192, 241, 162, 147, 189, 140, 223, 238, 121, 72, 27, 42, + 193, 240, 163, 146, 5, 52, 103, 86, 120, 73, 26, 43, 188, 141, 222, 239, + 130, 179, 224, 209, 70, 119, 36, 21, 59, 10, 89, 104, 255, 206, 157, 172 +) + + +def crc8(data): + """ + generates crc8 for the data vData + @param data: byte of data + @return: (int) the crc code + """ + crc = 0 + length = len(data) + i = 0 + while length > 0: + crc = crc8_table[data[i] ^ crc] + length = length - 1 + i = i + 1 + return crc + + +def calc_crc8(vstring, vdelimiter='.'): + """ + calculates crc8 for each character in string vString + @param vstring: (str) the bytes of data + @param vdelimiter: (character) the string delimiter + @return: the hex formatted crc of the given string + """ + new_str = vstring.replace(vdelimiter, '') + ba = bytearray.fromhex(new_str) + x = '{:02X}'.format(crc8(ba), 'x') + return x Index: shared/scripts/dialin/ui/dg_simulator.py =================================================================== diff -u --- shared/scripts/dialin/ui/dg_simulator.py (revision 0) +++ shared/scripts/dialin/ui/dg_simulator.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,645 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 dg_simulator.py +# +# @author (last) Quang Nguyen +# @date (last) 11-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 16-Mar-2021 +# +############################################################################ +from ..common import * +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils import * +from ..utils.base import AbstractSubSystem, LogManager +from . import messageBuilder + + +class DGSimulator(AbstractSubSystem): + instance_count = 0 + + def __init__(self, can_interface: str = "can0", + log_level: str = None, + console_out: bool = False, + passive_mode: bool = False, + auto_response: bool = False): + + super().__init__() + DGSimulator.instance_count = DGSimulator.instance_count + 1 + + self._log_manager = LogManager(log_level=log_level, log_filepath=self.__class__.__name__ + ".log") + self.logger = self._log_manager.logger + self.console_out = console_out + self.can_interface = DenaliCanMessenger(can_interface=can_interface, + logger=self.logger, + log_can=self._log_manager.log_level == "CAN_ONLY", + console_out=console_out, + passive_mode=passive_mode) + self.can_interface.start() + + if self.can_interface is not None: + channel_id = DenaliChannels.ui_to_dg_ch_id + if auto_response: + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_DG_SET_RTC_REQUEST.value, + self._handler_set_rtc_request) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_REQUEST_FW_VERSIONS.value, + self._handler_request_dg_version) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_UI_REQUEST_SERVICE_INFO.value, + self._handler_system_usage_response) + + def _handler_system_usage_response(self) -> None: + """ + Handles a request for system usage + + @return: None + """ + self.logger.debug("Handling request for system usage.") + + payload = integer_to_bytearray(1619628663) + payload += integer_to_bytearray(1619887863) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_SERVICE_SCHEDULE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_set_rtc_request(self, message: dict) -> None: + """ + Handles a request to set the DG RTC + @param message: (dict) the message content + @return: None + """ + + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + self.logger.debug("DG: Request to set the DG epoch to {0}".format(epoch)) + + self.cmd_send_set_rtc_response(YES, 0) + + def cmd_send_set_rtc_response(self, response: int, reason: int) -> None: + """ + Sends a set RTC response message + + @param response: integer - 0=NO, 1=YES + @param reason: integer - the rejection reason + @return: None + """ + self.logger.debug("DG: Sending response {0} reason {1}".format(response, reason)) + + payload = integer_to_bytearray(response) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_UI_SET_RTC_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_checkin_dg(self) -> None: + """ + check-in (keep alive) message from DG + @return: none + """ + + payload = ["A5", "01", "00", "06", "00", "00", "76", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.dg_to_hd_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_set_dg_ro_pump_data(self, set_pt_pressure: int, flow_rate: float, pwm: float) -> None: + """ + the DG RO Pump Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(F32) | #3:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x1F00| 0x080 | 8 | 1 Hz | N | DG | All | DG RO Pump Data | \ref Data::mPressure | \ref Data::mFlowRate | \ref Data::mPWM | @param vSetPtPressure: + + @param set_pt_pressure: (int) set Point Pressure + @param flow_rate: float - Flow Rate + @param pwm: float - PWM + @return: none + """ + + payload = integer_to_bytearray(set_pt_pressure) + payload += float_to_bytearray(flow_rate) + payload += float_to_bytearray(pwm) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_RO_PUMP_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_pressures_data(self, ro_inlet_pressure: float, ro_outlet_pressure: float, + drain_inlet_pressure: float, drain_outlet_pressure: float) -> None: + """ + the DG Pressures Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | #4:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x2000| 0x080 | 8 | 1 Hz | N | DG | All | DG Pressures Data | \ref Data::mROInletPSI | \ref Data::mROOutletPSI | \ref Data::mDrainInletPSI | \ref Data::mDrainOutletPSI | + + @param ro_inlet_pressure: float - RO Inlet PSI + @param ro_outlet_pressure: float - RO Outlet PSI + @param drain_inlet_pressure: float - Drain Inlet PSI + @param drain_outlet_pressure: float - Drain Outlet PSI + @return: none + """ + + payload = float_to_bytearray(ro_inlet_pressure) + payload += float_to_bytearray(ro_outlet_pressure) + payload += float_to_bytearray(drain_inlet_pressure) + payload += float_to_bytearray(drain_outlet_pressure) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_PRESSURES_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_drain_pump_data(self, set_pt_pwm: int, dac_value: int) -> None: + """ + the DG Drain Pump Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x2400| 0x080 | 8 | 1 Hz | N | DG | All | DG Drain Pump Data | \ref Data::mRPM | \ref Data::mDAC | + + @param set_pt_pwm: integer - Set Point RPM + @param dac_value: integer - DAC Value + @return: none + """ + + payload = integer_to_bytearray(set_pt_pwm) + payload += integer_to_bytearray(dac_value) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DRAIN_PUMP_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_operation_mode(self, dg_op_mode: int) -> None: + """ + the DG Operation Mode Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x2700| 0x080 | 8 | 1 Hz | N | DG | All | DG Operation Mode Data | \ref Data::mOpMode | + + @param dg_op_mode: integer - DG Operation Mode + @return: none + """ + + payload = integer_to_bytearray(dg_op_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_OP_MODE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_reservoir_data(self, active_reservoir: int, fill_to_vol_ml: int, drain_to_vol_ml: int) -> None: + """ + the DG Reservoir Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2800| 0x080 | 8 | 1 Hz | N | DG | All | DG Reservoir Data | \ref Data::mActiveReservoir | \ref Data::mFillToVol | \ref Data::mDrainToVol | + + @param active_reservoir: integer - Active Reservoir + @param fill_to_vol_ml: integer - Fill To Volume ML + @param drain_to_vol_ml: integer - Drain To Vol ML + @return: none + """ + + payload = integer_to_bytearray(active_reservoir) + payload += integer_to_bytearray(fill_to_vol_ml) + payload += integer_to_bytearray(drain_to_vol_ml) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_RESERVOIRS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_valves_states(self, valves_states): + """ + the DG Valves States Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U16) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x2A00| 0x080 | 8 | 2 Hz | N | DG | All | DG Valves States Data | \ref Data::mStates | + + @param valves_states: integer - Valves states + @return: none + """ + + payload = integer_to_bytearray(valves_states) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_VALVES_STATES.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_heaters_data(self, main_primary_dc: int, small_primary_dc: int, trimmer_dc: int) -> None: + """ + the DG Heaters Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2C00| 0x080 | 8 | 2 Hz | N | DG | All | DG Heaters Data | \ref Data::mMainPrimaryDC | \ref Data::mSmallPrimaryDC | \ref Data::mTrimmerDC | + + @param main_primary_dc: integer - Main PriMary DC + @param small_primary_dc: integer - Small Primary DC + @param trimmer_dc: integer - Trimmer DC + @return: none + """ + + payload = integer_to_bytearray(main_primary_dc) + payload += integer_to_bytearray(small_primary_dc) + payload += integer_to_bytearray(trimmer_dc) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_HEATERS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_load_cell_readings_data(self, reservoir1_primary: float, reservoir1_backup: float, + reservoir2_primary: float, reservoir2_backup: float) -> None: + """ + The DG Load Cell Readings Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | #4:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x0C00| 0x080 | 8 | 10 Hz | N | DG | All | DG Load Cell Readings Data | \ref Data::mReservoir1Prim | \ref Data::mReservoir1Bkup | \ref Data::mReservoir2Prim | \ref Data::mReservoir2Bkup | + @param reservoir1_primary: float - Reservoir 1 Primary + @param reservoir1_backup: float - Reservoir 1 Backup + @param reservoir2_primary: float - Reservoir 2 Primary + @param reservoir2_backup: float - Reservoir 2 Backup + @return: none + """ + + payload = float_to_bytearray(reservoir1_primary) + payload += float_to_bytearray(reservoir1_backup) + payload += float_to_bytearray(reservoir2_primary) + payload += float_to_bytearray(reservoir2_backup) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_LOAD_CELL_READINGS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_dg_temperatures_data(self, inlet_primary_heater: float, outlet_primary_heater: float, + conductivity_sensor1: float, conductivity_sensor2: float, + outlet_redundancy: float, inlet_dialysate: float, + primary_heater_thermocouple: float, trimmer_heater_thermocouple: float, + primary_heater_cold_junction: float, trimmer_heater_cold_junction: float, + primary_heater_internal_temp: float, trimmer_heater_internal_temp: float) -> None: + """ + the DG Temperatures Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2D00| 0x080 | 8 | 2 Hz | N | DG | All | DG Temperatures Data | \ref Data::mInletPrimaryHeater | \ref Data::mOutletPrimaryHeater | \ref Data::mConductivitySensor1 | + + | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | #8:(F32) | + |:--: |:--: |:--: |:--: |:--: | + | \ref Data::mConductivitySensor2 | \ref Data::mOutletRedundancy | \ref Data::mInletDialysate | \ref Data::mPrimaryHeaterThermoCouple | \ref Data::mTrimmerHeaterThermoCouple | + + | #9:(F32) | #10:(F32) | #11:(F32) | #12:(F32) | + | :--: |:--: |:--: |:--: | + | \ref Data::mPrimaryHeaterColdJunction | \ref Data::mTrimmerHeaterColdJunction | \ref Data::mPrimaryHeaterInternal | \ref Data::mTrimmerHeaterInternal | + @param inlet_primary_heater: (float) Inlet Primary Heater + @param outlet_primary_heater: (float) Outlet Primary Heater + @param conductivity_sensor1: (float) Conductivity Sensor 1 + @param conductivity_sensor2: (float) Conductivity Sensor 2 + @param outlet_redundancy: (float) Outlet Redundancy + @param inlet_dialysate: (float) Inlet Dialysate + @param primary_heater_thermocouple: (float) Primary Heater Thermocouple + @param trimmer_heater_thermocouple: (float) Trimmer Heater Thermocouple + @param primary_heater_cold_junction: (float) Primary Heater ColdJunction + @param trimmer_heater_cold_junction: (float) Trimmer Heater ColdJunction + @param primary_heater_internal_temp: (float) Primary Heater Internal Temperature + @param trimmer_heater_internal_temp: (float) Trimmer HeaterInternal Temperature + @return: none + """ + + payload = float_to_bytearray(inlet_primary_heater) + payload += float_to_bytearray(outlet_primary_heater) + payload += float_to_bytearray(conductivity_sensor1) + payload += float_to_bytearray(conductivity_sensor2) + payload += float_to_bytearray(outlet_redundancy) + payload += float_to_bytearray(inlet_dialysate) + payload += float_to_bytearray(primary_heater_thermocouple) + payload += float_to_bytearray(trimmer_heater_thermocouple) + payload += float_to_bytearray(primary_heater_cold_junction) + payload += float_to_bytearray(trimmer_heater_cold_junction) + payload += float_to_bytearray(primary_heater_internal_temp) + payload += float_to_bytearray(trimmer_heater_internal_temp) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_TEMPERATURE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_unknown_dg(self) -> None: + """ + the unknown message from DG setter/sender method + @return: none + """ + + payload = integer_to_bytearray(0) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_UNUSED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_accelerometer_dg_data(self, x: float, y: float, z: float, + x_max: float, y_max: float, z_max: float, + x_tilt: float, y_tilt: float, z_tilt: float) -> None: + """ + the accelerometer hd data message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: | + |0x3400| 0x080 | 8 | 1Hz | N | HD | UI | DG Accelerometer data | + + | #1:(F32) | #2:(F32) | #3:(U32) | + |:--: |:--: |:--: | + | \ref Data::mX | \ref Data::mY | \ref Data::mX | + + | #4:(F32) | #5:(F32) | #6:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXMax | \ref Data::mYMax | \ref Data::mXMax | + + | #7:(F32) | #8:(F32) | #9:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXTilt | \ref Data::mYTilt | \ref Data::mXTilt | + + @param x: float - x axis + @param y: float - y axis + @param z: float - z axis + @param x_max: float - x axis max + @param y_max: float - y axis max + @param z_max: float - z axis max + @param x_tilt: float - x axis tilt + @param y_tilt: float - y axis tilt + @param z_tilt: float - z axis tilt + @return: None + """ + + payload = float_to_bytearray(x) + payload += float_to_bytearray(y) + payload += float_to_bytearray(z) + payload += float_to_bytearray(x_max) + payload += float_to_bytearray(y_max) + payload += float_to_bytearray(z_max) + payload += float_to_bytearray(x_tilt) + payload += float_to_bytearray(y_tilt) + payload += float_to_bytearray(z_tilt) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_ACCELEROMETER_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_request_dg_version(self) -> None: + """ + Handles a request for the HD version + + @return: None + """ + self.logger.debug("Handling request for dg version.") + + self.cmd_send_version_dg_data(9, 9, 9, 9, 9, 9, 9, 9) + self.cmd_send_dg_serial_number() + + def cmd_send_dg_serial_number(self) -> None: + """ + Sends the dg serial number + @return: None + """ + payload = b'0123456789\0' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_SERIAL_NUMBER.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_version_dg_data(self, major: int, minor: int, micro: int, build: int, + fpga_id: int, fpga_major: int, fpga_minor: int, fpga_lab: int) -> None: + """ + the dg version response message method + @param major: integer - Major version number + @param minor: integer - Minor version number + @param micro: integer - Micro version number + @param build: integer - Build version number + @param fpga_id: integer - FPGA id version number + @param fpga_major: integer - FPGA Major version number + @param fpga_minor: integer - FPGA Minor version number + @param fpga_lab: integer - FPGA Lab version number + @return: None + """ + + payload = byte_to_bytearray(major) + payload += byte_to_bytearray(minor) + payload += byte_to_bytearray(micro) + payload += short_to_bytearray(build) + payload += byte_to_bytearray(fpga_id) + payload += byte_to_bytearray(fpga_major) + payload += byte_to_bytearray(fpga_minor) + payload += byte_to_bytearray(fpga_lab) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_VERSION.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_serial_dg_data(self, serial: str): + """ + the dg version serial response message method + @param serial: serial number + @return: None + """ + + payload = bytes(serial, 'ascii') + b'\x00' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_SERIAL_NUMBER.value, + payload=payload) + + self.can_interface.send(message, 0) + + @staticmethod + def build_dg_debug_text(text: str) -> list: + """ + the debug text message from DG builder method + @param text: string - the debug text + @return: none + """ + message_length = 40 + txt = messageBuilder.textToByte(text, message_length) # + 1 null term + msg = messageBuilder.buildMessage(GuiActionType.DGDebugText, 1 * (message_length + 1), False, txt) + return messageBuilder.toFrames(msg) + + def cmd_send_dg_pre_treatment_filter_flush_progress_data(self, total, countdown) -> None: + """ + send the pretreatment filter flush progress data + @param total: (U32) Total time in second + @param countdown: (U32) count down time in second + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_FILTER_FLUSH_PROGRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_disinfect_progress_time_flush(self, total: int, countdown: int) -> None: + """ + the broadcast progress water flush time + @param total: the total time + @param countdown: the gradual countdown time + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_FLUSH_TIME_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_disinfect_progress_time_heat(self, total: int, countdown: int) -> None: + """ + the broadcast progress heat disinfect time + @param total: the total time + @param countdown: the gradual countdown time + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_HEAT_DISINFECT_TIME_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_post(self, item: int, passed: bool, done: bool = False) -> None: + """ + send hd post message the single(item) or the final(done) + @param item: the post state/item index + @param passed: the post result single or final + @param done: if this is the final post message this should be true + @return: None + """ + payload = integer_to_bytearray(passed) + if not done: + payload += integer_to_bytearray(item) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DG_POST_FINAL_TEST_RESULT.value if done + else MsgIds.MSG_ID_DG_POST_SINGLE_TEST_RESULT.value, + payload=payload) + + self.can_interface.send(message, 0) + + # ------------------------------------------------ GENERAL MESSAGES ------------------------------------------------ + + def cmd_send_dg_disinfect_progress_time_checmical(self, total: int, countdown: int) -> None: + """ + the broadcast progress chemical disinfect time + @param total: the total time + @param countdown: the gradual countdown time + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=MsgIds.MSG_ID_DG_CHEM_DISINFECT_TIME_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_general_response(self, message_id: int, accepted: int, reason: int, + is_pure_data: bool = False, + has_parameters: bool = False, + parameters_payload: any = 0x00) -> None: + """ + a general method to send any standard response message, by it's id and list of paramters. + @param message_id: the id of the message + @param accepted: the standard accepted parameter of any response message + @param reason: the standard rejection reason parameter of any response message + @param is_pure_data: The message only has data + @param has_parameters: if the message has parameter this needs to be true. + @param parameters_payload: the list of parameters pre-converted and ready to be concatenated to the payload. + @return: None + """ + payload = "" + + if not is_pure_data: + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + if has_parameters: + payload += parameters_payload + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=message_id, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_sg_general_progress_data(self, message_id: int, total: int, countdown: int) -> None: + """ + a general method t send any standard progress data message, by it's id + @param message_id: the id of the message + @param total: the total value of the progress data + @param countdown: the remaining or countdown value + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=message_id, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_dg_ack(self, seq: int) -> None: + """ + sending dg ack message by the sequence seq + @param seq: the message sequence number + @return: None + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_ui_ch_id, + message_id=GuiActionType.Acknow, + seq=seq) + + self.can_interface.send(message, 0) Index: shared/scripts/dialin/ui/hd_simulator.py =================================================================== diff -u --- shared/scripts/dialin/ui/hd_simulator.py (revision 0) +++ shared/scripts/dialin/ui/hd_simulator.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,2085 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 hd_simulator.py +# +# @author (last) Quang Nguyen +# @date (last) 11-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 06-Aug-2020 +# +############################################################################ +import enum +import test +from time import sleep + +from . import messageBuilder +from ..common import * +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from ..utils import * +from ..utils.base import AbstractSubSystem, LogManager + + +class HDSimulator(AbstractSubSystem): + NUM_TREATMENT_PARAMETERS = 19 + instance_count = 0 + + # 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_COMPAT = END_POS_BUILD + END_POS_COMPAT = START_POS_COMPAT + 4 + + def __init__(self, can_interface: str = "can0", + log_level: bool = None, + console_out: bool = False, + passive_mode: bool = False, + auto_response: bool = False): + """ + The HDSimulator constructor + + @param can_interface: (str) the can interface name + @param log_level: (str) or (None) if not set, contains the logging level + @param console_out: (bool) If True, write each dialin message to the console. + """ + super().__init__() + HDSimulator.instance_count = HDSimulator.instance_count + 1 + + self.checked_in = False + self.checked_in_last = 0 + self._log_manager = LogManager(log_level=log_level, log_filepath=self.__class__.__name__ + ".log") + self.logger = self._log_manager.logger + self.console_out = console_out + self.can_interface = DenaliCanMessenger(can_interface=can_interface, + logger=self.logger, + log_can=self._log_manager.log_level == "CAN_ONLY", + console_out=console_out, + passive_mode=passive_mode) + self.can_interface.start() + + if self.can_interface is not None: + channel_id = DenaliChannels.ui_to_hd_ch_id + + if auto_response: + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_CHECK_IN.value, + self._handler_ui_first_check_in) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_INITIATE_TREATMENT_REQUEST.value, + self._handler_ui_initiate_treatment) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_SET_UF_VOLUME_PARAMETER.value, + self._handler_ui_pre_treatment_uf_request) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_NEW_TREATMENT_PARAMS.value, + self._handler_ui_validate_parameters) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_USER_CONFIRM_TREATMENT_PARAMS.value, + self._handler_ui_confirm_treatment) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_TX_END_CMD.value, + self._handler_ui_end_treatment) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_HD_SET_RTC_REQUEST.value, + self._handler_set_rtc_request) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_REQUEST_FW_VERSIONS.value, + self._handler_request_hd_version) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_sync_broadcast_ch_id, + MsgIds.MSG_ID_UI_REQUEST_SERVICE_INFO.value, + self._handler_system_usage_response) + 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_post_ui_version_compatibility) + 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) + + self.treatment_parameter_rejections = TreatmentParameterRejections() + + # initialize variables that will be populated by UI version response + self.ui_version = None + + def get_ui_version(self): + """ + Gets the ui version + + @return: The ui version + """ + return self.ui_version + + def _handler_system_usage_response(self) -> None: + """ + Handles a request for system usage + + @return: None + """ + + payload = integer_to_bytearray(1619628663) + payload += integer_to_bytearray(1619887863) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SERVICE_SCHEDULE_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_set_rtc_request(self, message: dict) -> None: + """ + Handles a UI request to set the HD RTC + @param message: (dict) the message containing the request + @return: None + """ + + epoch = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + self.logger.debug("Request to set the HD epoch to {0}".format(epoch)) + + self.cmd_send_set_rtc_response(YES, 0) + + def cmd_send_set_rtc_response(self, response, reason): + """ + Sends a set RTC response message + + @param response: (int) 0=NO, 1=YES + @param reason: the rejection reason + @return: None + """ + self.logger.debug("HD: Sending response {0} reason {1}".format(response, reason)) + + payload = integer_to_bytearray(response) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_UI_SET_RTC_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_parameter_validation_response(self, rejections: List[RequestRejectReasons]): + """ + Sends a treatment parameter validation response + + @param rejections: A list of rejection code enums + @return: True if successful, False otherwise + """ + if len(rejections) != self.NUM_TREATMENT_PARAMETERS: + self.logger.error("Invalid number of treatment parameter enums provided.") + return False + + if not all([isinstance(each, enum.Enum) for each in rejections]): + self.logger.error("Not all rejections are enums.") + return False + + payload = bytearray() + + for rejection in rejections: + payload += integer_to_bytearray(rejection.value) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE.value, + payload=payload) + + # Send message + self.can_interface.send(message, 0) + + return True + + def cmd_send_treatment_parameter_manual_validation_response(self, rejections: List[int]): + """ + Sends a manually built treatment parameter validation response + + @param rejections: A list of rejection code enums + @return: True if successful, False otherwise + """ + if len(rejections) != self.NUM_TREATMENT_PARAMETERS: + self.logger.error("Invalid number of treatment parameter enums provided.") + return False + + if not all([isinstance(each, int) for each in rejections]): + self.logger.error("Not all rejections are the correct type.") + return False + + payload = bytearray() + + for rejection in rejections: + payload += integer_to_bytearray(rejection) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_NEW_TREATMENT_PARAMS_RESPONSE.value, + payload=payload) + + # Send message + self.can_interface.send(message, 0) + + return True + + def cmd_send_poweroff_button_pressed(self): + """ + Broadcast that the poweroff button was pressed + + @return: None + """ + + payload = bytearray(0x01) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_PRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_broadcast_poweroff_imminent(self): + """ + Broadcast that the system will shut down + + @return: None + """ + + payload = integer_to_bytearray(1) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_POWER_OFF_WARNING.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_poweroff_timeout(self): + """ + Sends a poweroff timeout + + @return: None + """ + + payload = integer_to_bytearray(1) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_OFF_BUTTON_PRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_ui_confirm_treatment(self, message): + """ + Handler function to detect when a treatment is confirmed + + @param message: the confirm treatment message + @return: None + """ + + request = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if request == 0: + self.logger.debug("Received UI cancel confirmation of Treatment Parameters. ") + return + + self.logger.debug("Received UI confirmation of Treatment Parameters. ") + + self.logger.debug("Priming ...") + state = 0 + total_seconds = 100 + for seconds_remaining in range(total_seconds, -1, -1): + if seconds_remaining % (total_seconds // 3) == 0: + state += 1 + self.cmd_send_priming_time_remaining(state, seconds_remaining, total_seconds) + sleep(0.05) + self.logger.debug("Finished priming.") + + def _handler_ui_pre_treatment_uf_request(self, message): + """ + Handles the ui pre treatment uf request and sends a response + @param message: The ui pretreatment uf request message + @return: None + """ + + uf_volume = struct.unpack('f', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + self.logger.debug("Received UF Volume: {0} mL".format(uf_volume)) + + self.cmd_send_uf_treatment_response(1, 0, uf_volume) + + def _handler_ui_first_check_in(self, message)->None: + """ + Handler function to first check in to start the post + @param message: the check-in message + @return: None + """ + now = time.time() + # if the application is not checking-in (the simulatoe ) within 2 sec it means it has been stopped, + # so do the check-in again. + print("check-in: ", now, now - self.checked_in_last) + if now - self.checked_in_last > 5: + self.checked_in = False + + self.checked_in_last = now + + if self.checked_in: + return None + + print("_handler_ui_first_check_in") + self.cmd_send_power_on_self_test_version_request() + for i in range(20): + self.cmd_send_hd_post(i, True, False) + sleep(0.1) + self.cmd_send_hd_post(0, True, True) + self.cmd_send_hd_operation_mode(4, 0) + self.checked_in = True + + def _handler_ui_initiate_treatment(self, message): + """ + Handler function to start a treatment + + @param message: the start treatment message + @return: None + """ + + request = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if request == 0: + self.logger.debug("Selecting treatment parameters") + self.cmd_send_hd_operation_mode(HDOpModes.MODE_PRET.value) + elif request == 1: + self.logger.debug("Canceling treatment") + self.cmd_send_hd_operation_mode(HDOpModes.MODE_STAN.value) + elif request == 2: + self.logger.debug("Starting treatment") + self.cmd_send_hd_operation_mode(HDOpModes.MODE_TREA.value) + + self.cmd_initiate_treatment_response(YES, 0) + + def cmd_initiate_treatment_response(self, response: int, reason: int): + """ + Sends a start treatment response message + + @param response: 0=NO, 1=YES + @param reason: the rejection reason + @return: None + """ + self.logger.debug("Sending: {0}".format(response)) + + payload = integer_to_bytearray(response) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_INITIATE_TREATMENT_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_operation_mode(self, op_mode: int, sub_mode: int = 0): + """ + Broadcasts the current HD operation mode + @param op_mode: hd operation mode + @param sub_mode: hd operation sub-mode + @return: None + """ + + if not isinstance(op_mode, int): + raise ValueError("Provided mode is not of type 'int'") + if not isinstance(sub_mode, int): + raise ValueError("Provided mode is not of type 'int'") + + payload = integer_to_bytearray(op_mode) + payload += integer_to_bytearray(sub_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_OP_MODE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_uf_treatment_response(self, accepted, reason, volume): + """ + Sends the uf volume adjustment response message in pre-treatment + @param accepted: (uint) the acceptance, 1 = accepted, 0 = rejected + @param reason: (uint) the reason for rejection + @param volume: (float) the acceptable/accepted ultrafiltration volume + @return: none + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_UF_VOLUME_PARAMETER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_end_treatment_response(self): + """ + Sends an end treatment response + @return: None + """ + + payload = integer_to_bytearray(YES) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TX_END_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_ui_validate_parameters(self) -> None: + """ + handler for UI parameters validation + + @return: None + """ + rejections = [ + self.treatment_parameter_rejections.param_request_valid, + self.treatment_parameter_rejections.param_blood_flow_rate, + self.treatment_parameter_rejections.param_dialysate_flow_rate, + self.treatment_parameter_rejections.param_duration, + self.treatment_parameter_rejections.param_heparin_stop_time, + self.treatment_parameter_rejections.param_saline_bolus, + self.treatment_parameter_rejections.param_acid_concentrate, + self.treatment_parameter_rejections.param_bicarbonate_concentrate, + self.treatment_parameter_rejections.param_dialyzer_type, + self.treatment_parameter_rejections.param_blood_pressure_measure_interval, + self.treatment_parameter_rejections.param_rinseback_flow_rate, + self.treatment_parameter_rejections.param_arterial_pressure_limit_low, + self.treatment_parameter_rejections.param_arterial_pressure_limit_high, + self.treatment_parameter_rejections.param_venous_pressure_limit_low, + self.treatment_parameter_rejections.param_venous_pressure_limit_high, + self.treatment_parameter_rejections.param_heparin_dispensing_rate, + self.treatment_parameter_rejections.param_heparin_bolus_volume, + self.treatment_parameter_rejections.param_dialysate_temp + ] + + self.cmd_send_treatment_parameter_validation_response(rejections) + + def test_started(self, test_name: str): + """ + Logs that a test was started + + @param test_name: The name of the test + @return: None + """ + self.logger.info("Test Started: {0}".format(test_name)) + + def test_completed(self): + """ + Logs that a test was completed + + @return: None + """ + self.logger.info("Test Completed") + + def cmd_send_acknowledge_hd(self): + """ + the acknowledge from HD + @return: none + """ + + payload = ["A5", "01", "00", "FF", "FF", "00", "19", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_send_acknowledge_ui(self): + """ + the acknowledge from UI + @return: none + """ + + payload = ["A5", "01", "00", "FF", "FF", "00", "19", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.ui_to_hd_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_show_poweroff_dialog(self): + """ + the message from HD to UI to show the power off dialog + @return: none + """ + + payload = ["A5", "01", "00", "01", "00", "01", "00", "38"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_hide_poweroff_dialog(self): + """ + the message from HD to UI to hide the power off dialog + @return: none + """ + + payload = ["A5", "01", "00", "01", "00", "01", "01", "09"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_show_poweroff_notification_dialog(self): + """ + the message from HD to UI to show the shutting down notification box + @return: none + """ + + payload = ["A5", "01", "00", "0E", "00", "00", "24", "00"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_sync_broadcast_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + def cmd_show_poweroff_rejection_dialog(self): + """ + the message from HD to UI to show the power off dialog + @return: none + """ + + payload = ["A5", "01", "00", "01", "00", "01", "02", "5A"] + payload = [int(each, 16) for each in payload] + + message = {"channel_id": DenaliChannels.hd_to_ui_ch_id, + "message": payload} + + self.can_interface.send(message, 0) + + @staticmethod + def wait_for_message_to_be_sent(delay=0.050): + """ + After each multi-frame message put a 50ms sleep, time.sleep(0.1) + it seems it's needed otherwise the test will check a value which has not been received yet. + :@param delay: the number of seconds to wait + @return: none + """ + time.sleep(delay) + + @staticmethod + def build_hd_debug_text(vtext): + """ + the debug text message from HD builder method + @param vtext: (str) the debug text + @return: none + """ + message_length = 40 + txt = messageBuilder.textToByte(vtext, message_length) # + 1 null term + msg = messageBuilder.buildMessage(GuiActionType.HDDebugText, 1 * (message_length + 1), False, txt) + return messageBuilder.toFrames(msg) + + @staticmethod + def cmd_set_hd_debug_text(debug_text): + """ + the debug text message from HD setter/sender method + @param debug_text: (str) the debug text + @return: none + """ + + frames = HDSimulator.build_hd_debug_text(debug_text) + frames = messageBuilder.toCandumpFormat(frames) + for frame in frames: + subprocess.call(['cansend', 'can0', '020#{}'.format(frame)]) + HDSimulator.wait_for_message_to_be_sent() + + @staticmethod + def cmd_set_dg_debug_text(debug_text): + """ + the debug text message from DG setter/sender method + @param debug_text: (str) the debug text + @return: none + """ + frames = HDSimulator.build_dg_debug_text(debug_text) + frames = messageBuilder.toCandumpFormat(frames) + for frame in frames: + subprocess.call(['cansend', 'can0', '070#{}'.format(frame)]) + HDSimulator.wait_for_message_to_be_sent() + + def cmd_set_treatment_parameter_ranges(self, min_treatment_duration: int, max_treatment_duration: int, + min_uf_volume: float, max_uf_volume: float, + min_dialysate_flow_rate: int, max_dialysate_flow_rate: int) -> None: + """ + The Treatment adjustment parameter ranges data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(F32) | #4:(F32) | #5:(U32) | #6:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x1A00| 0x020 | 6 | 1/60 Hz| Y | HD | UI | Treatment adjustment param ranges Data | \ref Data::mDuration_Min | \ref Data::mDuration_Max | \ref Data::mUltrafiltration_Volume_Min | \ref Data::mUltrafiltration_Volume_Max | \ref Data::mDialysate_Flow_Min | \ref Data::mDialysate_Flow_Max | + + @param min_treatment_duration: (int) Min Treatment Duration + @param max_treatment_duration: (int) Max Treatment Duration + @param min_uf_volume: (float) Min UF Volume + @param max_uf_volume: (float) Max UF Volume + @param min_dialysate_flow_rate: (int) Min Dialysate Flow Rate + @param max_dialysate_flow_rate: (int) Max Dialysate Flow Rate + @return: None + """ + + payload = integer_to_bytearray(min_treatment_duration) + payload += integer_to_bytearray(max_treatment_duration) + payload += float_to_bytearray(min_uf_volume) + payload += float_to_bytearray(max_uf_volume) + payload += integer_to_bytearray(min_dialysate_flow_rate) + payload += integer_to_bytearray(max_dialysate_flow_rate) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_PARAM_CHANGE_RANGES.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_blood_flow_rate(self, flow_set_pt: int, measured_flow: float, + rot_speed: float, mot_speed: float, mc_speed: float, + mc_current: float, pwm: float, signal_strength: float) -> None: + """ + The Blood Flow Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(S32) | #2:(F32) | #3:(F32) | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | #8:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x0500| 0x040 | 7 | 1 Hz | N | HD | All | Blood Flow Data | \ref Data::mFlowSetPoint | \ref Data::mMeasuredFlow | \ref Data::mRotorSpeed | \ref Data::mMotorSpeed | \ref Data::mMotorCtlSpeed | \ref Data::mMotorCtlCurrent | \ref Data::mPWMDutyCycle | \ref Data::mSigStrenght | + + @param flow_set_pt: (int) Flow Set Point + @param measured_flow: (float) Measured Flow + @param rot_speed: (float) Rot Speed + @param mot_speed: (float) Motor Speed + @param mc_speed: (float) MC Speed + @param mc_current: (float) MC Current + @param pwm: (float) PWM + @param signal_strength: (float) Signal strength in percent + @return: None + """ + + payload = integer_to_bytearray(flow_set_pt) + payload += float_to_bytearray(measured_flow) + payload += float_to_bytearray(rot_speed) + payload += float_to_bytearray(mot_speed) + payload += float_to_bytearray(mc_speed) + payload += float_to_bytearray(mc_current) + payload += float_to_bytearray(pwm) + payload += float_to_bytearray(signal_strength) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_BLOOD_FLOW_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_dialysate_flow_rate(self, flow_set_pt: int, measured_flow: float, + rot_speed: float, mot_speed: float, mc_speed: float, + mc_current: float, pwm: float, signal_strength: float) -> None: + """ + The Dialysate Flow Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(S32) | #2:(F32) | #3:(F32) | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | #8:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x0800| 0x040 | 7 | 1 Hz | N | HD | All | Dialysate Flow Data | mFlowSetPoint | mMeasuredFlow | mRotorSpeed | mMotorSpeed | mMotorCtlSpeed | mMotorCtlCurrent | mPWMDutyCycle | \ref Data::mSigStrenght | + + @param flow_set_pt: (signed int) Flow Set Point + @param measured_flow: (float) Measured Flow + @param rot_speed: (float) Rot Speed + @param mot_speed: (float) Motor Speed + @param mc_speed: (float) MC Speed + @param mc_current: (float) MC Current + @param pwm: (float) PWM + @param signal_strength: (float) Signal strength in percent + @return: None + """ + + payload = integer_to_bytearray(flow_set_pt) + payload += float_to_bytearray(measured_flow) + payload += float_to_bytearray(rot_speed) + payload += float_to_bytearray(mot_speed) + payload += float_to_bytearray(mc_speed) + payload += float_to_bytearray(mc_current) + payload += float_to_bytearray(pwm) + payload += float_to_bytearray(signal_strength) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DIALYSATE_FLOW_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_blood_dialysate_response(self, accepted: int, reason: int, + blood_rate: int, dialysate_flow_rate: int) -> None: + """ + The Blood/dialysate rate change Response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | #4:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x1800| 0x020 | 6 | Rsp | Y | HD | UI | Blood/dialysate rate change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mBloodRate | \ref Data::mDialysateRate | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param blood_rate: (int) Blood Flow Rate + @param dialysate_flow_rate: (int) Dialysate Flow Rate + @return: None + """ + + if not isinstance(accepted, int): + accepted = int(accepted) + if not isinstance(reason, int): + reason = int(reason) + if not isinstance(blood_rate, int): + blood_rate = int(blood_rate) + if not isinstance(dialysate_flow_rate, int): + dialysate_flow_rate = int(dialysate_flow_rate) + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(blood_rate) + payload += integer_to_bytearray(dialysate_flow_rate) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_BLOOD_DIAL_RATE_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_duration_response(self, accepted: int, reason: int, + duration: int, ultrafiltration: float) -> None: + """ + The Treatment Duration change Response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | #5:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: | + |0x1B00| 0x020 | 6 | Rsp | Y | HD | UI | Treatment Duration change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mDuration | \ref Data::mUFVolume | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param duration: (int) Treatment Duration + @param ultrafiltration: (float) Ultrafiltration Volume + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(duration) + payload += float_to_bytearray(ultrafiltration) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_TREATMENT_TIME_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_state_response(self, accepted: int, reason: int, state: int) -> None: + """ + the Treatment ultrafiltration adjustment response message setter/sender method + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param state: (int) Ultrafiltration State + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_PAUSE_RESUME_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_accepted(self, state: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_state_response which sends accept true + @param state: (int) Ultrafiltration State + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_state_response(EResponse.Accepted, 0, state) + + def cmd_set_treatment_adjust_ultrafiltration_rejected(self, reason: int, state: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_state_response which sends accept false + @param reason: (int) rejection reason + @param state: (int) Ultrafiltration State + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_state_response(EResponse.Rejected, reason, state) + + def cmd_set_treatment_adjust_ultrafiltration_init_response(self, accepted: int, reason: int, volume: float) -> None: + """ + the ultrafiltration volume change response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(F32) + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: + |0x5000| 0x020 | 6 | Rsp | Y | HD | UI | Pre UF Volume Adjustment Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mVolume + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param volume: (float) Ultrafiltration Volume + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SET_UF_VOLUME_PARAMETER_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_edit_response(self, accepted: int, reason: int, volume: float, + duration: int, duration_diff: int, + rate: float, rate_diff: float, rate_old: float) -> None: + """ + the ultrafiltration volume change response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #1:(U32) | #2:(U32) | #3:(F32) | #4:(U32) | #5:(F32) | #6:(U32) | #7:(U32) | #8:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x1300| 0x020 | 6 | Rsp | Y | HD | UI | UF Vol. Change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mVolume | \ref Data::mDuration | \ref Data::mRate | \ref Data::mDurationDiff | \ref Data::mRateDiff | \ref Data::mRateOld | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param volume: (float) Ultrafiltration Volume + @param duration: (int) Treatment Duration + @param duration_diff: (int) Duration Difference + @param rate: (float) Ultrafiltration Rate + @param rate_diff: (float) Ultrafiltration Rate Difference + @param rate_old: (float) Ultrafiltration Rate Old + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + payload += integer_to_bytearray(duration) + payload += integer_to_bytearray(duration_diff) + payload += float_to_bytearray(rate) + payload += float_to_bytearray(rate_diff) + payload += float_to_bytearray(rate_old) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_edit_rejected(self, reason: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_edit_response which only sends a rejection reason + and sends other values all as zero + @param reason: (int) rejection reason + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_edit_response(0, reason, 0, 0, 0, 0, 0, 0) + + def cmd_set_treatment_adjust_ultrafiltration_confirm_response(self, accepted: int, reason: int, volume: float, + duration: int, rate: float) -> None: + """ + the ultrafiltration volume Change Confirmation Response message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(F32) | #4:(U32) | #5:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: | + |0x2E00| 0x020 | 6 | Rsp | Y | HD | UI | UF Vol. Change Confirmation Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mVolume | \ref Data::mDuration | \ref Data::mRate | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param volume: (float) Ultrafiltration Volume + @param duration: (int) Treatment Duration + @param rate: (float) Ultrafiltration Rate + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += float_to_bytearray(volume) + payload += integer_to_bytearray(duration) + payload += float_to_bytearray(rate) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_UF_SETTINGS_CHANGE_CONFIRMATION_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_adjust_ultrafiltration_confirm_rejected(self, reason: int) -> None: + """ + a convenient method for cmd_set_treatment_adjust_ultrafiltration_confirm_response which only sends a rejection reason + and sends other values all as zero + + @param reason: (int) rejection reason + @return: none + """ + self.cmd_set_treatment_adjust_ultrafiltration_confirm_response(0, reason, 0, 0, 0) + + def cmd_set_treatment_time(self, sec_total: int, sec_elapsed: int, sec_remain: int = 0) -> None: + """ + the Treatment Time Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x0D00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment Time Data | \ref Data::mTotal | \ref Data::mElapsed | \ref Data::mRemaining | + + @param sec_total: (int) Treatment Total Duration in Seconds + @param sec_elapsed: (int) Treatment Total Elapsed Time in Seconds + @param sec_remain: (int) Treatment Remaining Time in Seconds + @return: none + """ + if sec_remain is None: + sec_remain = sec_total - sec_elapsed + + payload = integer_to_bytearray(sec_total) + payload += integer_to_bytearray(sec_elapsed) + payload += integer_to_bytearray(sec_remain) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_TIME.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_ultrafiltration_outlet_flow_data(self, ref_uf_vol: float, measured_uf_vol: float, + rot_speed: float, mot_speed: float, mc_speed: float, + mc_current: float, pwm: float) -> None: + """ + the Outlet Flow Data message setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(F32) | #4:(F32) | #5:(F32) | #6:(F32) | #7:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x0B00| 0x040 | 7 | 1 Hz | N | HD | All | Outlet Flow Data | \ref Data::mRefUFVol | \ref Data::mMeasUFVol | \ref Data::mRotorSpeed | \ref Data::mMotorSpeed | \ref Data::mMotorCtlSpeed | \ref Data::mMotorCtlCurrent | \ref Data::mPWMDtCycle | + + @param ref_uf_vol: (float) Ref UF Volume + @param measured_uf_vol: (float) Measured UF Volume + @param rot_speed: (float) Rot Speed + @param mot_speed: (float) Motor Speed + @param mc_speed: (float) MC Speed + @param mc_current: (float) MC Current + @param pwm: (float) PWM + @return: none + """ + + payload = float_to_bytearray(ref_uf_vol) + payload += float_to_bytearray(measured_uf_vol) + payload += float_to_bytearray(rot_speed) + payload += float_to_bytearray(mot_speed) + payload += float_to_bytearray(mc_speed) + payload += float_to_bytearray(mc_current) + payload += float_to_bytearray(pwm) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_DIALYSATE_OUT_FLOW_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_pressure_occlusion_data(self, arterial_prs: float, venous_prs: float, blood_pump_occlusion: int, + dialysate_inlet_pump_occlusion: int, dialysate_outlet_pump_occlusion) -> None: + """ + the Pressure/Occlusion Data messages setter/sender method + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(U32) | #4:(U32) | #5:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: | + |0x0900| 0x040 | 7 | 1 Hz | N | HD | All | PressureOcclusion Data | \ref Data::mArterialPressure | \ref Data::mVenousPressure | \ref Data::mBloodPumpOcclusion | \ref Data::mDialysateInletPumpOcclusion | \ref Data::mDialysateOutletPumpOcclusion | + + @param arterial_prs: (float) Arterial Pressure + @param venous_prs: (float) Venous Pressure + @param blood_pump_occlusion: (uint) Blood Pump Occlusion + @param dialysate_inlet_pump_occlusion: (uint) Dialysate Inlet Pump Occlusion + @param dialysate_outlet_pump_occlusion: (uint) Dialysate Outlet Pump Occlusion + @return: none + """ + + payload = float_to_bytearray(arterial_prs) + payload += float_to_bytearray(venous_prs) + payload += integer_to_bytearray(blood_pump_occlusion) + payload += integer_to_bytearray(dialysate_inlet_pump_occlusion) + payload += integer_to_bytearray(dialysate_outlet_pump_occlusion) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_PRESSURE_OCCLUSION_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_states_data(self, sub_mode: int, uf_state: int, saline_state: int, heparin_state: int, + rinseback_state: int, recirculate_state: int, blood_prime_state: int, + treatment_end_state: int, treatment_stop_state: int) -> None: + """ + the Treatment States Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: | + |0x0F00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment States Data | + + | #1:(U32) | #2:(U32) | #3:(U32) || + |:--: |:--: |:--: || + | \ref Data::mSubMode | \ref Data::mUFState | \ref Data::mSalineState || + |||| + | #4:(U32) | #5:(U32) | #6:(U32) || + |:--: |:--: |:--: || + | \ref Data::mHeparinState | \ref Data::mRinsebackState | \ref Data::mRecirculateState || + |||| + | #7:(U32) | #8:(U32) | #9:(U32) || + |:--: |:--: |:--: || + | \ref Data::vBloodPrimeState | \ref Data::mTreatmentEndState | \ref Data::vTreammentStopState || + + @param sub_mode: (int) Sub-Mode + @param uf_state: (int) UF State + @param saline_state: (int) Saline Bolus State + @param heparin_state: (int) Heparin State + @param rinseback_state: (int) Rinseback State + @param recirculate_state: (int) Recirculate State + @param blood_prime_state: (int) Blood Prime State + @param treatment_end_state: (int) Treatment End State + @param treatment_stop_state: (int) Treatment Stop State + @return: none + """ + + payload = integer_to_bytearray(sub_mode) + payload += integer_to_bytearray(uf_state) + payload += integer_to_bytearray(saline_state) + payload += integer_to_bytearray(heparin_state) + payload += integer_to_bytearray(rinseback_state) + payload += integer_to_bytearray(recirculate_state) + payload += integer_to_bytearray(blood_prime_state) + payload += integer_to_bytearray(treatment_end_state) + payload += integer_to_bytearray(treatment_stop_state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_TREATMENT_STATE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_start_state(self) -> None: + """ + starting the treatment for user convenience since Tx is not by default running + @return: none + """ + self.cmd_set_treatment_states_data(TXStates.TREATMENT_DIALYSIS_STATE, + TXStates.UF_OFF_STATE, + TXStates.SALINE_BOLUS_STATE_IDLE, + TXStates.HEPARIN_STATE_OFF, + TXStates.RINSEBACK_STOP_INIT_STATE, + TXStates.TREATMENT_RECIRC_RECIRC_STATE, + TXStates.BLOOD_PRIME_RAMP_STATE, + TXStates.TREATMENT_END_WAIT_FOR_RINSEBACK_STATE, + TXStates.TREATMENT_STOP_RECIRC_STATE) + + def cmd_set_hd_operation_mode_data(self, operation_mode: int, operation_sub_mode: int) -> None: + """ + The HD Operation Mode Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x2500| 0x040 | 7 | 1 Hz | N | HD | All | HD Operation Mode Data | \ref Data::mOpMode | + @param operation_mode: (int) Operation Mode + @param operation_sub_mode: (int) Operation Sub-Mode + @return: None + """ + + payload = integer_to_bytearray(operation_mode) + payload += integer_to_bytearray(operation_sub_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_OP_MODE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_saline_bolus_data(self, target: int, cumulative: float, delivered: float) -> None: + """ + the Treatment Saline Bolus Data message sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(F32) | #3:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x2F00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment Saline Bolus Data | \ref Data::mTarget | \ref Data::mCumulative | \ref Data::mDelivered | + + @param target: (int) Saline Bolus Target Volume + @param cumulative: (float) Saline Bolus Cumulative Volume + @param delivered: (float) Saline Bolus Delivered Volume + @return: none + """ + + payload = integer_to_bytearray(target) + payload += float_to_bytearray(cumulative) + payload += float_to_bytearray(delivered) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_SALINE_BOLUS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_saline_bolus_response(self, accepted: int, reason: int, target: int, state: int) -> None: + """ + the Saline Bolus Response message sender method + | MSG | CAN ID | M.Box | Type | Ack | Src | Dest | Description | #1:(U32) | #2:(U32) | #3:(U32) | #3:(U32) | + |:---:|:------:|:-----:|:----:|:---:|:---:|:----:|:---------------------:|:--------------------:|:-------------------:|:-------------------:|:-------------------:| + | 20 | 0x020 | 6 | Rsp | Y | HD | UI | Saline Bolus Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mTarget | \ref Data::mState | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param target: (int) Saline Bolus Target Volume + @param state: (int) Saline Bolus current State + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(target) + payload += integer_to_bytearray(state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_USER_SALINE_BOLUS_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_canbus_fault_count(self, count: int) -> None: + """ + the CANBus fault count message setter/sender method + @param count: (int) Fault Count + @return: none + """ + + payload = integer_to_bytearray(count) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_CAN_ERROR_COUNT.value, + payload=payload) + + self.can_interface.send(message, 0) + HDSimulator.wait_for_message_to_be_sent() + + def cmd_send_unknown_hd(self) -> None: + """ + the unknown message from HD setter/sender method + @return: none + """ + + payload = integer_to_bytearray(0) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_UNUSED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_treatment_heparin_data(self, cumulative: float) -> None: + """ + the Treatment Heparin Data message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x4D00| 0x040 | 7 | 1 Hz | N | HD | All | Treatment Heparin Data | \ref Data::mCumulative | + + @param cumulative: (float) Heparin Cumulative Volume + @return: none + """ + payload = float_to_bytearray(cumulative) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_HEPARIN_DATA_BROADCAST.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_heparin_pause_resume_response(self, accepted: int, reason: int, state: int) -> None: + """ + the Heparin Response message setter/sender method + | MSG | CAN ID | M.Box | Type | Ack | Src | Dest | Description | #1:(U32) | #2:(U32) | #3:(U32) | + |:----:|:------:|:-----:|:----:|:---:|:---:|:----:|:----------------:|:--------------------:|:-------------------:|:-------------------:| + |0x4C00| 0x020 | 6 | Rsp | Y | HD | UI | Heparin Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mState | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param state: (int) Heparin current State + @return: none + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_HEPARIN_PAUSE_RESUME_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_pressures_limit_response(self, accepted: int, reason: int, + arterial_low: int, arterial_high: int, + venous_low: int, venous_high: int) -> None: + """ + the Blood/dialysate rate change Response message setter/sender method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | #3:(S32) | #4:(S32) | #3:(S32) | #4:(S32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: |:--: | + |0x4700| 0x020 | 6 | Rsp | Y | HD | UI | A/V BP Limit Change Response | \ref Data::mAccepted | \ref Data::mReason | \ref Data::mArterialLow | \ref Data::mArterialHigh | \ref Data::mVenousLow | \ref Data::mVenousHigh | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @param arterial_low: (int) Arterial Pressure Limit Low (mmHg) + @param arterial_high: (int) Arterial Pressure Limit High (mmHg) + @param venous_low: (int) Venous Pressure Limit Low (mmHg) + @param venous_high: (int) Venous Pressure Limit High (mmHg) + @return: none + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(arterial_low) + payload += integer_to_bytearray(arterial_high) + payload += integer_to_bytearray(venous_low) + payload += integer_to_bytearray(venous_high) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_PRESSURE_LIMITS_CHANGE_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_rinseback_response(self, accepted: int, reason: int) -> None: + """ + the rinseback state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5300| 0x020 | 6 | Rsp | Y | HD | UI | Rinseback State Change Response | \ref Data::mAccepted | \ref Data::mReason | + + @param accepted: (int) boolean accept/reject response + @param reason : (int) rejection reason + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_rinseback_data(self, vTarget, vCurrent, vRate, vTimeoutTotal, vTimeoutCountDown, vSafetyVolume): + """ + the rinseback state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #3:(U32) | #4:(U32) | #5:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: |:--: |:--: | + |0x5600| 0x020 | 6 | 1Hz | N | HD | UI | Rinseback progress data | \ref Data::mTarget | \ref Data::mCurrent | \ref Data::mRate | \ref Data::mTimeoutTotal | \ref Data::mTimeoutCountDown | + + :param vTarget : (float) the target volume in mL + :param vCurrent : (float) the current volume in mL + :param vRate : (uint ) the current flow rate in mL/min + :param vTimeoutTotal : (uint ) Total Timeout + :param vTimeoutCountDown: (uint ) Current Timeout count down + :param vSafetyVolume : (float) Safety Volume + :return: None + """ + + payload = float_to_bytearray(vTarget) + payload += float_to_bytearray(vCurrent) + payload += integer_to_bytearray(vRate) + payload += integer_to_bytearray(vTimeoutTotal) + payload += integer_to_bytearray(vTimeoutCountDown) + payload += float_to_bytearray(vSafetyVolume) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RINSEBACK_PROGRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_recirculate_data(self, timeout_total: int, timeout_count_down: int) -> None: + """ + the rinseback state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5A00| 0x020 | 6 | 1Hz | N | HD | UI | Rinseback progress data | \ref Data::mTimeoutTotal | \ref Data::mTimeoutCountDown | + + @param timeout_total: (int) Total Timeout + @param timeout_count_down: (int) Current Timeout count down + @return: None + """ + + payload = integer_to_bytearray(timeout_total) + payload += integer_to_bytearray(timeout_count_down) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RECIRC_PROGRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_blood_prime_data(self, target: float, current: float): + """ + the bloodprime state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(F32) | #2:(F32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: |:--: | + |0x5900| 0x020 | 6 | 1Hz | N | HD | UI | bloodprime progress data | \ref Data::mTarget | \ref Data::mCurrent | \ref Data::mRate | + + @param target : (float) the target volume in mL + @param current : (float) the current volume in mL + @return: None + """ + + payload = float_to_bytearray(target) + payload += float_to_bytearray(current) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_BLOOD_PRIME_PROGRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_adjust_recirculate_response(self, accepted: int, reason: int) -> None: + """ + the recirculate state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5500| 0x020 | 6 | Rsp | Y | HD | UI | Recirculate State Change Response | \ref Data::mAccepted | \ref Data::mReason | + + @param accepted: (int) boolean accept/reject response + @param reason : (int) rejection reason + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_RECIRC_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_ui_end_treatment(self, message: dict) -> None: + """ + Handler function when received a request to end a treatment + + @param message: (dict) the end treatment request + @return: None + """ + self.logger.debug("End treatment requested") + request = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + if request == 0: + self.logger.debug("Request to start rinseback") + self.cmd_send_treatment_adjust_end_response(accepted=YES, reason=0) + else: + self.logger.debug("End treatment unknown request") + + def cmd_send_treatment_adjust_end_response(self, accepted: int, reason: int): + """ + the treatment end state change Response message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | #2:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: |:--: | + |0x5800| 0x020 | 6 | Rsp | Y | HD | UI | Treatment End State Change Response | \ref Data::mAccepted | \ref Data::mReason | + + @param accepted: (int) boolean accept/reject response + @param reason: (int) rejection reason + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TX_END_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_accelerometer_hd_data(self, x: float, y: float, z: float, + x_max: float, y_max: float, z_max: float, + x_tilt: float, y_tilt: float, z_tilt: float) -> None: + """ + the accelerometer hd data message method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: | + |0x3300| 0x040 | 7 | 1Hz | N | HD | UI | HD Accelerometer data | + + | #1:(F32) | #2:(F32) | #3:(U32) | + |:--: |:--: |:--: | + | \ref Data::mX | \ref Data::mY | \ref Data::mX | + + | #4:(F32) | #5:(F32) | #6:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXMax | \ref Data::mYMax | \ref Data::mXMax | + + | #7:(F32) | #8:(F32) | #9:(U32) | + |:--: |:--: |:--: | + | \ref Data::mXTilt | \ref Data::mYTilt | \ref Data::mXTilt | + + @param x: float - x axis + @param y: float - y axis + @param z: float - z axis + @param x_max: float - x axis max + @param y_max: float - y axis max + @param z_max: float - z axis max + @param x_tilt: float - x axis tilt + @param y_tilt: float - y axis tilt + @param z_tilt: float - z axis tilt + @return: None + """ + + payload = float_to_bytearray(x) + payload += float_to_bytearray(y) + payload += float_to_bytearray(z) + payload += float_to_bytearray(x_max) + payload += float_to_bytearray(y_max) + payload += float_to_bytearray(z_max) + payload += float_to_bytearray(x_tilt) + payload += float_to_bytearray(y_tilt) + payload += float_to_bytearray(z_tilt) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_ACCELEROMETER_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_request_hd_version(self) -> None: + """ + Handles a request for the HD version + + @return: None + """ + + self.logger.debug("Handling request for hd version.") + self.cmd_send_version_hd_data(9, 9, 9, 9, 9, 9, 9, 9) + self.cmd_send_hd_serial_number() + + def cmd_send_hd_serial_number(self) -> None: + """ + Sends the hd serial number response + + @return: None + """ + self.logger.debug("Sending hd serial number...") + + payload = bytearray('0123456789\0', encoding="utf-8") + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SERIAL_NUMBER.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_version_hd_data(self, major: int, minor: int, micro: int, build: int, + fpga_id: int, fpga_major: int, fpga_minor: int, fpga_lab: int) -> None: + """ + the hd version response message method + @param major: integer - Major version number + @param minor: integer - Minor version number + @param micro: integer - Micro version number + @param build: integer - Build version number + @param fpga_id: integer - FPGA id version number + @param fpga_major: integer - FPGA Major version number + @param fpga_minor: integer - FPGA Minor version number + @param fpga_lab: integer - FPGA Lab version number + @return: None + """ + + payload = byte_to_bytearray(major) + payload += byte_to_bytearray(minor) + payload += byte_to_bytearray(micro) + payload += short_to_bytearray(build) + payload += byte_to_bytearray(fpga_id) + payload += byte_to_bytearray(fpga_major) + payload += byte_to_bytearray(fpga_minor) + payload += byte_to_bytearray(fpga_lab) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_VERSION.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_serial_hd_data(self, serial: str): + """ + the hd version serial response message method + @param serial: serial number + @return: None + """ + + payload = bytes(serial, 'ascii') + b'\x00' + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SERIAL_NUMBER.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_state_data(self, + sub_mode: int, + water_sample_state: int, + consumables_self_test_state: int, + no_cartridge_self_test_state: int, + installation_state: int, + dry_self_test_state: int, + prime_state: int, + recirculate_state: int, + patient_connection_state: int) -> None: + """ + sends the broadcast message of the pre-treatment states + @param sub_mode : (int) the main pre treatment state + @param water_sample_state : (int) water sample state + @param consumables_self_test_state : (int) consumables self test state + @param no_cartridge_self_test_state : (int) no cartridge self-test state + @param installation_state : (int) installation state + @param dry_self_test_state : (int) dry self-test state + @param prime_state : (int) prime state + @param recirculate_state : (int) recirculate state + @param patient_connection_state : (int) patient connection state + @return: + """ + payload = integer_to_bytearray(sub_mode) + payload += integer_to_bytearray(water_sample_state) + payload += integer_to_bytearray(consumables_self_test_state) + payload += integer_to_bytearray(no_cartridge_self_test_state) + payload += integer_to_bytearray(installation_state) + payload += integer_to_bytearray(dry_self_test_state) + payload += integer_to_bytearray(prime_state) + payload += integer_to_bytearray(recirculate_state) + payload += integer_to_bytearray(patient_connection_state) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_PRE_TREATMENT_STATE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_water_sample_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment water sample response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_SAMPLE_WATER_CMD_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_self_test_no_cartridge_progress_data(self, total: int, countdown: int) -> None: + """ + send the pretreatment no cartridge self-tests progress data + @param total: (int) Total time in second + @param countdown: (int) count down time in second + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_NO_CART_SELF_TEST_PROGRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_self_test_dry_progress_data(self, total: int, countdown: int) -> None: + """ + send the pretreatment dry self-tests progress data + @param total: (int) Total time in second + @param countdown: (int) count down time in second + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_DRY_SELF_TEST_PROGRESS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_disposables_prime_progress_data(self, timeout: int, countdown: int) -> None: + """ + Broadcasts the progress time data of pre-treatment disposables priming to the UI + @param timeout : (int) the total progress time timeout in seconds + @param countdown: (int) the remaining time in seconds + @return: None + """ + payload = bytearray() + payload += integer_to_bytearray(timeout) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_PRIMING_STATUS_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_prime_start_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment prime start response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_START_PRIME_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_continue_to_treament_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment continue to treatment response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_START_TREATMENT_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_pre_treatment_patient_connection_confirm_response(self, accepted: int, reason: int) -> None: + """ + send the pretreatment patient connection confirm response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_PATIENT_CONNECTION_CONFIRM_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_post_treatment_disposable_removal_confirm_response(self, accepted: int, reason: int) -> None: + """ + send post treatment disposable removal confirm response + @param accepted: (int) accept or reject + @param reason: (int) rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_DISPOSABLE_REMOVAL_CONFIRM_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_post_treatment_disposable_removal_confirm_response(self, accepted: int, reason: int) -> None: + """ + send post treatment disposable removal confirm response + :param accepted: (U32) accept or reject + :param reason: (U32) rejection reason + :return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_DISPOSABLE_REMOVAL_CONFIRM_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_post_treatment_log_response(self, accepted: bool, reason: int, + bood_flow_rate: int, + dialysate_flow_rate: int, + treatment_duration: int, + actual_treatment_duration: int, + acid_concentrate_type: int, + bicarbonate_concentrate_type: int, + potassium_concentration: int, + calcium_concentration: int, + bicarbonate_concentration: int, + sodium_concentration: int, + dialysate_temperature: float, + dialyzer_type: int, + treatment_start_date_time: int, + treatment_end_date_time: int, + average_blood_flow: float, + average_dialysate_flow: float, + dialysate_volume_used: float, + average_dialysate_temp: float, + origin_uf_volume: float, + target_uf_volume: float, + actual_uf_volume: float, + origin_uf_rate: float, + target_uf_rate: float, + actual_uf_rate: float, + saline_bolus_volume: int, + heparin_bolus_volume: float, + heparin_dispense_rate: float, + heparin_pre_stop: int, + heparin_delivered_volume: float, + heparin_type: int, + average_arterial_pressure: float, + average_venous_pressure: float, + device_id: int, + water_sample_test_result: int + ) -> None: + """ + send post treatment log response + @param accepted: true if accpeted + @param reason: the rejection reason + @param bood_flow_rate: bood flow rate + @param dialysate_flow_rate: dialysate flow rate + @param treatment_duration: treatment duration + @param actual_treatment_duration: actual treatment duration + @param acid_concentrate_type: acid concentrate type + @param bicarbonate_concentrate_type: bicarbonate concentrate type + @param potassium_concentration: potassium concentration + @param calcium_concentration: calcium concentration + @param bicarbonate_concentration: bicarbonate concentration + @param sodium_concentration: sodium concentration + @param dialysate_temperature: dialysate temperature + @param dialyzer_type: dialyzer type + @param treatment_start_date_time: treatment start date time + @param treatment_end_date_time: treatment end date time + @param average_blood_flow: average blood flow + @param average_dialysate_flow: average dialysate flow + @param dialysate_volume_used: dialysate volume used + @param average_dialysate_temp: average dialysate temp + @param origin_uf_volume: origin uf volume + @param target_uf_volume: target uf volume + @param actual_uf_volume: actual uf volume + @param origin_uf_rate: origin uf rate + @param target_uf_rate: target uf rate + @param actual_uf_rate: actual uf rate + @param saline_bolus_volume: saline bolus volume + @param heparin_bolus_volume: heparin bolus volume + @param heparin_dispense_rate: heparin dispense rate + @param heparin_pre_stop: heparin pre stop + @param heparin_delivered_volume: heparin delivered volume + @param heparin_type: heparin type + @param average_arterial_pressure: average arterial pressure + @param average_venous_pressure: average venous pressure + @param device_id: device id + @param water_sample_test_result: water sample test result + @return: None + """ + + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + payload += unsigned_to_bytearray(int(bood_flow_rate)) + payload += unsigned_to_bytearray(int(dialysate_flow_rate)) + payload += unsigned_to_bytearray(int(treatment_duration)) + payload += unsigned_to_bytearray(int(actual_treatment_duration)) + payload += unsigned_to_bytearray(int(acid_concentrate_type)) + payload += unsigned_to_bytearray(int(bicarbonate_concentrate_type)) + payload += unsigned_to_bytearray(int(potassium_concentration)) + payload += unsigned_to_bytearray(int(calcium_concentration)) + payload += unsigned_to_bytearray(int(bicarbonate_concentration)) + payload += unsigned_to_bytearray(int(sodium_concentration)) + payload += float_to_bytearray(float(dialysate_temperature)) + payload += unsigned_to_bytearray(int(dialyzer_type)) + payload += unsigned_to_bytearray(int(treatment_start_date_time)) + payload += unsigned_to_bytearray(int(treatment_end_date_time)) + payload += float_to_bytearray(float(average_blood_flow)) + payload += float_to_bytearray(float(average_dialysate_flow)) + payload += float_to_bytearray(float(dialysate_volume_used)) + payload += float_to_bytearray(float(average_dialysate_temp)) + payload += float_to_bytearray(float(origin_uf_volume)) + payload += float_to_bytearray(float(target_uf_volume)) + payload += float_to_bytearray(float(actual_uf_volume)) + payload += float_to_bytearray(float(origin_uf_rate)) + payload += float_to_bytearray(float(target_uf_rate)) + payload += float_to_bytearray(float(actual_uf_rate)) + payload += unsigned_to_bytearray(int(saline_bolus_volume)) + payload += float_to_bytearray(float(heparin_bolus_volume)) + payload += float_to_bytearray(float(heparin_dispense_rate)) + payload += unsigned_to_bytearray(int(heparin_pre_stop)) + payload += float_to_bytearray(float(heparin_delivered_volume)) + payload += unsigned_to_bytearray(int(heparin_type)) + payload += float_to_bytearray(float(average_arterial_pressure)) + payload += float_to_bytearray(float(average_venous_pressure)) + payload += unsigned_to_bytearray(int(device_id)) + payload += unsigned_to_bytearray(int(water_sample_test_result)) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_DATA_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_log_data(self, blood_flow_rate: int, dialysate_flow_rate: int, uf_rate: float, + arterial_pressure: float, venous_pressure: float) -> None: + """ + send the treatment log data + @param blood_flow_rate: (int) blood flow rate + @param dialysate_flow_rate: (int) Dialysate Flow Rate + @param uf_rate: (float) UF Rate + @param arterial_pressure: (float) Arterial Pressure + @param venous_pressure: (float) Venous Pressure + @return: None + """ + payload = integer_to_bytearray(blood_flow_rate) + payload += integer_to_bytearray(dialysate_flow_rate) + payload += float_to_bytearray(uf_rate) + payload += float_to_bytearray(arterial_pressure) + payload += float_to_bytearray(venous_pressure) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_PERIODIC_DATA.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_log_alarm(self, alarm_id: int, parameter1: float, parameter2: float) -> None: + """ + send the treatment log data + @param alarm_id: (U32) alarm ID + @param parameter1: (F32) paramter 1 (it's not clear yet how many paramters with what type is required and this is only plceholder) + @param parameter2: (F32) paramter 2 (it's not clear yet how many paramters with what type is required and this is only plceholder) + @return: None + """ + payload = integer_to_bytearray(alarm_id) + payload += float_to_bytearray(parameter1) + payload += float_to_bytearray(parameter2) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_ALARM_EVENT.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_treatment_log_event(self, event_id: int, old_value: float, new_value: float) -> None: + """ + send the treatment log data + @param event_id: (U32) alarm ID + @param old_value: (F32) the old value + @param new_value: (F32) the new value + @return: none + """ + payload = integer_to_bytearray(event_id) + payload += float_to_bytearray(old_value) + payload += float_to_bytearray(new_value) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_TREATMENT_LOG_EVENT.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_disinfection_state(self, sub_mode: int, flush_mode: int, heat_mode: int, chemical_mode: int) -> None: + """ + Broadcasts the current DG disinfection mode + @param sub_mode: (int) disinfect states + @param flush_mode: (int) flush states + @param heat_mode: (int) heat states + @param chemical_mode: (int) chemical states + @return: None + """ + payload = integer_to_bytearray(sub_mode) + payload += integer_to_bytearray(flush_mode) + payload += integer_to_bytearray(heat_mode) + payload += integer_to_bytearray(chemical_mode) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_DISINFECT_STANDBY_DATA.value, + payload=payload) + self.can_interface.send(message, 0) + + def cmd_send_hd_disinfect_response(self, accepted: bool, reason: int) -> None: + """ + the HD response to the request from UI to initiate a disinfection/flush + @param accepted: boolean accepted or rejected + @param reason: the rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_DISINFECT_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_disinfect_chemical_confirm(self, accepted: bool, reason: int) -> None: + """ + the HD response to the UI sending the user chimical disinfection steps confirm. + @param accepted: boolean accepted or rejected + @param reason: the rejection reason + @return: None + """ + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_CHEM_DISINFECT_CONFIRM_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + @publish(["ui_version"]) + def _handler_ui_version(self, message) -> None: + """ + Handles the ui version response + @param message: The ui version response message + @return: None + """ + major = struct.unpack('B', bytearray( + message['message'][self.START_POS_MAJOR:self.END_POS_MAJOR])) + minor = struct.unpack('B', bytearray( + message['message'][self.START_POS_MINOR:self.END_POS_MINOR])) + micro = struct.unpack('B', bytearray( + message['message'][self.START_POS_MICRO:self.END_POS_MICRO])) + build = struct.unpack('H', bytearray( + message['message'][self.START_POS_BUILD:self.END_POS_BUILD])) + compatibility = struct.unpack('H', bytearray( + message['message'][self.START_POS_COMPAT:self.END_POS_COMPAT])) + + if all([len(each) > 0 for each in [major, minor, micro, build]]): + self.ui_version = f"v{major[0]}.{minor[0]}.{micro[0]}-{build[0]}-{compatibility[0]}" + self.logger.debug(f"UI VERSION: {self.ui_version}") + + def cmd_send_hd_request_ui_version(self) -> None: + """ + send HD request for UI version + + @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, + payload=None) + self.can_interface.send(message, 0) + + def cmd_send_hd_post(self, item: int, passed: bool, done: bool = False) -> None: + """ + send HD post message the single(item) or the final(done) + @param item: the post state/item index + @param passed: the post result single or final + @param done: if this is the final post message this should be true + @return: None + """ + payload = integer_to_bytearray(passed) + if not done: + payload += integer_to_bytearray(item) + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_POST_FINAL_TEST_RESULT.value if done + else MsgIds.MSG_ID_HD_POST_SINGLE_TEST_RESULT.value, + payload=payload) + self.can_interface.send(message, 0) + + # ------------------------------------------------ GENERAL MESSAGES ------------------------------------------------ + + def cmd_send_hd_general_response(self, message_id: int, accepted: int, reason: int, + is_pure_data: bool = False, + has_parameters: bool = False, + parameters_payload: any = 0x00) -> None: + """ + a general method to send any standard response message, by it's id and list of paramters. + @param message_id: the id of the message + @param accepted: the standard accepted parameter of any response message + @param reason: the standard rejection reason parameter of any response message + @param is_pure_data: The message only has data + @param has_parameters: if the message has parameter this needs to be true. + @param parameters_payload: the list of parameters pre-converted and ready to be concatenated to the payload. + @return: None + """ + + payload = "" + if not is_pure_data: + payload = integer_to_bytearray(accepted) + payload += integer_to_bytearray(reason) + + if has_parameters: + payload += integer_to_bytearray(parameters_payload) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=message_id, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_send_hd_general_progress_data(self, message_id: int, total: int, countdown: int) -> None: + """ + a general method t send any standard progress data message, by it's id + @param message_id: the id of the message + @param total: the total value of the progress data + @param countdown: the remaining or countdown value + @return: None + """ + payload = integer_to_bytearray(total) + payload += integer_to_bytearray(countdown) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=message_id, + payload=payload) + self.can_interface.send(message, 0) + + def cmd_send_hd_ack(self, seq: int) -> None: + """ + sending hd ack message by the sequence seq + @param seq: the message sequence number + @return: None + """ + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=GuiActionType.Acknow, + seq=seq) + + self.can_interface.send(message, 0) + + def cmd_send_power_on_self_test_version_request(self) -> None: + """ + Sends the power on self test version request + + @return: None + """ + + payload = bytearray() + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIdsDialin.MSG_DIALIN_ID_HD_VERSION_REQUEST.value, + payload=payload) + self.can_interface.send(message, 0) + + def _handler_ui_post_ui_version_compatibility(self, message: dict) -> None: + """ + Handles the UI's reporting of its version during the power on self tests + + @param message: The message data + @return: None + """ + + ui_major = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + ui_minor = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_2:MsgFieldPositions.END_POS_FIELD_2]))[0] + ui_micro = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_3:MsgFieldPositions.END_POS_FIELD_3]))[0] + ui_build = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_4:MsgFieldPositions.END_POS_FIELD_4]))[0] + ui_compatibility = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_5:MsgFieldPositions.END_POS_FIELD_5]))[0] + self.logger.debug("UI version power on self test received: " + "Major: {0} " + "Minor: {1} " + "Micro: {2} " + "Build: {3} " + "Compat: {4} " + .format(ui_major, ui_minor, ui_micro, ui_build, ui_compatibility)) + + + + Index: shared/scripts/dialin/ui/hd_simulator_alarms.py =================================================================== diff -u --- shared/scripts/dialin/ui/hd_simulator_alarms.py (revision 0) +++ shared/scripts/dialin/ui/hd_simulator_alarms.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,536 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 hd_simulator_alarms.py +# +# @author (last) Quang Nguyen +# @date (last) 11-Aug-2021 +# @author (original) Peter Lucia +# @date (original) 28-Sep-2020 +# +############################################################################ +import struct +from time import sleep, time + +from ..utils import YES, NO +from ..protocols.CAN import DenaliMessage, DenaliCanMessenger, DenaliChannels +from logging import Logger +from ..utils.base import AbstractSubSystem +from ..utils.conversions import integer_to_bytearray, float_to_bytearray +from ..common.msg_defs import MsgIds, MsgFieldPositions +from ..common.alarm_defs import AlarmList +from dialin.common.prs_defs import AlarmDataTypes + +HIGH = 3 +MED = 2 +LOW = 1 +NONE = 0 + +HIGH_PRIORITY_COLOR = "#c53b33" +MED_LOW_PRIORITY_COLOR = "#f5a623" + + +class Alarms: + # TODO: this should be generated from FW + # ALARM_ID = (priority, alarmID, escalates in, silent_espires_in, flags) + ALARM_ID_NO_ALARM = (NONE, 0, 0, 0, 0) + ALARM_ID_SOFTWARE_FAULT = (HIGH, 1, 0, 0, 0) + ALARM_ID_STUCK_BUTTON_TEST_FAILED = (HIGH, 2, 0, 0, 0) + ALARM_ID_FPGA_POST_TEST_FAILED = (HIGH, 3, 0, 0, 0) + ALARM_ID_WATCHDOG_POST_TEST_FAILED = (HIGH, 4, 0, 0, 0) + ALARM_ID_UI_COMM_POST_FAILED = (HIGH, 5, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_MC_CURRENT_CHECK = (MED, 6, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_OFF_CHECK = (MED, 7, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_MC_DIRECTION_CHECK = (MED, 8, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_ROTOR_SPEED_CHECK = (HIGH, 9, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_MC_CURRENT_CHECK = (MED, 10, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_OFF_CHECK = (MED, 11, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_MC_DIRECTION_CHECK = (MED, 12, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_ROTOR_SPEED_CHECK = (HIGH, 13, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_MC_CURRENT_CHECK = (MED, 14, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_OFF_CHECK = (MED, 15, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_MC_DIRECTION_CHECK = (MED, 16, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_ROTOR_SPEED_CHECK = (HIGH, 17, 0, 0, 0) + ALARM_ID_WATCHDOG_EXPIRED = (HIGH, 18, 0, 0, 0) + ALARM_ID_RTC_COMM_ERROR = (HIGH, 19, 0, 0, 0) + ALARM_ID_RTC_CONFIG_ERROR = (HIGH, 20, 0, 0, 0) + ALARM_ID_DG_COMM_TIMEOUT = (HIGH, 21, 0, 0, 0) + ALARM_ID_UI_COMM_TIMEOUT = (HIGH, 22, 0, 0, 0) + ALARM_ID_COMM_TOO_MANY_BAD_CRCS = (HIGH, 23, 0, 0, 0) + ALARM_ID_TREATMENT_STOPPED_BY_USER = (LOW, 24, 0, 0, 0) + ALARM_ID_BLOOD_SITTING_WARNING = (MED, 25, 0, 0, 0) + ALARM_ID_BLOOD_SITTING_TOO_LONG_NO_RESUME = (MED, 26, 0, 0, 0) + ALARM_ID_BLOOD_SITTING_TOO_LONG_NO_RINSEBACK = (HIGH, 27, 0, 0, 0) + ALARM_ID_CAN_MESSAGE_NOT_ACKED = (HIGH, 28, 0, 0, 0) + ALARM_ID_OCCLUSION_BLOOD_PUMP = (HIGH, 29, 0, 0, 0) + ALARM_ID_OCCLUSION_DIAL_IN_PUMP = (HIGH, 30, 0, 0, 0) + ALARM_ID_OCCLUSION_DIAL_OUT_PUMP = (HIGH, 31, 0, 0, 0) + ALARM_ID_ARTERIAL_PRESSURE_LOW = (HIGH, 32, 0, 0, 0) + ALARM_ID_ARTERIAL_PRESSURE_HIGH = (HIGH, 33, 0, 0, 0) + ALARM_ID_VENOUS_PRESSURE_LOW = (HIGH, 34, 0, 0, 0) + ALARM_ID_VENOUS_PRESSURE_HIGH = (HIGH, 35, 0, 0, 0) + ALARM_ID_UF_RATE_TOO_HIGH_ERROR = (HIGH, 36, 0, 0, 0) + ALARM_ID_UF_VOLUME_ACCURACY_ERROR = (HIGH, 37, 0, 0, 0) + ALARM_ID_RTC_BATTERY_LOW = (HIGH, 38, 0, 0, 0) + ALARM_ID_RTC_OR_TIMER_ACCURACY_FAILURE = (HIGH, 39, 0, 0, 0) + ALARM_ID_RTC_RAM_OPS_ERROR = (HIGH, 40, 0, 0, 0) + ALARM_ID_NVDATA_EEPROM_OPS_FAILURE = (HIGH, 41, 0, 0, 0) + ALARM_ID_NVDATA_MFG_RECORD_CRC_ERROR = (HIGH, 42, 0, 0, 0) + ALARM_ID_NVDATA_SRVC_RECORD_CRC_ERROR = (HIGH, 43, 0, 0, 0) + ALARM_ID_NVDATA_CAL_RECORD_CRC_ERROR = (HIGH, 44, 0, 0, 0) + ALARM_ID_NVDATA_HW_USAGE_DATA_CRC_ERROR = (HIGH, 45, 0, 0, 0) + ALARM_ID_NVDATA_DISINFECTION_DATE_CRC_ERROR = (HIGH, 46, 0, 0, 0) + ALARM_ID_RO_PUMP_OUT_PRESSURE_OUT_OF_RANGE = (HIGH, 47, 0, 0, 0) + ALARM_ID_TEMPERATURE_SENSORS_OUT_OF_RANGE = (HIGH, 48, 0, 0, 0) + ALARM_ID_TEMPERATURE_SENSORS_INCONSISTENT = (HIGH, 49, 0, 0, 0) + ALARM_ID_HD_COMM_TIMEOUT = (HIGH, 50, 0, 0, 0) + ALARM_ID_VALVE_CONTROL_FAILURE = (HIGH, 51, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = (HIGH, 52, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = (HIGH, 53, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_FLOW_VS_MOTOR_SPEED_CHECK = (HIGH, 54, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_MOTOR_SPEED_CHECK = (HIGH, 55, 0, 0, 0) + ALARM_ID_DIAL_IN_PUMP_MOTOR_SPEED_CHECK = (HIGH, 56, 0, 0, 0) + ALARM_ID_DIAL_OUT_PUMP_MOTOR_SPEED_CHECK = (HIGH, 57, 0, 0, 0) + ALARM_ID_BLOOD_PUMP_ROTOR_SPEED_TOO_HIGH = (HIGH, 58, 0, 0, 0) + ALARM_ID_INLET_WATER_TEMPERATURE_OUT_OF_RANGE = (HIGH, 59, 0, 0, 0) + ALARM_ID_DOES_NOT_EXIST = (HIGH, 99, 0, 0, 0) + + +class HDAlarmsSimulator(AbstractSubSystem): + instance_count = 0 + + def __init__(self, can_interface: DenaliCanMessenger, logger: Logger): + """ + @param can_interface: Denali Can Messenger object + """ + super().__init__() + HDAlarmsSimulator.instance_count = HDAlarmsSimulator.instance_count + 1 + + self.can_interface = can_interface + self.logger = logger + self.flags = 0 + self.clear_after_user_action = False + self.current_alarm_volume = 5 + + if self.can_interface is not None: + channel_id = DenaliChannels.ui_to_hd_ch_id + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_USER_REQUEST_ALARM_SILENCE.value, + self._handler_alarm_silence) + self.can_interface.register_receiving_publication_function(channel_id, + MsgIds.MSG_ID_UI_ALARM_USER_ACTION.value, + self._handler_user_action) + self.can_interface.register_receiving_publication_function(DenaliChannels.ui_to_hd_ch_id, + MsgIds.MSG_ID_UI_SET_ALARM_AUDIO_VOLUME_LEVEL_CMD.value, + self._handler_request_override_alarm_volume) + + def _send_alarm_volume_broadcast(self): + """ + Sends the alarm volume broadcast message + @return: None + """ + + payload = integer_to_bytearray(5) # alarm volume + payload += float_to_bytearray(1.0) # alarm audio current high gain (mA) + payload += float_to_bytearray(1.0) # alarm audio current low gain (mA) + payload += float_to_bytearray(1.0) # alarm backup audio current (mA) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_INFORMATION.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_request_override_alarm_volume(self, message: dict) -> None: + """ + Handler for a UI request to override the alarm volume level + @param message: + @return: + """ + vol = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.START_POS_FIELD_1 + 4]))[0] + + self.logger.debug("Received request to override alarm volume to {0}".format(vol)) + + if 1 <= vol <= 5: + self.current_alarm_volume = vol + payload = integer_to_bytearray(YES) + payload += integer_to_bytearray(0) + else: + payload = integer_to_bytearray(NO) + payload += integer_to_bytearray(1) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_sync_broadcast_ch_id, + message_id=MsgIds.MSG_ID_HD_ALARM_AUDIO_VOLUME_SET_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_activate_alarm_id(self, state: int = HIGH, + alarm: int = 0, + escalates_in: int = 0, + silence_expires: int = 0, + flags: int = 0): + """ + Activates the specified alarm + + @param state: (int) Alarm priority + @param alarm: (int) the alarm id + @param escalates_in: (int) how long until the alarm escalates + @param silence_expires: (int) seconds until silence expires + @param flags: (int) See 'cmd_make_alarm_flags' + @return: None + """ + + state = integer_to_bytearray(state) + top = integer_to_bytearray(alarm) + escalates_in = integer_to_bytearray(escalates_in) + silence_expires = integer_to_bytearray(silence_expires) + flags = integer_to_bytearray(flags) + + payload = state + top + escalates_in + silence_expires + flags + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATUS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_activate_alarm(self, alarm: AlarmList, + state: int = HIGH, + escalates_in: int = 0, + silence_expires: int = 0, + flags: int = 0): + """ + Activates the specified alarm + + + @param alarm: the alarm enum + @param state: Alarm priority + @param escalates_in: how long until the alarm escalates + @param silence_expires: seconds until silence expires + @param flags: additional alarm flags + Alarm flags: + eFlag_systemFault = 0 + eFlag_stop = 1 + eFlag_noClear = 2 + eFlag_noResume = 3 + eFlag_noRinseback = 4 + eFlag_noEndTreatment = 5 + eFlag_noNewTreatment = 6 + eFlag_bypassDialyzer = 7 + eFlag_alarmsToEscalate = 8 + eFlag_alarmsSilenced = 9 + eFlag_userAcknowledged = 10 + ... unused = 11 - 16 + @return: None + """ + + state = integer_to_bytearray(state) + top = integer_to_bytearray(alarm.value) + escalates_in = integer_to_bytearray(escalates_in) + silence_expires = integer_to_bytearray(silence_expires) + flags = integer_to_bytearray(flags) + + payload = state + top + escalates_in + silence_expires + flags + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATUS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_make_alarm_flags(self, + system_fault=0, + stop=0, + no_clear=0, + no_resume=0, + no_rinseback=0, + no_end_treatment=0, + no_new_treatment=0, + user_must_ack=0, + alarms_to_escalate=0, + alarms_silenced=0, + lamp_on=0, + unused_1=0, + unused_2=0, + unused_3=0, + no_minimize=0, + top_condition=0 + ): + """ + Helper function to construct the flags + + @param system_fault: One or more system faults has been triggered + @param stop: Alarm(s) have stopped treatment/activity and placed system in a safe state + @param no_clear: One or more active alarms is not recoverable + @param no_resume: The "resume" user recovery option is disabled + @param no_rinseback: The "rinseback" user recovery option is disabled + @param no_end_treatment: The "end treatment" user recovery option is disabled + @param no_new_treatment: A new treatment may not be started without cycling power to system + @param user_must_ack: The "ok" user recovery option is enabled + @param alarms_to_escalate: One or more active alarms will escalate in time + @param alarms_silenced: Alarms have been temporarily silenced by user + @param lamp_on: Alarm lamp is currently on (for syncing to UI) + @param unused_1: unused + @param unused_2: unused + @param unused_3: unused + @param no_minimize: Prevent user from minimizing alarm window + @param top_condition: The top alarm's condition is still being detected + @return: (int) containing all the flags + """ + flags = 0 + flags ^= system_fault * 2 ** 0 \ + | stop * 2 ** 1 \ + | no_clear * 2 ** 2 \ + | no_resume * 2 ** 3 \ + | no_rinseback * 2 ** 4 \ + | no_end_treatment * 2 ** 5 \ + | no_new_treatment * 2 ** 6 \ + | user_must_ack * 2 ** 7 \ + | alarms_to_escalate * 2 ** 8 \ + | alarms_silenced * 2 ** 9 \ + | lamp_on * 2 ** 10 \ + | unused_1 * 2 ** 11 \ + | unused_2 * 2 ** 12 \ + | unused_3 * 2 ** 13 \ + | no_minimize * 2 ** 14 \ + | top_condition * 2 ** 15 + return flags + + def cmd_send_clear_alarms(self): + """ + Broadcasts a clear alarms message + + @return: None + """ + + state = integer_to_bytearray(0) + top = integer_to_bytearray(0) + escalates_in = integer_to_bytearray(0) + silence_expires = integer_to_bytearray(0) + flags = integer_to_bytearray(0) + + payload = state + top + escalates_in + silence_expires + flags + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_STATUS.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_alarm_cleared(self, alarm_id: int = 0): + """ + Broadcasts to clear a specific alarm ID + + the Alarm Cleared message builder method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x0400| 0x001 | 1 | Event | Y | HD | All | Alarm Cleared | \ref Data::mAlarmID | + + @return: None + """ + + payload = integer_to_bytearray(alarm_id) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_CLEARED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_alarm_cleared_condition(self, alarm_id: int = 0): + """ + Broadcasts that the alarm condition has been cleared + + the Alarm Cleared message builder method + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:---------:|:---:|:------:|:---:|:---:|:---:|:---------------------: |:--: | + |0x3F00| 0x001,2,4 | 1 | Event | Y | HD | All | Alarm Condition Cleared | \ref Data::mAlarmID | + + @return: None + """ + + payload = integer_to_bytearray(alarm_id) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_set_alarm_triggered(self, alarm_id, field_descriptor_1: int, data_field_1: str, + field_descriptor_2: int, data_field_2: str) -> None: + """ + Triggers an alarm. + + | MSG | CAN ID | Box | Type | Ack | Src | Dst | Description | #1:(U32) | + |:----:|:------:|:---:|:------:|:---:|:---:|:---:|:-----------: |:--: | + |0x0300| 0x001 | 1 | Event | Y | HD | All | Alarm Triggered | \ref Data::mAlarmID | + + @param alarm_id: int, the alarm id to trigger + @param field_descriptor_1: alarm data 1 type + @param data_field_1: alarm data 1 + @param field_descriptor_2: alarm data 2 type + @param data_field_2: alarm data 2 + @return: None + """ + zero = integer_to_bytearray(0) + payload = integer_to_bytearray(alarm_id) + if field_descriptor_1 == AlarmDataTypes.ALARM_DATA_TYPE_NONE: + payload += zero + payload += zero + payload += zero + payload += zero + else: + if field_descriptor_1 == AlarmDataTypes.ALARM_DATA_TYPE_F32: + payload += integer_to_bytearray(field_descriptor_1) + payload += float_to_bytearray(float(data_field_1)) + else: # BOOL, S32, U32 + payload += integer_to_bytearray(field_descriptor_1) + payload += integer_to_bytearray(int(data_field_1)) + if field_descriptor_2 == AlarmDataTypes.ALARM_DATA_TYPE_NONE: + payload += zero + payload += zero + else: + if field_descriptor_2 == AlarmDataTypes.ALARM_DATA_TYPE_F32: + payload += integer_to_bytearray(field_descriptor_2) + payload += float_to_bytearray(float(data_field_2)) + else: # BOOL, S32, U32 + payload += integer_to_bytearray(field_descriptor_2) + payload += integer_to_bytearray(int(data_field_2)) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_TRIGGERED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def cmd_repeat_broadcast_alarm(self, freq: int = 4, timeout: float = float('inf'), **kwargs): + """ + Broadcast the specified alarm message at particular frequency + + + @param freq: cycles / s of the broadcast + @param timeout: How long to broadcast the alarm for + @param kwargs: arguments to pass to cmd_activate_alarm + @return: None + """ + start = time() + current = time() + counter = 0 + silence_expires = 60 + + while current - start < timeout: + self.cmd_activate_alarm(flags=self.flags, silence_expires=silence_expires, **kwargs) + sleep(1.0 / freq) + current = time() + counter += 1 + if silence_expires > 0: + silence_expires -= 1 + else: + self.flags = self.cmd_make_alarm_flags(alarms_silenced=0) + + def set_flags(self, flags): + """ + Sets the alarm flags + + @param flags: + @return: None + """ + self.flags = flags + + def _handler_alarm_acknowledge(self) -> None: + """ + TODO: Remove + Handles the alarm acknowledge message + + @return: None + """ + + self.logger.debug("Alarm acknowledged") + self.flags = self.flags | 2 ** 10 + + def _handler_alarm_silence(self, message): + """ + Handles the alarm silence message + + @param message: the message with 0 = cancel, 1 = silence + @return: None + """ + + request = message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1][0] + if request: + self.logger.debug("Alarm Silence Request Start {0}".format(request)) + self.flags = self.flags | 2 ** 9 + else: + self.logger.debug("Alarm Silence Request Cancel {0}".format(request)) + self.flags = self.flags & ~2 ** 9 + + def cmd_alarm_condition_cleared(self, alarm_id: int): + """ + Sends the alarm condition cleared message + @param alarm_id: (int) The alarm ID + @return: None + """ + payload = integer_to_bytearray(alarm_id) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_alarm_broadcast_ch_id, + message_id=MsgIds.MSG_ID_ALARM_CONDITION_CLEARED.value, + payload=payload) + + self.can_interface.send(message, 0) + + def _handler_user_action(self, message): + """ + Called when the user responds to an alarm + @return: None + """ + response = struct.unpack('i', bytearray( + message['message'][MsgFieldPositions.START_POS_FIELD_1:MsgFieldPositions.END_POS_FIELD_1]))[0] + + self.logger.debug("User response to alarm: {0}".format(response)) + if self.clear_after_user_action: + self.cmd_send_clear_alarms() + + def cmd_send_active_list_response(self, accept: bool, reason: int = 0, + a0: int = 0, a1: int = 0, a2: int = 0, a3: int = 0, a4: int = 0, + a5: int = 0, a6: int = 0, a7: int = 0, a8: int = 0, a9: int = 0) -> None: + """ + send the list of active alarms + @param accept: boolean value true if the request accepted + @param reason: the rejection reason + @param a0: alarm id 0 in the list - First + @param a1: alarm id 1 in the list + @param a2: alarm id 2 in the list + @param a3: alarm id 3 in the list + @param a4: alarm id 4 in the list + @param a5: alarm id 5 in the list + @param a6: alarm id 6 in the list + @param a7: alarm id 7 in the list + @param a8: alarm id 8 in the list + @param a9: alarm id 9 in the list - Last + @return: None + """ + payload = integer_to_bytearray(accept) + payload += integer_to_bytearray(reason) + payload += integer_to_bytearray(a0) + payload += integer_to_bytearray(a1) + payload += integer_to_bytearray(a2) + payload += integer_to_bytearray(a3) + payload += integer_to_bytearray(a4) + payload += integer_to_bytearray(a5) + payload += integer_to_bytearray(a6) + payload += integer_to_bytearray(a7) + payload += integer_to_bytearray(a8) + payload += integer_to_bytearray(a9) + + message = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_ui_ch_id, + message_id=MsgIds.MSG_ID_HD_ACTIVE_ALARMS_LIST_REQUEST_RESPONSE.value, + payload=payload) + + self.can_interface.send(message, 0) Index: shared/scripts/dialin/ui/messageBuilder.py =================================================================== diff -u --- shared/scripts/dialin/ui/messageBuilder.py (revision 0) +++ shared/scripts/dialin/ui/messageBuilder.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,97 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 messageBuilder.py +# +# @author (last) Quang Nguyen +# @date (last) 22-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ + +from dialin.ui import utils, crc + +syncByte = 'A5' + + +def toCandumpFormat(msg: any) -> str: + """ + the converter method which converts the message vMsg to the string that candump tool understands. + @param msg: the message + @return: converted string to candump tool format + """ + if type(msg) == list: + for index, value in enumerate(msg): + msg[index] = ".".join(utils.partition(value, 2, False)) + else: + msg = ".".join(utils.partition(msg, 2, False)) + return msg + + +def toFrames(msg: any) -> list: + """ + converts the message vMsg to frames + @param msg: adds the crc8 checksum at the end of the message vMsg + @return: the frames + """ + mlen = 16 + padded = utils.padding(msg, mlen) + frames = utils.partition(padded, mlen, False) + return frames + + +def addCRC8(string: str, delimiter: str = "") -> str: + """ + adds the crc8 checksum at the end of the string vString + @param string: (str) the string to be used + @param delimiter: (str) the string delimiter + @return: the string with crc8 + """ + return string + delimiter + crc.calc_crc8(string, delimiter) + + +def textToByte(text: str, length) -> str: + """ + converts the string text to bytes by the given length + @param text: (str) given string + @param length: (int) given length + @return: converted text + """ + new_text = "" + text_len = len(text) + for i in range(length): + if i < text_len: + new_text += utils.toI08(ord(text[i])) + else: + new_text += utils.toI08(0) + new_text += utils.toI08(0) # null /0 + return new_text + + +def buildMessage(msg_id: int, length: int, ack: bool, *args) -> str: + """ + builds message from the parameter givven + @param msg_id: (int) the message ID + @param length: (int) length of the message payload in bytes + @param ack: (bool) if true the message requires acknowledge back + @param args: payload arguments. + @return: built message + """ + msg = "" + if ack: + seq = -1 + else: + seq = 1 + + msg += utils.toI16(seq) # always used seq# (-)1 (for now) + msg += utils.toI16(msg_id) + msg += utils.toI08(length) + for arg in args: + msg += arg + msg += crc.calc_crc8(msg) + return syncByte + msg Index: shared/scripts/dialin/ui/sim_setup.sh =================================================================== diff -u --- shared/scripts/dialin/ui/sim_setup.sh (revision 0) +++ shared/scripts/dialin/ui/sim_setup.sh (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,7 @@ +#!/bin/bash + +cd "$HOME"/denali +nohup ./setupVCAN.sh & +sleep 2 +nohup ./simulator.sh & + Index: shared/scripts/dialin/ui/unittests.py =================================================================== diff -u --- shared/scripts/dialin/ui/unittests.py (revision 0) +++ shared/scripts/dialin/ui/unittests.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,76 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ + +import test +import sys + +from dialin.ui import crc +from dialin.ui.utils import check_can0 + +MICRO = [8, 9] + + +def test_python_version(): + """ + tests the current python version compatibility + @return: None + """ + test.compare(sys.version_info.major, 3) + test.compare(sys.version_info.minor, 6) + + test.compare(sys.version_info.micro in MICRO, True) + + +def test_crc8(): + """ + test case for crc8 method + @return: None + """ + str_byte1 = ("4B 43 09 00 14 00 00" + "00 00 00 00 00 00 00 00" + "00 00 00 00 00 00 00 00" + "00 00" # 9D + ) + + str_byte2 = ("4C 43 02 00 12 03 00" + "00 00 14 00 00 00 00 00" + "00 00 00 00 00 00 7F 00" # 55 + ) + + str_byte3 = ("4A 43 05 00 1C 00 00" + "00 00 00 00 00 00 00 00" + "00 00 00 00 00 00 6A B6" + "99 43 D5 68 6F 44 00 00" + "00 00" # 4F + ) + + str_byte4 = ("FB 18 07 00 04 00 00" + "00 00" # 7F + ) + + test.compare(crc.calc_crc8(str_byte1, ' '), '9D') + test.compare(crc.calc_crc8(str_byte2, ' '), '55') + test.compare(crc.calc_crc8(str_byte3, ' '), '4F') + test.compare(crc.calc_crc8(str_byte4, ' '), '7F') + + +def test_can0(): + """ + tests if the can0 bus driver presents + @return: None + """ + ok, msg = check_can0() + test.compare(ok, True, msg) Index: shared/scripts/dialin/ui/utils.py =================================================================== diff -u --- shared/scripts/dialin/ui/utils.py (revision 0) +++ shared/scripts/dialin/ui/utils.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,290 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 utils.py +# +# @author (last) Quang Nguyen +# @date (last) 22-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 11-Nov-2020 +# +############################################################################ + +import time +import os +import squish +import struct +from subprocess import check_output + + +RainierMonitorApplication = "denaliSquish" + +COMMON_PATH = f"{os.environ['HOME']}/denali/Projects/dialin" + + +def srsui(vstr=""): + """ + for user convenience if wanted to put a note for the SRSUI + it makes sure all the numbers are 3 digit aligned to easily read on test results. + """ + return "SRSUI " + "{}".format(vstr).rjust(3, '0') + + +def isVisible(object, vobject): + """ + checks if the object is visible. + Note: in SquishQt it's a little different since usual check is wait + Note : object is the squish's builtin variable which should be available within the test + I don't thing importing would be a good idea here since it might be a runtime assigned value + and it may have different/undefined value when imported out of test + """ + if object.exists(vobject): + o_object = findObject(vobject) + if o_object is None: + return False + else: + return o_object.visible + else: + return False + + +def waitForGUI(delay_s: int = 2): + """ + a global 2 seconds default wait method which is used to wait for GUI animations mostly + to make sure the object is available. + @param delay_s: integer - amount of second for delay + @return: none + """ + time.sleep(delay_s) + + +def toUXX(value: int, byte_count: int, delimiter: str) -> str: + """ + converts the value to hex + @param value: (int) the value + @param byte_count: (int) number of bytes the value will take eg. U32=4, ... + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the value + """ + x = '{0:0{1}X}'.format(int(value) & (2 ** (4 * byte_count) - 1), byte_count, 'x') + byte_arr = partition(x, 2) + return delimiter.join(byte_arr) + + +def toI32(value: int, delimiter: str = "") -> str: + """ + a convenient method for toUXX to convert the value to integer 4 bytes + @param value: (int) the value + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the vValue to int 4 bytes + """ + return toUXX(value, 8, delimiter) + + +def toI16(value: int, delimiter: str = "") -> str: + """ + a convenient method for toUXX to convert the value to integer 2 bytes + @param value: (int) the value + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the vValue to int 2 bytes + """ + return toUXX(value, 4, delimiter) + + +def toI08(value: int, delimiter: str = "") -> str: + """ + a convenient method for toUXX to convert the value vValue to integer 1 byte + @param value: (int) the value + @param delimiter: (str) the output delimiter + @return: hex formated conversion of the vValue to int 1 bytes + """ + return toUXX(value, 2, delimiter) + + +def toF32(value: float) -> str: + """ + converts value to floating point 4 bytes. + @param value: (float) the value + @return: hex formated conversion of the vValue to float 4 bytes + """ + return '{:08X}'.format(struct.unpack('f', value))[0], 'X') + + +def partition(string: str, part: int, right_rirection: bool = True) -> list: + """ + splits the given string into sections of vPart long + and puts the sections from left to right if right_rirection is False + and puts the sections from right to left if right_rirection is True + after the split is done + @param string: (str) the given string + @param part: (int) length of the section + @param right_rirection: (bool) order of sections in the output list + @return: (list) the section of the string in list + """ + return [string[i: i + part] for i in range(0, len(string), part)][::-1 if right_rirection else 1] + + +def padding(string: str, length: int) -> any: + """ + added zero at the right side of the string to be of length of vLen + @param string: (str) the string to add trailing zero to + @param length: (int) the entire length of the string + @return: (str) padded string + """ + str_len = len(string) + pad_len = int(str_len / length) * length + (length * (1 if str_len % length else 0)) + return string.ljust(pad_len, "0") + + +def tstStart(test_name) -> None: + """ + test case start print out with time + @param test_name: (str) name of the test case + @return: none - prints out on the console + """ + print(time.strftime("%H:%M:%S Start", time.localtime()) + " - " + test_name) + + +def tstDone() -> None: + """ + test case end print out with time + @return: none - prints out on the console + """ + print(time.strftime("%H:%M:%S Done ", time.localtime())) + + +def l2ml(value) -> int: + """ + converts liter to milliliter + @param (int) value: the value in liter. + @return: (int) value converted to milliliter. + """ + return int(round(value, 3) * 1000) + + +def ml2l(value): + """ + converts milliliter to liter + @param (int) value: the value in milliliter. + @return: (int) value converted to liter. + """ + return value / 1000 + + +def dict_update(obj: dict, key: str, value): + """ + Adds a key and value to a dictionary object without updating the original + dictionary + + @param obj: (dict) the object to update + @param key: (str) the key name + @param value: the new value of the field + @return: (dict) the updated dictionary object + """ + obj = obj.copy() + obj[key] = value + return obj + + +def check_can0(): + """ + check if the can0 bus driver presents + @return: (list) false if can0 not exists, msg as a message + """ + canid = "can0" + ipa = "ip a" + ipa = check_output(ipa, shell=True) + loc = str(ipa).find(canid) + if loc >= 0: + msg = "can device '{}' found".format(canid) + else: + msg = "No can device registered as '{}'".format(canid) + return loc >= 0, msg + + +def cleanup(): + """ + Cleanup for simulator + @return: None + """ + os.chdir(f"{COMMON_PATH}/ui") + try: + res = os.system("sudo ./cleanup.sh") + waitForGUI(2) + if (res != 0) and (res != 256): + raise Exception("Incorrect command to kill the running simulator processes. " + \ + "Please check the cleanup.sh and provide required permissions") + except Exception as msg: + test.log(pyStr(msg)) + test.log("Cleaned up running simulator") + + +def startApplication(app_executable, app_name): + """ + Function to start application and verify application status [running] + If application does not start or running status is false, test stops + Argument: + @param app_name : (str) - Name of the application + @param app_executable : (str) - Actual application + @return: handle for the application if the application is in running state, + or error (exist the application) + """ + counter = 0 + while True: + try: + counter += 1 + test.log("Starting Application {}".format(app_name)) + startApplication(app_executable) +# if app.isRunning == True: +# test.passes("{} application is running".format(app_name)) + if counter == 1: + test.log(f"Application launched at the {counter}'st try.") + elif counter == 2: + test.log(f"Application launched at the {counter}'nd try.") + elif counter == 3: + test.log(f"Application launched at the {counter}'rd try.") + else: + test.log(f"Application launched at the {counter}'th try.") + squish.snooze(20) + break +# else: +# test.fail("{} startup failure or the application is hung".format(app_name)) + except RuntimeError: + if counter == 1: + test.log(f"Application failed to launch after {counter} try - Please refer logs") + elif counter == 20: + test.log(f"Exiting after {counter} tries..") + sys.exit(1) + else: + test.log(f"Application failed to launch after {counter} tries - Please refer logs") + except: + logErrorDetails("Failed to start the application") + sys.exit(1) + + +def launch_application(test_name ): + """ + Method to enables simulator and launch application + @param test_name: (str) name of the test case + @return: None, print out in the console + """ + cleanup() # cleanup before setup + + test.log("Launching a new instance of File Simulator") + try: + os.chdir(f"{COMMON_PATH}/ui") + res = os.system("./sim_setup.sh") + waitForGUI(delay_s = 5) + if (res != 0) and (res != 256): + raise Exception("Wrong path to the simulator executable was given. Script not executed") + except Exception as msg: + test.log(pyStr(msg)) + test.log("Launched a new instance of Simulator") + + startApplication(RainierMonitorApplication, "Denali Application") + + Index: shared/scripts/dialin/utils/__init__.py =================================================================== diff -u --- shared/scripts/dialin/utils/__init__.py (revision 0) +++ shared/scripts/dialin/utils/__init__.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,10 @@ +from .base import * +from .checks import * +from .helpers import * +from .conversions import * +from .excel_ops import * +from .nv_ops_utils import * +from .singleton import * +from .data_logger import DataLogger +YES = 1 +NO = 0 Index: shared/scripts/dialin/utils/base.py =================================================================== diff -u --- shared/scripts/dialin/utils/base.py (revision 0) +++ shared/scripts/dialin/utils/base.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,287 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 base.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 22-Jun-2020 +# +############################################################################ +import logging +import os +from abc import ABC, abstractmethod +from datetime import datetime +from enum import Enum + + +class AbstractObserver(ABC): + """ + Publicly accessible parent class for all observers. + + The update method will receive data when data is made available + """ + + @abstractmethod + def update(self): + """ + Attach an observer + """ + pass + + +class _FauxLogger: + + def __init__(self, printing_enabled=False): + self.printing_enabled = printing_enabled + + def debug(self, msg): + if self.printing_enabled: + print("DEBUG: {0}".format(msg)) + + def info(self, msg): + if self.printing_enabled: + print("INFO: {0}".format(msg)) + + def warn(self, msg): + if self.printing_enabled: + print("WARN: {0}".format(msg)) + + def warning(self, msg): + if self.printing_enabled: + print("WARNING: {0}".format(msg)) + + def error(self, msg): + if self.printing_enabled: + print("ERROR: {0}".format(msg)) + + def critical(self, msg): + if self.printing_enabled: + print("CRITICAL: {0}".format(msg)) + + +class LogManager: + + LOG_FMT = '%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s' + LOG_FMT_NO_METADATA = '%(message)s' + LOG_DT_FMT = '%m-%d-%Y:%H:%M:%S' + + def __init__(self, log_level=None, log_filepath="Dialin.log"): + """ + + @param log_level: (str) or (None) if not set, contains the logging level + @param log_filepath: (str) the log filepath + """ + + self.log_level = log_level + self.logging_enabled = self.log_level_enables_logging(log_level) + self.log_filepath = self.get_available_log_path(log_filepath) + self.logger = None + self.configure_logging(self.log_filepath) + + def log_level_enables_logging(self, log_level: str): + """ + Check if the log level string is a valid logging level + + @param log_level: (str) the logging level + @return: True if the log level is valid, False otherwise + """ + return log_level is not None and log_level.upper() in self.get_logging_levels() + + @staticmethod + def get_logging_levels(): + """ + Gets all possible logging levels + + @return: All possible logging levels + """ + return ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "CAN_ONLY", "PRINT_ONLY"] + + def request_new_log_path(self, path: str): + """ + Clears the logger, gets a new log filepath + and configures the logger to use it. + + @param path: the requested log filepath + @return: The new log filepath + """ + self.clear_logger() + self.log_filepath = self.get_available_log_path(path) + self.configure_logging(self.log_filepath) + return self.log_filepath + + def clear_logger(self): + """ + If the logger has been created, clear its handlers + + @return: True if successful, False otherwise + """ + + # remove existing handlers + if self.logger is not None: + for handler in self.logger.handlers: + self.logger.removeHandler(handler) + return True + return False + + def configure_logging(self, log_path): + """ + Sets up the logger to use the provided log path + + @param log_path: Path to the log file + @return: True if success, False otherwise + """ + if self.logger is not None: + print("Logger already configured. Please clear the logger first.") + return False + + # configure the logging + if self.logging_enabled and self.log_level not in ["PRINT_ONLY", "CAN_ONLY"]: + numeric_level = getattr(logging, self.log_level.upper(), logging.ERROR) + self.logger = logging.getLogger("Dialin") + self.logger.setLevel(numeric_level) + + fh = logging.FileHandler(log_path) + fh.setLevel(numeric_level) + ch = logging.StreamHandler() + ch.setLevel(numeric_level) + formatter = logging.Formatter(fmt=self.LOG_FMT, + datefmt=self.LOG_DT_FMT) + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + self.logger.addHandler(fh) + self.logger.addHandler(ch) + else: + self.logger = _FauxLogger(printing_enabled=self.log_level == "PRINT_ONLY") + + return True + + def print_and_log(self, message, log_level=logging.DEBUG): + """ + Prints a message if its severity is >= the current log level. + Also logs the message. + + @param message: The message to print and log + @param log_level: The logging level, indicates the severity of the message + + @return:: None + """ + if not self.logging_enabled: + return + + if log_level == logging.DEBUG: + self.logger.debug(message) + elif log_level == logging.INFO: + self.logger.info(message) + elif log_level == logging.WARNING: + self.logger.warning(message) + elif log_level == logging.ERROR: + self.logger.error(message) + elif log_level == logging.CRITICAL: + self.logger.critical(message) + + @staticmethod + def get_available_log_path(filepath: str): + """ + Gets an available log path from filepath + appends integer to the end if file already exists. + + @param filepath: The full path to the file + @return: (str) The available log filepath + """ + + if not os.path.exists(filepath): + return filepath + + path, ext = os.path.splitext(filepath) + i = 0 + while os.path.exists("{0}{1}{2}".format(path, i, ext)): + i += 1 + return "{0}{1}{2}".format(path, i, ext) + + +class AbstractSubSystem: + + @abstractmethod + def __init__(self): + """ + Initialization function for the sub system + # The abstract base class requires all abstract methods are overridden by children classes + + """ + self._observers = [] + self._datetime_fmt = "%m.%d.%Y_%I.%M.%S.%f" + pass + + def attach(self, observer: AbstractObserver): + """ + Attach an observer so it is updated upon published events + """ + self._observers.append(observer) + + def detach(self, observer: AbstractObserver): + """ + Detach an observer + """ + self._observers.remove(observer) + + +def publish(keys): + """ + Decorator that accepts a list of variable names to publish + To be used in any AbstractSubSystem + + @param keys: The variable names to publish + @return: A function that will take a function and return another function + """ + + def _decorator(func): + """ + + @param func: The handler function + @return: The function to wrap around _publish + """ + + def _wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + result = {} + + if not self._observers: + return None + + result["datetime"] = datetime.now() + result["subsystem"] = self.__class__.__name__ + + for key in keys: + result[key] = getattr(self, key) + + for observer in self._observers: + observer.update(result) + + return _wrapper + + return _decorator + + +class DialinEnum(Enum): + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ + + +class AlarmEnum(Enum): + def __init__(self, *args): + cls = self.__class__ + if any(self.value == member.value for member in cls): + raise ValueError("aliases not allowed: %r --> %r" % (self.name, cls(self.value).name)) + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ Index: shared/scripts/dialin/utils/checks.py =================================================================== diff -u --- shared/scripts/dialin/utils/checks.py (revision 0) +++ shared/scripts/dialin/utils/checks.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,27 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 checks.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Sean Nash +# @date (original) 27-May-2021 +# +############################################################################ + +def check_broadcast_interval_override_ms(ms: int): + """ + Checks whether a given broadcast interval override (in ms) is valid. + + @param ms: (int) number of ms being used to override a broadcast interval + @return: True if valid, False if not + """ + result = False + if ms > 0 and ms % 50 == 0: + result = True + return result Index: shared/scripts/dialin/utils/conversions.py =================================================================== diff -u --- shared/scripts/dialin/utils/conversions.py (revision 0) +++ shared/scripts/dialin/utils/conversions.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,102 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 conversions.py +# +# @author (last) Sean Nash +# @date (last) 12-Nov-2021 +# @author (original) Peter Lucia +# @date (original) 02-Apr-2020 +# +############################################################################ +import struct +from typing import List, Union + + +def byte_to_bytearray(val: int) -> bytes: + """ + Converts a byte value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts a short integer (2 bytes) value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts an unsigned short integer (2 bytes) value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int or val < 0: + raise ValueError("Expected unsigned integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts an integer value into a byte array (little endian) + + @param val: integer to convert to byte array, bool is accepted + @return: byte array + """ + if type(val) != bool and type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts an integer value into a byte array (little endian) + + @param val: (int) integer to convert to byte array + @return: byte array + """ + if type(val) != int or val < 0: + raise ValueError("Expected unsigned integer but received {0} with type {1}".format(val, type(val))) + return struct.pack(" bytes: + """ + Converts a float value into a byte array (little endian) + + @param val: float to convert to byte array, int is accepted + @return:: byte array + """ + if type(val) != float and type(val) != int: + raise ValueError("Expected float but received {0} with type {1}".format(val, type(val))) + return struct.pack(' List[int]: + """ + Converts an integer to a bit array. For direct conversion to binary please use bin(val), + as this function has a separate purpose + @param val: (int) the number to convert + @param num_bits: (int) the number of bits needed + @return: (List[int]) The number represented in binary form as a list of 1's and 0's + """ + if type(val) != int: + raise ValueError("Expected integer but received {0} with type {1}".format(val, type(val))) + return [(val >> bit) & 1 for bit in range(num_bits - 1, -1, -1)] Index: shared/scripts/dialin/utils/data_logger.py =================================================================== diff -u --- shared/scripts/dialin/utils/data_logger.py (revision 0) +++ shared/scripts/dialin/utils/data_logger.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,161 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 data_logger.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 30-Apr-2021 +# +############################################################################ +import glob +import os +import shutil +import threading +from collections import deque +from logging import Logger +from time import sleep +from typing import Tuple +from multiprocessing import Process +from pathlib import Path +import pandas as pd + +from dialin.utils.base import _FauxLogger + + +class DataLogger: + + MAX_CHUNK_SIZE = 20 + + def __init__(self, + folder: str = "/tmp/", # must be the full path + logger: Logger = _FauxLogger()): + """ + Logs data to xlsx file + @param folder: (str) the destination filepath + + """ + super().__init__() + self.queue = deque() # Thread safe + self.disable_logging = False + self.thread = threading.Thread(target=self.logging_scheduler, daemon=True) + self.base_folder = folder + self.csv_writers = {} + self.logger = logger + self.thread.start() + self.path_disable_logging = "/tmp/DIALIN_DISABLE_LOGGING" + + def logging_scheduler(self) -> None: + """ + Called on each timer event. Calls the data logger method + + @return: None + """ + while True: + self.do_data_logging() + sleep(0.05) + + def add_data(self, data: Tuple): + """ + + @param data: (Tuple) the data to add to the queue + @return: None + """ + # ("module", "timestamp", "header", "value") + if not os.path.exists(self.path_disable_logging): + self.queue.append(data) + # TODO: Remove later - keep for debugging + # else: + # self.logger.debug("Ignoring {0}".format(data)) + + def do_data_logging(self) -> None: + """ + Called on teach timer event. Logs the data currently in deque + @return: None + """ + + i = 0 + while self.queue: + module, timestamp, data_name, data_value = self.queue.pop() + filename = module + folder = os.path.join(self.base_folder, filename + "Log") + if not os.path.isdir(folder): + os.mkdir(folder) + filepath = os.path.join(folder, data_name + ".csv") + + if not os.path.exists(filepath): + header = ["timestamp", data_name] + with open(filepath, 'w') as f: + f.write(",".join(header) + "\n") + else: + with open(filepath, 'a') as f: + f.write(timestamp + ", " + data_value + "\n") + + i += 1 + if i >= self.MAX_CHUNK_SIZE: + break + + def export_to_xlsx(self, output_path: str) -> None: + """ + Called when the user wishes to export all captured logs to a xlsx file + + @param output_path: (str) the destination output path + @return: None + """ + process = Process(target=self._do_export_to_xlsx, args=(output_path,)) + process.start() + + def _do_export_to_xlsx(self, output_path: str) -> None: + """ + Performs the actual export to xlsx + + @param output_path: (str) the destination output path + @return: None + """ + + Path(self.path_disable_logging).touch() + self.logger.debug("Starting data export to {0}".format(output_path)) + log_path = os.path.join(self.base_folder, "*Log") + folders = glob.glob(log_path) + if len(folders) == 0: + self.logger.debug("No folder with data to export") + os.remove(self.path_disable_logging) + return + writer = pd.ExcelWriter(output_path) + for folder in folders: + files = os.path.join(folder, "*.csv") + csv_files = glob.glob(files) + module_name = os.path.basename(folder) + df = pd.DataFrame() + for csv_file in csv_files: + df_data = pd.read_csv(csv_file) + df_data = df_data.set_index("timestamp") + df = df.join(df_data, how="outer") + try: + df.to_excel(writer, sheet_name=module_name) + self.logger.debug("Added {0} to {1}".format(module_name, output_path)) + except ValueError as e: + self.logger.error("Error during write to excel: {0}".format(e)) + + writer.save() + self.logger.debug("Finished data export to {0}".format(output_path)) + os.remove(self.path_disable_logging) + + def clear_logs(self): + """ + Called when the user clears the logs + @return: None + """ + Path(self.path_disable_logging).touch() + log_path = os.path.join(self.base_folder, "*Log") + folders = glob.glob(log_path) + for folder in folders: + self.logger.debug("Removing {0}".format(folder)) + shutil.rmtree(folder) + os.remove(self.path_disable_logging) + Index: shared/scripts/dialin/utils/excel_ops.py =================================================================== diff -u --- shared/scripts/dialin/utils/excel_ops.py (revision 0) +++ shared/scripts/dialin/utils/excel_ops.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,207 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 excel_ops.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Dara Navaei +# @date (original) 21-Feb-2021 +# +############################################################################ + +import os +import math +import datetime +from openpyxl.styles import PatternFill, Font, Alignment +from openpyxl.utils import get_column_letter +from openpyxl import Workbook, load_workbook + +COLUMN_WIDTH_TOLERANCE = 3.0 +COLUMN_MAX_WIDTH = 50.0 +ROW_DEFAULT_HEIGHT = 15.0 +GREEN = PatternFill(start_color='00FF00', end_color='00FF00', fill_type='solid') +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') + + +def get_an_excel_workbook(): + """ + This function returns an excel workbook object + + @return excel workbook object + """ + return Workbook() + + +def setup_excel_worksheet(workbook_obj, title, index=None): + """ + 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) + + @return none + """ + # Assume the new sheet does not exist + is_sheet_name_available = False + + # Loop through the list of the sheet names in the excel + for sheet_name in workbook_obj.sheetnames: + # If the requested title exists in the excel document, set as True + # and break out of the loop. + if title == sheet_name: + is_sheet_name_available = True + break + # If the requested sheet does not exist, create it + if is_sheet_name_available is False: + # Create a tab and name the tab with the name of the projects dictionary + workbook_obj.create_sheet(title=title, index=index) + + # 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': + sheet = workbook_obj.get_sheet_by_name(workbook_obj.sheetnames[0]) + workbook_obj.remove_sheet(sheet) + + +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): + """ + This function writes data at the specified row and column in an excel file (object). + + @param workbook_obj: Excel workbook object + @param project: Name of the sheet (i.e HD-DEN-4308) + @param row: Current row number + @param column: Current column number + @param data: Data to be written at the cell + @param name: Font type (default Calibri) + @param font: Font size (default 11) + @param bold: Bold or un-bold (default un-bold) + @param color: Color of the cell (default no color) + @param merge: Merge cells. Cells must be provided with A1:A4 format (default none) + @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) + + @return: None + """ + row_height = 0 + # Get the number of an alphabetic column (i.e A -> 1) + column_letter = get_column_letter(column) + cell_name = column_letter + str(row) + + # Set the active worksheet to the provided worksheet + active_sheet = workbook_obj[project] + # Set the provided data into the specified row and column and set the bold, color and horizontal alignment + active_sheet.cell(row=row, column=column).value = data + active_sheet.cell(row=row, column=column).font = Font(size=font, bold=bold, name=name) + # Wrapping text is not needed unless the length of the data is more than maximum column + # length + active_sheet[cell_name].alignment = Alignment(vertical='center', horizontal=horizontal, wrap_text=False) + # Get the width of the current column + column_width = active_sheet.column_dimensions[column_letter].width + # When the column width is on the default, openpyxl reports None. If the width is reported as None, + # it will be set to 0 for math comparison + column_width = 0 if column_width is None else column_width + # If the length is not provided, use the default maximum length + max_len = COLUMN_MAX_WIDTH if max_col_len is None else max_col_len + # Remove all the end of the line artifacts + length_of_data = len(str(data).rstrip()) + + # If the length of data was greater than the maximum length, calculate the number of + # rows is needed with the + # default height + if length_of_data > max_len: + # Since the length is greater than maximum, enable wrap text + active_sheet[cell_name].alignment = Alignment(vertical='center', horizontal=horizontal, wrap_text=True) + # Calculate what the row height should be when the cell is extended + row_height = math.ceil(length_of_data / max_len) * ROW_DEFAULT_HEIGHT + active_sheet.column_dimensions[column_letter].width = max_len + # If the length of the data provided is already less than the length of cell, + # do nothing + elif column_width < length_of_data: + active_sheet.column_dimensions[column_letter].width = length_of_data + COLUMN_WIDTH_TOLERANCE + row_height = ROW_DEFAULT_HEIGHT + + # If the current row height is not defined or the row height is less than the calculated new height, set it + if active_sheet.row_dimensions[row].height is None or active_sheet.row_dimensions[row].height < row_height: + active_sheet.row_dimensions[row].height = row_height + + # If color has been defined, set the color of the cell + if color is not None: + active_sheet[cell_name].fill = color + + # If merge has been requested + if merge is not None: + # The format of merge for this function is A1:C1 + active_sheet.merge_cells(str(merge)) + + if freeze: + # To freeze row 1, make the cell is not row 1, that's why A2 was chosen + active_sheet.freeze_panes = 'A2' + + +def load_excel_report(path): + """ + This function returns an object of a currently existing excel workbook + + @return loaded excel workbook object + """ + return load_workbook(path) + + +def get_cell_value(workbook_obj, project, row, col): + """ + This function returns the value of a written excel cell + + @return excel workbook object + """ + # Set the active worksheet to the provided worksheet + active_sheet = workbook_obj[project] + # Get the cell object + cell_obj = active_sheet.cell(row=row, column=col) + # Convert it to the actual value + return cell_obj.value + + +def merge_cells(workbook_obj, project, start_row, start_col, end_row, end_col): + """ + This function merges the specified cells. + + @param workbook_obj: Excel workbook object + @param project: Name of the sheet (i.e HD-DEN-4308) + @param start_row: Row number at the beginning of merge + @param start_col: Column number at the beginning of merge + @param end_row: Row number at the end of merge + @param end_col: Column number at the end of merge + + @return: None + """ + # Go to the define sheet + active_sheet = workbook_obj[project] + # Convert the cell numbers to text and number (i.e row=1 and col=1 -> A1) for the start and end cells + merge_start_cell = get_column_letter(start_col) + str(start_row) + merge_end_cell = get_column_letter(end_col) + str(end_row) + # The format of merge for this function is A1:C1 + active_sheet.merge_cells(str(merge_start_cell) + ':' + str(merge_end_cell)) + + +def save_report(excel_workbook, save_dir, record_name): + """ + This function overrides the save function in the Base class. The function saves the excel file. + + @returns none + """ + # Get the current date + current_date = str(datetime.datetime.now().date()) + # 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') + excel_workbook.save(filename=path) Index: shared/scripts/dialin/utils/helpers.py =================================================================== diff -u --- shared/scripts/dialin/utils/helpers.py (revision 0) +++ shared/scripts/dialin/utils/helpers.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,238 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 helpers.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 29-Apr-2021 +# +############################################################################ +import logging +import subprocess +from datetime import datetime +from .base import LogManager + + +def setup_virtual_can_interface(): + """ + Convenience function to setup a virtual can interface using the most common settings + See the setup_can function to setup a can interface with custom settings. + + @return: True if successful, false otherwise + """ + bring_interface_down(interface="can0", delete=True) + is_present = is_interface_present("can0") + if not is_present: + setup_can(interface="can0", + is_virtual=True) + result = is_interface_up("can0") + if result: + print("Success") + return True + + print("Failure") + return False + + +def setup_real_can_interface(): + """ + Convenience function to setup a real can interface using the most common settings + See the setup_can function to setup a can interface with custom settings. + + @return: True if successful, False otherwise + """ + delete_virtual = input("Delete virtual can interface? (y,n)") + if delete_virtual.upper() in ["Y", "YES"]: + bring_interface_down(interface="can0", delete=True) + if is_interface_present("can0"): + print("Failure") + return False + else: + bring_interface_down(interface="can0", delete=False) + if not is_interface_present("can0") or is_interface_up("can0"): + print("Failure") + return False + input("Press 'Enter' after plugging in the PCAN-USB.") + setup_can(interface="can0", + is_virtual=False, + bitrate=250000, + restart_ms=100, + txqueue=10000) + + if is_interface_present("can0") and is_interface_up("can0"): + print("Success") + return True + else: + print("Failure") + return False + + +def bring_interface_down(interface: str = "can0", delete=False): + """ + Brings the specified can interface down + + @param interface: (str) the interface name + @param delete: (bool) whether to delete the interface after bringing it down + @return: None + """ + cmd_iface_down = "sudo ifconfig {0} down > /dev/null 2>&1".format(interface) + cmd_iface_delete = "sudo ip link delete dev {0} > /dev/null 2>&1".format(interface) + + success, info = run_cmd(cmd_iface_down) + if not success: + print("Warning: Could not bring interface down: {0}".format(info)) + if delete: + run_cmd(cmd_iface_delete) + if not success: + print("Warning: Could not delete the interface: {0}".format(info)) + + +def run_cmd(cmd: str): + """ + Runs the provided command, returns if it was successful and output or error information + + @param cmd: (str) the command to run + @return: tuple(bool, info) Whether the command was successful. If true info is the output, otherwise + info contains the error information + """ + try: + result = subprocess \ + .check_output(cmd, + shell=True).decode("utf-8").strip() + return True, result + except subprocess.CalledProcessError as e: + return False, str(e) + + +def is_interface_up(interface: str = "can0"): + """ + Checks if the specified interface is listed and up + + @param interface: + @return: (bool) True if up, False if not or if we can't tell + """ + cmd_check_interface = "ifconfig {0}".format(interface) + + success, info = run_cmd(cmd_check_interface) + if success: + if interface in info and "UP,RUNNING" in info: + return True + return False + + +def is_interface_present(interface: str = "can0"): + """ + Checks if the specified interface is listed + + @param interface: + @return: (bool) True if present, False if not or if we can't tell + """ + cmd_check_interface = "ip a" + + success, info = run_cmd(cmd_check_interface) + if success: + if interface in info: + return True + return False + + +def setup_can(interface: str = "can0", is_virtual=False, bitrate=250000, restart_ms=100, txqueue=10000): + """ + Convenience function to setup a can interface + + @param interface: (str) The interface name + @param is_virtual: (bool) If the interface is virtual or not + @param bitrate: (int) The desired bitrate (applies to non-virtual interfaces only) + @param restart_ms: (int) The desired restart_ms (applies to non-virtual interfaces only) + @param txqueue: (int) The desired txqueue length + @return: + """ + cmd_iface_up = "sudo ip link set {0} up type can bitrate {1} restart-ms {2}" \ + .format(interface, bitrate, restart_ms) + cmd_txqueue = "sudo ifconfig {0} txqueuelen {1}".format(interface, txqueue) + + cmd_iface_type_virtual = "sudo ip link add dev {0} type vcan".format(interface) + cmd_iface_up_virtual = "sudo ip link set {0} up".format(interface) + + if is_virtual: + success, info = run_cmd(cmd_iface_type_virtual) + if not success: + print("Warning: Could not set iface type to virtual: {0}".format(info)) + success, info = run_cmd(cmd_iface_up_virtual) + if not success: + print("Warning: Could not bring up virtual interface: {0}".format(info)) + else: + success, info = run_cmd(cmd_iface_up) + if not success: + print("Warning: Could not bring interface up: {0}".format(info)) + + success, info = run_cmd(cmd_txqueue) + if not success: + print("Warning: Could not set txtqueue length: {0}".format(cmd_txqueue)) + + +def create_logger(log_path: str = "/tmp/DialinScript.log", + level: str = "ERROR", + enable_metadata=True, + clear_before_write=False): + """ + Convenience function to create a logger for external Dialin scripts + + @param log_path: (str) The full path to the output log file. + @param level: (str) The logging level (e.g. INFO, WARN, DEBUG, ERROR, CRITICAL) + @param enable_metadata: (bool) if True, include metadata, otherwise, log only the message + @param clear_before_write: (bool) if True, clear log file before write to it + @return: (logging.Logger) The logger object + """ + + numeric_level = getattr(logging, level, logging.ERROR) + + current_time = datetime.now() + + # create a new unique logger using current date and time + logger = logging.getLogger("DialinScript{0}{1}{2}".format(current_time.hour, + current_time.minute, + current_time.second)) + logger.setLevel(numeric_level) + + if clear_before_write: + fh = logging.FileHandler(log_path, mode='w') + else: + fh = logging.FileHandler(log_path) + fh.setLevel(numeric_level) + ch = logging.StreamHandler() + ch.setLevel(numeric_level) + if enable_metadata: + formatter = logging.Formatter(fmt=LogManager.LOG_FMT, + datefmt=LogManager.LOG_DT_FMT) + else: + formatter = logging.Formatter(fmt=LogManager.LOG_FMT_NO_METADATA) + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + logger.addHandler(fh) + logger.addHandler(ch) + return logger + + +def find_variables_in_object(obj, value, starts_with: str = ""): + """ + Returns a list of variable names that in the object that match the searched value + + @param obj: (object) The object to search through + @param value: (object) The value to lookup + @param starts_with: (str) The + @return: (List[str]) A list of variable names matching the searched value + """ + result = [] + for attr in dir(obj): + if not callable(getattr(obj, attr)) and attr.startswith(starts_with): + if value == getattr(obj, attr): + result.append(attr) + return result Index: shared/scripts/dialin/utils/nv_ops_utils.py =================================================================== diff -u --- shared/scripts/dialin/utils/nv_ops_utils.py (revision 0) +++ shared/scripts/dialin/utils/nv_ops_utils.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,813 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 nv_ops_utils.py +# +# @author (last) Quang Nguyen +# @date (last) 07-Jul-2021 +# @author (original) Dara Navaei +# @date (original) 21-Feb-2021 +# +############################################################################ +import struct +import time +from logging import Logger +from typing import List +from collections import OrderedDict +from .excel_ops import * + + +class NVOpsUtils: + """ + + Processes the calibration_record records, service records, system records, and the scheduled runs records. + The records are prepared to be sent to firmware or to be received from firmware. + """ + CRC_16_TABLE = ( + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 + ) + + DEFAULT_CHAR_VALUE = ' ' + _RECORD_START_INDEX = 6 + _RECORD_SPECS_BYTES = 12 + _RECORD_SPECS_BYTE_ARRAY = 3 + + _CURRENT_MESSAGE_NUM_INDEX = 0 + + _DATA_TYPE_INDEX = 0 + _DATA_VALUE_INDEX = 1 + _CHAR_LENGTH_INDEX = 2 + _DICT_VALUE_LIST_LEN = 2 + + _TARGET_BYTES_TO_SEND_TO_FW = 150 + _MIN_PAYLOAD_BYTES_SPACE = 4 + + _PAYLOAD_CURRENT_MSG_INDEX = 0 + _PAYLOAD_TOTAL_MSG_INDEX = 1 + _PAYLOAD_TOTAL_BYTES_INDEX = 2 + + def __init__(self, logger: Logger): + """ + Constructor for the NVOptsUtils class + + @param logger: (Logger) the logger + """ + + self.logger = logger + self._workspace_dir = '' + self._excel_workbook = '' + self._record_name = '' + self._firmware_stack = '' + self._is_writing_to_excel_done = False + self._is_read_done = False + + # This list contains different data packet lists + # i.e. [[message 1, payload], [message 2, payload], ...] + self._record_packets_to_send_to_fw = [] + + # Buffer that is used to keep the groups data except the crc to convert them to bytes and calculate the crc + # of the group. + self._temp_groups_data_to_calculate_crc = [] + + def prepare_excel_report(self, firmware_stack: str, record_name: str, directory: str): + """ + 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 + @return none + """ + path = '' + is_report_found = False + + if not os.path.isdir(directory): + # Create the directory and go to it + os.mkdir(directory) + + self._workspace_dir = directory + + self._record_name = record_name + self._firmware_stack = firmware_stack + + # List all the files in the workspace directory + for file in os.listdir(self._workspace_dir): + # If the file has an extension of .xlsx + if file.endswith('.xlsx'): + # 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 + + if is_report_found: + # Load the excel workbook + self._excel_workbook = load_excel_report(path) + else: + # Get an excel workbook object + self._excel_workbook = get_an_excel_workbook() + + # Setup worksheet and create the current tab + setup_excel_worksheet(self._excel_workbook, self._record_name) + + def write_fw_record_to_excel(self, calibration_record: dict): + """ + Writes a calibration record to excel + @param calibration_record: (dict) the record to write to excel + @return: None + """ + + try: + + row = 1 + for group in calibration_record.keys(): + + start_row = row + start_col = 1 + col = 1 + if isinstance(calibration_record[group], dict): + + write_to_excel(self._excel_workbook, self._record_name, row, col, + group, bold=True, max_col_len=len(group)) + + for hardware in calibration_record[group].keys(): + list_of_keys = list(calibration_record[group].keys()) + + col = 2 + write_to_excel(self._excel_workbook, self._record_name, row, col, hardware) + col += 1 + + if isinstance(calibration_record[group][hardware], dict): + for spec in calibration_record[group][hardware]: + + spec_value = calibration_record[group][hardware][spec][1] + + write_to_excel(self._excel_workbook, self._record_name, row, col, spec) + col += 1 + write_to_excel(self._excel_workbook, self._record_name, row, col, spec_value) + col += 1 + else: + spec_value = calibration_record[group][hardware][1] + write_to_excel(self._excel_workbook, self._record_name, row, col, spec_value) + + if list_of_keys.index(hardware) == len(list_of_keys) - 1: + merge_cells(self._excel_workbook, self._record_name, + start_row, start_col, row, start_col) + row += 1 + + row += 1 + + save_report(self._excel_workbook, self._workspace_dir, self._firmware_stack) + # Signal reading is done + self._is_writing_to_excel_done = True + except Exception as e: + self.logger.error("Failed to write calibration record to excel: {0}".format(e)) + + def write_excel_record_to_calibration_record(self, calibration_record: dict): + """ + Writes an excel record to a calibration record + + @param calibration_record: (dict) the calibration record to write + @return True if reading to firmware records to excel is done otherwise False + """ + try: + temp_buffer = [] + is_value_different = False + row = 1 + for group in calibration_record.keys(): + + col = 1 + cell_value = get_cell_value(self._excel_workbook, self._record_name, row, col) + + if cell_value == group: + + if isinstance(calibration_record[group], dict): + for hardware in calibration_record[group].keys(): + if isinstance(calibration_record[group][hardware], dict): + col = 4 + for spec in calibration_record[group][hardware]: + cell_value = get_cell_value(self._excel_workbook, self._record_name, row, col) + # Check if the cell value is not none. If it is none, + # it cannot be converted to a number like float or integer + is_cell_value_valid = True if cell_value is not None else False + if 'crc' not in spec and 'time' not in spec and is_cell_value_valid: + temp_buffer.append( + struct.pack(calibration_record[group][hardware][spec][0], cell_value)) + if calibration_record[group][hardware][spec][1] != cell_value: + is_value_different = True + calibration_record[group][hardware][spec][1] = cell_value + + if is_value_different and 'time' in spec: + epoch_time = self.get_current_time_in_epoch() + calibration_record[group][hardware][spec][1] = epoch_time + temp_buffer.append( + struct.pack(calibration_record[group][hardware][spec][0], epoch_time)) + date_time = self.get_current_date_time(epoch_time) + write_to_excel(self._excel_workbook, self._record_name, row, col, date_time) + elif 'time' in spec and isinstance(cell_value, str) and is_cell_value_valid: + epoch_time = self.get_date_time_in_epoch(cell_value) + calibration_record[group][hardware][spec][1] = epoch_time + + if is_value_different and 'crc' in spec: + # Convert the data in the temp buffer to bytes and calculate its crc. + data = b''.join(temp_buffer) + crc_value = self.crc_16(data) + # Clear the temp buffer for the next round of data + is_value_different = False + calibration_record[group][hardware][spec][1] = crc_value + write_to_excel(self._excel_workbook, self._record_name, row, col, crc_value) + col += 2 + else: + col = 3 + for _ in calibration_record[group][hardware]: + cell_value = get_cell_value(self._excel_workbook, self._record_name, row, col) + # Check if the cell value is not none. If it is none, + # it cannot be converted to a number like float or integer + is_cell_value_valid = True if cell_value is not None else False + if is_cell_value_valid: + calibration_record[group][hardware][1] = cell_value + temp_buffer.clear() + row += 1 + row += 1 + + save_report(self._excel_workbook, self._workspace_dir, self._firmware_stack) + + except Exception as e: + self.logger.error("Failed to load the excel record as a calibration record: {0}".format(e)) + + def get_writing_to_excel_status(self): + """ + Publicly accessible function to get the reading status + + @return True if reading to firmware records to excel is done otherwise False + """ + + return self._is_writing_to_excel_done + + def get_reading_record_status(self): + """ + Gets the reading record status + @return: True if done, false otherwise + """ + return self._is_read_done + + @staticmethod + def get_processed_characters(record: list) -> bytearray: + """ + Gets the list of the characters and makes sure their length is to the define length + + @param record: (list) the list that contains the characters, data type and the target character length + @return characters (bytearray) that are converted to bytearrays + """ + data_type = record[0] + char = record[1] + char_len = record[2] + temp = bytearray() + + if len(char) > char_len: + char = char[:char_len] + elif len(char) < char_len: + for i in range(len(char), char_len): + char += NVOpsUtils.DEFAULT_CHAR_VALUE + + for ch in char: + temp += struct.pack(data_type, ch.encode('ascii')) + + return temp + + @staticmethod + def reset_fw_record(record: OrderedDict) -> OrderedDict: + """ + Gets a record and updated the calibration date and crc + + @param record: (dict) the record to calculate the calibration time and crc + @return record (OrderedDict) the record with updated calibration time and crc + """ + for key, value in record.items(): + if isinstance(value, dict): + for sub_key, sub_value in value.items(): + if sub_key == 'cal_time': + sub_value[sub_key][1] = NVOpsUtils.get_current_time_in_epoch() + crc = NVOpsUtils.get_group_record_crc(sub_value) + + sub_value['crc'][1] = crc + return record + + @staticmethod + def reset_fw_system_service_record(record: OrderedDict) -> OrderedDict: + """ + Gets a record and updated the calibration date and crc + + @param record: (dict) the record to calculate the calibration time and crc + @return record (OrderedDict) the record with updated calibration time and crc + """ + for key, value in record.items(): + if isinstance(value, dict): + crc = NVOpsUtils.get_group_record_crc(value) + value['crc'][1] = crc + return record + + @staticmethod + def get_group_record_crc(group_record: dict) -> int: + """ + Gets a group record and calculates the crc for the group + + @param group_record: (dict) the record to calculate the crc + @return crc (int) the calculated crc + """ + value_in_bytes = b'' + temp = [] + for key, value in group_record.items(): + if key is not 'crc': + data_type = value[0] + if data_type == ' self._MIN_PAYLOAD_BYTES_SPACE: + current_payload_length += data_type_bytes + temp_buffer[self._PAYLOAD_TOTAL_MSG_INDEX] = struct.pack(' self._MIN_PAYLOAD_BYTES_SPACE: + current_payload_length += data_type_bytes + # Insert a 4-byte 0 to the index of the total messages. This is a place holder and it will + # be updated with the right value later. + temp_buffer[self._PAYLOAD_TOTAL_MSG_INDEX] = struct.pack(' self._DICT_VALUE_LIST_LEN: + byte_size += current_byte_size * group[key][self._CHAR_LENGTH_INDEX] + else: + byte_size += current_byte_size + + return byte_size + + @classmethod + def crc_16(cls, data): + """ + generates crc16 for the provided data + @param data: byte of data + @return: (int) the crc code + """ + crc = 0xFFFF + length = len(data) + i = 0 + + while length > 0: + # Make the sure variables are 16-bit integers + left = (crc << 8) & 0x0000FFFF + right = (crc >> 8) & 0x0000FFFF + crc = left ^ cls.CRC_16_TABLE[data[i] ^ (right & 0x00FF)] + length -= 1 + i += 1 + + return crc + + @staticmethod + def get_data_type_bytes(data): + """ + Handles converting the string representation of the bytes of the data types in a struct to numbers. + This is a static method. + + @param data: the data to be converted to bytes in number + @return: calculated byte size + """ + number_of_bytes = struct.calcsize(data) + + return number_of_bytes + + @staticmethod + def calculate_padding_byte_size(total_byte_size, max_buffer_size): + """ + Handles calculating the padding length based on the provided buffer sizes. This is a static method. + + @param total_byte_size: total byte size of a record dictionary + @param max_buffer_size: max buffer size that is allowed to be used in the dictionary + + @return: padding size in bytes + """ + + # Calculate the padding size: + # If bytes in the dictionary % max write bytes to RTC RAM (64) or EEPROM (16) is 0, not padding is needed + # Else padding = (ceil(dictionary bytes/max write) * max write)-dictionary bytes + if (total_byte_size % max_buffer_size) == 0: + padding_size = 0 + else: + padding_size = (math.ceil(total_byte_size / max_buffer_size) * max_buffer_size) - total_byte_size + + return padding_size Index: shared/scripts/dialin/utils/singleton.py =================================================================== diff -u --- shared/scripts/dialin/utils/singleton.py (revision 0) +++ shared/scripts/dialin/utils/singleton.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,45 @@ +########################################################################### +# +# Copyright (c) 2019-2021 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 singleton.py +# +# @author (last) Quang Nguyen +# @date (last) 19-Jul-2021 +# @author (original) Peter Lucia +# @date (original) 29-Apr-2021 +# +############################################################################ +from threading import Lock + + +class SingletonMeta(type): + """ + Thread-safe implementation of Singleton. + """ + + _instances = {} + + _lock: Lock = Lock() + _creation_attempts = 0 + """ + Synchronizes threads during first access to the Singleton. + """ + + def __call__(cls, *args, **kwargs): + """ + Possible changes to the value of the `__init__` argument do not affect + the returned instance. + """ + cls._creation_attempts += 1 + # TODO: Remove later - keep for debugging + # print("CAN interface constructor call counter: {0}".format(cls._creation_attempts)) + # First thread acquires the lock + with cls._lock: + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + return cls._instances[cls] Index: shared/scripts/dialin/version.py =================================================================== diff -u --- shared/scripts/dialin/version.py (revision 0) +++ shared/scripts/dialin/version.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,69 @@ +########################################################################### +# +# Copyright (c) 2019-2020 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 version.py +# +# @author (last) Peter Lucia +# @date (last) 10-Nov-2020 +# @author (original) Peter Lucia +# @date (original) 18-Jun-2020 +# +############################################################################ +import subprocess + +VERSION = "0.9.0" + + +def get_branch(): + """ + Gets the current branch name in the current git repository + + @return: The current branch name, None if it can't be determined + """ + + try: + return subprocess.check_output("git rev-parse --abbrev-ref HEAD", shell=True).decode("utf-8").strip() + except subprocess.CalledProcessError: + return None + + +def get_last_commit(): + """ + Gets the latest commit in the current git repository + + @return: (str) the latest commit in the current git repository, None if it can't be determined + """ + try: + return subprocess.check_output("git rev-parse --short=7 HEAD", shell=True).decode("utf-8").strip() + except subprocess.CalledProcessError: + return None + + +def check_if_git_repo(): + """ + Checks if we're in a git repo or not to know if we can get the git branch and commit + + @return: True if in a git repo, False otherwise + """ + + return subprocess.call(["git", "branch"], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) == 0 + + +branch = None +commit = None + +DEV_VERSION = VERSION + +if check_if_git_repo(): + branch = get_branch() + commit = get_last_commit() + DEV_VERSION += ".{0}".format(branch) + DEV_VERSION += ".{0}".format(commit) + + +if __name__ == '__main__': + print(VERSION) Index: shared/scripts/names.py =================================================================== diff -u -r48849c79a27af51cb7c8c580b26694b3a880c603 -ra06097b6c375bec6088f8fa6feecc8851eaa5889 --- shared/scripts/names.py (.../names.py) (revision 48849c79a27af51cb7c8c580b26694b3a880c603) +++ shared/scripts/names.py (.../names.py) (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -56,7 +56,20 @@ fluid_text = {"container": treatmentStack_treatmentHome_TreatmentHome, "text": "Volume Delivered", "type": "Text", "unnamed": 1, "visible": True} cumulative_fluid_text = {"container": treatmentStack_treatmentHome_TreatmentHome, "text": "Cumulative Delivered", "type": "Text", "unnamed": 1, "visible": True} +#authentication +confirm_button = {"container": o_PreTreatmentCreateStack_PreTreatmentBase_TreatmentFlowBase, "text": "CONFIRM", "type": "Text", "unnamed": 1, "visible": True} +back_button ={"container": o_PreTreatmentCreateStack_PreTreatmentCreate_PreTreatmentCreate, "id": "_image", "source": "qrc:/images/iBack", "type": "Image", "unnamed": 1, "visible": True} +o_PreTreatmentBase_confirmButton_TouchRect = {"container": o_PreTreatmentCreateStack_PreTreatmentBase_TreatmentFlowBase, "text": "CONFIRM", "type": "Text", "unnamed": 1, "visible": True} +o_PreTreatmentCreate_bloodFlowRate_SliderCreateTreatment = {"container": o_PreTreatmentCreateStack_PreTreatmentCreate_PreTreatmentCreate, "gradient": 0, "objectName": "_bloodFlowRate", "type": "SliderCreateTreatment", "visible": True} +input_patient_id = {"container": o_PreTreatmentCreateStack_PreTreatmentBase_TreatmentFlowBase, "echoMode": 0, "id": "_input", "type": "TextInput", "unnamed": 1, "visible": True} +keboard_input = {"container": o_Gui_MainView, "type": "Text", "unnamed": 1, "visible": True} +custom_treatment = {"container": o_PreTreatmentCreateStack_PreTreatmentCreate_PreTreatmentCreate, "text": "Create a Custom Treatment", "type": "Text", "unnamed": 1, "visible": True} + +treatment_create_flickable = {"container": o_PreTreatmentCreateStack_PreTreatmentCreate_PreTreatmentCreate, "objectName": "TreatmentCreateFlickable", "type": "Flickable", "visible": True} +operating_parameters = {"container": o_PreTreatmentCreateStack_PreTreatmentCreate_PreTreatmentCreate, "type": "Text", "unnamed": 1, "visible": True} +o_PreTreatmentBase_confirmButton_TouchRect_2 = {"container": o_PreTreatmentCreateStack_PreTreatmentBase_TreatmentFlowBase, "gradient": 0, "objectName": "_confirmButton", "type": "TouchRect", "visible": True} +o_PreTreatmentCreate_heparinDispensingRate_SliderCreateTreatment = {"container": o_PreTreatmentCreateStack_PreTreatmentCreate_PreTreatmentCreate, "gradient": 0, "objectName": "_heparinDispensingRate", "type": "SliderCreateTreatment", "visible": True} +o_PreTreatmentBase_backgroundRect_Rectangle_2 = {"container": o_PreTreatmentCreateStack_PreTreatmentBase_TreatmentFlowBase, "gradient": 0, "id": "_backgroundRect", "type": "Rectangle", "unnamed": 1, "visible": True} - Index: tst_authentication/test.py =================================================================== diff -u --- tst_authentication/test.py (revision 0) +++ tst_authentication/test.py (revision a06097b6c375bec6088f8fa6feecc8851eaa5889) @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +## +# Copyright (c) 2019-2020 Diality Inc. - All Rights Reserved. +# copyright +# 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 tst_create_patient_ID +# date 2020/12/27 +# author Joseph varghese +# + +# NOTE: +# This test is intended to be used to authenticate and authorize patient id in application . + +import names +import test +from dialin.ui import utils +from dialin.ui import unittests + +from builtins import str as pyStr +from dialin.configuration import utility + + +CHARACTER_PATIENT_ID = ["qwerty","sampleid","abcdefghijklmnop","patientid", "username"] +ALPHANUMERIC_PATIENT_ID = ["sample@123", "AJHDJ-BH78","678GHJ"] + + +def keyboard_object_map_helper(text): + """ + Method for setting custom object property's for keyboard keys + + @return: required object property's for keys + """ + if isinstance(text, pyStr): + names.keboard_input["text"] = text + return names.keboard_input + else: + test.log(f"Invalid \"text\": {text} for object.") + names.keyboard_input["text"] = "Q" + + +def user_input_clear_option(): + """ + Tests to clear retained patient id + + @return: N/A + """ + patient_id_input = waitForObject(names.input_patient_id) + patient_id = str(patient_id_input.text) + patient_id_length = len(patient_id) + while (patient_id_length != 0): + type(waitForObject(names.input_patient_id), "") + type(waitForObject(names.input_patient_id), "") + patient_id_length = patient_id_length - 1 + + +def authentication_of_valid_patient_id_through_keypad(expected_value): + """ + Tests verifies valid patient id set through application keyboard setup . + + @return: N/A + """ + patient_id_input = waitForObject(names.input_patient_id) + mouseClick(patient_id_input) + patient_id = list(expected_value) + for text in patient_id: + keyboard_value = keyboard_object_map_helper(text) + mouseClick(waitForObject(keyboard_value)) + utils.waitForGUI(.2) + + test.compare(expected_value, (patient_id_input.text)) + test.log(f"Patient id should be -> {expected_value}") + + mouseClick(waitForObject(names.confirm_button)) + + custom_treatment = waitForObject(names.custom_treatment) + test.compare("Create a Custom Treatment", custom_treatment.text) + test.log(f"user successfully authenticated through user id {expected_value} using keypad.") + + mouseClick(waitForObject(names.back_button)) + + +def authentication_of_valid_patient_id_through_user_input(expected_value): + """ + Tests verifies valid patient id set through user's input. + + @return: N/A + """ + patient_id_input = waitForObject(names.input_patient_id) + mouseClick(waitForObject(patient_id_input)) + type(waitForObject(names.input_patient_id), expected_value) + test.compare(expected_value, (patient_id_input.text)) + test.log(f"Patient id should be -> {expected_value}") + + mouseClick(waitForObject(names.confirm_button)) + + custom_treatment = waitForObject(names.custom_treatment) + test.compare("Create a Custom Treatment", custom_treatment.text) + test.log(f"user successfully authenticated through user id {expected_value}.") + + mouseClick(waitForObject(names.back_button)) + + +def authentication_of_invalid_patient_id_through_user_input(expected_value): + """ + Tests verifies invalid patient id set through user's input. + + @return: N/A + """ + patient_id_input = waitForObject(names.input_patient_id) + mouseClick(waitForObject(patient_id_input)) + type(waitForObject(names.input_patient_id), expected_value) + if expected_value is not patient_id_input.text or patient_id_input.text is None: + test.passes(f"Patient ID -> {expected_value} should be invalid") + else: + test.xfail(f"Patient ID {expected_value} is valid") + user_input_clear_option() + + +def main(): + + utils.tstStart("tst_authentication") + utility.launch_application("Authentication and authorization using patient ID") + unittests.test_python_version() + + mouseClick(waitForObject(names.input_patient_id)) + for patient_id in CHARACTER_PATIENT_ID: + authentication_of_valid_patient_id_through_keypad(patient_id) + authentication_of_valid_patient_id_through_user_input(patient_id) + + for patient_id in ALPHANUMERIC_PATIENT_ID: + authentication_of_valid_patient_id_through_user_input(patient_id) + + utils.tstDone()