Index: .gitignore =================================================================== diff -u -r1e42fa562ee87c4f7e4e0f7e6e6a65aa6ac5fa8e -r8c39fe1f9affe360ee6a97c5e6243e58a5c27509 --- .gitignore (.../.gitignore) (revision 1e42fa562ee87c4f7e4e0f7e6e6a65aa6ac5fa8e) +++ .gitignore (.../.gitignore) (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -8,6 +8,8 @@ tags tags.* html/ - - *.log +dialin_pkg_diality.egg-info/ +dist/ +build/ +dialin.egg-info/ Index: LICENSE =================================================================== diff -u --- LICENSE (revision 0) +++ LICENSE (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,4 @@ +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. Index: README.md =================================================================== diff -u -r1e42fa562ee87c4f7e4e0f7e6e6a65aa6ac5fa8e -r8c39fe1f9affe360ee6a97c5e6243e58a5c27509 --- README.md (.../README.md) (revision 1e42fa562ee87c4f7e4e0f7e6e6a65aa6ac5fa8e) +++ README.md (.../README.md) (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -20,7 +20,7 @@ ### Windows -Note that windows support for the python-can library is still a work in progress. +Note that windows support for the python-can library has not been verified. If the windows environment has been previously set up, using command prompt: Index: build.sh =================================================================== diff -u --- build.sh (revision 0) +++ build.sh (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,5 @@ +#!/bin/bash + +python3 setup.py bdist_wheel + +cp dist/dialin-0.0.1-py3-none-any.whl ../test_scripts/ \ No newline at end of file Fisheye: Tag 8c39fe1f9affe360ee6a97c5e6243e58a5c27509 refers to a dead (removed) revision in file `dg/dialysate_generator.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 8c39fe1f9affe360ee6a97c5e6243e58a5c27509 refers to a dead (removed) revision in file `dg/firmware_simulator.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 8c39fe1f9affe360ee6a97c5e6243e58a5c27509 refers to a dead (removed) revision in file `dg/protocols'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 8c39fe1f9affe360ee6a97c5e6243e58a5c27509 refers to a dead (removed) revision in file `dg/utils'. Fisheye: No comparison available. Pass `N' to diff? Index: dialin/__init__.py =================================================================== diff -u --- dialin/__init__.py (revision 0) +++ dialin/__init__.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ \ No newline at end of file Index: dialin/dg/__init__.py =================================================================== diff -u --- dialin/dg/__init__.py (revision 0) +++ dialin/dg/__init__.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ \ No newline at end of file Index: dialin/dg/dialysate_generator.py =================================================================== diff -u --- dialin/dg/dialysate_generator.py (revision 0) +++ dialin/dg/dialysate_generator.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,257 @@ +########################################################################### +# +# 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 DialysateGenerator.py +# +# @date 31-Mar-2019 +# @author P. Lucia +# +# @brief This class allows sending to and receiving from the DG device. +# +############################################################################ +# TODO: Needs to be restructured and existing TODO items need to be addressed. + +from .protocols.CAN import (DenaliCanMessenger, + DenaliMessage, + DenaliChannels) +from .utils.conversions import integer_to_bytearray, float_to_bytearray +from time import sleep +import unittest + + +class DG: + """ + \class DG + + Dialysate Generator (DG) Dialin object API. It provides the basic interface to communicate with + the DG board + + """ + + DG_MSG_ID_FILL_COMMAND = 0x2000 + DG_MSG_ID_BROADCAST = 0X2100 + MSG_ID_LOAD_CELL_A1_OVERRIDE = 0xA005 + MSG_ID_LOAD_CELL_A2_OVERRIDE = 0xA006 + MSG_ID_LOAD_CELL_B1_OVERRIDE = 0xA007 + MSG_ID_LOAD_CELL_B2_OVERRIDE = 0xA008 + + def __init__(self, can_interface="can0"): + """ + DG constructor using can bus + + \param can_interface: string with can bus name, e.g. "can0" + \returns DG object that allows communication with board via port + + \details For example: + + dg_object = DG(can_interface='can0') or + dg_object = DG('can0') + + """ + # Create listener + self.can_interface = DenaliCanMessenger(can_interface=can_interface) + self.can_interface.register_receiving_publication_function(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=self.DG_MSG_ID_BROADCAST, + function=( + lambda message: print(".", end='', flush=True))) + self.can_interface.start() + + def fill(self, start_or_stop='start'): + """ + Request the DG board to 'start' or to 'stop' fill + + \param start_or_stop is a string indicating which action to take, e.g., 'start' or 'stop' + + \returns True if ran the command, False otherwise, returns None if timeout + """ + payload = [1] if start_or_stop == 'start' else [0] + + msg = DenaliMessage.build_message(channel_id=DenaliChannels.hd_to_dg_ch_id, + message_id=self.DG_MSG_ID_FILL_COMMAND, + payload=payload) + + # Send message + received_msg = self.can_interface.send(msg) + return_value = None + + if received_msg is not None: + return_value = True if DenaliMessage.get_payload(received_msg)[0] == 1 else False + + return return_value + + def cmd_load_cell_a1_override(self, reset, adc_raw): + """ + Constructs and sends the load cell A1 override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param adc_raw: unsigned int - raw adc value. 0.0894 per gram. 1000 ml = 11,186 + \returns 1 if successful, zero otherwise + + TODO: This is built based on HD but needs more infrastructure made for DG before being operational + """ + raise NotImplementedError + + """ + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(adc_raw) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_LOAD_CELL_A1_OVERRIDE, + payload=payload) + + print("override load cell A1 raw adc value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # print(received_message) + if reset == HD.RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + print("Load cell A1 raw adc (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: + print("Timeout!!!!") + return False + """ + + + def cmd_load_cell_a2_override(self, reset, adc_raw): + """ + Constructs and sends the load cell A2 override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param adc_raw: unsigned int - raw adc value. 0.0894 per gram. 1000 ml = 11,186 + \returns 1 if successful, zero otherwise + + TODO: This is built based on HD but needs more infrastructure made for DG before being operational + """ + raise NotImplementedError + + """ + rst = integer_to_bytearray(reset) + cur = float_to_bytearray(adc_raw) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_LOAD_CELL_A2_OVERRIDE, + payload=payload) + + print("override load cell A2 raw adc value") + + # Send message + received_message = self.can_interface.send(message) + + # If there is content... + if received_message is not None: + # print(received_message) + if reset == HD.RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + print("Load cell A2 raw adc (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: + print("Timeout!!!!") + return False + """ + + + def cmd_load_cell_b1_override(self, reset, adc_raw): + """ + Constructs and sends the load cell B1 override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param adc_raw: unsigned int - raw adc value. 0.0894 per gram. 1000 ml = 11,186 + \returns 1 if successful, zero otherwise + + TODO: This is built based on HD but needs more infrastructure made for DG before being operational + """ + raise NotImplementedError + + """ + rst = self.outer_instance.integer_to_bytearray(reset) + cur = self.outer_instance.float_to_bytearray(adc_raw) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_LOAD_CELL_B1_OVERRIDE, + payload=payload) + + print("override load cell B1 raw adc value") + + # Send message + received_message = self.outer_instance.can_interface.send(message) + + # If there is content... + if received_message is not None: + # print(received_message) + if reset == HD.RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + print("Load cell B1 raw adc (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: + print("Timeout!!!!") + return False + """ + + + def cmd_load_cell_b2_override(self, reset, adc_raw): + """ + Constructs and sends the load cell B2 override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param adc_raw: unsigned int - raw adc value. 0.0894 per gram. 1000 ml = 11,186 + \returns 1 if successful, zero otherwise + + TODO: This is built based on HD but needs more infrastructure made for DG before being operational + """ + raise NotImplementedError + + """ + rst = self.outer_instance.integer_to_bytearray(reset) + cur = self.outer_instance.float_to_bytearray(adc_raw) + payload = rst + cur + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_LOAD_CELL_B2_OVERRIDE, + payload=payload) + + print("override load cell B2 raw adc value") + + # Send message + received_message = self.outer_instance.can_interface.send(message) + + # If there is content... + if received_message is not None: + # print(received_message) + if reset == HD.RESET: + str_res = "reset back to normal" + else: + str_res = str(curr) + print("Load cell B2 raw adc (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: + print("Timeout!!!!") + return False + """ + + Index: dialin/dg/protocols =================================================================== diff -u --- dialin/dg/protocols (revision 0) +++ dialin/dg/protocols (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ +../protocols/ \ No newline at end of file Index: dialin/dg/utils =================================================================== diff -u --- dialin/dg/utils (revision 0) +++ dialin/dg/utils (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ +../utils/ \ No newline at end of file Index: dialin/hd/__init__.py =================================================================== diff -u --- dialin/hd/__init__.py (revision 0) +++ dialin/hd/__init__.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ \ No newline at end of file Index: dialin/hd/alarms.py =================================================================== diff -u --- dialin/hd/alarms.py (revision 0) +++ dialin/hd/alarms.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,277 @@ +########################################################################### +# +# 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 alarms.py +# +# @date 2-Apr-2020 +# @author P. Lucia +# +# @brief +# +############################################################################ +from .protocols.CAN import (DenaliMessage, + DenaliChannels) +from .utils.conversions import integer_to_bytearray +from .constants import RESET +import struct + + +class HDAlarms: + """ + \class HD_Alarms + + \brief Hemodialysis Device (HD) Dialin API sub-class for alarm related commands. + """ + + # alarms message IDs + MSG_ID_HD_ALARMS_PUBLISHED_STATUS = 0x0002 + MSG_ID_HD_ALARM_ACTIVATE = 0x0003 + MSG_ID_HD_ALARM_CLEAR = 0x0004 + MSG_ID_HD_ALARM_LAMP_OVERRIDE = 0x8004 + MSG_ID_HD_ALARM_STATE_OVERRIDE = 0x8006 + MSG_ID_HD_ALARM_TIME_OVERRIDE = 0x8007 + + # 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 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_SILENCE_EXPIRES_IN = END_POS_ALARM_TOP + END_POS_ALARM_SILENCE_EXPIRES_IN = START_POS_ALARM_SILENCE_EXPIRES_IN + 4 + START_POS_ALARM_ESCALATES_IN = END_POS_ALARM_SILENCE_EXPIRES_IN + END_POS_ALARM_ESCALATES_IN = START_POS_ALARM_ESCALATES_IN + 4 + START_POS_ALARMS_FLAGS = END_POS_ALARM_ESCALATES_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): + """ + HD_Alarms constructor + + \param outer_instance: reference to the HD (outer) class. + + \returns HD_Alarms object. + """ + self.can_interface = can_interface + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_alarm_broadcast_ch_id + msg_id = self.MSG_ID_HD_ALARMS_PUBLISHED_STATUS + 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 = self.MSG_ID_HD_ALARM_ACTIVATE + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self.handler_alarm_activate) + channel_id = DenaliChannels.hd_alarm_broadcast_ch_id + msg_id = self.MSG_ID_HD_ALARM_CLEAR + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self.handler_alarm_clear) + + # 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] * 50 + + 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 + \returns 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_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_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_flags = int.from_bytes(bytearray( + message['message'][self.START_POS_ALARMS_FLAGS:self.END_POS_ALARMS_FLAGS]), + byteorder=DenaliMessage.BYTE_ORDER) + + def handler_alarm_activate(self, message): + """ + Handles published HD alarm activation messages. + + \param message: published HD alarm activation message + \returns none + """ + + alarm_id = struct.unpack(' After STOP:{} <---".format(hd.DialOut.DialOutBroadcast)) + + sleep(3) + hd.DialOut.set_uf_state(DialOutStates.RUN) + sleep(2) + state_stop = hd.DialOut.DialOutBroadcast['state'] + print("After RUN again: {}".format(hd.DialOut.DialOutBroadcast)) + + sleep(3) + hd.DialOut.set_uf_state(DialOutStates.STOP) + sleep(2) + state_stop = hd.DialOut.DialOutBroadcast['state'] + print("After STOP: {}".format(hd.DialOut.DialOutBroadcast)) + + sleep(5) + hd.DialOut.plot_broadcast_signals() + + exit() + tgtRate = 0 + hd.bloodflow.cmd_blood_flow_broadcast_interval_override(NO_RESET, 2000) + + while True: + if hd.bloodflow.target_blood_flow_rate == 0: + if tgtRate != 0: + hd.bloodflow.cmd_blood_flow_broadcast_interval_override(NO_RESET, 2000) + tgtRate = 0 + else: + if tgtRate == 0: + hd.bloodflow.cmd_blood_flow_broadcast_interval_override(NO_RESET, 200) + tgtRate = hd.bloodflow.target_blood_flow_rate + + sleep(1) + + print(hd.bloodflow.measured_blood_flow_rate) + +# hd.bloodflow.cmd_blood_flow_broadcast_interval_override(RESET,0) +""" Index: dialin/hd/pressure_occlusion.py =================================================================== diff -u --- dialin/hd/pressure_occlusion.py (revision 0) +++ dialin/hd/pressure_occlusion.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,317 @@ +########################################################################### +# +# 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 pressure_occlusion.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief +# +# +############################################################################ +from .protocols.CAN import (DenaliMessage, + DenaliChannels) +from .utils.conversions import integer_to_bytearray, float_to_bytearray +from .constants import RESET +import struct + + +class HDPressureOcclusion: + """ + \class HDPressureOcclusion + + \brief Hemodialysis Device (HD) Dialin API sub-class for pressure related commands. + """ + + # Pressure/Occlusion message IDs + MSG_ID_HD_PRESSURE_OCCLUSION_DATA = 0x0009 + MSG_ID_HD_PRESSURE_ARTERIAL_OVERRIDE = 0x8017 + MSG_ID_HD_PRESSURE_VENOUS_OVERRIDE = 0x8018 + MSG_ID_HD_OCCLUSION_BLOOD_PUMP_OVERRIDE = 0x8019 + MSG_ID_HD_OCCLUSION_DIAL_IN_PUMP_OVERRIDE = 0x801A + MSG_ID_HD_OCCLUSION_DIAL_OUT_PUMP_OVERRIDE = 0x801B + MSG_ID_HD_PRES_OCCL_SEND_INTERVAL_OVERRIDE = 0x801C + + # Pressure/Occlusion broadcast message field positions + START_POS_ART_PRES = DenaliMessage.PAYLOAD_START_INDEX + END_POS_ART_PRES = START_POS_ART_PRES + 4 + START_POS_VEN_PRES = END_POS_ART_PRES + END_POS_VEN_PRES = START_POS_VEN_PRES + 4 + START_POS_BP_OCCL = END_POS_VEN_PRES + END_POS_BP_OCCL = START_POS_BP_OCCL + 4 + START_POS_DIP_OCCL = END_POS_BP_OCCL + END_POS_DIP_OCCL = START_POS_DIP_OCCL + 4 + START_POS_DOP_OCCL = END_POS_DIP_OCCL + END_POS_DOP_OCCL = START_POS_DOP_OCCL + 4 + + def __init__(self, can_interface=None): + """ + HDPressureOcclusion constructor + + \param outer_instance: reference to the HD (outer) class. + + \returns HDPressureOcclusion object. + """ + self.can_interface = can_interface + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = self.MSG_ID_HD_PRESSURE_OCCLUSION_DATA + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self.handler_pressure_occlusion_sync) + + self.arterialPressure = 0 + self.venousPressure = 0.0 + self.bloodPumpOcclusion = 0.0 + self.dialysateInletPumpOcclusion = 0.0 + self.dialysateOutletPumpOcclusion = 0.0 + + 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 + \returns none + """ + + art = struct.unpack('f', bytearray( + message['message'][self.START_POS_ART_PRES:self.END_POS_ART_PRES])) + ven = struct.unpack('f', bytearray( + message['message'][self.START_POS_VEN_PRES:self.END_POS_VEN_PRES])) + bp = struct.unpack('f', bytearray( + message['message'][self.START_POS_BP_OCCL:self.END_POS_BP_OCCL])) + dpi = struct.unpack('f', bytearray( + message['message'][self.START_POS_DIP_OCCL:self.END_POS_DIP_OCCL])) + dpo = struct.unpack('f', bytearray( + message['message'][self.START_POS_DOP_OCCL:self.END_POS_DOP_OCCL])) + + self.arterialPressure = art[0] + self.venousPressure = ven[0] + self.bloodPumpOcclusion = bp[0] + self.dialysateInletPumpOcclusion = dpi[0] + self.dialysateOutletPumpOcclusion = dpo[0] + + def cmd_arterial_pressure_measured_override(self, reset, pres): + """ + Constructs and sends the measured arterial pressure override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param pres: float - measured arterial pressure (in mmHg) to override with + \returns 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=self.MSG_ID_HD_PRESSURE_ARTERIAL_OVERRIDE, + payload=payload) + + print("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. " + print("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: + print("Timeout!!!!") + return False + + def cmd_venous_pressure_measured_override(self, reset, pres): + """ + Constructs and sends the measured venous pressure \n + override command. + + \param reset: integer - 1 to reset a previous override, 0 to override + \param pres: float - venous pressure (in mmHg) to override with + \returns 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=self.MSG_ID_HD_PRESSURE_VENOUS_OVERRIDE, + payload=payload) + + print("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. " + print("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: + print("Timeout!!!!") + return False + + def cmd_blood_pump_measured_occlusion_override(self, reset, occl): + """ + Constructs and sends the measured blood pump occlusion pressure override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param occl: float - pressure (in mmHg) to override with + \returns 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + occ = float_to_bytearray(occl) + payload = rst + occ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_HD_OCCLUSION_BLOOD_PUMP_OVERRIDE, + payload=payload) + + print("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. " + print("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: + print("Timeout!!!!") + return False + + def cmd_dialysate_inlet_pump_measured_occlusion_override(self, reset, occl): + """ + Constructs and sends the measured dialysate inlet pump occlusion pressure override \n + command. + + \param reset: integer - 1 to reset a previous override, 0 to override + \param occl: float - pressure (in mmHg) to override with + \returns 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + occ = float_to_bytearray(occl) + payload = rst + occ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_HD_OCCLUSION_DIAL_IN_PUMP_OVERRIDE, + payload=payload) + + print("override measured dialysate inlet 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. " + print("Dialysate inlet 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: + print("Timeout!!!!") + return False + + def cmd_dialysate_outlet_pump_measured_occlusion_override(self, reset, occl): + """ + Constructs and sends the measured dialysate outlet pump occlusion pressure override \n + command. + + \param reset: integer - 1 to reset a previous override, 0 to override + \param occl: float - pressure (in mmHg) to override with + \returns 1 if successful, zero otherwise + """ + + rst = integer_to_bytearray(reset) + occ = float_to_bytearray(occl) + payload = rst + occ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_HD_OCCLUSION_DIAL_OUT_PUMP_OVERRIDE, + payload=payload) + + print("override measured dialysate outlet 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. " + print("Dialysate outlet 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: + print("Timeout!!!!") + return False + + def cmd_pressure_occlusion_broadcast_interval_override(self, reset, ms): + """ + Constructs and sends the pressure/occlusion broadcast interval override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param ms: integer - interval (in ms) to override with + \returns 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=self.MSG_ID_HD_PRES_OCCL_SEND_INTERVAL_OVERRIDE, + payload=payload) + + print("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: " + print("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: + print("Timeout!!!!") + return False Index: dialin/hd/protocols =================================================================== diff -u --- dialin/hd/protocols (revision 0) +++ dialin/hd/protocols (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ +../protocols/ \ No newline at end of file Index: dialin/hd/rtc.py =================================================================== diff -u --- dialin/hd/rtc.py (revision 0) +++ dialin/hd/rtc.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,99 @@ +########################################################################### +# +# 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 rtc.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief +# +# +############################################################################ +from .protocols.CAN import (DenaliMessage, + DenaliChannels) +from .utils.conversions import integer_to_bytearray +import ctypes + + +class HDRTC: + """ + \class HDRTC + + \brief Hemodialysis Device (HD) Dialin API sub-class for rtc commands. + """ + + MSG_ID_SET_RTC_DATE_TIME = 0x801D + MSG_ID_RTC_EPOCH = 0x000A + + START_POS_SET_PT = DenaliMessage.PAYLOAD_START_INDEX + END_POS_SET_PT = START_POS_SET_PT + 4 + + def __init__(self, outer_instance, can_interface=None): + """ + HD_BloodFlow constructor + + \param outer_instance: reference to the HD (outer) class. + + \returns HD_BloodFlow object. + """ + self.can_interface = can_interface + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = self.MSG_ID_RTC_EPOCH + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self.handler_rtc_epoch) + self.rtc_epoch = 0 + + def handler_rtc_epoch(self, message): + """ + Publishes the rtc time in epoch + + \param message: published rtc epoch message + \returns none + """ + epoch = int.from_bytes(bytearray( + message['message'][self.START_POS_SET_PT:self.END_POS_SET_PT]), + byteorder=DenaliMessage.BYTE_ORDER) + self.rtc_epoch = ctypes.c_uint32(epoch) + + def cmd_set_rtc_time_and_date(self, secs, mins, hours, days, months, years): + """ + Constructs and sends the time and date to be written to rtc + + \returns 1 if successful, zero otherwise + """ + sec = bytes([secs]) + min = bytes([mins]) + hour = bytes([hours]) + day = bytes([days]) + month = bytes([months]) + year = integer_to_bytearray(years) + payload = sec + min + hour + day + month + year + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_SET_RTC_DATE_TIME, + payload=payload) + + print("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: + print(received_message) + # str_res = str(flow) + print("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: + print("Timeout!!!!") + return False Index: dialin/hd/treatment.py =================================================================== diff -u --- dialin/hd/treatment.py (revision 0) +++ dialin/hd/treatment.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,113 @@ +########################################################################### +# +# 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 treatment.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief +# +# +############################################################################ +from .protocols.CAN import (DenaliMessage, + DenaliChannels) +import struct + + +class HDTreatment: + """ + \class HD_Treatment + + \brief Hemodialysis Device (HD) Dialin API sub-class for treatment related commands. + + """ + + # treatment message IDs + MSG_ID_HD_TREATMENT_TIME_PUBLISHED_DATA = 0x000D + MSG_ID_HD_TREATMENT_STATE_PUBLISHED_DATA = 0X000F + + # treatment time broadcast message field positions + START_POS_TIME_PRES = DenaliMessage.PAYLOAD_START_INDEX + END_POS_TIME_PRES = START_POS_TIME_PRES + 4 + START_POS_TIME_ELAPSED = END_POS_TIME_PRES + END_POS_TIME_ELAPSED = START_POS_TIME_ELAPSED + 4 + START_POS_TIME_REMAINING = END_POS_TIME_ELAPSED + END_POS_TIME_REMAINING = START_POS_TIME_REMAINING + 4 + + # treatment state broadcast message field positions + START_POS_TREATMENT_STATE = DenaliMessage.PAYLOAD_START_INDEX + END_POS_TREATMENT_STATE = START_POS_TREATMENT_STATE + 4 + START_POS_UF_STATE = END_POS_TREATMENT_STATE + END_POS_UF_STATE = START_POS_UF_STATE + 4 + START_POS_SALINE_BOLUS_IN_PROGRESS = END_POS_UF_STATE + END_POS_SALINE_BOLUS_IN_PROGRESS = START_POS_SALINE_BOLUS_IN_PROGRESS + 4 + + def __init__(self, can_interface=None): + """ + HD_Treatment constructor + """ + self.can_interface = can_interface + + if self.can_interface is not None: + channel_id = DenaliChannels.hd_sync_broadcast_ch_id + msg_id = self.MSG_ID_HD_TREATMENT_TIME_PUBLISHED_DATA + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self.handler_treatment_time_sync) + msg_id = self.MSG_ID_HD_TREATMENT_STATE_PUBLISHED_DATA + self.can_interface.register_receiving_publication_function(channel_id, msg_id, + self.handler_treatment_state_sync) + + self.treatment_time_prescribed = 0 + self.treatment_time_elapsed = 0 + self.treatment_time_remaining = 0 + self.treatment_state = 0 + self.treatment_uf_state = 0 + self.saline_bolus_in_progress = False + + 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 + \returns none + """ + + tot = struct.unpack('i', bytearray( + message['message'][self.START_POS_TIME_PRES:self.END_POS_TIME_PRES])) + ela = struct.unpack('i', bytearray( + message['message'][self.START_POS_TIME_ELAPSED:self.END_POS_TIME_ELAPSED])) + rem = struct.unpack('i', bytearray( + message['message'][self.START_POS_TIME_REMAINING:self.END_POS_TIME_REMAINING])) + + self.treatment_time_prescribed = tot[0] + self.treatment_time_elapsed = ela[0] + self.treatment_time_remaining = rem[0] + + 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 + \returns none + """ + + tst = struct.unpack('i', bytearray( + message['message'][self.START_POS_TREATMENT_STATE:self.END_POS_TREATMENT_STATE])) + ufs = struct.unpack('i', bytearray( + message['message'][self.START_POS_UF_STATE:self.END_POS_UF_STATE])) + bol = struct.unpack('i', bytearray( + message['message'][self.START_POS_SALINE_BOLUS_IN_PROGRESS:self.END_POS_SALINE_BOLUS_IN_PROGRESS])) + + self.treatment_state = tst[0] + self.treatment_uf_state = ufs[0] + if bol[0] == 1: + self.saline_bolus_in_progress = True + else: + self.saline_bolus_in_progress = False Index: dialin/hd/ui.py =================================================================== diff -u --- dialin/hd/ui.py (revision 0) +++ dialin/hd/ui.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,440 @@ +########################################################################### +# +# 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 ui.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief +# +# +############################################################################ +from .protocols.CAN import (DenaliMessage, + DenaliChannels) +from .utils.conversions import integer_to_bytearray, float_to_bytearray +import struct + + +class HDUI: + """ + \class HD_UI + + \brief Hemodialysis Device (HD) Dialin API sub-class for ui commands. + """ + + # ui message IDs + MSG_ID_UI_CHECKIN_WITH_HD = 0x0007 + MSG_ID_HD_UF_PAUSE_RESUME_REQUEST = 0x0010 + MSG_ID_HD_TREATMENT_PARAMS_RANGES = 0x001A + MSG_ID_UF_SETTINGS_CHANGE_REQUEST_BY_USER = 0x0011 + MSG_ID_UF_SETTINGS_CHANGE_RESPONSE_FROM_HD = 0x0013 + MSG_ID_UF_SETTINGS_CHANGE_CONFIRMED_BY_USER = 0x0015 + MSG_ID_TREATMENT_DURATION_SETTING_CHANGE_REQUEST = 0x0016 + MSG_ID_TREATMENT_DURATION_SETTING_CHANGE_RESPONSE_FROM_HD = 0x001B + MSG_ID_BLOOD_DIALYSATE_FLOW_SETTING_CHANGE_REQUEST_BY_USER = 0x0017 + MSG_ID_BLOOD_DIALYSATE_FLOW_SETTING_CHANGE_RESPONSE_FROM_HD = 0x0018 + + LITER_TO_ML_CONVERSION_FACTOR = 1000.0 + UF_CMD_PAUSE = 0 + UF_CMD_RESUME = 1 + UF_CMD_CHANGE_TIME_TO_ADJUST = 0 + UF_CMD_CHANGE_RATE_TO_ADJUST = 1 + RESPONSE_REJECTED = 0 + RESPONSE_ACCEPTED = 1 + + # HD update on valid treatment parameter ranges message field positions + START_POS_MIN_TREAT_TIME = DenaliMessage.PAYLOAD_START_INDEX + END_POS_MIN_TREAT_TIME = START_POS_MIN_TREAT_TIME + 4 + START_POS_MAX_TREAT_TIME = END_POS_MIN_TREAT_TIME + END_POS_MAX_TREAT_TIME = START_POS_MAX_TREAT_TIME + 4 + START_POS_MIN_UF_VOL = END_POS_MAX_TREAT_TIME + END_POS_MIN_UF_VOL = START_POS_MIN_UF_VOL + 4 + START_POS_MAX_UF_VOL = END_POS_MIN_UF_VOL + END_POS_MAX_UF_VOL = START_POS_MAX_UF_VOL + 4 + START_POS_MIN_DIAL_RATE = END_POS_MAX_UF_VOL + END_POS_MIN_DIAL_RATE = START_POS_MIN_DIAL_RATE + 4 + START_POS_MAX_DIAL_RATE = END_POS_MIN_DIAL_RATE + END_POS_MAX_DIAL_RATE = START_POS_MAX_DIAL_RATE + 4 + + # HD response to treatment duration change request message field positions + START_POS_TIME_CHG_RSP_ACCEPTED = DenaliMessage.PAYLOAD_START_INDEX + END_POS_TIME_CHG_RSP_ACCEPTED = START_POS_TIME_CHG_RSP_ACCEPTED + 4 + START_POS_TIME_CHG_RSP_REASON = END_POS_TIME_CHG_RSP_ACCEPTED + END_POS_TIME_CHG_RSP_REASON = START_POS_TIME_CHG_RSP_REASON + 4 + START_POS_TIME_CHG_RSP_TIME = END_POS_TIME_CHG_RSP_REASON + END_POS_TIME_CHG_RSP_TIME = START_POS_TIME_CHG_RSP_TIME + 4 + START_POS_TIME_CHG_RSP_UF_VOL = END_POS_TIME_CHG_RSP_TIME + END_POS_TIME_CHG_RSP_UF_VOL = START_POS_TIME_CHG_RSP_UF_VOL + 4 + + # HD response to UF volume change request message field positions + START_POS_UF_CHG_RSP_RESP = DenaliMessage.PAYLOAD_START_INDEX + END_POS_UF_CHG_RSP_RESP = START_POS_UF_CHG_RSP_RESP + 4 + START_POS_UF_CHG_RSP_REJECT_REASON = END_POS_UF_CHG_RSP_RESP + END_POS_UF_CHG_RSP_REJECT_REASON = START_POS_UF_CHG_RSP_REJECT_REASON + 4 + START_POS_UF_CHG_RSP_VOL = END_POS_UF_CHG_RSP_RESP + END_POS_UF_CHG_RSP_VOL = START_POS_UF_CHG_RSP_VOL + 4 + START_POS_UF_CHG_RSP_TIME = END_POS_UF_CHG_RSP_VOL + END_POS_UF_CHG_RSP_TIME = START_POS_UF_CHG_RSP_TIME + 4 + START_POS_UF_CHG_RSP_TIME_DIFF = END_POS_UF_CHG_RSP_TIME + END_POS_UF_CHG_RSP_TIME_DIFF = START_POS_UF_CHG_RSP_TIME_DIFF + 4 + START_POS_UF_CHG_RSP_RATE = END_POS_UF_CHG_RSP_TIME_DIFF + END_POS_UF_CHG_RSP_RATE = START_POS_UF_CHG_RSP_RATE + 4 + START_POS_UF_CHG_RSP_RATE_DIFF = END_POS_UF_CHG_RSP_RATE + END_POS_UF_CHG_RSP_RATE_DIFF = START_POS_UF_CHG_RSP_RATE_DIFF + 4 + + # HD response to blood/dialysate flow change request message field positions + START_POS_BLD_DIAL_CHG_RSP_ACCEPTED = DenaliMessage.PAYLOAD_START_INDEX + END_POS_BLD_DIAL_CHG_RSP_ACCEPTED = START_POS_BLD_DIAL_CHG_RSP_ACCEPTED + 4 + START_POS_BLD_DIAL_CHG_RSP_REASON = END_POS_BLD_DIAL_CHG_RSP_ACCEPTED + END_POS_BLD_DIAL_CHG_RSP_REASON = START_POS_BLD_DIAL_CHG_RSP_REASON + 4 + START_POS_BLD_DIAL_CHG_RSP_BLD_RATE = END_POS_BLD_DIAL_CHG_RSP_REASON + END_POS_BLD_DIAL_CHG_RSP_BLD_RATE = START_POS_BLD_DIAL_CHG_RSP_BLD_RATE + 4 + START_POS_BLD_DIAL_CHG_RSP_DIAL_RATE = END_POS_BLD_DIAL_CHG_RSP_BLD_RATE + END_POS_BLD_DIAL_CHG_RSP_DIAL_RATE = START_POS_BLD_DIAL_CHG_RSP_DIAL_RATE + 4 + + def __init__(self, can_interface): + """ + HD_UI constructor + + \param can_interface: the denali can interface object + """ + self.can_interface = can_interface + + # 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, + self.MSG_ID_UF_SETTINGS_CHANGE_RESPONSE_FROM_HD, + self.handler_uf_change_response) + self.can_interface.register_receiving_publication_function(channel_id, + self.MSG_ID_TREATMENT_DURATION_SETTING_CHANGE_RESPONSE_FROM_HD, + self.handler_treatment_duration_change_response) + self.can_interface.register_receiving_publication_function(channel_id, + self.MSG_ID_BLOOD_DIALYSATE_FLOW_SETTING_CHANGE_RESPONSE_FROM_HD, + self.handler_blood_and_dialysate_change_response) + self.can_interface.register_receiving_publication_function(channel_id, self.MSG_ID_HD_TREATMENT_PARAMS_RANGES, + self.handler_treatment_param_ranges) + + # 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 + self.max_uf_volume_ml = 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 + 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 + # 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 + + 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 + U32 Minimum ultrafiltration volume (in mL). \n + U32 Maximum ultrafiltration volume (in mL). \n + U32 Minimum dialysate flow rate (in mL/min). \n + U32 Maximum dialysate flow rate (in mL/min). + + \returns none + """ + minTime = struct.unpack('i', bytearray( + message['message'][self.START_POS_MIN_TREAT_TIME:self.END_POS_MIN_TREAT_TIME])) + maxTime = struct.unpack('i', bytearray( + message['message'][self.START_POS_MAX_TREAT_TIME:self.END_POS_MAX_TREAT_TIME])) + minUFVol = struct.unpack('i', bytearray( + message['message'][self.START_POS_MIN_UF_VOL:self.END_POS_MIN_UF_VOL])) + maxUFVol = struct.unpack('i', bytearray( + message['message'][self.START_POS_MAX_UF_VOL:self.END_POS_MAX_UF_VOL])) + minDialRt = struct.unpack('i', bytearray( + message['message'][self.START_POS_MIN_DIAL_RATE:self.END_POS_MIN_DIAL_RATE])) + maxDialRt = struct.unpack('i', bytearray( + message['message'][self.START_POS_MAX_DIAL_RATE:self.END_POS_MAX_DIAL_RATE])) + + 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] + + 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 + U32 UF volue (mL) \n + + \returns none + """ + rsp = struct.unpack('i', bytearray( + message['message'][self.START_POS_TIME_CHG_RSP_ACCEPTED:self.END_POS_TIME_CHG_RSP_ACCEPTED])) + rea = struct.unpack('i', bytearray( + message['message'][self.START_POS_TIME_CHG_RSP_REASON:self.END_POS_TIME_CHG_RSP_REASON])) + tim = struct.unpack('i', bytearray( + message['message'][self.START_POS_TIME_CHG_RSP_TIME:self.END_POS_TIME_CHG_RSP_TIME])) + vol = struct.unpack('i', bytearray( + message['message'][self.START_POS_TIME_CHG_RSP_UF_VOL:self.END_POS_TIME_CHG_RSP_UF_VOL])) + + self.duration_change_succeeded = rsp[0] + self.duration_change_reject_reason = rea[0] + self.duration_change_time_min = tim[0] + self.duration_change_uf_vol_ml = vol[0] + + 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 + + \returns none + """ + rsp = struct.unpack('i', bytearray( + message['message'][self.START_POS_BLD_DIAL_CHG_RSP_ACCEPTED:self.END_POS_BLD_DIAL_CHG_RSP_ACCEPTED])) + rea = struct.unpack('i', bytearray( + message['message'][self.START_POS_BLD_DIAL_CHG_RSP_REASON:self.END_POS_BLD_DIAL_CHG_RSP_REASON])) + bld = struct.unpack('i', bytearray( + message['message'][self.START_POS_BLD_DIAL_CHG_RSP_BLD_RATE:self.END_POS_BLD_DIAL_CHG_RSP_BLD_RATE])) + dil = struct.unpack('i', bytearray( + message['message'][self.START_POS_BLD_DIAL_CHG_RSP_DIAL_RATE:self.END_POS_BLD_DIAL_CHG_RSP_DIAL_RATE])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.blood_and_dialysate_flow_rate_change_succeeded = resp + self.blood_and_dialysate_flow_rate_change_reject_reason = rea[0] + self.target_blood_flow_rate = bld[0] + self.target_dialysate_flow_rate = dil[0] + + 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) + + \returns none + """ + rsp = struct.unpack('i', bytearray( + message['message'][self.START_POS_UF_CHG_RSP_RESP:self.END_POS_UF_CHG_RSP_RESP])) + rea = struct.unpack('i', bytearray( + message['message'][self.START_POS_UF_CHG_RSP_REJECT_REASON:self.END_POS_UF_CHG_RSP_REJECT_REASON])) + vol = struct.unpack('f', bytearray( + message['message'][self.START_POS_UF_CHG_RSP_VOL:self.END_POS_UF_CHG_RSP_VOL])) + tim = struct.unpack('i', bytearray( + message['message'][self.START_POS_UF_CHG_RSP_TIME:self.END_POS_UF_CHG_RSP_TIME])) + tmd = struct.unpack('i', bytearray( + message['message'][self.START_POS_UF_CHG_RSP_TIME_DIFF:self.END_POS_UF_CHG_RSP_TIME_DIFF])) + rat = struct.unpack('f', bytearray( + message['message'][self.START_POS_UF_CHG_RSP_RATE:self.END_POS_UF_CHG_RSP_RATE])) + rtd = struct.unpack('f', bytearray( + message['message'][self.START_POS_UF_CHG_RSP_RATE_DIFF:self.END_POS_UF_CHG_RSP_RATE_DIFF])) + + if rsp[0] == self.RESPONSE_REJECTED: + resp = False + else: + resp = True + self.uf_change_succeeded = resp + self.uf_change_reject_reason = rea[0] + self.UFChangeVolumeL = 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.UFChangeRateDifff = rtd[0] + + def cmd_ui_checkin_with_hd(self): + """ + Constructs and sends the ui check-in message + + \returns 0 + """ + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_UI_CHECKIN_WITH_HD) + + print("Sending ui checkin w/ HD") + + self.can_interface.send(message, 0) + + return 0 + + def cmd_ui_uf_pause_resume(self, cmd=UF_CMD_PAUSE): + """ + Constructs and sends a ui UF command message + + \param cmd: 0 for pause, 1 for resume + + \returns none + """ + + payload = integer_to_bytearray(cmd) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_HD_UF_PAUSE_RESUME_REQUEST, + payload=payload) + + if cmd == self.UF_CMD_PAUSE: + str_cmd = "pause" + else: + str_cmd = "resume" + print("Sending UF " + str_cmd + " command.") + + self.can_interface.send(message, 0) + + return 0 + + def cmd_ui_uf_settings_change_request(self, vol=0.0): + """ + Constructs and sends a ui UF change settings command message + + \param vol (float): new ultrafiltration volume setting (in L) + + \returns 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=self.MSG_ID_UF_SETTINGS_CHANGE_REQUEST_BY_USER, + payload=volume) + + print("Sending UF settings change request.") + + self.can_interface.send(message, 0) + + return 0 + + def cmd_ui_uf_settings_change_confirm(self, vol=0.0, adj=UF_CMD_CHANGE_TIME_TO_ADJUST): + """ + 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 + + \returns 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=self.MSG_ID_UF_SETTINGS_CHANGE_REQUEST_BY_USER, + payload=payload) + + print("Sending UF settings change request.") + + self.can_interface.send(message, 0) + + return 0 + + def cmd_ui_uf_change_settings_confirmed_by_user(self, response=RESPONSE_REJECTED, vol=0.0, tm=0, rate=0.0): + """ + Constructs and sends a ui UF change settings confirmed by user message + + \param response (int): 0 for rejected, 1 for confirmed + \param vol (float): volume (in L) that was confirmed + \param tm (int): treatment time (in min) that was confirmed + \param rate (float): ultrafiltration rate (in mL/min) that was confirmed + + \returns none + """ + + resp = integer_to_bytearray(response) + volume = float_to_bytearray(vol * self.LITER_TO_ML_CONVERSION_FACTOR) + min = integer_to_bytearray(tm) + ufRate = float_to_bytearray(rate) + payload = resp + volume + min + ufRate + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_UF_SETTINGS_CHANGE_CONFIRMED_BY_USER, + payload=payload) + + print("Sending UF settings change confirmation.") + + self.can_interface.send(message, 0) + + return 0 + + def cmd_ui_treatment_duration_setting_change_request(self, timeMin=0): + """ + Constructs and sends a ui UF change settings confirmed by user message + + \param timeMin (int): treatment time (in min). + + \returns none + """ + + payload = integer_to_bytearray(timeMin) + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_TREATMENT_DURATION_SETTING_CHANGE_REQUEST, + payload=payload) + + print("Sending treatment duration setting change request.") + + # Send message + self.can_interface.send(message, 0) + + return 0 + + def cmd_ui_blood_and_dialysate_flow_settings_change_request(self, bloodFlow, dialFlow): + """ + Constructs and sends a ui blood & dialysate flow settings change request by user message + + \param bloodFlow (int): blood flow rate set point (in mL/min). + \param dialFlow (int): dialysate flow rate set point (in mL/min). + + \returns none + """ + + bld = integer_to_bytearray(bloodFlow) + dial = integer_to_bytearray(dialFlow) + payload = bld + dial + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=self.MSG_ID_BLOOD_DIALYSATE_FLOW_SETTING_CHANGE_REQUEST_BY_USER, + payload=payload) + + print("Sending blood & dialysate flow rate settings change request.") + + self.can_interface.send(message, 0) + + return 0 Index: dialin/hd/utils =================================================================== diff -u --- dialin/hd/utils (revision 0) +++ dialin/hd/utils (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ +../utils/ \ No newline at end of file Index: dialin/hd/watchdog.py =================================================================== diff -u --- dialin/hd/watchdog.py (revision 0) +++ dialin/hd/watchdog.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,81 @@ +########################################################################### +# +# 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 watchdog.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief +# +# +############################################################################ +from .protocols.CAN import (DenaliMessage, + DenaliChannels) +from .utils.conversions import integer_to_bytearray +from .constants import RESET + + +class HDWatchdog: + """ + \class HD_Watchdog + + \brief Hemodialysis Device (HD) Dialin API sub-class for watchdog related commands. + """ + + # watchdog message IDs + MSG_ID_HD_WD_CHECKIN_OVERRIDE = 0x8005 + + def __init__(self, can_interface): + """ + HD_Watchdog constructor + + \param outer_instance: reference to the HD (outer) class. + + \returns HD_Watchdog object. + """ + + self.can_interface = can_interface + + def cmd_watchdog_task_check_in_override(self, reset, state, task): + """ + Constructs and sends the watchdog task check-in override command + + \param reset: integer - 1 to reset a previous override, 0 to override + \param state: integer - 1 for task checked in, 0 for task not checked in + \param task: integer - ID of task to override + \returns 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=self.MSG_ID_HD_WD_CHECKIN_OVERRIDE, + payload=payload) + + print("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: + # print(received_message) + if reset == RESET: + str_res = "reset back to normal" + else: + str_res = ("checked in" if state != 0 else "not checked in") + print("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: + print("Timeout!!!!") + return False Index: dialin/protocols/CAN.py =================================================================== diff -u --- dialin/protocols/CAN.py (revision 0) +++ dialin/protocols/CAN.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,704 @@ +########################################################################### +# +# 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 CAN.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief Classes in this file facilitate sending and receiving of Denali Messages over CAN +# +############################################################################ + +import threading +import can +import math +import time +from time import sleep +import sys +import argparse +from argparse import RawTextHelpFormatter +import logging + + +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 + ] + + @staticmethod + def build_basic_message(channel_id=0, message=None): + """ + Builds a basic message dictionary containing the channel id and message + + \param channel_id: integer with channel id + \param message: an array of ints forming the message + \return: dictionary with channel_id and message keys + """ + if message is None: + message = [] + return {'channel_id': channel_id, 'message': message} + + @staticmethod + def build_message(channel_id=0, message_id=0, payload=None): + """ + buildPacket builds a Diality Packet + + \param channel_id: is an integer with channel ID + \param message_id: is an integer indicating request ID + \param payload: list with payload. It does not include length + + \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: + # Add a zero seq # for dialin messages for now + seq_no = 0 + message_seq_in_bytes = seq_no.to_bytes(2, byteorder=DenaliMessage.BYTE_ORDER) + + 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]] + + 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): + """ + 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_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_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, can_message): + """ + LongDialityMessageBuilder is a utility object that helps construct a diality message + that is longer than 8 bytes. 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 can_message: an 8 byte message needed to build a diality message. + + \returns object + + """ + self.message = can_message + self.number_of_can_packets_needed = DenaliMessage.get_total_packets(can_message) + self.number_of_can_packets_up_to_now = 1 + + def push(self, can_message, first_packet=False): + """ + push appends the can_message to the current packet. + + \param can_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 + """ + if first_packet: + self.message = can_message + self.number_of_can_packets_needed = DenaliMessage.get_total_packets(can_message) + self.number_of_can_packets_up_to_now = 1 + + else: + self.message += can_message + 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 + self.message = None + return return_message + + else: + return None + + +class DenaliCanMessenger: + START_BYTE = DenaliMessage.START_BYTE + DIALIN_MSG_RESP_TO = 0.1 # number of seconds to wait for a response to a sent command + + def __init__(self, can_interface='can0', log_level="error"): + """ + DenaliCanMessenger constructor + + \param can_interface - string containing the can interface, e.g., 'can0" + + \returns DialityCanMessenger object + + """ + self.bus = can.interfaces.socketcan.SocketcanBus(channel=can_interface) + self.listener_buffer = can.BufferedReader() + self.notifier = can.Notifier(self.bus, [self.listener_buffer]) + + self.send_packet_request_id = -1 + self.send_event = threading.Event() + + self.long_message_builders = {} + self.long_msg_channel_id_set = set() + + self.messages = None + self.last_sent_message = None + self.command_response_message = None + self.response_channel_id = -1 + + self.run = False + + self.sync_response_dictionary = {} + numeric_level = getattr(logging, log_level.upper(), logging.ERROR) + logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', + filename="dialin.log", + datefmt='%m-%d-%Y:%H:%M:%S', + level=numeric_level) # DEBUG, INFO, WARNING, ERROR, CRITICAL + + if self.bus is not None: + self.serial_listener_thread = threading.Thread(target=self.listener, daemon=True) + + else: + self.serial_listener_thread = None + print_and_log("Can connection is not valid") + + def start(self): + """ + starts listening to the can interface. + + """ + + if self.bus is None: + print_and_log("Cannot start can listener.") + return + else: + self.run = True + if self.serial_listener_thread is not None: + self.serial_listener_thread.start() + print_and_log("Can listener has started.") + else: + print_and_log("Cannot start listener...") + + def stop(self): + """ + Stop listening the can interface + + """ + self.run = False + print_and_log("\nCan listener has stopped.") + + def listener(self): + """ + Listens for diality message on the can interface passed during construction. + """ + + while self.run: + + message = self.listener_buffer.get_message(0.0) + print_and_log("Message = {}".format(message)) + + if message is not None: + + 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 + message_length = can_data[DenaliMessage.PAYLOAD_LENGTH_INDEX] + + print_and_log(str(time.time()) + " " + str(channel_id) + " " + str(can_data), + log_level=logging.INFO) + + # 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(can_data) + + 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 channel_id not in self.long_message_builders.keys(): # if we don't have a builder. Create it! + self.long_message_builders[channel_id] = LongDenaliMessageBuilder(can_data) + self.messages = None + + else: # if we do have a builder. This is the first time + # self.messages = self.long_message_builders[channel_id].push(can_data, first_packet=True) + self.long_message_builders[channel_id].push(can_data, 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 DenaliMessage.verify_crc(complete_dialin_message) is False: + # if verify is False, let's drop (ignore) this message + message_valid = False + dialin_ch_id = None + dialin_msg_id = None + sys.stderr.write( + "Incorrect CRC, received message: {}, crc: {}, calculated crc: {}\n".format( + self.messages, DenaliMessage.get_crc(complete_dialin_message), + DenaliMessage.crc8(self.messages))) + + if message_valid == True: + # We first check if this is a response to a send request that is pending + if dialin_msg_id == self.send_packet_request_id: + + self.command_response_message = complete_dialin_message + self.send_event.set() + self.send_packet_request_id = -1 + + # 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.sync_response_dictionary[dialin_ch_id][dialin_msg_id](complete_dialin_message) + + # Done with this message, let's get the next one + self.messages = None + + else: # no new packets in receive buffer + # We have received nothing, let's sleep 1 msec and let's check again + sleep(0.01) + + 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, time_out=DIALIN_MSG_RESP_TO): + """ + sends can_packet to channel_id. + + \param built_message: message built using DialinMessage class + \param time_out: time it will wait for a response in seconds + + \returns: Diality Packet, it it times out it returns None + """ + + msg_sent = False + + # keep trying to send message until we get a response + while msg_sent is not True: + self.last_sent_message = built_message + + channel_id = DenaliMessage.get_channel_id(built_message) + + padded_can_message_array = built_message['message'] + + self.send_packet_request_id = DenaliMessage.get_message_id(built_message) + + # 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) + + print_and_log(packet) + self.bus.send(packet, 0) # 0.1) + + # Sending + self.command_response_message = None + + # After all message has been sent, we clear a flag + self.send_event.clear() + + # At this point, we sleep until the system times out or flag is set + self.send_event.wait(time_out) + + if self.command_response_message is not None: + msg_sent = True + elif time_out == 0: + msg_sent = True + else: + # msg_sent = False + print_and_log("No response. Re-sending message.") + + # We are ready to send again. Reset request ID appropriately + self.send_packet_request_id = -1 + + # This value is None or it has a message depending of the listener + return self.command_response_message + + +def print_and_log(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 + + \returns: None + """ + if logging.getLogger().getEffectiveLevel() <= log_level: + print(message) + + if log_level == "debug": + logging.debug(message) + elif log_level == "info": + logging.info(message) + elif log_level == "warning": + logging.warning(message) + elif log_level == "error": + logging.error(message) + elif log_level == "critical": + logging.critical(message) + +def test_print_received_packet(self, message, sync=False): + channel_id = message[0] + message[0] = DenaliMessage.START_BYTE + introduction = "Received: " + + if sync: + introduction = "Sync " + introduction + + print_and_log("{0} {1} in channel: {3}".format(introduction, message, channel_id)) + + +def test_print_to_screen(message): + if message is None: + print_and_log("Timeout!!!") + else: + test_print_received_packet(message) + + +def test_function_for_sync(message): + test_print_received_packet(message, sync=True) + + +def test_print_sending_dg_board(message): + print_and_log("Sending to board: {0} on channel: {1}".format(message['message'], message['channel_id'])) + + +def test_print_sending_dg_sim(message): + print_and_log("Sending to DG simulator: {0} on channel {1}".format(message["message"], message["channel_id"])) + + +def test_dg(log_level): + test_messenger = DenaliCanMessenger(log_level=log_level) + + test_received_channel_id = DenaliChannels.hd_to_ui_ch_id + test_received_message_id = 0x100 + + test_messenger.register_receiving_publication_function(test_received_channel_id, test_received_message_id, + test_function_for_sync) + + test_dg_simulator_received_channel_id = DenaliChannels.dialin_to_dg_ch_id + test_dg_simulator_sync_msg_id = 0x05 + test_dg_simulator_msg_id = 0x03 + + test_messenger.register_receiving_publication_function(test_dg_simulator_received_channel_id, + test_dg_simulator_sync_msg_id, + test_function_for_sync) + + test_messenger.start() + + test_msg = DenaliMessage.build_message(channel_id=1000, + message_id=0x01, + payload=[1]) + test_dg_msg = DenaliMessage.build_message(channel_id=test_dg_simulator_received_channel_id, + message_id=test_dg_simulator_msg_id, + payload=[]) + + sleep(3.0) + test_print_sending_dg_board(test_msg) + test_response = test_messenger.send(test_msg) + test_print_to_screen(test_response) + + sleep(3.0) + test_print_sending_dg_board(test_msg) + test_response = test_messenger.send(test_msg) + test_print_to_screen(test_response) + + sleep(3.0) + test_print_sending_dg_sim(test_dg_msg) + test_response = test_messenger.send(test_dg_msg) + test_print_to_screen(test_response) + + sleep(3.0) + test_print_sending_dg_sim(test_dg_msg) + test_response = test_messenger.send(test_dg_msg) + test_print_to_screen(test_response) + + +def test_can(self): + # TODO: Parse received input and setup as a sender + test_messenger = DenaliCanMessenger() + test_messenger.start() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Dial-In Core Can Protocol \n" + "\tExample: \n" + "\tpython3 CoreCanProtocol.py --test-dg --log-level=\"debug\"", + formatter_class=RawTextHelpFormatter) + parser.add_argument("--test-dg", action="store_true") + parser.add_argument("--log-level", default="error") + args = parser.parse_args() + + if args.test_dg: + test_dg(args.log_level) + + if len(sys.argv) < 2: + parser.print_help() Index: dialin/protocols/__init__.py =================================================================== diff -u --- dialin/protocols/__init__.py (revision 0) +++ dialin/protocols/__init__.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ \ No newline at end of file Index: dialin/utils/__init__.py =================================================================== diff -u --- dialin/utils/__init__.py (revision 0) +++ dialin/utils/__init__.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1 @@ \ No newline at end of file Index: dialin/utils/conversions.py =================================================================== diff -u --- dialin/utils/conversions.py (revision 0) +++ dialin/utils/conversions.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,26 @@ +import struct +from binascii import unhexlify + + +def integer_to_bytearray(val): + """ + Converts an integer value into a byte array (little endian) + + \param val: integer to convert to byte array + \returns byte array + """ + fmt = '%08x' # integer to hex string formatter + b = unhexlify(fmt % val) # convert int to byte array + b = b[::-1] # little endian byte order + return b + + +def float_to_bytearray(val): + """ + Converts a float value into a byte array (little endian) + + \param val: float to convert to byte array + \returns byte array + """ + b = struct.pack(' response to fill command") + print(". -> publication message") + print("") + + + def respond_to_command(message): + dialin_messenger.send(response_msg) + print("o", end='', flush=True) + + + # Register response command for the DG + dialin_messenger.register_receiving_publication_function(channel_id=DenaliChannels.ui_to_hd_ch_id, + message_id=1000, + function=respond_to_command) + + dialin_messenger.start() + + while True: + dialin_messenger.send(publication_msg) + print(".", end='', flush=True) + sleep(1) + + """ + + + +if __name__ == '__main__': + unittest.main(verbosity=2) Index: tests/hd_test_script.py =================================================================== diff -u --- tests/hd_test_script.py (revision 0) +++ tests/hd_test_script.py (revision 8c39fe1f9affe360ee6a97c5e6243e58a5c27509) @@ -0,0 +1,83 @@ +########################################################################### +# +# Copyright (c) 2019-2019 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_tests_script.py +# +# @date 2-Apr-2019 +# @author P. Lucia +# +# @brief This is an example test script for the HD. +# +############################################################################ +from ..dialin.hd.constants import RESET, NO_RESET +from ..dialin.hd.hemodialysis_device import HD +from time import sleep +import unittest + + +class Test(unittest.TestCase): + + # @unittest.skip("Skipping test_hd_1.") + def test_hd_1(self): + hd = HD() + sleep(2) + + if hd.basics.cmd_log_in_to_hd() == 0: + exit(1) + + hd.bloodflow.cmd_blood_flow_broadcast_interval_override(RESET, 0) + + sleep(2) + print("Blood Flow Target = {}".format(hd.bloodflow.target_blood_flow_rate)) + print("Blood Pump Current = {}".format(hd.bloodflow.measured_blood_pump_mc_current)) + sleep(5) + print("Blood Pump Current = {}".format(hd.bloodflow.measured_blood_pump_mc_current)) + + hd.bloodflow.cmd_blood_pump_measured_current_override(NO_RESET, 140) + + sleep(1) + print("Blood Pump Current= {}".format(hd.bloodflow.measured_blood_pump_mc_current)) + sleep(5) + hd.bloodflow.cmd_blood_pump_measured_current_override(RESET, 0) + + i = 0 + while True: + sleep(0.5) + print("Measured Flow = {} mL/min".format(hd.bloodflow.measured_blood_flow_rate)) + if i > 0 and i % 60 == 0: + resp = input("Press 'Enter' to continue or 'q' to quit: ") + if resp.lower() == "q": + break + i += 1 + tgtRate = 0 + hd.bloodflow.cmd_blood_flow_broadcast_interval_override(NO_RESET, 2000) + + i = 0 + while True: + if hd.bloodflow.target_blood_flow_rate == 0: + if tgtRate != 0: + hd.bloodflow.cmd_blood_flow_broadcast_interval_override(NO_RESET, 2000) + tgtRate = 0 + else: + if tgtRate == 0: + hd.bloodflow.cmd_blood_flow_broadcast_interval_override(NO_RESET, 200) + tgtRate = hd.bloodflow.target_blood_flow_rate + if i > 0 and i % 60 == 0: + resp = input("Press 'Enter' to continue or 'q' to quit: ") + if resp.lower() == "q": + break + i += 1 + + # hd.bloodflow.cmd_blood_flow_broadcast_interval_override(RESET,0) + + # FIXME: Update passing criteria + self.assertTrue(True) + + +if __name__ == '__main__': + unittest.main(verbosity=2) + Fisheye: Tag 8c39fe1f9affe360ee6a97c5e6243e58a5c27509 refers to a dead (removed) revision in file `utils/conversions.py'. Fisheye: No comparison available. Pass `N' to diff?