Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `DG/DG_Firmware_Simulator.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `DG/DialIn'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `DG/DialysateGenerator.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `DialIn/CoreCANProtocol.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/Alarms.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/Basics.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/BloodFlow.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/Buttons.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/DialIn'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/DialysateInletFlow.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/DialysateOutletFlow.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/HD_DialOutFlow.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/HD_TestScript.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/HemodialysisDevice.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/PressureOcclusion.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/RTC.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/Treatment.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/UI.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/Watchdog.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `HD/utils.py'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `Tools/document.sh'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `Tools/setup_canbus.sh'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `Tools/setup_environment.sh'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `Tools/setup_environment_windows.bat'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 619cd8f345bf168dc37bb12befef3db0290e5329 refers to a dead (removed) revision in file `Tools/setup_virtual_can.sh'. Fisheye: No comparison available. Pass `N' to diff? Index: dg/dialysate_generator.py =================================================================== diff -u --- dg/dialysate_generator.py (revision 0) +++ dg/dialysate_generator.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,280 @@ +########################################################################### +# +# 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.CoreCANProtocol 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 + """ + + +# TODO: Eventually will be moved to a separate repository +class Test(unittest.TestCase): + + # @unittest.skip("Skipping dg_start.") + def test_dg_start(self): + dg = DG() + sleep(2) + success = dg.fill("start") + self.assertTrue(success) + + # @unittest.skip("Skipping dg_start_stop.") + def test_dg_start_stop(self): + dg = DG() + sleep(2) + success = dg.fill("start") + self.assertTrue(success) + sleep(2) + success = dg.fill('stop') + self.assertTrue(success) + + +if __name__ == '__main__': + unittest.main(verbosity=2) Index: dg/firmware_simulator.py =================================================================== diff -u --- dg/firmware_simulator.py (revision 0) +++ dg/firmware_simulator.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,38 @@ +from protocols.CoreCANProtocol import DenaliMessage, DenaliCanMessenger, DenaliChannels +from time import sleep + +dialin_messenger = DenaliCanMessenger() + +# Building response message +response_msg = DenaliMessage.build_message(channel_id=DenaliChannels.dg_to_hd_ch_id, + message_id=1000, + payload=[1]) + +# Building Publication message +publication_msg = DenaliMessage.build_message(channel_id=DenaliChannels.dg_sync_broadcast_ch_id, + message_id=0x7100, + payload=[1, 2, 3, 4, 5]) + +print("") +print("o -> response to fill command") +print(". -> publication message") +print("") + + +def respondToCommand(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=respondToCommand) + +dialin_messenger.start() + +# This is the main loop +while True: + dialin_messenger.send(publication_msg) + print(".", end='', flush=True) + sleep(1) Index: dg/protocols =================================================================== diff -u --- dg/protocols (revision 0) +++ dg/protocols (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1 @@ +../protocols/ \ No newline at end of file Index: dg/utils =================================================================== diff -u --- dg/utils (revision 0) +++ dg/utils (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1 @@ +../utils/ \ No newline at end of file Index: hd/Alarms.py =================================================================== diff -u --- hd/Alarms.py (revision 0) +++ hd/Alarms.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,262 @@ +from protocols.CoreCANProtocol 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(' 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) + Index: hd/HemodialysisDevice.py =================================================================== diff -u --- hd/HemodialysisDevice.py (revision 0) +++ hd/HemodialysisDevice.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,180 @@ +########################################################################### +# +# 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 HemodialysisDevice.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief This class provides the basic interface to communicate with +# the HD board. +# +############################################################################ +from time import sleep +from Alarms import HDAlarms +from Buttons import HDButtons +from UI import HDUI +from Watchdog import HDWatchdog +from RTC import HDRTC +from BloodFlow import HDBloodFlow +from DialysateInletFlow import HDDialysateInletFlow +from DialysateOutletFlow import HDDialysateOutletFlow +from Treatment import HDTreatment +from Basics import HDBasics +from PressureOcclusion import HDPressureOcclusion +from utils.conversions import integer_to_bytearray +from constants import RESET, NO_RESET +from protocols.CoreCANProtocol import (DenaliMessage, + DenaliCanMessenger, + DenaliChannels) + + +class HD: + + def __init__(self, can_interface_name="can0"): + """ + HD constructor using can bus + + \param bus: can bus, e.g. "can0" + \returns HD object provides test/service commands for the HD sub-system. + \details For example: + + hd_object = HD(can_interface='can0') or + hd_object = HD('can0') + """ + # Create listener + self.can_interface = DenaliCanMessenger(can_interface=can_interface_name) + self.can_interface.start() + + # Create command groups + self.basics = HDBasics(self.can_interface) + self.alarms = HDAlarms(self.can_interface) + self.buttons = HDButtons(self.can_interface) + self.ui = HDUI(self.can_interface) + self.rtc = HDRTC(self.can_interface) + self.watchdog = HDWatchdog(self.can_interface) + self.bloodflow = HDBloodFlow(self.can_interface) + self.dialysate_inlet_flow = HDDialysateInletFlow(self.can_interface) + self.dialysate_outlet_flow = HDDialysateOutletFlow(self.can_interface) + self.treatment = HDTreatment(self.can_interface) + self.pressure_occlusion = HDPressureOcclusion(self.can_interface) + + def test_can_message(self, seq): + """ + Sends a test can message + \param seq: the starting integer of the sequence + """ + zero = integer_to_bytearray(0) + seq1 = integer_to_bytearray(seq) + seq2 = integer_to_bytearray(seq + 1) + seq3 = integer_to_bytearray(seq + 2) + seq4 = integer_to_bytearray(seq + 3) + seq5 = integer_to_bytearray(seq + 4) + + payload = seq1 + zero + seq2 + zero + seq3 + zero + seq4 + zero + seq5 + + message = DenaliMessage.build_message(channel_id=DenaliChannels.dialin_to_hd_ch_id, + message_id=6, + payload=payload) + + self.can_interface.send(message, 0) + + +if __name__ == "__main__": + # create an HD object called hd + hd = HD() + + while True: + sleep(1) + print(hd.alarms.alarm_states) + + # wait 2 seconds and then login to HD as a tester + sleep(2) + hd.Basics.cmd_log_in_to_hd() + + # FIXME: Delete commented code: + # hd.rtc.cmd_set_rtc_time_and_date(0, 2, 1, 5, 1, 2020) + + while True: + print(hd.RTC.RTCEpoch) + sleep(1) + +# FIXME: This should either be deleted or moved to a file: +"""" + hd.bloodflow.cmd_blood_pump_measured_current_override(NO_RESET,149) + totalVolumeInMl = 2400 + rxTimeInMins = 30 + flowRateMlmin = 100 + + sleep(2) + resp = hd.DialOut.set_uf_rx(totalVolumeInMl, rxTimeInMins, flowRateMlmin) + print("Set TotalVolume(mL): {}, TotalTime(mins): {} and flowrate (ml/min): {}, resp: {}".format(totalVolumeInMl, + rxTimeInMins, flowRateMlmin, resp)) + + sleep(2) + rxTimeInMins = 60 + resp = hd.DialOut.set_uf_rx(totalVolumeInMl, rxTimeInMins, flowRateMlmin) + print("Set TotalVolume(mL): {}, TotalTime(mins): {} and flowrate (ml/min): {}, resp: {}".format(totalVolumeInMl, + rxTimeInMins, flowRateMlmin, resp)) + + sleep(5) + hd.DialOut.set_uf_state(DialOutStates.RUN) + state_run = hd.DialOut.DialOutBroadcast['state'] + sleep(2) + print("After RUN: {}".format(hd.DialOut.DialOutBroadcast)) + + sleep(20) + print("After 20 secs RUN: {}".format(hd.DialOut.DialOutBroadcast)) + + sleep(20) + print("After 40 secs RUN: {}".format(hd.DialOut.DialOutBroadcast)) + + hd.DialOut.set_uf_state(DialOutStates.PAUSE) + sleep(2) + print("After PAUSE: {}".format(hd.DialOut.DialOutBroadcast)) + state_pause = hd.DialOut.DialOutBroadcast['state'] + + sleep(10) + hd.DialOut.set_uf_state(DialOutStates.STOP) + sleep(2) + print("---> 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: hd/PressureOcclusion.py =================================================================== diff -u --- hd/PressureOcclusion.py (revision 0) +++ hd/PressureOcclusion.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -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 PressureOcclusion.py +# +# @date 31-Mar-2020 +# @author P. Lucia +# +# @brief +# +# +############################################################################ +from protocols.CoreCANProtocol 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: hd/RTC.py =================================================================== diff -u --- hd/RTC.py (revision 0) +++ hd/RTC.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -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.CoreCANProtocol 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: hd/Treatment.py =================================================================== diff -u --- hd/Treatment.py (revision 0) +++ hd/Treatment.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -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.CoreCANProtocol 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: hd/UI.py =================================================================== diff -u --- hd/UI.py (revision 0) +++ hd/UI.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -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.CoreCANProtocol 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: hd/Watchdog.py =================================================================== diff -u --- hd/Watchdog.py (revision 0) +++ hd/Watchdog.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -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.CoreCANProtocol 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: hd/constants.py =================================================================== diff -u --- hd/constants.py (revision 0) +++ hd/constants.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,19 @@ +########################################################################### +# +# 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 constants.py +# +# @date 1-Apr-2020 +# @author P. Lucia +# +# @brief +# +# +############################################################################ + +RESET = 1 +NO_RESET = 0 Index: hd/protocols =================================================================== diff -u --- hd/protocols (revision 0) +++ hd/protocols (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1 @@ +../protocols/ \ No newline at end of file Index: hd/utils =================================================================== diff -u --- hd/utils (revision 0) +++ hd/utils (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1 @@ +../utils/ \ No newline at end of file Index: protocols/CoreCANProtocol.py =================================================================== diff -u --- protocols/CoreCANProtocol.py (revision 0) +++ protocols/CoreCANProtocol.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -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 CoreCANProtocol.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: protocols/protocol =================================================================== diff -u --- protocols/protocol (revision 0) +++ protocols/protocol (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1 @@ +protocol \ No newline at end of file Index: tools/document.sh =================================================================== diff -u --- tools/document.sh (revision 0) +++ tools/document.sh (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,7 @@ +#!/bin/bash +cd ../ +export PROJECT_DIRECTORY=$(pwd) +rm -rf html +doxygen Doxyfile +cd html +xdg-open index.html Index: tools/setup_canbus.sh =================================================================== diff -u --- tools/setup_canbus.sh (revision 0) +++ tools/setup_canbus.sh (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,5 @@ +#!/bin/bash + + +sudo ip link set can0 up type can bitrate 250000 restart-ms 100 + Index: tools/setup_environment.sh =================================================================== diff -u --- tools/setup_environment.sh (revision 0) +++ tools/setup_environment.sh (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,8 @@ +#!/bin/bash +cd ../ + +pip3 install virtualenv +virtualenv --python=python3 venv +source venv/bin/activate +pip3 install -r requirements.txt +echo "Please cd up a directory and run 'source venv/bin/activate' to use the newly created environment." Index: tools/setup_environment_windows.bat =================================================================== diff -u --- tools/setup_environment_windows.bat (revision 0) +++ tools/setup_environment_windows.bat (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,6 @@ +cd ../ +pip3 install virtualenv +virtualenv --python=python3 venv +source venv/Scripts/activate +pip3 install -r requirements.txt +venv\Scripts\activate.bat Index: tools/setup_virtual_can.sh =================================================================== diff -u --- tools/setup_virtual_can.sh (revision 0) +++ tools/setup_virtual_can.sh (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -0,0 +1,15 @@ +#!/bin/bash +sudo ifconfig vcan0 down +sudo ifconfig vcan1 down +sudo ip link delete dev vcan0 +sudo ip link delete dev vcan1 + +sudo ip link add dev vcan0 type vcan +sudo ip link add dev vcan1 type vcan + +sudo ip link set vcan0 up +sudo ifconfig vcan0 txqueuelen 10000 + +sudo ip link set vcan1 up +sudo ifconfig vcan1 txqueuelen 10000 + Index: utils/conversions.py =================================================================== diff -u --- utils/conversions.py (revision 0) +++ utils/conversions.py (revision 619cd8f345bf168dc37bb12befef3db0290e5329) @@ -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('