Example #1
0
class SerialDriver(Driver):
    """
    An implementation of a serial ANT+ device driver
    """
    def __init__(self,
                 device: str,
                 baudRate: int = 115200,
                 logger: Logger = None):
        super().__init__(logger=logger)
        self._device = device
        self._baudRate = baudRate
        self._serial = None

    def __str__(self):
        if self.isOpen():
            return self._device + " @ " + str(self._baudRate)
        return None

    def _isOpen(self) -> bool:
        return self._serial is not None

    def _open(self) -> None:
        try:
            self._serial = Serial(port=self._device, baudrate=self._baudRate)
        except SerialException as e:
            raise DriverException(str(e))

        if not self._serial.isOpen():
            raise DriverException("Could not open specified device")

    def _close(self) -> None:
        self._serial.close()
        self._serial = None

    def _read(self, count: int, timeout=None) -> bytes:
        return self._serial.read(count)

    def _write(self, data: bytes) -> None:
        try:
            self._serial.write(data)
            self._serial.flush()
        except SerialTimeoutException as e:
            raise DriverException(str(e))

    def _abort(self) -> None:
        if self._serial is not None:
            self._serial.cancel_read()
            self._serial.cancel_write()
            self._serial.reset_input_buffer()
            self._serial.reset_output_buffer()
Example #2
0
class HdlcppSerial(Hdlcpp):
    def __init__(self,
                 port,
                 baudrate=115200,
                 bufferSize=256,
                 writeTimeout=100,
                 writeRetries=1):
        self.serial = Serial(port, baudrate)
        super().__init__(self._transportRead, self._transportWrite, bufferSize,
                         writeTimeout, writeRetries)

    def stop(self):
        self.serial.cancel_read()

    def _transportRead(self, length):
        return self.serial.read(length)

    def _transportWrite(self, data):
        return self.serial.write(data)
