def send_serial(self, data): """ Send what is assumed a Modbus serial packet. If the serial port is NOT open (so self.ser == None), this this routine tries to open it. If the open fails, a ConnectError is raised, which is the SDK apps' signal to quit/abort running. :param bytes data: the Modbus serial message, which could be either Modbus/RTU or ASCII :return: the response data, which might be b'' (empty/no response) """ import logging if self.ser is None: self.logger.info("Open serial port:{}".format(self.serial_name)) try: self.ser = serial.Serial(port=self.serial_name, baudrate=self.serial_baud, bytesize=8, parity=self.serial_parity, stopbits=1, timeout=1, xonxoff=0, rtscts=0) self.ser.setDTR(True) # self.ser.setRTS(True) except serial.SerialException: self.ser = None self.logger.exception("Open of serial port failed") raise ConnectionError("Open of serial port failed") self.logger.info("Serial Protocol:{}".format(self.serial_protocol)) if self.logger.getEffectiveLevel() <= logging.DEBUG: if self.serial_protocol == IA_PROTOCOL_MBASC: # for ASCII, just print as string self.logger.debug("ASC-REQ:{}".format(data)) else: # for RTU, we want HEX form logger_buffer_dump(self.logger, "RTU-REQ", data) self.ser.write(data) # we have 1 second response timeout in the Serial() open time.sleep(0.25) response = self.ser.read(256) if self.logger.getEffectiveLevel() <= logging.DEBUG: if response is None or response == b'': self.logger.debug("SER-RSP:None/No response") elif self.serial_protocol == IA_PROTOCOL_MBASC: # for ASCII, just print as string self.logger.debug("ASC-RSP:{}".format(response)) else: # for RTU, we want HEX form logger_buffer_dump(self.logger, "RTU-RSP", response) return response
def handle_data(self, data): """ :param bytes data: :return: """ logger_buffer_dump(self.logger, "TCP-REQ", data) self.last_activity = time.time() response = None try: # parse the MB/TCP request self.modbus.set_request(data, self.host_protocol) # self.logger.debug("REQ:{}".format(self.modbus.attrib)) except (ModbusBadForm, ModbusBadChecksum): # this could be a bad setting self.logger.warning("Bad Modbus Form") return None # retrieve as MB/RTU request modbus_rtu = self.modbus.get_request(self.serial_protocol) # self.logger.debug("SER:{}".format(modbus_rtu)) # if Serial() open fails, we'll throw ConnectionError, which this # code assumes the CALLER of handle_data() handles response = self.send_serial(modbus_rtu) if response and len(response): # parse the MB/RTU in try: self.modbus.set_response(response, self.serial_protocol) response = self.modbus.get_response(self.host_protocol) except ModbusBadForm: # this could be a bad setting self.logger.warning("Bad Modbus Form") response = None except ModbusBadChecksum: # likely line noise or loose wire self.logger.warning("Bad Checksum") response = None else: # in truth, if client is: # Modbus/TCP - should return exception 0x0B # Modbus/RTU - no response self.logger.debug("No response") response = None if response is None: # then re-form as the correct err response, which may be None response = self.modbus.get_no_response_error(self.host_protocol) logger_buffer_dump(self.logger, "TCP-RSP", response) # else was in error, hang-up return response
def _fetch_content_length(data, logger=None): """ handle the situation where response might be either: form A = 'content-length: 12\n\r\n\r\n"IBR1150LPE"' form B = 'content-length: 189' :param data: :return: """ if isinstance(data, bytes): data = data.decode('ascii') data = data.strip() if logger is not None: logger_buffer_dump(logger, 'length', data, show_ascii=True) if not data.startswith('content-length: '): # then it is mal-formed! return None, None # chop off the 'content-length: ' with the ending space data = data[16:] # form A now = '12\n\r\n\r\n"IBR1150LPE"' # form B now = '189' offset = data.find('\n') if offset <= 0: # then form B like, so data_length = '189' data_length = data.strip() all_data = "" else: # else is form A like # data_length = '12' # data = '"IBR1150LPE"' data_length = data[:offset].strip() all_data = data[offset + 4:].strip() try: data_length = int(data_length) except ValueError: # then bad length field! if logger is not None: logger.error("CSClient() content length not INT()") return None, None if logger is not None: logger.debug("data_length={}".format(data_length)) if len(all_data): logger_buffer_dump(logger, 'ready', all_data, show_ascii=True) # else is empty # change to be REMAINING data data_length -= len(all_data) return data_length, all_data
def delete(self, base, query=''): """ Send a delete request. :param str base: 'tree' element path, like '/status/gpio/LED_USB1_G' :param str query: the text :return str: """ self._logger.debug("CSClient() DEL={}".format(base)) self.last_url = "delete\n{}\n{}\n".format(base, query) logger_buffer_dump(self._logger, 'DELETE', self.last_url, show_ascii=True) return self._dispatch(self.last_url)
def get(self, base, query='', tree=0): """ Send a get request. - example: self.state = self.client.get('/status/gpio/%s' % self.name) :param str base: 'tree' element path, like '/status/gpio/LED_USB1_G' :param str query: ??? :param int tree: ??? """ self._logger.debug("CSClient() GET={}".format(base)) self.last_url = "get\n{}\n{}\n{}\n".format(base, query, tree) if self.show_rsp: logger_buffer_dump(self._logger, 'GET', self.last_url, show_ascii=True) return self._dispatch(self.last_url)
def append(self, base, value, query=''): """ Send an append request. :param str base: 'tree' element path, like '/status/gpio/LED_USB1_G' :param value: the payload, as JSON - such as {"LED_USB1_G":1} :type value: str or dict :param str query: ??? :return str: """ value = json.dumps(value).replace(' ', '') self._logger.debug("CSClient() APPEND={} data={}".format(base, value)) self.last_url = "post\n{}\n{}\n{}\n".format(base, query, value) logger_buffer_dump(self._logger, 'APPEND', self.last_url, show_ascii=True) return self._dispatch(self.last_url)
def put(self, base, value, query='', tree=0): """ Send a put request. - example: self.client.put('/control/gpio', {self.name: self.state}) :param str base: 'tree' element path, like '/status/gpio/LED_USB1_G' :param value: the payload, as JSON - such as {"LED_USB1_G":1} :type value: str or dict :param str query: ??? :param int tree: ??? """ if isinstance(value, dict): # convert dictionary to JSON without white-space value = json.dumps(value).replace(' ', '') self._logger.debug("CSClient() PUT={} data={}".format(base, value)) self.last_url = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) logger_buffer_dump(self._logger, 'PUT', self.last_url, show_ascii=True) return self._dispatch(self.last_url)
def _dispatch(self, cmd): """ Send the command and return the response. How the router actually responds is a bit fuzzy, and I've seen several conflicting solutions which assume a more line-by-line behavior, which I am not seeing in 6.1 (Mar-16) :param str cmd: the prepared command :return str: """ self.last_reply = None with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((self.DEFAULT_HOST, self.DEFAULT_PORT)) sock.sendall(cmd.encode('ascii')) data = sock.recv(self.DEFAULT_SIZE).decode('ascii').strip() """ :type data: str """ # logger_buffer_dump(self._logger, 'header', data, show_ascii=True) if data.startswith('status: ok'): if len(data) > 18: # on some occasions, 'content-length: ' will be appended! # the length 18 is a bit arbitrary. The desired status # is "status: ok\n", so 11 bytes self._logger.debug( "Special STATUS() packing, len={}".format(len(data))) data = data[11:].strip() else: data = sock.recv(self.DEFAULT_SIZE) if self.show_rsp: data_expected, self.last_reply = _fetch_content_length( data, self._logger) else: data_expected, self.last_reply = _fetch_content_length( data, None) retry = 0 while data_expected > 0: # data = sock.recv(self.DEFAULT_SIZE).decode('ascii') data = sock.recv(self.DEFAULT_SIZE).decode('ascii') if self.show_rsp: logger_buffer_dump(self._logger, 'loop', data, show_ascii=True) if data.startswith('\r\n\r\n'): # this was form B, so second block of data started # with the 2 dummy lines data = data[4:] if len(data) == 0: if retry > 3: self._logger.debug("CSClient len(data)==0, BREAK") break retry += 1 self._logger.debug( "CSClient() len(data)==0, retry={}".format(retry)) self.last_reply += data data_expected -= len(data) if data_expected: # only show is NOT zero self._logger.debug( "pst expected={}".format(data_expected)) if len(self.last_reply) >= 2: # reply might be nothing, but if string, we make sure to handle if self.last_reply[0] == '\"': # remove leading/trailing quotes, so make "\"IBR1100LPE\"" # into "IBR1100LPE" self.last_reply = unquote_string(self.last_reply) elif self.last_reply[0] == '{': # convert JSON string to dict(), like: # '{"enable_gps_keepalive": false, "pwd_enabled": false, # "enabled": true}' # self._logger.debug("final{}".format(self.last_reply)) try: self.last_reply = json.loads(self.last_reply) except ValueError: # some idiotic API calls return malformed JSON such as # "{'enabled': true}" so not double quotes! self.last_reply = self.last_reply.replace("\'", "\"") # if it still fails, then assume worse error self.last_reply = json.loads(self.last_reply) return self.last_reply
def test_logger_buffer_dump(self): """ Test buffer to lines function :return: """ from cp_lib.buffer_dump import logger_buffer_dump tests = [ { "msg": "fruit", "dat": "Apple", "asc": False, "exp": ['dump:fruit, len=5', '[000] 41 70 70 6C 65'] }, { "msg": "fruit", "dat": "Apple", "asc": True, "exp": ['dump:fruit, len=5', '[000] 41 70 70 6C 65 \'Apple\''] }, { "msg": "fruit", "dat": b"Apple", "asc": False, "exp": ['dump:fruit, len=5 bytes()', '[000] 41 70 70 6C 65'] }, { "msg": "fruit", "dat": b"Apple", "asc": True, "exp": [ 'dump:fruit, len=5 bytes()', '[000] 41 70 70 6C 65 b\'Apple\'' ] }, { "msg": "longer", "dat": "Apple\nIs found in the country of my birth\n", "asc": False, "exp": [ 'dump:longer, len=42', '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69', '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66', '[032] 20 6D 79 20 62 69 72 74 68 0A' ] }, { "msg": "longer", "dat": "Apple\nIs found in the country of my birth\n", "asc": True, "exp": [ 'dump:longer, len=42', '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69' + ' \'Apple\\nIs found i\'', '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66' + ' \'n the country of\'', '[032] 20 6D 79 20 62 69 72 74 68 0A \' my birth\\n\'' ] }, { "msg": "longer", "dat": b"Apple\nIs found in the country of my birth\n", "asc": False, "exp": [ 'dump:longer, len=42 bytes()', '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69', '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66', '[032] 20 6D 79 20 62 69 72 74 68 0A' ] }, { "msg": "longer", "dat": b"Apple\nIs found in the country of my birth\n", "asc": True, "exp": [ 'dump:longer, len=42 bytes()', '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69' + ' b\'Apple\\nIs found i\'', '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66' + ' b\'n the country of\'', '[032] 20 6D 79 20 62 69 72 74 68 0A b\' my birth\\n\'' ] }, { "msg": "Modbus", "dat": "\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09", "asc": False, "exp": [ 'dump:Modbus, len=11', '[000] 01 1F 00 01 02 03 04 05 06 08 09' ] }, { "msg": "Modbus", "dat": b"\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09", "asc": False, "exp": [ 'dump:Modbus, len=11 bytes()', '[000] 01 1F 00 01 02 03 04 05 06 08 09' ] }, { "msg": "Modbus+", "asc": False, "dat": "\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09\x00\x01\x02\x03\x04\x05\x06\x08", "exp": [ 'dump:Modbus+, len=19', '[000] 01 1F 00 01 02 03 04 05 06 08 09 00 01 02 03 04', '[016] 05 06 08' ] }, { "msg": "Modbus+", "asc": False, "dat": b"\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09\x00\x01\x02\x03\x04\x05\x06\x08", "exp": [ 'dump:Modbus+, len=19 bytes()', '[000] 01 1F 00 01 02 03 04 05 06 08 09 00 01 02 03 04', '[016] 05 06 08' ] }, ] logging.info("") logger = logging.getLogger('unitest') logger.setLevel(logging.DEBUG) for test in tests: # logging.debug("Test:{}".format(test)) logger_buffer_dump(logger, test['msg'], test['dat'], test['asc']) logging.info("") return