openrvdas.logger.readers.polled_serial_reader
No module-level documentation available.
1#!/usr/bin/env python3 2 3import logging 4import time 5from itertools import cycle 6 7# Don't freak out if pyserial isn't installed - unless they actually 8# try to instantiate a SerialReader 9try: 10 import serial 11 SERIAL_MODULE_FOUND = True 12except ModuleNotFoundError: 13 SERIAL_MODULE_FOUND = False 14 15from logger.readers.serial_reader import SerialReader # noqa: E402 16 17 18############################ 19def is_string_or_list_of_strings(cmd): 20 if isinstance(cmd, str): 21 return True 22 elif isinstance(cmd, list): 23 return all(isinstance(item, str) for item in cmd) 24 return False 25 26 27############################ 28def is_dict_of_lists_of_strings(cmd): 29 if not isinstance(cmd, dict): 30 return False 31 32 # So we have a dict. Check that all values are either strings 33 # or lists of strings 34 for value in cmd.values(): 35 if isinstance(value, str): 36 continue 37 elif (isinstance(value, list) and 38 all(isinstance(item, str) for item in value)): 39 continue 40 else: 41 return False 42 43 # If got this far, it all checks out 44 return True 45 46 47################################################################################ 48class PolledSerialReader(SerialReader): 49 """ 50 Read text records from a serial port. 51 """ 52 53 def __init__(self, port, baudrate=9600, bytesize=8, parity='N', 54 stopbits=1, timeout=None, xonxoff=False, rtscts=False, 55 write_timeout=None, dsrdtr=False, inter_byte_timeout=None, 56 exclusive=None, max_bytes=None, eol=None, 57 encoding='utf-8', encoding_errors='ignore', 58 start_cmd=None, pre_read_cmd=None, stop_cmd=None, **kwargs): 59 """Extends the standard serial reader by allowing the user to define 60 strings to send to the serial host on startup, before each read and 61 just prior to the reader being destroyed. 62 63 Notable arguments: 64 ``` 65 start_cmd 66 If not None, may be string or list of strings. If a single string, 67 it is sent to the serial port as soon as it is opened. If a list of 68 strings, each string in the list is sent in sequence. 69 70 stop_cmd 71 Much as start_cmd, but is sent when the PolledSerialReader is closed 72 or destroyed. 73 74 pre_read_cmd 75 Much as start_cmd and stop_cmd, except the string or list of strings 76 are sent each time the PolledSerialReader's read() method is called, 77 prior to trying to read from the port. 78 79 In addition to a string or list of strings, pre_read_cmd may be a *dict* 80 of lists of strings: 81 82 pre_read_cmd: 83 key1: ['command 1_1', 'command 1_2] 84 key2: ['command 2_1', 'command 2_2] 85 key3: ... 86 ... 87 The first time read() is called, the strings associated with key1 will be 88 sent. The second time, those associated with key2, and so on. When the end 89 of the dict is reached, it will start again with key1. 90 91 timeout <seconds> 92 If timeout is specified, then the serial read will time out after this many 93 seconds. It will then reissue the pre_read_cmds, if there is one, and try 94 reading again. If there is a dict of pre_read_cmds, it will progress to the 95 next one in the dict. 96 ``` 97 For all of these arguments, a special string, ``__PAUSE__``, is recognized. If 98 followed by a number (e.g. ``__PAUSE__ 5``), it will be interpreted as a command 99 to pause for that many seconds prior to sending the next command. If no number 100 is given, it will pause for one second. 101 """ 102 # Can we even run this? 103 if not SERIAL_MODULE_FOUND: 104 raise RuntimeError('Serial port functionality not available. Please ' 105 'install Python module pyserial.') 106 107 # Type check our pre_read commands 108 if start_cmd and not is_string_or_list_of_strings(start_cmd): 109 raise ValueError('PolledSerialReader start_cmd must either be None, ' 110 f'a string, or a list of strings. Found: {start_cmd}') 111 if stop_cmd and not is_string_or_list_of_strings(stop_cmd): 112 raise ValueError('PolledSerialReader stop_cmd must either be None, ' 113 f'a string, or a list of strings. Found: {stop_cmd}') 114 if pre_read_cmd and not (is_string_or_list_of_strings(pre_read_cmd) or 115 is_dict_of_lists_of_strings(pre_read_cmd)): 116 raise ValueError('PolledSerialReader pre_read_cmd must either be None, ' 117 'a string, or a list of strings, or a dict of lists of ' 118 f'strings. Found: {pre_read_cmd}') 119 120 # Okay, let's go and build 121 super().__init__(port=port, baudrate=baudrate, bytesize=bytesize, 122 parity=parity, stopbits=stopbits, timeout=timeout, 123 xonxoff=xonxoff, rtscts=rtscts, write_timeout=write_timeout, 124 dsrdtr=dsrdtr, inter_byte_timeout=inter_byte_timeout, 125 exclusive=exclusive, max_bytes=max_bytes, eol=eol, 126 encoding=encoding, encoding_errors=encoding_errors, **kwargs) 127 128 self.start_cmd = start_cmd 129 self.pre_read_cmd = pre_read_cmd 130 self.stop_cmd = stop_cmd 131 132 if isinstance(pre_read_cmd, dict): 133 self.command_cycle = cycle(pre_read_cmd.items()) 134 135 if self.start_cmd: 136 try: 137 if isinstance(self.start_cmd, list): # list of commands 138 for cmd in self.start_cmd: 139 self._send_command(cmd) 140 else: 141 self._send_command(start_cmd) 142 except serial.serialutil.SerialException as e: 143 logging.error(str(e)) 144 145 ############################ 146 def _send_command(self, cmd): 147 """Check if command is a 'pause'; if so, sleep, otherwise send it to serial port.""" 148 logging.debug(f'Sending {cmd}') 149 if cmd.find('__PAUSE__') == 0: 150 pause_cmd = cmd.split() 151 if len(pause_cmd) == 1: 152 pause_length = 1 153 elif len(pause_cmd) == 2: 154 try: 155 pause_length = float(pause_cmd[1]) 156 except ValueError: 157 raise ValueError('__PAUSE__ interval in PolledSerialReader must be ' 158 'a float. Found: %s' % cmd) 159 else: 160 raise ValueError('Pause format "__PAUSE__ <seconds>"; found %s' % cmd) 161 logging.info('Pausing %g seconds', pause_length) 162 time.sleep(pause_length) 163 else: 164 # If it's a normal command we're sending 165 logging.info('Sending serial command "%s"', cmd) 166 self.serial.write(self._encode_str(cmd, unescape=True)) 167 self.serial.flush() 168 169 logging.debug(f'Done sending {cmd}') 170 171 ############################ 172 def read(self): 173 try: 174 # Try again if our read times out, or our serial port returns None 175 while True: 176 # Do we need to send anything prior to reading? 177 if not self.pre_read_cmd: # no pre_read_cmd 178 pass 179 elif isinstance(self.pre_read_cmd, list): # list of commands 180 for cmd in self.pre_read_cmd: 181 self._send_command(cmd) 182 elif isinstance(self.pre_read_cmd, dict): # dict of lists of commands 183 key, commands = next(self.command_cycle) 184 for cmd in commands: 185 self._send_command(cmd) 186 elif self.pre_read_cmd: # simple string command 187 self._send_command(self.pre_read_cmd) 188 189 logging.debug('read() is being called') 190 record = super().read() 191 logging.debug(f'Returned from read() with: {record}') 192 if record is not None: 193 return record 194 logging.debug('Serial read returned None; trying again.') 195 196 except serial.serialutil.SerialException as e: 197 logging.error(str(e)) 198 return None 199 200 ############################ 201 def __del__(self): 202 if self.stop_cmd: 203 try: 204 if isinstance(self.stop_cmd, list): # list of commands 205 for cmd in self.stop_cmd: 206 self._send_command(cmd) 207 else: 208 self._send_command(self.stop_cmd) 209 except serial.serialutil.SerialException as e: 210 logging.error(str(e))
def
is_string_or_list_of_strings(cmd):
def
is_dict_of_lists_of_strings(cmd):
29def is_dict_of_lists_of_strings(cmd): 30 if not isinstance(cmd, dict): 31 return False 32 33 # So we have a dict. Check that all values are either strings 34 # or lists of strings 35 for value in cmd.values(): 36 if isinstance(value, str): 37 continue 38 elif (isinstance(value, list) and 39 all(isinstance(item, str) for item in value)): 40 continue 41 else: 42 return False 43 44 # If got this far, it all checks out 45 return True
class
PolledSerialReader(logger.readers.serial_reader.SerialReader):
49class PolledSerialReader(SerialReader): 50 """ 51 Read text records from a serial port. 52 """ 53 54 def __init__(self, port, baudrate=9600, bytesize=8, parity='N', 55 stopbits=1, timeout=None, xonxoff=False, rtscts=False, 56 write_timeout=None, dsrdtr=False, inter_byte_timeout=None, 57 exclusive=None, max_bytes=None, eol=None, 58 encoding='utf-8', encoding_errors='ignore', 59 start_cmd=None, pre_read_cmd=None, stop_cmd=None, **kwargs): 60 """Extends the standard serial reader by allowing the user to define 61 strings to send to the serial host on startup, before each read and 62 just prior to the reader being destroyed. 63 64 Notable arguments: 65 ``` 66 start_cmd 67 If not None, may be string or list of strings. If a single string, 68 it is sent to the serial port as soon as it is opened. If a list of 69 strings, each string in the list is sent in sequence. 70 71 stop_cmd 72 Much as start_cmd, but is sent when the PolledSerialReader is closed 73 or destroyed. 74 75 pre_read_cmd 76 Much as start_cmd and stop_cmd, except the string or list of strings 77 are sent each time the PolledSerialReader's read() method is called, 78 prior to trying to read from the port. 79 80 In addition to a string or list of strings, pre_read_cmd may be a *dict* 81 of lists of strings: 82 83 pre_read_cmd: 84 key1: ['command 1_1', 'command 1_2] 85 key2: ['command 2_1', 'command 2_2] 86 key3: ... 87 ... 88 The first time read() is called, the strings associated with key1 will be 89 sent. The second time, those associated with key2, and so on. When the end 90 of the dict is reached, it will start again with key1. 91 92 timeout <seconds> 93 If timeout is specified, then the serial read will time out after this many 94 seconds. It will then reissue the pre_read_cmds, if there is one, and try 95 reading again. If there is a dict of pre_read_cmds, it will progress to the 96 next one in the dict. 97 ``` 98 For all of these arguments, a special string, ``__PAUSE__``, is recognized. If 99 followed by a number (e.g. ``__PAUSE__ 5``), it will be interpreted as a command 100 to pause for that many seconds prior to sending the next command. If no number 101 is given, it will pause for one second. 102 """ 103 # Can we even run this? 104 if not SERIAL_MODULE_FOUND: 105 raise RuntimeError('Serial port functionality not available. Please ' 106 'install Python module pyserial.') 107 108 # Type check our pre_read commands 109 if start_cmd and not is_string_or_list_of_strings(start_cmd): 110 raise ValueError('PolledSerialReader start_cmd must either be None, ' 111 f'a string, or a list of strings. Found: {start_cmd}') 112 if stop_cmd and not is_string_or_list_of_strings(stop_cmd): 113 raise ValueError('PolledSerialReader stop_cmd must either be None, ' 114 f'a string, or a list of strings. Found: {stop_cmd}') 115 if pre_read_cmd and not (is_string_or_list_of_strings(pre_read_cmd) or 116 is_dict_of_lists_of_strings(pre_read_cmd)): 117 raise ValueError('PolledSerialReader pre_read_cmd must either be None, ' 118 'a string, or a list of strings, or a dict of lists of ' 119 f'strings. Found: {pre_read_cmd}') 120 121 # Okay, let's go and build 122 super().__init__(port=port, baudrate=baudrate, bytesize=bytesize, 123 parity=parity, stopbits=stopbits, timeout=timeout, 124 xonxoff=xonxoff, rtscts=rtscts, write_timeout=write_timeout, 125 dsrdtr=dsrdtr, inter_byte_timeout=inter_byte_timeout, 126 exclusive=exclusive, max_bytes=max_bytes, eol=eol, 127 encoding=encoding, encoding_errors=encoding_errors, **kwargs) 128 129 self.start_cmd = start_cmd 130 self.pre_read_cmd = pre_read_cmd 131 self.stop_cmd = stop_cmd 132 133 if isinstance(pre_read_cmd, dict): 134 self.command_cycle = cycle(pre_read_cmd.items()) 135 136 if self.start_cmd: 137 try: 138 if isinstance(self.start_cmd, list): # list of commands 139 for cmd in self.start_cmd: 140 self._send_command(cmd) 141 else: 142 self._send_command(start_cmd) 143 except serial.serialutil.SerialException as e: 144 logging.error(str(e)) 145 146 ############################ 147 def _send_command(self, cmd): 148 """Check if command is a 'pause'; if so, sleep, otherwise send it to serial port.""" 149 logging.debug(f'Sending {cmd}') 150 if cmd.find('__PAUSE__') == 0: 151 pause_cmd = cmd.split() 152 if len(pause_cmd) == 1: 153 pause_length = 1 154 elif len(pause_cmd) == 2: 155 try: 156 pause_length = float(pause_cmd[1]) 157 except ValueError: 158 raise ValueError('__PAUSE__ interval in PolledSerialReader must be ' 159 'a float. Found: %s' % cmd) 160 else: 161 raise ValueError('Pause format "__PAUSE__ <seconds>"; found %s' % cmd) 162 logging.info('Pausing %g seconds', pause_length) 163 time.sleep(pause_length) 164 else: 165 # If it's a normal command we're sending 166 logging.info('Sending serial command "%s"', cmd) 167 self.serial.write(self._encode_str(cmd, unescape=True)) 168 self.serial.flush() 169 170 logging.debug(f'Done sending {cmd}') 171 172 ############################ 173 def read(self): 174 try: 175 # Try again if our read times out, or our serial port returns None 176 while True: 177 # Do we need to send anything prior to reading? 178 if not self.pre_read_cmd: # no pre_read_cmd 179 pass 180 elif isinstance(self.pre_read_cmd, list): # list of commands 181 for cmd in self.pre_read_cmd: 182 self._send_command(cmd) 183 elif isinstance(self.pre_read_cmd, dict): # dict of lists of commands 184 key, commands = next(self.command_cycle) 185 for cmd in commands: 186 self._send_command(cmd) 187 elif self.pre_read_cmd: # simple string command 188 self._send_command(self.pre_read_cmd) 189 190 logging.debug('read() is being called') 191 record = super().read() 192 logging.debug(f'Returned from read() with: {record}') 193 if record is not None: 194 return record 195 logging.debug('Serial read returned None; trying again.') 196 197 except serial.serialutil.SerialException as e: 198 logging.error(str(e)) 199 return None 200 201 ############################ 202 def __del__(self): 203 if self.stop_cmd: 204 try: 205 if isinstance(self.stop_cmd, list): # list of commands 206 for cmd in self.stop_cmd: 207 self._send_command(cmd) 208 else: 209 self._send_command(self.stop_cmd) 210 except serial.serialutil.SerialException as e: 211 logging.error(str(e))
Read text records from a serial port.
PolledSerialReader( port, baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, write_timeout=None, dsrdtr=False, inter_byte_timeout=None, exclusive=None, max_bytes=None, eol=None, encoding='utf-8', encoding_errors='ignore', start_cmd=None, pre_read_cmd=None, stop_cmd=None, **kwargs)
54 def __init__(self, port, baudrate=9600, bytesize=8, parity='N', 55 stopbits=1, timeout=None, xonxoff=False, rtscts=False, 56 write_timeout=None, dsrdtr=False, inter_byte_timeout=None, 57 exclusive=None, max_bytes=None, eol=None, 58 encoding='utf-8', encoding_errors='ignore', 59 start_cmd=None, pre_read_cmd=None, stop_cmd=None, **kwargs): 60 """Extends the standard serial reader by allowing the user to define 61 strings to send to the serial host on startup, before each read and 62 just prior to the reader being destroyed. 63 64 Notable arguments: 65 ``` 66 start_cmd 67 If not None, may be string or list of strings. If a single string, 68 it is sent to the serial port as soon as it is opened. If a list of 69 strings, each string in the list is sent in sequence. 70 71 stop_cmd 72 Much as start_cmd, but is sent when the PolledSerialReader is closed 73 or destroyed. 74 75 pre_read_cmd 76 Much as start_cmd and stop_cmd, except the string or list of strings 77 are sent each time the PolledSerialReader's read() method is called, 78 prior to trying to read from the port. 79 80 In addition to a string or list of strings, pre_read_cmd may be a *dict* 81 of lists of strings: 82 83 pre_read_cmd: 84 key1: ['command 1_1', 'command 1_2] 85 key2: ['command 2_1', 'command 2_2] 86 key3: ... 87 ... 88 The first time read() is called, the strings associated with key1 will be 89 sent. The second time, those associated with key2, and so on. When the end 90 of the dict is reached, it will start again with key1. 91 92 timeout <seconds> 93 If timeout is specified, then the serial read will time out after this many 94 seconds. It will then reissue the pre_read_cmds, if there is one, and try 95 reading again. If there is a dict of pre_read_cmds, it will progress to the 96 next one in the dict. 97 ``` 98 For all of these arguments, a special string, ``__PAUSE__``, is recognized. If 99 followed by a number (e.g. ``__PAUSE__ 5``), it will be interpreted as a command 100 to pause for that many seconds prior to sending the next command. If no number 101 is given, it will pause for one second. 102 """ 103 # Can we even run this? 104 if not SERIAL_MODULE_FOUND: 105 raise RuntimeError('Serial port functionality not available. Please ' 106 'install Python module pyserial.') 107 108 # Type check our pre_read commands 109 if start_cmd and not is_string_or_list_of_strings(start_cmd): 110 raise ValueError('PolledSerialReader start_cmd must either be None, ' 111 f'a string, or a list of strings. Found: {start_cmd}') 112 if stop_cmd and not is_string_or_list_of_strings(stop_cmd): 113 raise ValueError('PolledSerialReader stop_cmd must either be None, ' 114 f'a string, or a list of strings. Found: {stop_cmd}') 115 if pre_read_cmd and not (is_string_or_list_of_strings(pre_read_cmd) or 116 is_dict_of_lists_of_strings(pre_read_cmd)): 117 raise ValueError('PolledSerialReader pre_read_cmd must either be None, ' 118 'a string, or a list of strings, or a dict of lists of ' 119 f'strings. Found: {pre_read_cmd}') 120 121 # Okay, let's go and build 122 super().__init__(port=port, baudrate=baudrate, bytesize=bytesize, 123 parity=parity, stopbits=stopbits, timeout=timeout, 124 xonxoff=xonxoff, rtscts=rtscts, write_timeout=write_timeout, 125 dsrdtr=dsrdtr, inter_byte_timeout=inter_byte_timeout, 126 exclusive=exclusive, max_bytes=max_bytes, eol=eol, 127 encoding=encoding, encoding_errors=encoding_errors, **kwargs) 128 129 self.start_cmd = start_cmd 130 self.pre_read_cmd = pre_read_cmd 131 self.stop_cmd = stop_cmd 132 133 if isinstance(pre_read_cmd, dict): 134 self.command_cycle = cycle(pre_read_cmd.items()) 135 136 if self.start_cmd: 137 try: 138 if isinstance(self.start_cmd, list): # list of commands 139 for cmd in self.start_cmd: 140 self._send_command(cmd) 141 else: 142 self._send_command(start_cmd) 143 except serial.serialutil.SerialException as e: 144 logging.error(str(e))
Extends the standard serial reader by allowing the user to define strings to send to the serial host on startup, before each read and just prior to the reader being destroyed.
Notable arguments:
start_cmd
If not None, may be string or list of strings. If a single string,
it is sent to the serial port as soon as it is opened. If a list of
strings, each string in the list is sent in sequence.
stop_cmd
Much as start_cmd, but is sent when the PolledSerialReader is closed
or destroyed.
pre_read_cmd
Much as start_cmd and stop_cmd, except the string or list of strings
are sent each time the PolledSerialReader's read() method is called,
prior to trying to read from the port.
In addition to a string or list of strings, pre_read_cmd may be a *dict*
of lists of strings:
pre_read_cmd:
key1: ['command 1_1', 'command 1_2]
key2: ['command 2_1', 'command 2_2]
key3: ...
...
The first time read() is called, the strings associated with key1 will be
sent. The second time, those associated with key2, and so on. When the end
of the dict is reached, it will start again with key1.
timeout <seconds>
If timeout is specified, then the serial read will time out after this many
seconds. It will then reissue the pre_read_cmds, if there is one, and try
reading again. If there is a dict of pre_read_cmds, it will progress to the
next one in the dict.
For all of these arguments, a special string, __PAUSE__, is recognized. If
followed by a number (e.g. __PAUSE__ 5), it will be interpreted as a command
to pause for that many seconds prior to sending the next command. If no number
is given, it will pause for one second.
def
read(self):
173 def read(self): 174 try: 175 # Try again if our read times out, or our serial port returns None 176 while True: 177 # Do we need to send anything prior to reading? 178 if not self.pre_read_cmd: # no pre_read_cmd 179 pass 180 elif isinstance(self.pre_read_cmd, list): # list of commands 181 for cmd in self.pre_read_cmd: 182 self._send_command(cmd) 183 elif isinstance(self.pre_read_cmd, dict): # dict of lists of commands 184 key, commands = next(self.command_cycle) 185 for cmd in commands: 186 self._send_command(cmd) 187 elif self.pre_read_cmd: # simple string command 188 self._send_command(self.pre_read_cmd) 189 190 logging.debug('read() is being called') 191 record = super().read() 192 logging.debug(f'Returned from read() with: {record}') 193 if record is not None: 194 return record 195 logging.debug('Serial read returned None; trying again.') 196 197 except serial.serialutil.SerialException as e: 198 logging.error(str(e)) 199 return None
read() should return None when there are no more records.