Example #3
0
class SniffleHW:
    def __init__(self, serport):
        self.decoder_state = SniffleDecoderState()
        self.ser = Serial(serport, 2000000)
        self.ser.write(b'@@@@@@@@\r\n')  # command sync
        self.recv_cancelled = False

    def _send_cmd(self, cmd_byte_list):
        b0 = (len(cmd_byte_list) + 3) // 3
        cmd = bytes([b0, *cmd_byte_list])
        msg = b64encode(cmd) + b'\r\n'
        self.ser.write(msg)

    def cmd_chan_aa_phy(self, chan=37, aa=0x8E89BED6, phy=0, crci=0x555555):
        if not (0 <= chan <= 39):
            raise ValueError("Channel must be between 0 and 39")
        if not (0 <= phy <= 2):
            raise ValueError("PHY must be 0 (1M), 1 (2M), or 2 (coded)")
        self._send_cmd([0x10, *list(pack("<BLBL", chan, aa, phy, crci))])

    def cmd_pause_done(self, pause_when_done=False):
        if pause_when_done:
            self._send_cmd([0x11, 0x01])
        else:
            self._send_cmd([0x11, 0x00])

    def cmd_rssi(self, rssi=-80):
        self._send_cmd([0x12, rssi & 0xFF])

    def cmd_mac(self, mac_byte_list=None, hop3=True):
        if mac_byte_list is None:
            self._send_cmd([0x13])
        else:
            if len(mac_byte_list) != 6:
                raise ValueError("MAC must be 6 bytes!")
            self._send_cmd([0x13, *mac_byte_list])
            if hop3:
                # hop with advertisements between 37/38/39
                # unnecessary/detrimental with extended advertising
                self._send_cmd([0x14])

    def cmd_follow(self, enable=True):
        if enable:
            self._send_cmd([0x15, 0x01])
        else:
            self._send_cmd([0x15, 0x00])

    def cmd_auxadv(self, enable=True):
        if enable:
            self._send_cmd([0x16, 0x01])
        else:
            self._send_cmd([0x16, 0x00])

    def cmd_reset(self):
        self._send_cmd([0x17])

    def cmd_marker(self):
        self._send_cmd([0x18])

    # for master or slave modes
    def cmd_transmit(self, llid, pdu):
        if not (0 <= llid <= 3):
            raise ValueError("Out of bounds LLID")
        if len(pdu) > 255:
            raise ValueError("Too long PDU")
        self._send_cmd([0x19, llid, len(pdu), *pdu])

    def cmd_connect(self, peerAddr, llData, is_random=True):
        if len(peerAddr) != 6:
            raise ValueError("Invalid peer address")
        if len(llData) != 22:
            raise ValueError("Invalid LLData")
        self._send_cmd([0x1A, 1 if is_random else 0, *peerAddr, *llData])

    def cmd_setaddr(self, addr, is_random=True):
        if len(addr) != 6:
            raise ValueError("Invalid MAC address")
        self._send_cmd([0x1B, 1 if is_random else 0, *addr])

    def cmd_advertise(self, advData, scanRspData):
        if len(advData) > 31:
            raise ValueError("advData too long!")
        if len(scanRspData) > 31:
            raise ValueError("scanRspData too long!")
        paddedAdvData = [len(advData), *advData] + [0] * (31 - len(advData))
        paddedScnData = [len(scanRspData), *scanRspData
                         ] + [0] * (31 - len(scanRspData))
        self._send_cmd([0x1C, *paddedAdvData, *paddedScnData])

    def cmd_adv_interval(self, intervalMs):
        if not (20 < intervalMs < 0xFFFF):
            raise ValueError("Advertising interval out of bounds")
        self._send_cmd([0x1D, intervalMs & 0xFF, intervalMs >> 8])

    def cmd_irk(self, irk=None, hop3=True):
        if irk is None:
            self._send_cmd([0x1E])
        elif len(irk) != 16:
            raise ValueError("Invalid IRK length!")
        else:
            self._send_cmd([0x1E, *irk])
            if hop3:
                self._send_cmd([0x14])

    def recv_msg(self):
        got_msg = False
        while not got_msg:
            pkt = self.ser.readline()
            try:
                data = b64decode(pkt.rstrip())
            except BAError as e:
                print(str(pkt, encoding='ascii').rstrip())
                print("Ignoring message:", e, file=stderr)
                continue
            got_msg = True

        if self.recv_cancelled:
            self.recv_cancelled = False
            return -1, None, b''

        # msg type, msg body
        return data[0], data[1:], pkt

    def recv_and_decode(self):
        mtype, mbody, pkt = self.recv_msg()
        try:
            if mtype == 0x10:
                return PacketMessage(mbody, self.decoder_state)
            elif mtype == 0x11:
                return DebugMessage(mbody)
            elif mtype == 0x12:
                return MarkerMessage(mbody, self.decoder_state)
            elif mtype == 0x13:
                return StateMessage(mbody, self.decoder_state)
            elif mtype == -1:
                return None  # receive cancelled
            else:
                raise SniffleHWPacketError("Unknown message type 0x%02X!" %
                                           mtype)
        except BaseException as e:
            print(str(pkt, encoding='ascii').rstrip())
            print("Ignoring message:", e, file=stderr)
            print_exc()
            return None

    def cancel_recv(self):
        self.recv_cancelled = True
        self.ser.cancel_read()

    def mark_and_flush(self):
        # use marker to zero time, flush every packet before marker
        # also tolerate errors from incomplete lines in UART buffer
        self.cmd_marker()
        while True:
            try:
                msg = self.recv_and_decode()
            except SniffleHWPacketError:
                print("WARNING: invalid message during flush, ignoring...")
                continue
            if isinstance(msg, MarkerMessage):
                break

    def random_addr(self):
        # generate a random static address, set it
        addr = [randint(0, 255) for i in range(6)]
        addr[5] |= 0xC0  # make it static
        self.cmd_setaddr(bytes(addr))

    # automatically generate sane LLData
    def initiate_conn(self, peerAddr, is_random=True):
        llData = []

        # access address
        llData.extend([randint(0, 255) for i in range(4)])

        # initial CRC
        llData.extend([randint(0, 255) for i in range(3)])

        # WinSize, WinOffset, Interval, Latency, Timeout
        llData.append(3)
        llData.extend(pack("<H", randint(5, 15)))
        llData.extend(pack("<H", 24))
        llData.extend(pack("<H", 1))
        llData.extend(pack("<H", 50))

        # Channel Map
        llData.extend([0xFF, 0xFF, 0xFF, 0xFF, 0x1F])

        # Hop, SCA = 0
        llData.append(randint(5, 16))

        self.cmd_connect(peerAddr, bytes(llData), is_random)

        # return the access address
        return unpack("<L", bytes(llData[:4]))[0]
