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):
20def is_string_or_list_of_strings(cmd):
21    if isinstance(cmd, str):
22        return True
23    elif isinstance(cmd, list):
24        return all(isinstance(item, str) for item in cmd)
25    return False
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.

start_cmd
pre_read_cmd
stop_cmd
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.