From b2c7d367a908ce1eebf2bf51a1e9719b2e85ae95 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 9 Sep 2020 21:27:23 -0700 Subject: [PATCH] Add UART and CAN support to desktop interface library --- README.md | 14 +- tools/rpc/README.md | 47 +++++ tools/rpc/rpc.py | 191 +++++++++++++++++- ...atures_as_the_controller_device_example.py | 32 +++ 4 files changed, 279 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 52ffebaaf..3af8abdd6 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,16 @@ With the RPC Library you can easily get image processing results, stream RAW or You can find examples that run on the OpenMV Cam under `File->Examples->Remote Control` in OpenMV IDE and online [here](scripts/examples/34-Remote-Control). Finally, OpenMV provides the following libraries for interfacing your OpenMV Cam to other systems below: -* [Generic Python Interface Library for USB and WiFi Comms](tools/rpc/README.md) - * Provides Python code for connecting your OpenMV Cam to a Windows, Mac, or Linux computer (or RaspberryPi/Beaglebone, etc.) with python programmatically over USB VCP or Ethernet/WiFi (i.e. with sockets). -* [Arduino Interface Library for I2C, SPI, UART Comms](https://github.com/openmv/openmv-arduino-rpc) -* RaspberryPi Interface Library for I2C, SPI, UART Comms - comming soon! +* [Generic Python Interface Library for USB VCP, Ethernet/WiFi, UART, Kvarser CAN, and I2C/SPI Comms](tools/rpc/README.md) + * Provides Python code for connecting your OpenMV Cam to a Windows, Mac, or Linux computer (or RaspberryPi/Beaglebone, etc.). + * Supports USB VCP on all systems. E.g. direct USB connection to the OpenMV Cam. + * Supports Ethernet/WiFi on all systems. + * Supports RS232/RS422/RS485/TTL UARTs on all systems. E.g. the old school DB9 port on the back of a PC, USB to serial RS232/RS422/RS485/TTL adapters, and TTL serial on I/O pins on SBCs like the RaspberryPi/Beaglebone. + * Supports Kvarser CAN on Windows and Linux (Kvarser does not support Mac). + * Supports I2C/SPI on Linux SBCs like the RaspberryPi/Beaglebone, etc. (coming soon) +* [Arduino Interface Library for CAN, I2C, SPI, UART Comms](https://github.com/openmv/openmv-arduino-rpc) + * Works on all Arduino variants. + * CAN support via the MCP2515 over SPI or via the CAN peripheral on the ESP32. #### Note diff --git a/tools/rpc/README.md b/tools/rpc/README.md index ddf5de683..b8aac97b6 100644 --- a/tools/rpc/README.md +++ b/tools/rpc/README.md @@ -16,6 +16,29 @@ However, the examples depend on [pygame](https://www.pygame.org/news) so you nee Because the interface library is implemented in pure python with no external dependencies it works on Windows, Mac, and Linux. +## UART Support + +pySerial provides support for pure USB virtual COM ports, USB to RS232/RS422/RS485/TTL COM ports, and standard RS232/RS422/RS485/TTL COM ports. Please use the `rpc_usb_vcp_master` and `rpc_usb_vcp_slave` for pure USB virtual COM port communication and `rpc_uart_master` and `rpc_uart_slave` for USB to RS232/RS422/RS485/TTL COM ports and standard RS232/RS422/RS485/TTL COM ports. + +### USB to Serial Converter Issues + +Pure hardware RS232/RS422/RS485/TTL COM ports should work using the `rpc_uart_master` and `rpc_uart_slave` interfaces without issues. However, FTDI like USB to serial converter chips may add unexpected latency to communication. In particular, FTDI chips have a latency timer which is used to buffer bytes for transmission over USB to improve bandwidth... but, that also increases the worse case latency of a single byte being sent over USB to **16 ms** by default. You need to reduce the latency timer to **1 ms** as detailed below or the interface library will fail to work: + +* https://stackoverflow.com/questions/54230836/inconsistent-delay-in-reading-serial-data-in-python-3-7-2-using-pyserial +* https://projectgus.com/2011/10/notes-on-ftdi-latency-with-arduino/ + +## CAN Support + +You may use the RPC Interface Library over CAN using [Kvaser](https://www.kvaser.com/) hardware on Windows and Linux (Kvaser does not support Mac). You need to install the following: + +* For Windows + * [Kvaser Drivers for Windows](https://www.kvaser.com/download/?utm_source=software&utm_ean=7330130980013&utm_status=latest) + * [Kvaser CANlib SDK](https://www.kvaser.com/download/?utm_source=software&utm_ean=7330130980150&utm_status=latest) + * [Python module](https://www.kvaser.com/download/?utm_source=software&utm_ean=7330130981911&utm_status=latest) +* For Linux + * [Kvaser Linux Drivers and SDK](https://www.kvaser.com/download/?utm_source=software&utm_ean=7330130980754&utm_status=latest) + * [Python module](https://www.kvaser.com/download/?utm_source=software&utm_ean=7330130981911&utm_status=latest) + # How to use the Library Please checkout the following scripts for how to control your OpenMV Cam from the comptuer: @@ -144,6 +167,22 @@ Starts execution of the `rpc` library on the slave to receive data. This method `recv_timeout` defines how long to wait to receive a command from the master device before trying again. `send_timeout` defines how long the slave will wait for the master to receive the call back response before going back to trying to receive. The loop call back will be executed before trying to receive again. +## rpc_uart_master(port, baudrate=9600) + +Creates a master implementation of the `rpc` library to communicate over a hardware RS232/RS422/RS485 or TTL COM port. `port` is the string name of the serial port. `baudrate` is the bits per second to run at. + +Please use the `rpc_usb_vcp_master` to talk to the OpenMV Cam over its USB interface. This class is for talking to the OpenMV Cam over its hardware UART. + +If you are using this class with USB to serial converters you will likely experience intermittent synchronization issues. The RPC library uart interface on the OpenMV Cam is designed to work with real-time UARTs like hardware RS232/RS422/RS485 COM ports on PCs and TTL UARTS on single board computers like the RaspberryPi and Beaglebone. That said, the interface library can handle this and will continue to work. + +## rpc_uart_slave(port, baudrate=9600) + +Creates a slave implementation of the `rpc` library to communicate over a hardware RS232/RS422/RS485 or TTL COM port. `port` is the string name of the serial port. `baudrate` is the bits per second to run at. + +Please use the `rpc_usb_vcp_slave` to talk to the OpenMV Cam over its USB interface. This class is for talking to the OpenMV Cam over its hardware UART. + +If you are using this class with USB to serial converters you will likely experience intermittent synchronization issues. The RPC library uart interface on the OpenMV Cam is designed to work with real-time UARTs like hardware RS232/RS422/RS485 COM ports on PCs and TTL UARTS on single board computers like the RaspberryPi and Beaglebone. That said, the interface library can handle this and will continue to work. + ## rpc_usb_vcp_master(port): Creates a master implementation of the `rpc` library to communicate over a USB VCP (virtual COM port). `port` is the string name of the serial port. @@ -163,3 +202,11 @@ Creates a master implementation of the `rpc` library to communicate over WiFi or ## rpc_wifi_or_ethernet_slave(my_ip="", port=0x1DBA): Creates a slave implementation of the `rpc` library to communicate over WiFi or Ethernet. `my_ip` can be `""` which binds the slave to any interface adapter to communicate to the master. If `my_ip` is not `""` then it should be an IP address on the same subnet as the master. `port` is a free port to use for UDP and TCP traffic. + +## rpc_kvarser_can_master(channel, message_id=0x7FF, bit_rate=250000, sampling_point=75) + +Creates a master implementation of the `rpc` library to communicate over CAN using Kvarser hardware. `channel` is the Kvarser channel to use. `message_id` is the 11-bit CAN ID to use for data transport. `bit_rate` is the CAN bus rate. `sampling_point` is the percentage sample point on the CAN bus. + +## rpc_kvarser_can_slave(channel, message_id=0x7FF, bit_rate=250000, sampling_point=75) + +Creates a slave implementation of the `rpc` library to communicate over CAN using Kvarser hardware. `channel` is the Kvarser channel to use. `message_id` is the 11-bit CAN ID to use for data transport. `bit_rate` is the CAN bus rate. `sampling_point` is the percentage sample point on the CAN bus. diff --git a/tools/rpc/rpc.py b/tools/rpc/rpc.py index 1c167d85d..a1212250a 100644 --- a/tools/rpc/rpc.py +++ b/tools/rpc/rpc.py @@ -21,6 +21,44 @@ class rpc: for j in range(8): crc = (crc << 1) ^ (0x1021 if crc & 0x8000 else 0) return crc & 0xFFFF + __crc_16_table = [0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, + 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, + 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, + 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, + 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, + 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, + 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, + 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, + 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, + 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, + 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, + 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, + 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, + 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, + 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, + 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, + 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, + 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, + 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, + 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, + 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0] + + def __tab_crc_16(self, data, size): # private + crc = 0xFFFF + for i in range(size): crc = self.__crc_16_table[((crc >> 8) ^ data[i]) & 0xff] ^ (crc << 8) + return crc & 0xFFFF + def _zero(self, buff, size): # private for i in range(size): buff[i] = 0 @@ -41,7 +79,7 @@ class rpc: return h & 0xFFFFFFFF def __init__(self): # private - self.__crc_16 = self.__def_crc_16 + self.__crc_16 = self.__tab_crc_16 self._stream_writer_queue_depth_max = 255 def _get_packet_pre_alloc(self, payload_len=0): @@ -259,6 +297,76 @@ class rpc_slave(rpc): self.__schedule_cb = None if self.__loop_cb is not None: self.__loop_cb() +class rpc_uart_master(rpc_master): + + # We need to do reads this way so that we get a short timeout while waiting for data and then + # no timeout while data is coming in. pyserial inter_byte_timeout does not work. + def __get_bytes(self, buff): + i = 0 + l = len(buff) + while l: + data = self.__ser.read(min(l, 1024)) # Starts a new timeout per call. + data_len = len(data) + if not data_len: return None + buff[i:i+data_len] = data + i += data_len + l -= data_len + return buff + + def __init__(self, port, baudrate=9600): # private + self.__ser = serial.Serial(port, baudrate=baudrate, timeout=0.01) + rpc_master.__init__(self) + + def _flush(self): # protected + self.__ser.reset_input_buffer() + + def get_bytes(self, buff, timeout_ms): # protected + if int(self.__ser.timeout * 100) != 1: self.__ser.timeout = 0.01 # Changing this causes control transfers. + result = self.__get_bytes(buff) + if result is None: time.sleep(self._get_short_timeout * 0.001) + return result + + def put_bytes(self, data, timeout_ms): # protected + self.__ser.write(data) + + def _stream_get_bytes(self, buff, timeout_ms): # protected + if int(self.__ser.timeout) != 1: self.__ser.timeout = 1 # Changing this causes control transfers. + return self.__get_bytes(buff) + +class rpc_uart_slave(rpc_slave): + + # We need to do reads this way so that we get a short timeout while waiting for data and then + # no timeout while data is coming in. pyserial inter_byte_timeout does not work. + def __get_bytes(self, buff): + i = 0 + l = len(buff) + while l: + data = self.__ser.read(min(l, 1024)) # Starts a new timeout per call. + data_len = len(data) + if not data_len: return None + buff[i:i+data_len] = data + i += data_len + l -= data_len + return buff + + def __init__(self, port, baudrate=9600): # private + self.__ser = serial.Serial(port, baudrate=baudrate, timeout=0.01) + rpc_slave.__init__(self) + + def _flush(self): # protected + self.__ser.reset_input_buffer() + + def get_bytes(self, buff, timeout_ms): # protected + if int(self.__ser.timeout * 100) != 1: self.__ser.timeout = 0.01 # Changing this causes control transfers. + return self.__get_bytes(buff) + + def put_bytes(self, data, timeout_ms): # protected + self.__ser.write(data) + + def _stream_get_bytes(self, buff, timeout_ms): # protected + if int(self.__ser.timeout) != 1: self.__ser.timeout = 1 # Changing this causes control transfers. + return self.__get_bytes(buff) + class rpc_usb_vcp_master(rpc_master): # We need to do reads this way so that we get a short timeout while waiting for data and then @@ -619,3 +727,84 @@ class rpc_wifi_or_ethernet_slave(rpc_slave): if l: self.__close_tcp_socket() except (socket.timeout, socket.error): self.__close_tcp_socket() if l: raise OSError # Stop Stream. + +def get_can_settings(sampling_point): + for bs1 in range(8): + for bs2 in range(8): + if (1 + bs1 + bs2) == 8 and (sampling_point * 10) == (((1 + bs1) * 1000) // (1 + bs1 + bs2)): + return (bs1, bs2) + raise ValueError("Invalid sampling_point!") + +class rpc_kvarser_can_master(rpc_master): + + def __init__(self, channel, message_id=0x7FF, bit_rate=250000, sampling_point=75): # private + from canlib import canlib + self.__message_id = message_id + bs1, bs2 = get_can_settings(sampling_point) + self.__can = canlib.openChannel(channel=channel) + self.__can.setBusParams(freq=bit_rate, tseg1=bs1, tseg2=bs2, sjw=1) + self.__can.canSetAcceptanceFilter(code=message_id, mask=0x3FF) + self.__can.busOn() + rpc_master.__init__(self) + + def _flush(self): # private + from canlib import canlib + self.__can.iocontrol.flush_rx_buffer() + + def get_bytes(self, buff, timeout_ms): # protected + from canlib import canlib + l = len(buff) + for i in range(0, l, 8): + expected = min(l - i, 8) + try: + frame = self.__can.read(timeout=self._get_short_timeout) + if frame.id == self.__message_id and frame.dlc == expected: buff[i:i+8] = frame.data + else: + time.sleep(self._get_short_timeout * 0.001) + return None + except canlib.CanError: + time.sleep(self._get_short_timeout * 0.001) + return None + return buff + + def put_bytes(self, data, timeout_ms): # protected + from canlib import canlib, Frame + view = memoryview(data) + for i in range(0, len(view), 8): + try: self.__can.writeWait(Frame(id_=self.__message_id, data=view[i:i+8]), timeout=self._put_short_timeout) + except canlib.CanError: break + +class rpc_kvarser_can_slave(rpc_slave): + + def __init__(self, channel, message_id=0x7FF, bit_rate=250000, sampling_point=75): # private + from canlib import canlib + self.__message_id = message_id + bs1, bs2 = get_can_settings(sampling_point) + self.__can = canlib.openChannel(channel=channel) + self.__can.setBusParams(freq=bit_rate, tseg1=bs1, tseg2=bs2, sjw=1) + self.__can.canSetAcceptanceFilter(code=message_id, mask=0x3FF) + self.__can.busOn() + rpc_slave.__init__(self) + + def _flush(self): # private + from canlib import canlib + self.__can.iocontrol.flush_rx_buffer() + + def get_bytes(self, buff, timeout_ms): # protected + from canlib import canlib + l = len(buff) + for i in range(0, l, 8): + expected = min(l - i, 8) + try: + frame = self.__can.read(timeout=self._get_short_timeout) + if frame.id == self.__message_id and frame.dlc == expected: buff[i:i+8] = frame.data + else: return None + except canlib.CanError: return None + return buff + + def put_bytes(self, data, timeout_ms): # protected + from canlib import canlib, Frame + view = memoryview(data) + for i in range(0, len(view), 8): + try: self.__can.writeWait(Frame(id_=self.__message_id, data=view[i:i+8]), timeout=self._put_short_timeout) + except canlib.CanError: break diff --git a/tools/rpc/rpc_popular_features_as_the_controller_device_example.py b/tools/rpc/rpc_popular_features_as_the_controller_device_example.py index a8a14bd51..ee0a53d91 100644 --- a/tools/rpc/rpc_popular_features_as_the_controller_device_example.py +++ b/tools/rpc/rpc_popular_features_as_the_controller_device_example.py @@ -36,6 +36,38 @@ sys.stdout.flush() # # interface = rpc.rpc_wifi_or_ethernet_master(slave_ip="xxx.xxx.xxx.xxx", my_ip="", port=0x1DBA) +# Uncomment the below lines to setup your OpenMV Cam for controlling over a UART (Serial / COM Port). +# +# * port - Serial Port Name. +# * baudrate - Bits per second. +# +# print("\nAvailable Ports:\n") +# for port, desc, hwid in serial.tools.list_ports.comports(): +# print("{} : {} [{}]".format(port, desc, hwid)) +# sys.stdout.write("\nPlease enter a port name: ") +# sys.stdout.flush() +# interface = rpc.rpc_uart_master(port=input(), baudrate=115200) +# print("") +# sys.stdout.flush() + +# Uncomment the below line to setup your OpenMV Cam for controlling over CAN. +# +# * channel - Kvarser CAN channel number. +# * message_id - 11-bit message ID to use for data transport. +# * bit_rate - CAN baud rate. +# * sampling_point - Sampling point percentage. +# +# from canlib import canlib +# print("\nAvailable Channels:\n") +# for channel in range(canlib.getNumberOfChannels()): +# chdata = canlib.ChannelData(channel) +# print("%d. %s (%s / %s)" % (channel, chdata.channel_name, chdata.card_upc_no, chdata.card_serial_no)) +# sys.stdout.write("\nPlease enter a channel name: ") +# sys.stdout.flush() +# interface = rpc.rpc_kvarser_can_master(channel=int(input()), message_id=0x7FF, bit_rate=250000, sampling_point=75) +# print("") +# sys.stdout.flush() + ############################################################## # Call Back Handlers ##############################################################