Example #4
0
class SniffleHW:
    def __init__(self, serport):
        self.decoder_state = SniffleDecoderState()
        self.ser = Serial(serport, 921600)
        self.ser.write(b'@@@@@@@@\r\n')  # command sync
        self.recv_cancelled = False

    def _send_cmd(self, cmd_byte_list):
        b0 = (len(cmd_byte_list) + 3) // 3
        cmd = bytes([b0, *cmd_byte_list])
        msg = b64encode(cmd) + b'\r\n'
        self.ser.write(msg)

    def cmd_chan_aa_phy(self, chan=37, aa=0x8E89BED6, phy=0, crci=0x555555):
        if not (0 <= chan <= 39):
            raise ValueError("Channel must be between 0 and 39")
        if not (0 <= phy <= 2):
            raise ValueError("PHY must be 0 (1M), 1 (2M), or 2 (coded)")
        self._send_cmd([0x10, *list(pack("<BLBL", chan, aa, phy, crci))])

    def cmd_pause_done(self, pause_when_done=False):
        if pause_when_done:
            self._send_cmd([0x11, 0x01])
        else:
            self._send_cmd([0x11, 0x00])

    def cmd_rssi(self, rssi=-80):
        self._send_cmd([0x12, rssi & 0xFF])

    def cmd_mac(self, mac_byte_list=None, hop3=True):
        if mac_byte_list is None:
            self._send_cmd([0x13])
        else:
            if len(mac_byte_list) != 6:
                raise ValueError("MAC must be 6 bytes!")
            self._send_cmd([0x13, *mac_byte_list])
            if hop3:
                # hop with advertisements between 37/38/39
                # unnecessary/detrimental with extended advertising
                self._send_cmd([0x14])

    def cmd_endtrim(self, end_trim=0x10):
        self._send_cmd([0x15, *list(pack("<L", end_trim))])

    def cmd_auxadv(self, enable=True):
        if enable:
            self._send_cmd([0x16, 0x01])
        else:
            self._send_cmd([0x16, 0x00])

    def cmd_reset(self):
        self._send_cmd([0x17])

    def cmd_marker(self):
        self._send_cmd([0x18])

    def recv_msg(self):
        got_msg = False
        while not got_msg:
            pkt = self.ser.readline()
            try:
                data = b64decode(pkt.rstrip())
            except BAError as e:
                print("Ignoring message:", e, file=stderr)
                continue
            got_msg = True

        if self.recv_cancelled:
            self.recv_cancelled = False
            return -1, None

        # msg type, msg body
        return data[0], data[1:]

    def recv_and_decode(self):
        mtype, mbody = self.recv_msg()
        if mtype == 0x10:
            return PacketMessage(mbody, self.decoder_state)
        elif mtype == 0x11:
            return DebugMessage(mbody)
        elif mtype == 0x12:
            return MarkerMessage(mbody, self.decoder_state)
        elif mtype == -1:
            return None  # receive cancelled
        else:
            raise SniffleHWPacketError("Unknown message type 0x%02X!" % mtype)

    def cancel_recv(self):
        self.recv_cancelled = True
        self.ser.cancel_read()

    def mark_and_flush(self):
        # use marker to zero time, flush every packet before marker
        # also tolerate errors from incomplete lines in UART buffer
        self.cmd_marker()
        while True:
            try:
                msg = self.recv_and_decode()
            except SniffleHWPacketError:
                print("WARNING: invalid message during flush, ignoring...")
                continue
            if isinstance(msg, MarkerMessage):
                break
