diff --git a/arduino/common/lib/CommsControl/CommsCommon.h b/arduino/common/lib/CommsControl/CommsCommon.h index 1d38defa1641ff8113cb23bc1d5bdad9f151a8ce..1a0ac7cf939514a4fc783d9f32cbece9e4e5c922 100644 --- a/arduino/common/lib/CommsControl/CommsCommon.h +++ b/arduino/common/lib/CommsControl/CommsCommon.h @@ -40,11 +40,11 @@ #define PACKET_SET 0x20 //set vs get ? // enum of all transfer types -enum PAYLOAD_TYPE { - DATA, - CMD, - ALARM, - UNSET +enum PRIORITY : uint8_t { + DATA_ADDR = PACKET_DATA, + CMD_ADDR = PACKET_CMD, + ALARM_ADDR = PACKET_ALARM, + UNSET_ADDR = 0x00 }; // payload consists of type and information @@ -52,7 +52,7 @@ enum PAYLOAD_TYPE { // information is set as information in the protocol class Payload { public: - Payload(PAYLOAD_TYPE type = PAYLOAD_TYPE::UNSET) {_type = type; } + Payload(PRIORITY type = PRIORITY::UNSET_ADDR) {_type = type; } Payload(const Payload &other) { _type = other._type; _size = other._size; @@ -66,15 +66,15 @@ public: } ~Payload() { unset(); } - void unset() { memset( _buffer, 0, PAYLOAD_MAX_SIZE_BUFFER); _type = PAYLOAD_TYPE::UNSET; _size = 0;} + void unset() { memset( _buffer, 0, PAYLOAD_MAX_SIZE_BUFFER); _type = PRIORITY::UNSET_ADDR; _size = 0;} - void setType(PAYLOAD_TYPE type) { _type = type; } - PAYLOAD_TYPE getType() {return _type; } + void setType(PRIORITY type) { _type = type; } + PRIORITY getType() {return _type; } void setSize(uint8_t size) { _size = size; } uint8_t getSize() { return _size; } - bool setPayload(PAYLOAD_TYPE type, void* information, uint8_t size) { + bool setPayload(PRIORITY type, void* information, uint8_t size) { if (information == nullptr) { return false; } @@ -87,12 +87,12 @@ public: } bool getPayload(void* information) { - PAYLOAD_TYPE type; + PRIORITY type; uint8_t size; return getPayload(information, type, size); } - bool getPayload(void* information, PAYLOAD_TYPE &type, uint8_t &size) { + bool getPayload(void* information, PRIORITY &type, uint8_t &size) { if (information == nullptr) { return false; } @@ -108,7 +108,7 @@ public: void *getInformation() { return reinterpret_cast<void*>(_buffer); } private: - PAYLOAD_TYPE _type; + PRIORITY _type; uint8_t _buffer[PAYLOAD_MAX_SIZE_BUFFER]; uint8_t _size; }; diff --git a/arduino/common/lib/CommsControl/CommsControl.cpp b/arduino/common/lib/CommsControl/CommsControl.cpp index d9e1ec68d623df8e42d403320c77b3df2532be68..e8f601da3c1bf219338b2df719d98e9cdc301603 100644 --- a/arduino/common/lib/CommsControl/CommsControl.cpp +++ b/arduino/common/lib/CommsControl/CommsControl.cpp @@ -88,7 +88,7 @@ void CommsControl::receiver() { uint8_t address = *_comms_tmp.getAddress(); // to decide what kind of packets received - PAYLOAD_TYPE type = getInfoType(address); + PRIORITY type = getInfoType(address); // switch on received data to know what to do - received ACK/NACK or other switch(control & COMMS_CONTROL_TYPES) { @@ -136,8 +136,8 @@ void CommsControl::receiver() { } bool CommsControl::writePayload(Payload &pl) { - PAYLOAD_TYPE payload_type = pl.getType(); - if (payload_type != PAYLOAD_TYPE::UNSET) { + PRIORITY payload_type = pl.getType(); + if (payload_type != PRIORITY::UNSET_ADDR) { // create comms format using payload, the type is deduced from the payload itself CommsFormat comms = CommsFormat(pl); @@ -243,7 +243,7 @@ void CommsControl::resendPacket(RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> // receiving anything of commsFormat -bool CommsControl::receivePacket(PAYLOAD_TYPE &type) { +bool CommsControl::receivePacket(PRIORITY &type) { _payload_tmp.unset(); _payload_tmp.setPayload(type, reinterpret_cast<void *>(_comms_tmp.getInformation()), _comms_tmp.getInfoSize()); @@ -258,7 +258,7 @@ bool CommsControl::receivePacket(PAYLOAD_TYPE &type) { } // if FCS is ok, remove from queue -void CommsControl::finishPacket(PAYLOAD_TYPE &type) { +void CommsControl::finishPacket(PRIORITY &type) { RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> *queue = getQueue(type); if (queue != nullptr && !queue->isEmpty()) { @@ -274,27 +274,19 @@ void CommsControl::finishPacket(PAYLOAD_TYPE &type) { } } -PAYLOAD_TYPE CommsControl::getInfoType(uint8_t &address) { - switch (address & PACKET_TYPE) { - case PACKET_ALARM: - return PAYLOAD_TYPE::ALARM; - case PACKET_CMD: - return PAYLOAD_TYPE::CMD; - case PACKET_DATA: - return PAYLOAD_TYPE::DATA; - default: - return PAYLOAD_TYPE::UNSET; - } +PRIORITY CommsControl::getInfoType(uint8_t &address) { + // return enum element corresponding to the address + return (PRIORITY)(address & PACKET_TYPE); } // get link to queue according to packet format -RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> *CommsControl::getQueue(PAYLOAD_TYPE &type) { +RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> *CommsControl::getQueue(PRIORITY &type) { switch (type) { - case PAYLOAD_TYPE::ALARM: + case PRIORITY::ALARM_ADDR: return &_ring_buff_alarm; - case PAYLOAD_TYPE::CMD: + case PRIORITY::CMD_ADDR: return &_ring_buff_cmd; - case PAYLOAD_TYPE::DATA: + case PRIORITY::DATA_ADDR: return &_ring_buff_data; default: return nullptr; diff --git a/arduino/common/lib/CommsControl/CommsControl.h b/arduino/common/lib/CommsControl/CommsControl.h index 13ebe690a37f6a19ef86b5a144f4d6ebece2973c..b623ebb2d0f9b47083ad895e7f312e616f6a3b45 100644 --- a/arduino/common/lib/CommsControl/CommsControl.h +++ b/arduino/common/lib/CommsControl/CommsControl.h @@ -26,13 +26,13 @@ public: void receiver(); private: - RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> *getQueue(PAYLOAD_TYPE &type); - PAYLOAD_TYPE getInfoType(uint8_t &address); + RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> *getQueue(PRIORITY &type); + PRIORITY getInfoType(uint8_t &address); void sendQueue (RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> *queue); void resendPacket (RingBuf<CommsFormat, COMMS_MAX_SIZE_RB_SENDING> *queue); - bool receivePacket(PAYLOAD_TYPE &type); - void finishPacket (PAYLOAD_TYPE &type); + bool receivePacket(PRIORITY &type); + void finishPacket (PRIORITY &type); bool encoder(uint8_t* payload, uint8_t data_size); bool decoder(uint8_t* payload, uint8_t dataStart, uint8_t data_stop); diff --git a/arduino/common/lib/CommsControl/CommsFormat.cpp b/arduino/common/lib/CommsControl/CommsFormat.cpp index 1d034227560e3c1e61f7505273e3b0897b581c60..01c4c882070e0e7b245f50eb726b93e01a4151d6 100644 --- a/arduino/common/lib/CommsControl/CommsFormat.cpp +++ b/arduino/common/lib/CommsControl/CommsFormat.cpp @@ -8,13 +8,13 @@ CommsFormat::CommsFormat(uint8_t info_size, uint8_t address, uint16_t control) { CommsFormat::CommsFormat(Payload &pl) { uint8_t address; switch (pl.getType()) { - case PAYLOAD_TYPE::ALARM: + case PRIORITY::ALARM_ADDR: address = PACKET_ALARM; break; - case PAYLOAD_TYPE::CMD: + case PRIORITY::CMD_ADDR: address = PACKET_CMD; break; - case PAYLOAD_TYPE::DATA: + case PRIORITY::DATA_ADDR: address = PACKET_DATA; break; default: diff --git a/arduino/hev_prototype_v1/src/UILoop.cpp b/arduino/hev_prototype_v1/src/UILoop.cpp index e841b3e7590feddb98cf210eb4e5895da0c002c3..61787fdaab7a576987dcca2b3a3bbdd661d5c350 100644 --- a/arduino/hev_prototype_v1/src/UILoop.cpp +++ b/arduino/hev_prototype_v1/src/UILoop.cpp @@ -25,7 +25,7 @@ void UILoop::receiveCommands() // check any received payload if(_comms->readPayload(_plReceive)) { - if (_plReceive.getType() == PAYLOAD_TYPE::CMD) { + if (_plReceive.getType() == PRIORITY::CMD_ADDR) { // apply received cmd to ui loop cmd_format cmd; _plReceive.getPayload(reinterpret_cast<void*>(&cmd)); @@ -33,7 +33,7 @@ void UILoop::receiveCommands() } // unset received type not to read it again - _plReceive.setType(PAYLOAD_TYPE::UNSET); + _plReceive.setType(PRIORITY::UNSET_ADDR); } } @@ -60,7 +60,7 @@ void UILoop::reportFastReadings() _fast_data.pressure_o2_regulated = readings.pressure_o2_regulated; _fast_data.pressure_diff_patient = readings.pressure_diff_patient; - _plSend.setPayload(PAYLOAD_TYPE::DATA, reinterpret_cast<void *>(&_fast_data), sizeof(_fast_data)); + _plSend.setPayload(PRIORITY::DATA_ADDR, reinterpret_cast<void *>(&_fast_data), sizeof(_fast_data)); _comms->writePayload(_plSend); _fast_report_time = tnow; } @@ -111,7 +111,7 @@ void UILoop::reportReadbackValues() // _readback_data.peep = _breathing_loop->peep(); _readback_data.inhale_exhale_ratio = _breathing_loop->getIERatio(); - _plSend.setPayload(PAYLOAD_TYPE::DATA, reinterpret_cast<void *>(&_readback_data), sizeof(_readback_data)); + _plSend.setPayload(PRIORITY::DATA_ADDR, reinterpret_cast<void *>(&_readback_data), sizeof(_readback_data)); _comms->writePayload(_plSend); _readback_report_time = tnow; } @@ -125,7 +125,7 @@ void UILoop::reportCycleReadings() _cycle_data.timestamp = tnow; - _plSend.setPayload(PAYLOAD_TYPE::DATA, reinterpret_cast<void *>(&_cycle_data), sizeof(_cycle_data)); + _plSend.setPayload(PRIORITY::DATA_ADDR, reinterpret_cast<void *>(&_cycle_data), sizeof(_cycle_data)); _comms->writePayload(_plSend); _cycle_report_time = tnow; } diff --git a/arduino/hev_prototype_v1/src/common.h b/arduino/hev_prototype_v1/src/common.h index 5a0501c858987cce51d03691d1976fc4e13ce8b0..a54938eff5ab88f859ac3c8cb5cd961cfb0bfd55 100644 --- a/arduino/hev_prototype_v1/src/common.h +++ b/arduino/hev_prototype_v1/src/common.h @@ -23,6 +23,16 @@ // const float MAX_VALVE_FRAC_OPEN = 0.68; // input params +enum PAYLOAD_TYPE : uint8_t { + UNSET = 0, + DATA = 1, + READBACK = 2, + CYCLE = 3, + THRESHOLDS = 4, + CMD = 5, + ALARM = 6 +}; + enum CMD_TYPE : uint8_t { GENERAL = 1, SET_DURATION = 2, @@ -65,11 +75,12 @@ enum VENTILATION_MODE : uint8_t { #pragma pack(1) struct cmd_format { - uint8_t version = HEV_FORMAT_VERSION; - uint32_t timestamp = 0; - uint8_t cmd_type = 0; - uint8_t cmd_code = 0; - uint32_t param = 0; + uint8_t version = HEV_FORMAT_VERSION; + uint32_t timestamp = 0; + uint8_t payload_type = PAYLOAD_TYPE::CMD; + uint8_t cmd_type = 0; + uint8_t cmd_code = 0; + uint32_t param = 0; }; #pragma pack() @@ -110,28 +121,22 @@ enum ALARM_CODES: uint8_t { #pragma pack(1) struct alarm_format { - uint8_t version = HEV_FORMAT_VERSION; - uint32_t timestamp = 0; - uint8_t alarm_type = 0; - uint8_t alarm_code = 0; - uint32_t param = 0; + uint8_t version = HEV_FORMAT_VERSION; + uint32_t timestamp = 0; + uint8_t payload_type = PAYLOAD_TYPE::ALARM; + uint8_t alarm_type = 0; + uint8_t alarm_code = 0; + uint32_t param = 0; }; #pragma pack() -enum DATA_TYPE: uint8_t { - FAST = 1, - READBACK = 2, - CYCLE = 3, - THRESHOLDS = 4 -}; - // struct for all data sent #pragma pack(1) struct fast_data_format { // fast values - read every ~10 ms uint8_t version = HEV_FORMAT_VERSION; uint32_t timestamp = 0; - uint8_t data_type = DATA_TYPE::FAST; + uint8_t payload_type = PAYLOAD_TYPE::DATA; uint8_t fsm_state = 0; //UNKNOWN uint16_t pressure_air_supply = 0; float pressure_air_regulated = 0; @@ -144,9 +149,9 @@ struct fast_data_format { float pressure_diff_patient = 0; uint16_t ambient_pressure = 0; uint16_t ambient_temperature = 0; - float airway_pressure = 0; - float flow = 0; - float volume = 0;//41 + float airway_pressure = 0.0; + float flow = 0.0; + float volume = 0.0; }; #pragma pack() @@ -155,7 +160,7 @@ struct readback_data_format { // readback values uint8_t version = HEV_FORMAT_VERSION; uint32_t timestamp = 0; - uint8_t data_type = DATA_TYPE::READBACK; + uint8_t payload_type = PAYLOAD_TYPE::READBACK; uint16_t duration_calibration = 0; uint16_t duration_buff_purge = 0; uint16_t duration_buff_flush = 0; @@ -192,20 +197,21 @@ struct cycle_data_format { // per breath values uint8_t version = HEV_FORMAT_VERSION; uint32_t timestamp = 0; - uint8_t data_type = DATA_TYPE::CYCLE; + uint8_t payload_type = PAYLOAD_TYPE::CYCLE; + - float respiratory_rate = 0; + float respiratory_rate = 0.0; - float tidal_volume = 0; - float exhaled_tidal_volume = 0; - float inhaled_tidal_volume = 0; + float tidal_volume = 0.0; + float exhaled_tidal_volume = 0.0; + float inhaled_tidal_volume = 0.0; - float minute_volume = 0; - float exhaled_minute_volume = 0; - float inhaled_minute_volume = 0; + float minute_volume = 0.0; + float exhaled_minute_volume = 0.0; + float inhaled_minute_volume = 0.0; - float lung_compliance = 0; - float static_compliance = 0; + float lung_compliance = 0.0; + float static_compliance = 0.0; uint16_t inhalation_pressure = 0; // mean airway pressure uint16_t peak_inspiratory_pressure = 0; diff --git a/raspberry-dataserver/CommsCommon.py b/raspberry-dataserver/CommsCommon.py index 2e1fbe049b4106c9b39768d73805b55b52f78ab4..fc1c9f7dd9bbb6d6f19521ca5accc06a7ec9ba27 100644 --- a/raspberry-dataserver/CommsCommon.py +++ b/raspberry-dataserver/CommsCommon.py @@ -8,7 +8,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # VERSIONING -# change version in BaseFormat for all data +# change version in PayloadFormat for all data # i.e. if and of DataFormat, CommandFormat or AlarmFormat change # then change the RPI_VERSION @@ -115,46 +115,67 @@ class BL_STATES(Enum): @unique class PAYLOAD_TYPE(IntEnum): + UNSET = 0 DATA = 1 READBACK = 2 CYCLE = 3 THRESHOLDS = 4 CMD = 5 ALARM = 6 - UNSET = 7 @dataclass -class BaseFormat(): +class PayloadFormat(): # class variables excluded from init args and output dict - _RPI_VERSION: ClassVar[int] = field(default=0xA3, init=False, repr=False) - _type: ClassVar[Any] = field(default=PAYLOAD_TYPE.UNSET, init=False, repr=False) - _dataStruct: ClassVar[Any] = field(default=Struct("<BI"), init=False, repr=False) - - # class variables - version: int = 0 - timestamp: int = 0 + _RPI_VERSION: ClassVar[int] = field(default=0xA3, init=False, repr=False) + _dataStruct: ClassVar[Any] = field(default=Struct("<BIB"), init=False, repr=False) + _byteArray: ClassVar[bytearray] = field(default=None, init=False, repr=False) + + # Meta information + version: int = 0 + timestamp: int = 0 + payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.UNSET + + @classmethod + def fromByteArray(cls, rec_bytes): + """Automatically determine which subclass to initialise as""" + DATA_TYPE_TO_CLASS = { + 1: DataFormat, + 2: ReadbackFormat, + 3: CycleFormat, + #4: ThresholdFormat, + 5: CommandFormat, + 6: AlarmFormat, + } + ReturnType = DATA_TYPE_TO_CLASS[rec_bytes[5]] + data = ReturnType._dataStruct.unpack(rec_bytes) + return ReturnType(*data) @property def byteArray(self) -> bytearray: - return self._dataStruct.pack(*[ + self.toByteArray() + return self._byteArray + + @byteArray.setter + def byteArray(self, byte_array) -> None: + self._byteArray = byte_array + + def toByteArray(self) -> None: + self.version = self._RPI_VERSION + self._byteArray = self._dataStruct.pack(*[ v.value if isinstance(v, IntEnum) or isinstance(v, Enum) else v for v in asdict(self).values() ]) - # at the minute not generalised. needs to be overridden - def fromByteArray(self, byteArray: bytearray) -> None: - (self.version, self.timestamp) = self._dataStruct.unpack(byteArray) - # check for mismatch between pi and microcontroller version def checkVersion(self): if self._RPI_VERSION != self.version : raise Exception('Version Mismatch', "PI:", self._RPI_VERSION, "uC:", self.version) def getSize(self) -> int: - return self._dataStruct.size + return len(self.byteArray) def getType(self) -> Any: - return self._type + return self.payload_type def getDict(self) -> Dict: return {k: v.name if isinstance(v, IntEnum) or isinstance(v, Enum) else v for k, v in asdict(self).items()} @@ -164,14 +185,13 @@ class BaseFormat(): # fast data payload # ======================================= @dataclass -class DataFormat(BaseFormat): +class DataFormat(PayloadFormat): # subclass dataformat _dataStruct = Struct("<BIBBHfHffffHfHHfff") - _type = PAYLOAD_TYPE.DATA + payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.DATA # subclass member variables - data_type: int = 1 - fsm_state: BL_STATES = BL_STATES.IDLE + fsm_state: BL_STATES = BL_STATES.IDLE pressure_air_supply: int = 0 pressure_air_regulated: float = 0.0 pressure_o2_supply: int = 0 @@ -196,7 +216,7 @@ class DataFormat(BaseFormat): tmp_state = 0 (self.version, self.timestamp, - _, # dummy to skip datatype + self.payload_type, tmp_state, self.pressure_air_supply, self.pressure_air_regulated, @@ -211,14 +231,9 @@ class DataFormat(BaseFormat): self.ambient_temperature, self.airway_pressure, self.flow, - self.volume - ) = self._dataStruct.unpack(byteArray) - try: - self.fsm_state = BL_STATES(tmp_state) - self.checkVersion() - except Exception: - # no longer silently die, catch Exceptions higher up - raise + self.volume) = self._dataStruct.unpack(byteArray) + self.fsm_state = BL_STATES(tmp_state) + self.checkVersion() self._byteArray = byteArray @@ -226,11 +241,10 @@ class DataFormat(BaseFormat): # readback data payload # ======================================= @dataclass -class ReadbackFormat(BaseFormat): +class ReadbackFormat(PayloadFormat): _dataStruct = Struct("<BIBHHHHHHHHHHHffBBBBBBBBBBBBf") - _type = PAYLOAD_TYPE.READBACK + payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.READBACK - data_type: int = 2 duration_calibration: int = 0 duration_buff_purge: int = 0 duration_buff_flush: int = 0 @@ -268,7 +282,7 @@ class ReadbackFormat(BaseFormat): tmp_mode = 0 (self.version, self.timestamp, - _, # dummy to skip datatype + self.payload_type, self.duration_calibration, self.duration_buff_purge, self.duration_buff_flush, @@ -308,12 +322,11 @@ class ReadbackFormat(BaseFormat): # cycle data payload # ======================================= @dataclass -class CycleFormat(BaseFormat): +class CycleFormat(PayloadFormat): # subclass dataformat _dataStruct = Struct("<BIBfffffffffHHHHBHHB") - _type = PAYLOAD_TYPE.CYCLE + payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.CYCLE - data_type: int = 3 respiratory_rate: float = 0.0 tidal_volume: float = 0.0 exhaled_tidal_volume: float = 0.0 @@ -339,7 +352,7 @@ class CycleFormat(BaseFormat): #logging.info(binascii.hexlify(byteArray)) (self.version, self.timestamp, - _, # dummy to skip datatype + self.payload_type, self.respiratory_rate, self.tidal_volume, self.exhaled_tidal_volume, @@ -371,9 +384,9 @@ class CycleFormat(BaseFormat): # cmd type payload # ======================================= @dataclass -class CommandFormat(BaseFormat): - _dataStruct = Struct("<BIBBI") - _type = PAYLOAD_TYPE.CMD +class CommandFormat(PayloadFormat): + _dataStruct = Struct("<BIBBBI") + payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.CMD cmd_type: int = 0 cmd_code: int = 0 @@ -382,6 +395,7 @@ class CommandFormat(BaseFormat): def fromByteArray(self, byteArray): (self.version, self.timestamp, + self.payload_type, self.cmd_type, self.cmd_code, self.param) = self._dataStruct.unpack(byteArray) @@ -392,18 +406,19 @@ class CommandFormat(BaseFormat): # alarm type payload # ======================================= @dataclass -class AlarmFormat(BaseFormat): - _dataStruct = Struct("<BIBBI") - _type = PAYLOAD_TYPE.ALARM +class AlarmFormat(PayloadFormat): + _dataStruct = Struct("<BIBBBI") + payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.ALARM alarm_type: int = 0 alarm_code: ALARM_CODES = ALARM_CODES.UNKNOWN - param: int = 0 + param: int = 0 def fromByteArray(self, byteArray): alarm = 0 (self.version, self.timestamp, + self.payload_type, self.alarm_type, alarm, self.param) = self._dataStruct.unpack(byteArray) diff --git a/raspberry-dataserver/CommsDebug.py b/raspberry-dataserver/CommsDebug.py index 73cc2c2a7b2adb097d5e103b38680e60851c3c3e..41ede571c56e0a2317a0cc60f9a9b204c2c667fa 100755 --- a/raspberry-dataserver/CommsDebug.py +++ b/raspberry-dataserver/CommsDebug.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 -from CommsControl import CommsControl +from CommsLLI import CommsLLI from CommsCommon import * +import asyncio import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') from serial.tools import list_ports import sys import time -port_device = "" -for port in list_ports.comports(): - vidpid = "" - if port.pid != None and port.vid != None: - vidpid = f"{ port.vid:04x}:{port.pid:04x}".upper() - print(vidpid) - if port.manufacturer and "ARDUINO" in port.manufacturer.upper(): - port_device = port.device - elif vidpid == "10C4:EA60" : - port_device = port.device - elif len(sys.argv) > 1: - port_device = sys.argv[1] -comms = CommsControl(port = port_device) +def getTTYPort(): + port_device = "" + for port in list_ports.comports(): + vidpid = "" + if port.pid != None and port.vid != None: + vidpid = f"{ port.vid:04x}:{port.pid:04x}".upper() + print(vidpid) + if port.manufacturer and "ARDUINO" in port.manufacturer.upper(): + port_device = port.device + elif vidpid == "10C4:EA60" : + port_device = port.device + elif len(sys.argv) > 1: + port_device = sys.argv[1] + return port_device class Dependant(object): @@ -33,32 +35,49 @@ class Dependant(object): if hasattr(payload, 'ventilation_mode'): logging.info(f"payload received: {payload.ventilation_mode}") #logging.info(f"payload received: {payload.fsm_state}") + if payload.getType() == 1: + logging.info(f"Fsm state: {payload.fsm_state}") #logging.info(f"payload received: {payload.timestamp}") - if hasattr(payload, 'fsm_state'): - logging.info(f"payload received: {payload.fsm_state}") if hasattr(payload, 'duration_inhale'): logging.info(f"payload received: inhale duration = {payload.duration_inhale} ") self._llipacket = payload.getDict() # returns a dict - # pop from queue - protects against Dependant going down and not receiving packets - self._lli.pop_payloadrecv() -dep = Dependant(comms) # initialise as start command, automatically executes toByteArray() -#cmd = CommandFormat(cmd_type=CMD_TYPE.GENERAL.value, cmd_code=CMD_GENERAL.START.value, param=0) -#cmd = CommandFormat(cmd_type=CMD_TYPE.SET_TIMEOUT.value, cmd_code=CMD_SET_TIMEOUT.INHALE.value, param=1111) -time.sleep(4) -cmd = CommandFormat(cmd_type=CMD_TYPE.GENERAL.value, cmd_code=CMD_GENERAL.START.value, param=0) -comms.writePayload(cmd) -print('sent cmd start') -while True: - time.sleep(15) - cmd = CommandFormat(cmd_type=CMD_TYPE.SET_MODE.value, cmd_code=VENTILATION_MODE.LAB_MODE_PURGE.value, param=0) +async def commsDebug(): + #cmd = CommandFormat(cmd_type=CMD_TYPE.GENERAL.value, cmd_code=CMD_GENERAL.START.value, param=0) + #cmd = CommandFormat(cmd_type=CMD_TYPE.SET_TIMEOUT.value, cmd_code=CMD_SET_TIMEOUT.INHALE.value, param=1111) + cmd = CommandFormat(cmd_type=CMD_TYPE.GENERAL.value, cmd_code=CMD_GENERAL.START.value, param=0) + await asyncio.sleep(4) comms.writePayload(cmd) - print('sent cmd purge') - time.sleep(15) - cmd = CommandFormat(cmd_type=CMD_TYPE.GENERAL.value, cmd_code=CMD_GENERAL.STOP.value, param=0) - comms.writePayload(cmd) - print('sent cmd stop') - pass + print('sent cmd start') + while True: + await asyncio.sleep(15) + cmd = CommandFormat(cmd_type=CMD_TYPE.SET_MODE.value, cmd_code=VENTILATION_MODE.LAB_MODE_PURGE.value, param=0) + comms.writePayload(cmd) + print('sent cmd purge') + await asyncio.sleep(15) + cmd = CommandFormat(cmd_type=CMD_TYPE.GENERAL.value, cmd_code=CMD_GENERAL.STOP.value, param=0) + comms.writePayload(cmd) + print('sent cmd stop') + +try: + # setup serial device and init server + loop = asyncio.get_event_loop() + comms = CommsLLI(loop) + dep = Dependant(comms) + # create tasks + lli = comms.main(getTTYPort(), 115200) + debug = commsDebug() + tasks = [lli, debug] + + # run tasks + asyncio.gather(*tasks, return_exceptions=True) + loop.run_forever() +except asyncio.CancelledError: + pass +except KeyboardInterrupt: + logging.info("Closing LLI") +finally: + loop.close() diff --git a/raspberry-dataserver/CommsFormat.py b/raspberry-dataserver/CommsFormat.py index 5e3b4e02676b1500d4b08141b42d585bad8dcb68..56b9d726d803217d043b8097aaa1cec1c3b8dce5 100644 --- a/raspberry-dataserver/CommsFormat.py +++ b/raspberry-dataserver/CommsFormat.py @@ -2,45 +2,61 @@ # Communication protocol based on HDLC format # author Peter Svihra <peter.svihra@cern.ch> +# adapted for async DM import libscrc +import binascii +import logging +from typing import ClassVar + +# Set up logging +logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s') +logging.getLogger().setLevel(logging.DEBUG) + def commsFromBytes(byteArray): comms = CommsFormat() comms.copyBytes(byteArray) - return comms def generateAlarm(payload): comms = CommsFormat(info_size = payload.getSize(), address = 0xC0) - comms.setInformation(payload.byteArray) + bytelist = bytes([byte for byte in bytearray(payload.byteArray)]) + comms.setInformation(bytelist) return comms def generateCmd(payload): comms = CommsFormat(info_size = payload.getSize(), address = 0x80) - comms.setInformation(payload.byteArray) + bytelist = bytes([byte for byte in bytearray(payload.byteArray)]) + comms.setInformation(bytelist) return comms def generateData(payload): comms = CommsFormat(info_size = payload.getSize(), address = 0x40) - comms.setInformation(payload.byteArray) + bytelist = bytes([byte for byte in bytearray(payload.byteArray)]) + comms.setInformation(bytelist) return comms +class CommsChecksumError(Exception): + pass # basic format based on HDLC class CommsFormat: - def __init__(self, info_size = 0, address = 0x00, control = [0x00, 0x00]): + start_flag: ClassVar[int] = bytes([0x7E]) + escape_flag: ClassVar[int] = bytes([0x7D]) + + def __init__(self, info_size=0, address=0x00, control=[0x00, 0x00]): self._data = bytearray(7 + info_size) self._info_size = info_size self._crc = None - - self.assignBytes(self.getStart() , bytes([0x7E]) , calc_crc = False) + + self.assignBytes(self.getStart() , self.start_flag , calc_crc = False) self.assignBytes(self.getAddress(), bytes([address]), calc_crc = False) self.assignBytes(self.getControl(), bytes(control) , calc_crc = False) - self.assignBytes(self.getStop() , bytes([0x7E]) , calc_crc = False) - + self.assignBytes(self.getStop() , self.start_flag , calc_crc = False) + self.generateCrc() - + def getStart(self): return 0 def getAddress(self): @@ -53,17 +69,17 @@ class CommsFormat: return 4 + self._info_size def getStop(self): return 4 + self._info_size + 2 - + def setAddress(self, address): self.assignBytes(self.getAddress(), bytes([address]), 1) - + def setControl(self, control): self.assignBytes(self.getControl(), bytes(control), 2) - + def setInformation(self, bytes_array): # convert provided value self.assignBytes(self.getInformation(), bytes_array) - + def setSequenceSend(self, value): # sequence sent valid only for info frames (not supervisory ACK/NACK) if (self._data[self.getControl() + 1] & 0x01) == 0: @@ -73,51 +89,129 @@ class CommsFormat: def setSequenceReceive(self, value): value = (value << 1) & 0xFE self.assignBytes(self.getControl() , value.to_bytes(1, byteorder='little'), 1) - + def getSequenceSend(self): # sequence sent valid only for info frames (not supervisory ACK/NACK) if (self._data[self.getControl() + 1] & 0x01) == 0: return (self._data[self.getControl() + 1] >> 1) & 0x7F else: return 0xFF - + def getSequenceReceive(self): return (self._data[self.getControl()] >> 1) & 0x7F - + def assignBytes(self, start, values, calc_crc = True): for idx in range(len(values)): self._data[start + idx] = values[idx] if calc_crc: self.generateCrc() - + # generate checksum def generateCrc(self, assign = True): self._crc = libscrc.x25(bytes(self._data[self.getAddress():self.getFcs()])).to_bytes(2, byteorder='little') if assign: self.assignBytes(self.getFcs(), self._crc, calc_crc = False) - + def compareCrc(self): self.generateCrc(False) fcs = self.getData()[self.getFcs():self.getFcs()+2] return self._crc in fcs - - + def getData(self): return self._data def copyData(self, data_array): self.copyBytes(data_array.to_bytes(self._info_size, byteorder='little')) - + def copyBytes(self, bytes_array): self._info_size = len(bytes_array) - 7 self._data = bytes_array + # escape any 0x7D or 0x7E with 0x7D and swap bit 5 + def encode(self): + data = self._data + stream = [[int.from_bytes(self.escape_flag, 'little'), byte ^ (1<<5)] if bytes([byte]) == self.escape_flag or bytes([byte]) == self.start_flag else [byte] for byte in data[1:-1]] + stream = [byte for byteList in stream for byte in byteList] + result = bytes([data[0]]) + bytes(stream) + bytes([data[-1]]) + return result + + +class CommsPacket(object): + start_flag: ClassVar[int] = bytes([0x7E]) + escape_flag: ClassVar[int] = bytes([0x7D]) + + def __init__(self, rawdata): + self._data = rawdata + self._sequence_receive = 0 + self._address = None + self._byteArray = None + self._datavalid = False + self._acked = None + + @property + def byteArray(self): + return self._byteArray + + @property + def sequence_receive(self): + return self._sequence_receive + + @property + def address(self): + return self._address + + @property + def acked(self): + return self._acked + + def undoEscape(self): + data = self._data + + packets = data[1:-1] + + # remove escape sequences for bytes containing escape patterns and convert back + indRemove = [idx for idx in range(len(packets)) if bytes([packets[idx]]) == self.escape_flag] + indChange = [idx+1 for idx in indRemove] + + stream = [packets[idx] ^ (1<<5) if idx in indChange else packets[idx] for idx in range(len(packets)) if idx not in indRemove] + result = bytes([data[0]]) + bytes(stream) + bytes([data[-1]]) + return result + + def decode(self): + """Returns payload bytearray or None for ack/nack""" + byteArray = self.undoEscape() + + # check checksum and control bytes + tmp_comms = CommsFormat() + tmp_comms.copyBytes(byteArray) + if tmp_comms.compareCrc(): + control = tmp_comms.getData()[tmp_comms.getControl()+1] + self._sequence_receive = (tmp_comms.getData()[tmp_comms.getControl()] >> 1) & 0x7F + self._address = tmp_comms.getData()[1] + + # get type of packet + ctrl_flag = control & 0x0F + if ctrl_flag == 0x05: + # received NACK + self._acked = False + elif ctrl_flag == 0x01: + # received ACK + self._acked = True + else: + # received data + self._sequence_receive = ((control >> 1) & 0x7F) + 1 + self._byteArray = tmp_comms.getData()[tmp_comms.getInformation():tmp_comms.getFcs()] + else: + raise CommsChecksumError + + return self._byteArray + # ACK specific formating class CommsACK(CommsFormat): def __init__(self, address): - super().__init__(control = [0x00, 0x01], address = address) - + super().__init__(control = [0x00, 0x01], address=address) + # NACK specific formating class CommsNACK(CommsFormat): def __init__(self, address): - super().__init__(control = [0x00, 0x05], address = address) + super().__init__(control = [0x00, 0x05], address=address) diff --git a/raspberry-dataserver/CommsLLI.py b/raspberry-dataserver/CommsLLI.py new file mode 100755 index 0000000000000000000000000000000000000000..b62ecfc9cbd9021572f1837a62c63493714b0546 --- /dev/null +++ b/raspberry-dataserver/CommsLLI.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 + +import asyncio +import serial_asyncio +import logging +import binascii +import time +from collections import deque +from struct import error as StructError +from CommsCommon import PayloadFormat, PAYLOAD_TYPE +from CommsFormat import CommsPacket, CommsACK, CommsNACK, CommsChecksumError, generateAlarm, generateCmd, generateData + +# Set up logging +logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s') +logging.getLogger().setLevel(logging.INFO) + +class CommsLLI: + def __init__(self, loop): + super().__init__() + # IO + self._loop = loop + self._reader = None + self._writer = None + self._connected = False + + # send + self._queue_size = 16 + self._alarms = asyncio.Queue(maxsize=self._queue_size, loop=self._loop) + self._commands = asyncio.Queue(maxsize=self._queue_size, loop=self._loop) + self._data = asyncio.Queue(maxsize=self._queue_size, loop=self._loop) + + # acks from arduino + self._dvAlarms = asyncio.Event(loop=self._loop) + self._dvCommands = asyncio.Event(loop=self._loop) + self._dvData = asyncio.Event(loop=self._loop) + # maps between address and queues/events/timeouts + self._queues = {0xC0: self._alarms, 0x80: self._commands, 0x40: self._data} + self._timeouts = {0xC0: 10, 0x80: 50, 0x40: 200} + self._acklist = {0xC0: self._dvAlarms, 0x80: self._dvCommands, 0x40: self._dvData} + + # receive + #self._payloadrecv = asyncio.Queue(maxsize=self._queue_size, loop=self._loop) + self._payloadrecv = deque(maxlen = self._queue_size) + self._observers = [] + + # packet counting + self._sequence_send = 0 + self._sequence_receive = 0 + + async def main(self, device, baudrate): + self._reader, self._writer = await serial_asyncio.open_serial_connection(url=device, baudrate=baudrate, timeout = 2, dsrdtr = True) + self._connected = True + while self._connected: + #sendAlarm = self.send(0xC0) + sendCmd = self.send(0x80) + #sendData = self.send(0x40) + receiver = self.recv() + await asyncio.gather(*[receiver, sendCmd], return_exceptions=True) + + async def send(self, address): + queue = self._queues[address] + while self._connected: + logging.debug("Waiting for Command") + packet = await queue.get() + packet.setSequenceSend(self._sequence_send) + for send_attempt in range(5): + # try to send the packet 5 times + try: + await self.sendPacket(packet) + await asyncio.wait_for(self._acklist[address].wait(), timeout=self._timeouts[address] / 1000) + except asyncio.TimeoutError: + pass + except Exception: + # catch everything else and propagate it + raise + else: + # acknowledged + break + + def writePayload(self, payload): + try: + # TODO replace these two dicts with address mappings like everywhere else + PAYLOAD_TYPE_TO_GEN = { + 1: generateData, + 2: generateData, + 3: generateData, + 4: generateData, + 5: generateCmd, + 6: generateAlarm, + } + generatePacket = PAYLOAD_TYPE_TO_GEN[payload.getType()] + tmp_comms = generatePacket(payload) + + qlist = [v for v in self._queues.values()] + PAYLOAD_TYPE_TO_QUEUE = { + 1: qlist[2], + 2: qlist[2], + 3: qlist[2], + 4: qlist[2], + 5: qlist[1], + 6: qlist[0], + } + queue = PAYLOAD_TYPE_TO_QUEUE[payload.getType()] + queue.put_nowait(tmp_comms) + + return True + except KeyError: + return False + + async def sendPacket(self, packet): + if isinstance(packet, CommsACK): + # don't log acks + pass + elif isinstance(packet, CommsNACK): + logging.warning(f"Sending NACK: {binascii.hexlify(packet.encode())}") + else: + logging.info(f"Sending {binascii.hexlify(packet.encode())}") + self._writer.write(packet.encode()) + + async def readPacket(self): + while True: + rawdata = await self._reader.readuntil(CommsPacket.start_flag) + # valid packets are minimum 6 bytes excluding start flag + if len(rawdata) >= 6: + # replace start flag which was stripped while searching + rawdata = CommsPacket.start_flag + rawdata + break + return CommsPacket(bytearray(rawdata)) + + def finishPacket(self, address): + self._sequence_send = (self._sequence_send + 1) % 128 + self._queues[address].task_done() + self._acklist[address].set() + + async def recv(self): + while self._connected: + packet = await self.readPacket() + + try: + data = packet.decode() + except CommsChecksumError: + # checksum failed! wait for it to be resent + logging.warning(f"Packet did not match checksum: {packet._data}") + continue + + if data is None: + # packet is an ack/nack from previously sent data + if packet.acked: + logging.info("Received ACK") + # increase packet counter + self.finishPacket(packet.address) + else: + logging.debug("Received NACK") + else: + # packet should contain valid data + try: + payload = PayloadFormat.fromByteArray(packet.byteArray) + self.payloadrecv = payload + logging.debug(f"Received payload type {payload.getType()} for timestamp {payload.timestamp}") + comms_response = CommsACK(packet.address) + except (StructError, ValueError): + # invalid payload, but valid checksum - this is bad! + logging.error(f"Invalid payload: {payload}") + # restart/reflash/swap to redundant microcontroller? + comms_response = CommsNACK(packet.address) + finally: + comms_response.setSequenceReceive(packet.sequence_receive) + await self.sendPacket(comms_response) + + # callback to dependants to read the received payload + @property + def payloadrecv(self): + return self._payloadrecv + + @payloadrecv.setter + def payloadrecv(self, payload): + for callback in self._observers: + # peek at the leftmost item, don't pop until receipt confirmed + callback(payload) + + def bind_to(self, callback): + self._observers.append(callback) + + +if __name__ == "__main__": + try: + # schedule async tasks + loop = asyncio.get_event_loop() + + # setup serial devices + comms = CommsLLI(loop) + + asyncio.gather(comms.main("/dev/ttyUSB0", 115200), return_exceptions=True) + loop.run_forever() + except asyncio.CancelledError: + pass + except KeyboardInterrupt: + logging.info("Closing LLI") + finally: + loop.close() \ No newline at end of file