Example #5
0
class MockDev:
    """Connects a mock device to read and write a virtual serial port.

    This uses ``socat`` to open a virtual com port that is used by the mock
    serial device and a virtual com port to connect to for serial testing.
    Remaining kwargs are used for instantiating the serial port.

    Attributes:
        logger (obj): Class level logger based on class name
        bytes_read (int): Counts the number of bytes that are read from the
            mock serial port.
        bytes_written (int): Counts the number of bytes that the mock serial
            port writes.
        dev (obj): The serial device instance.
    """
    def __init__(self, **kwargs):
        # pylint: disable=C0301
        """Initialize the mock serial device.

        Keyword Args:
            **dev_driver (obj, optional): Already instantiated serial driver,
                if not present serial port will be created.
            **port (string): Serial port of mock dev, defaults to "/tmp/mm_pal_mock_dev0"
            **baudrate (int): Baudrate for mock dev, defaults to 115200

        Note:
            Refer to the
            `Serial <https://pyserial.readthedocs.io/en/latest/pyserial_api.html#serial.Serial.__init__>`_
            class for additional ``**kwargs`` functionality.
        """  # noqa: E501
        self.logger = logging.getLogger(self.__class__.__name__)
        kwargs['port'] = kwargs.pop('port', "/tmp/mm_pal_mock_dev0")
        kwargs['baudrate'] = kwargs.pop('baudrate', 115200)
        self.mock_port = kwargs['port']
        self.logger.debug("Mock device = Serial(%r)", kwargs)

        self.bytes_read = 0
        self.bytes_written = 0
        self.wr_bytes = None
        self.wr_index = None
        self.rr_data = None

        self.force_fails = 0
        self.force_error_code = errno.EADDRNOTAVAIL
        self.force_timeout = 0
        self.force_parse_error = 0
        self.force_data_fail = 0
        self.force_write_fail = 0

        self._exit_thread = False
        self._run_thread = None
        if 'dev_driver' in kwargs:
            self.dev = kwargs.pop('dev_driver')
        else:
            self.dev = Serial(**kwargs)

    def __del__(self):
        """Destructor that ends the thread loop."""
        self.end_thread_loop()
        self.dev.close()

    def close(self):
        """Close serial port."""
        self.dev.close()

    def run_loopback_line(self):
        """Run loopback per line on mock serial dev.

        Read serial line and write line back to the serial port. Update the
        :py:attr:`~bytes_written` and :py:attr:`~bytes_read`. Loop until the
        :py:attr:`~_exit_thread` is set to `True`.
        """
        while True:
            self.logger.debug("run_loopback_line: readline()")
            read = self.dev.readline()
            if self.force_timeout > 0:
                self.force_timeout -= 1
                continue

            self.bytes_read += len(read)
            self.logger.debug("run_loopback_line: readline=%r", read)
            self.dev.write(read)
            self.bytes_written += len(read)
            self.logger.debug("bytes_read=%r", self.bytes_read)
            self.logger.debug("bytes_written=%r", self.bytes_written)
            if self._exit_thread:
                self._exit_thread = False
                break
        self.logger.debug("run_loopback_line exited")

    def run_loopback_bytes(self):
        """Run loopback for every byte on mock serial dev.

        Read byte and write it back. Update the :py:attr:`~bytes_written` and
        :py:attr:`~bytes_read`. Loop until the :py:attr:`~_exit_thread` is set
        to `True`.
        """
        while True:
            self.logger.debug("run_loopback_bytes: read()")
            read = self.dev.read()
            self.bytes_read += len(read)
            self.logger.debug("run_loopback_bytes: read=%r", read)
            self.dev.write(read)
            self.bytes_written += len(read)
            self.logger.debug("bytes_read=%r", self.bytes_read)
            self.logger.debug("bytes_written=%r", self.bytes_written)
            if self._exit_thread:
                self._exit_thread = False
                break
        self.logger.debug("run_loopback_bytes exited")

    def _parse_wr_cmd(self, args):
        if self.force_write_fail > 0:
            self.force_write_fail -= 1
            return {"result": errno.EINVAL}
        if len(args) < 3:
            response = {"result": errno.EINVAL}
            self.logger.debug("Invalid args")
        else:
            response = {"result": 0}
            index = int(args[1])
            self.wr_index = index
            self.wr_bytes = []
            for arg in args[2:]:
                num = int(arg)
                self.wr_bytes.append(num)
                if num > 255:
                    response = {"result": errno.EOVERFLOW}
                    break
                self.logger.debug("data[%r]=%r", index, num)
                index += 1

        return response

    def _parse_rr_cmd(self, args):
        if self.force_data_fail > 0:
            self.force_data_fail -= 1
            return {"result": 0, "data": "foo"}
        index = int(args[1])
        size = int(args[2])
        if size == 0:
            response = {"result": errno.EINVAL}
            self.logger.debug("Invalid size")
        else:
            if self.rr_data is None:
                data = list(range(index, index + size))
                data = [i & 0xFF for i in data]
            else:
                try:
                    data = list(
                        self.rr_data.to_bytes(size,
                                              byteorder='little',
                                              signed=True))
                except OverflowError:
                    data = list(
                        self.rr_data.to_bytes(size,
                                              byteorder='little',
                                              signed=False))
                self.rr_data = None
            response = {"data": data, "result": 0}
            self.logger.debug("response=%r", response)
        return response

    def _parse_json_cmd(self, args):
        if self.force_fails > 0:
            self.force_fails -= 1
            return {"result": self.force_error_code}
        if self.force_timeout > 0:
            self.force_timeout -= 1
            return {}

        try:
            if args[0] == b'rr':
                response = self._parse_rr_cmd(args)
            elif args[0] == b'version':
                response = {"version": "0.0.1", "result": 0}
            elif (args[0] == b'ex' or args[0] == b'mcu_rst'
                  or args[0] == b'special_cmd'):
                response = {"result": 0}
            elif args[0] == b'wr':
                response = self._parse_wr_cmd(args)
            else:
                response = {"result": errno.EPROTONOSUPPORT}

        except (IndexError) as exc:
            response = {"result": errno.EPROTONOSUPPORT}
            self.logger.debug("error=%r", exc)
        except (ValueError, TypeError) as exc:
            response = {"result": errno.EBADMSG}
            self.logger.debug("error=%r", exc)
        return response

    def run_app_json(self):
        """Run a basic json parsing app.

        Support a number a defined commands to simulate a device with parsed
        json structure.

        ``rr <index> <size>`` reads a byte value, mock values start at 0 when
        ``index`` is 0 and increase by one and truncate at 255. Respond with
        data and result.

        ``wr <index> <data0..datan>`` writes bytes to register. each number
        must be within 0-255 except for the index. Respond only with a result.

        ``ex`` or ``mcu_rst`` are basic commands that just respond with result.

        ``version`` indicates interface version. Respond with result and
        version.
        """
        while True:
            self.logger.debug("run_app_json: readline()")
            read = self.dev.readline()
            args = read.split()
            self.logger.debug("cmd=%r", args)
            response = self._parse_json_cmd(args)
            if self.force_parse_error > 0:
                self.force_parse_error -= 1
                response = f"foobar\n{{\"response\": {-999}}}\n"
            else:
                response = json.dumps(response)
                response = f"{response}\n"
            self.dev.write(response.encode())
            self.bytes_read += len(read)
            self.bytes_written += len(response)
            self.logger.debug("bytes_read=%r", self.bytes_read)
            self.logger.debug("bytes_written=%r", self.bytes_written)
            if self._exit_thread:
                self._exit_thread = False
                break
        self.logger.debug("run_app_json exited")

    # pylint: disable=W1113
    def start_thread_loop(self, func=None, *args):
        """Start a daemon thread to run a function.

        Only one thread can be active at a time. If another function is started
        the current thread will stop. The thread *should* get cleaned up in the
        :py:meth:`__del__` destructor.

        Args:
            func (function, optional): Function to run in the background,
                defaults to :py:meth:`run_loopback_line`.
            *args: Variable length arguments list.
        """
        self.end_thread_loop()
        if func is None:
            func = self.run_loopback_line
        self._run_thread = Thread(target=func, args=args)
        self._run_thread.setDaemon(True)
        self.logger.debug("start_loopback_line_thread_loop")
        self._run_thread.start()

    def end_thread_loop(self):
        """End the thread started with :py:meth:`start_thread_loop`.

        Set the :py:attr:`~_exit_thread` to True and cancel any serial reads.
        Wait until the thread has ended before returning.
        """
        self._exit_thread = True
        self.dev.cancel_read()
        if self._run_thread is not None:
            self.logger.debug("Exiting thread loop")
            while self._run_thread.is_alive():
                sleep(0.1)
        self._exit_thread = False
Example #6
0
class SniffleHW:
    def __init__(self, serport):
        self.decoder_state = SniffleDecoderState()
        self.ser = Serial(serport, 2000000)
        self.ser.write(b'@@@@@@@@\r\n') # command sync
        self.recv_cancelled = False
        self.rate_limiter = RateLimiter()

    def _send_cmd(self, cmd_byte_list):
        b0 = (len(cmd_byte_list) + 3) // 3
        cmd = bytes([b0, *cmd_byte_list])
        msg = b64encode(cmd) + b'\r\n'
        self.rate_limiter.do_cmd()
        self.ser.write(msg)

    def cmd_chan_aa_phy(self, chan=37, aa=0x8E89BED6, phy=0, crci=0x555555):
        if not (0 <= chan <= 39):
            raise ValueError("Channel must be between 0 and 39")
        if not (0 <= phy <= 3):
            raise ValueError("PHY must be 0 (1M), 1 (2M), 2 (coded S=8), or 3 (coded S=2)")
        self._send_cmd([0x10, *list(pack("<BLBL", chan, aa, phy, crci))])

    def cmd_pause_done(self, pause_when_done=False):
        if pause_when_done:
            self._send_cmd([0x11, 0x01])
        else:
            self._send_cmd([0x11, 0x00])

    def cmd_rssi(self, rssi=-80):
        self._send_cmd([0x12, rssi & 0xFF])

    def cmd_mac(self, mac_byte_list=None, hop3=True):
        if mac_byte_list is None:
            self._send_cmd([0x13])
        else:
            if len(mac_byte_list) != 6:
                raise ValueError("MAC must be 6 bytes!")
            self._send_cmd([0x13, *mac_byte_list])
            if hop3:
                # hop with advertisements between 37/38/39
                # unnecessary/detrimental with extended advertising
                self._send_cmd([0x14])

    def cmd_follow(self, enable=True):
        if enable:
            self._send_cmd([0x15, 0x01])
        else:
            self._send_cmd([0x15, 0x00])

    def cmd_auxadv(self, enable=True):
        if enable:
            self._send_cmd([0x16, 0x01])
        else:
            self._send_cmd([0x16, 0x00])

    def cmd_reset(self):
        self._send_cmd([0x17])

    def cmd_marker(self):
        self._send_cmd([0x18])

    # for master or slave modes
    def cmd_transmit(self, llid, pdu):
        if not (0 <= llid <= 3):
            raise ValueError("Out of bounds LLID")
        if len(pdu) > 255:
            raise ValueError("Too long PDU")
        self._send_cmd([0x19, llid, len(pdu), *pdu])

    def cmd_connect(self, peerAddr, llData, is_random=True):
        if len(peerAddr) != 6:
            raise ValueError("Invalid peer address")
        if len(llData) != 22:
            raise ValueError("Invalid LLData")
        self._send_cmd([0x1A, 1 if is_random else 0, *peerAddr, *llData])

    def cmd_setaddr(self, addr, is_random=True):
        if len(addr) != 6:
            raise ValueError("Invalid MAC address")
        self._send_cmd([0x1B, 1 if is_random else 0, *addr])

    def cmd_advertise(self, advData, scanRspData):
        if len(advData) > 31:
            raise ValueError("advData too long!")
        if len(scanRspData) > 31:
            raise ValueError("scanRspData too long!")
        paddedAdvData = [len(advData), *advData] + [0]*(31 - len(advData))
        paddedScnData = [len(scanRspData), *scanRspData] + [0]*(31 - len(scanRspData))
        self._send_cmd([0x1C, *paddedAdvData, *paddedScnData])

    def cmd_adv_interval(self, intervalMs):
        if not (20 < intervalMs < 0xFFFF):
            raise ValueError("Advertising interval out of bounds")
        self._send_cmd([0x1D, intervalMs & 0xFF, intervalMs >> 8])

    def cmd_irk(self, irk=None, hop3=True):
        if irk is None:
            self._send_cmd([0x1E])
        elif len(irk) != 16:
            raise ValueError("Invalid IRK length!")
        else:
            self._send_cmd([0x1E, *irk])
            if hop3:
                self._send_cmd([0x14])

    def cmd_instahop(self, enable=True):
        if enable:
            self._send_cmd([0x1F, 0x01])
        else:
            self._send_cmd([0x1F, 0x00])

    def cmd_setmap(self, chmap=b'\xFF\xFF\xFF\xFF\x1F'):
        if len(chmap) != 5:
            raise ValueError("Invalid channel map length!")
        self._send_cmd([0x20] + list(chmap))

    # triplets should be a list of 3-tuples of integers
    # each 3-tuple is (WinOffset, Interval, delta_Instant)
    def cmd_interval_preload(self, triplets=[]):
        if len(triplets) > 4:
            raise ValueError("Too many preload triplets")
        cmd_bytes = [0x21]
        for t in triplets:
            if len(t) != 3:
                raise ValueError("Not a triplet")
            cmd_bytes.extend(list(pack("<HHH", *t)))
        self._send_cmd(cmd_bytes)

    def _recv_msg(self, desync=False):
        got_msg = False
        while not got_msg:
            if desync:
                # readline is inefficient, but a good way to synchronize
                pkt = self.ser.readline()
                try:
                    data = b64decode(pkt.rstrip())
                except BAError as e:
                    print(str(pkt, encoding='ascii').rstrip())
                    print("Ignoring message:", e, file=stderr)
                    continue
            else:
                # minimum packet is 4 bytes base64 + 2 bytes CRLF
                pkt = self.ser.read(6)

                # decode header to get length byte
                try:
                    data = b64decode(pkt[:4])
                except BAError as e:
                    print(str(pkt, encoding='ascii').rstrip())
                    print("Ignoring message:", e, file=stderr)
                    self.ser.readline() # eat CRLF
                    continue

                # now read the rest of the packet (if there is anything)
                word_cnt = data[0]
                if word_cnt:
                    pkt += self.ser.read((word_cnt - 1) * 4)

                # make sure CRLF is present
                if pkt[-2:] != b'\r\n':
                    print("Ignoring message due to missing CRLF", file=stderr)
                    self.ser.readline() # eat CRLF
                    continue

                try:
                    data = b64decode(pkt[:-2])
                except BAError as e:
                    print(str(pkt, encoding='ascii').rstrip())
                    print("Ignoring message:", e, file=stderr)
                    self.ser.readline() # eat CRLF
                    continue

            got_msg = True

        if self.recv_cancelled:
            self.recv_cancelled = False
            return -1, None, b''

        # msg type, msg body, raw
        return data[1], data[2:], pkt

    def recv_and_decode(self):
        mtype, mbody, pkt = self._recv_msg()
        try:
            if mtype == 0x10:
                return PacketMessage(mbody, self.decoder_state)
            elif mtype == 0x11:
                return DebugMessage(mbody)
            elif mtype == 0x12:
                return MarkerMessage(mbody, self.decoder_state)
            elif mtype == 0x13:
                return StateMessage(mbody, self.decoder_state)
            elif mtype == 0x14:
                return MeasurementMessage.from_raw(mbody)
            elif mtype == -1:
                return None # receive cancelled
            else:
                raise SniffleHWPacketError("Unknown message type 0x%02X!" % mtype)
        except BaseException as e:
            print(str(pkt, encoding='ascii').rstrip())
            print("Ignoring message:", e, file=stderr)
            print_exc()
            return None

    def cancel_recv(self):
        self.recv_cancelled = True
        self.ser.cancel_read()

    def mark_and_flush(self):
        # use marker to zero time, flush every packet before marker
        # also tolerate errors from incomplete lines in UART buffer
        self.cmd_marker()
        while True:
            mtype, _, _ = self._recv_msg(True)
            if mtype == 0x12: # MarkerMessage
                break

    def random_addr(self):
        # generate a random static address, set it
        addr = [randint(0, 255) for i in range(6)]
        addr[5] |= 0xC0 # make it static
        self.cmd_setaddr(bytes(addr))

    # automatically generate sane LLData
    def initiate_conn(self, peerAddr, is_random=True):
        llData = []

        # access address
        llData.extend([randint(0, 255) for i in range(4)])

        # initial CRC
        llData.extend([randint(0, 255) for i in range(3)])

        # WinSize, WinOffset, Interval, Latency, Timeout
        llData.append(3)
        llData.extend(pack("<H", randint(5, 15)))
        llData.extend(pack("<H", 24))
        llData.extend(pack("<H", 1))
        llData.extend(pack("<H", 50))

        # Channel Map
        llData.extend([0xFF, 0xFF, 0xFF, 0xFF, 0x1F])

        # Hop, SCA = 0
        llData.append(randint(5, 16))

        self.cmd_connect(peerAddr, bytes(llData), is_random)

        # return the access address
        return unpack("<L", bytes(llData[:4]))[0]