├── LICENSE ├── README.md ├── _usbtest.sh ├── dso138mini.py ├── dso138mini.py.sh ├── ipy.sh ├── requirements.txt ├── usblib.py ├── usblib.py.sh ├── usbtest_read4ever.py ├── usbtest_read4ever.py.sh ├── usbtest_rw1.py ├── usbtest_rw1.py.sh ├── usbtest_rw_buf.py ├── usbtest_rw_buf.py.sh ├── usbtest_shell.py └── usbtest_shell.py.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Querela 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Termux (Android) LibUSB-1.0 Python 3 adapter (?) 3 | ================================================ 4 | 5 | See USB infos in termux wiki; 6 | - [example code and termux-usb reference](https://wiki.termux.com/wiki/Termux-usb) 7 | 8 | List connected USB devices and get ID: 9 | 10 | ```bash 11 | termux-usb -l 12 | ``` 13 | 14 | Run script (test) with selected device: 15 | 16 | ```bash 17 | termux-usb -r -e ./usbtest_rw1.py.sh /dev/bus/usb/001/002 18 | ``` 19 | 20 | ## Setup 21 | 22 | Working with **Python 3.8**. 23 | 24 | Theoretically only needs `PyUSB`. 25 | Enganced with `IPython`, `PyFtdi` (two formatting functions). 26 | Style check and formatting with `Flake8` and `Black`. 27 | 28 | Create environment: 29 | 30 | ```bash 31 | python3 -m venv venv 32 | source venv/bin/activate 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | ## Why? 37 | 38 | - Android restricts device access, [see comments on libusb](https://sourceforge.net/p/libusb/mailman/message/36486446/) 39 | - Termux only provides a file descriptor (probably queried from Android) 40 | - device handles etc. have to be retrieved from a single file descriptor 41 | - my module `usblib.py` provides a function `device_from_fd(fd)` that extends the [`PyUSB 1.0`](https://github.com/pyusb/pyusb) library to provide a `Device` object from a file descriptor number that can be used as usual 42 | 43 | ## CP210x Serial module 44 | 45 | - own implementation, guided by: 46 | - [pySerial](https://github.com/pyserial/pyserial), Timeout object, inspiration for `read_until` etc., only CP2110 handling (with another backend library `hid`) 47 | - [UsbSerial](https://github.com/felHR85/UsbSerial), command codes & logic flow, adopted from java implementation; flow control untested (_how?_) 48 | - only tested with _cp2102 usb-ttl board v4.2_ device 49 | - throughput, performance unknown, (seems to work for me) 50 | - flow control (RTS/CTS, DTR/DSR, Xon/Xoff) untested 51 | - does transmission have to be in chunks, with size reported in endpoint info or does the libusb1 library handles this? - works fine with chunks, but may drop in performance for heavy use? 52 | - sync writing?, can a chunk be transmitted in part only (read/write)? 53 | - no interrupting of transmissions 54 | - test scripts supplied for various _simple_ situations; tests currently only with connected device 55 | - example usage script for _DSO138mini_ data dumps 56 | 57 | ## Copyright and License Information 58 | 59 | Hopefully my _fix_ can be adopted in the original PyUSB library. Else, free for all. :-) 60 | 61 | Copyright (c) 2019 Querela. All rights reserved. 62 | 63 | See the file "LICENSE" for information on the history of this software, terms & conditions for usage, and a DISCLAIMER OF ALL WARRANTIES. 64 | 65 | All trademarks referenced herein are property of their respective holders. 66 | -------------------------------------------------------------------------------- /_usbtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Base script, should be sym linked for each python script. 4 | # Wrapper for Python script to source virtual environment and run script with 5 | # supplied USB file descriptor argument. 6 | 7 | # compute py script name 8 | P=$0 9 | F=$(basename -- "$P") 10 | S=${F%.*} 11 | 12 | echo "Run $S ..." 13 | 14 | # actual script 15 | cd /data/data/com.termux/files/home/tools/usb 16 | source venv/bin/activate 17 | 18 | python3 "$S" "$@" 19 | 20 | deactivate 21 | -------------------------------------------------------------------------------- /dso138mini.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import logging 5 | import threading 6 | import time 7 | 8 | from usblib import device_from_fd 9 | from usblib import shell_usbdevice 10 | from usblib import CP210xSerial 11 | 12 | 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | # ---------------------------------------------------------------------------- 17 | 18 | 19 | def main(fd, debug=False): 20 | device = device_from_fd(fd) 21 | 22 | if debug: 23 | shell_usbdevice(fd, device) 24 | 25 | print("\n", "#" * 40, "\n") 26 | print(device) 27 | 28 | assert device.idVendor == 0x10C4 and device.idProduct == 0xEA60 29 | 30 | ser = CP210xSerial(device, baudRate=115200) 31 | try: 32 | ser.open(_async=True) 33 | 34 | data = grab_data(ser) 35 | 36 | fn = "dso138mini.grab.json" 37 | with open(fn, "w") as fp: 38 | json.dump(data, fp, indent=2) 39 | finally: 40 | ser.close() 41 | 42 | 43 | def grab_data(ser): 44 | delay = 5000.0 / 1000.0 45 | header = None 46 | transfers = list() 47 | 48 | LOGGER.info("Trying to grab header for 30 sec ...") 49 | try: 50 | data = ser.read(16 * 1024, 30.0) 51 | if data: 52 | text = data.decode("utf-8").strip() 53 | header = text.splitlines() 54 | except KeyboardInterrupt: 55 | pass 56 | 57 | LOGGER.info("Waiting for dumps ...") 58 | while True: 59 | try: 60 | print(" Waiting", end="", flush=True) 61 | while not ser._buf_in: 62 | with ser._buf_in.changed: 63 | notified = ser._buf_in.changed.wait(delay) 64 | print(".", end="", flush=True) 65 | if not notified: 66 | continue 67 | print() 68 | 69 | # consume all to finish? 70 | # on mismatch? 71 | 72 | meta, rows = dict(), list() 73 | for i in range(19 + 1024): 74 | line = ser.read_until(b"\n", -1, 1.0) 75 | line = line.decode("utf-8").rstrip() 76 | if i < 19: 77 | key, value = line.split(",") 78 | key, value = key.strip(), value.strip() 79 | meta[key] = value 80 | else: 81 | idx, x, y = line.split(",") 82 | x, y = x.strip(), y.strip() 83 | x, y = int(x), float(y) 84 | rows.append((x, y)) 85 | 86 | transfers.append({"meta": meta, "data": rows}) 87 | LOGGER.info("Got record.") 88 | except KeyboardInterrupt: 89 | break 90 | 91 | return {"header": header, "transfers": transfers} 92 | 93 | 94 | if __name__ == "__main__": 95 | # https://wiki.termux.com/wiki/Termux-usb 96 | logging.basicConfig( 97 | level=logging.INFO, format="[%(levelname).1s] %(name)s: %(message)s" 98 | ) 99 | logging.getLogger(__name__).setLevel(logging.DEBUG) 100 | logging.getLogger("usblib").setLevel(logging.DEBUG) 101 | logging.getLogger("usblib.RXTX").setLevel(logging.INFO) 102 | logging.getLogger("usb").setLevel(logging.DEBUG) 103 | 104 | # grab fd number from args 105 | # (from termux wrapper) 106 | import sys 107 | 108 | LOGGER.debug("args: %s", sys.argv) 109 | 110 | fd = int(sys.argv[1]) 111 | main(fd) 112 | -------------------------------------------------------------------------------- /dso138mini.py.sh: -------------------------------------------------------------------------------- 1 | _usbtest.sh -------------------------------------------------------------------------------- /ipy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "" 4 | echo "USB file descriptor: $@" 5 | echo "" 6 | 7 | cd /data/data/com.termux/files/home/tools/usb 8 | source venv/bin/activate 9 | 10 | ipython 11 | 12 | deactivate 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # core lib, libusb1.0 interface 2 | pyusb 3 | 4 | # for hexline(), hexdump() 5 | pyftdi 6 | 7 | # unused?, just to test and compare 8 | pyserial 9 | 10 | # shell, see in scrapy ... 11 | IPython 12 | 13 | # check + format 14 | flake8 15 | black 16 | -------------------------------------------------------------------------------- /usblib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import struct 5 | import threading 6 | import time 7 | 8 | import usb.backend.libusb1 as libusb1 9 | import usb.control 10 | import usb.core 11 | import usb.util 12 | 13 | # from pyftdi.misc import hexdump 14 | from pyftdi.misc import hexline 15 | 16 | 17 | LOGGER = logging.getLogger(__name__) 18 | RXTXLOGGER = logging.getLogger("{}.RXTX".format(__name__)) 19 | 20 | # ---------------------------------------------------------------------------- 21 | 22 | 23 | CP210x_PURGE = 0x12 24 | CP210x_IFC_ENABLE = 0x00 25 | CP210x_SET_BAUDDIV = 0x01 26 | CP210x_SET_LINE_CTL = 0x03 27 | CP210x_GET_LINE_CTL = 0x04 28 | CP210X_SET_BREAK = 0x05 29 | CP210x_SET_MHS = 0x07 30 | CP210x_SET_BAUDRATE = 0x1E 31 | CP210x_SET_FLOW = 0x13 32 | CP210x_SET_XON = 0x09 33 | CP210x_SET_XOFF = 0x0A 34 | CP210x_SET_CHARS = 0x19 35 | CP210x_GET_MDMSTS = 0x08 36 | CP210x_GET_COMM_STATUS = 0x10 37 | 38 | CP210x_REQTYPE_HOST2DEVICE = 0x41 39 | CP210x_REQTYPE_DEVICE2HOST = 0xC1 40 | 41 | # ------------------------------------ 42 | 43 | CP210x_BREAK_ON = 0x0001 44 | CP210x_BREAK_OFF = 0x0000 45 | 46 | CP210x_MHS_RTS_ON = 0x202 47 | CP210x_MHS_RTS_OFF = 0x200 48 | CP210x_MHS_DTR_ON = 0x101 49 | CP210x_MHS_DTR_OFF = 0x100 50 | 51 | CP210x_PURGE_ALL = 0x000F 52 | 53 | SILABSER_FLUSH_REQUEST_CODE = 0x12 54 | FLUSH_READ_CODE = 0x0A 55 | FLUSH_WRITE_CODE = 0x05 56 | 57 | CP210x_UART_ENABLE = 0x0001 58 | CP210x_UART_DISABLE = 0x0000 59 | CP210x_LINE_CTL_DEFAULT = 0x0800 60 | CP210x_MHS_DEFAULT = 0x0000 61 | CP210x_MHS_DTR = 0x0001 62 | CP210x_MHS_RTS = 0x0010 63 | CP210x_MHS_ALL = 0x0011 64 | CP210x_XON = 0x0000 65 | CP210x_XOFF = 0x0000 66 | 67 | DEFAULT_BAUDRATE = 9600 68 | 69 | # ------------------------------------ 70 | 71 | DATA_BITS_5 = 5 72 | DATA_BITS_6 = 6 73 | DATA_BITS_7 = 7 74 | DATA_BITS_8 = 8 75 | 76 | STOP_BITS_1 = 1 77 | STOP_BITS_15 = 3 78 | STOP_BITS_2 = 2 79 | 80 | PARITY_NONE = 0 81 | PARITY_ODD = 1 82 | PARITY_EVEN = 2 83 | PARITY_MARK = 3 84 | PARITY_SPACE = 4 85 | 86 | FLOW_CONTROL_OFF = 0 87 | FLOW_CONTROL_RTS_CTS = 1 88 | FLOW_CONTROL_DSR_DTR = 2 89 | FLOW_CONTROL_XON_XOFF = 3 90 | 91 | 92 | #: in msec 93 | DEFAUL_TIMEOUT = 500 94 | 95 | # ---------------------------------------------------------------------------- 96 | 97 | 98 | def device_from_fd(fd): 99 | # setup library 100 | backend = libusb1.get_backend() 101 | lib = backend.lib 102 | ctx = backend.ctx 103 | 104 | # extend c wrapper with android functionality 105 | lib.libusb_wrap_sys_device.argtypes = [ 106 | libusb1.c_void_p, 107 | libusb1.c_int, 108 | libusb1.POINTER(libusb1._libusb_device_handle), 109 | ] 110 | 111 | lib.libusb_get_device.argtypes = [libusb1.c_void_p] 112 | lib.libusb_get_device.restype = libusb1._libusb_device_handle 113 | 114 | LOGGER.debug("usb fd: %s", fd) 115 | 116 | # get handle from file descriptor 117 | handle = libusb1._libusb_device_handle() 118 | libusb1._check(lib.libusb_wrap_sys_device(ctx, fd, libusb1.byref(handle))) 119 | LOGGER.debug("usb handle: %s", handle) 120 | 121 | # get device (id?) from handle 122 | devid = lib.libusb_get_device(handle) 123 | LOGGER.debug("usb devid: %s", devid) 124 | 125 | # device: devid + handle wrapper 126 | class DummyDevice: 127 | def __init__(self, devid, handle): 128 | self.devid = devid 129 | self.handle = handle 130 | 131 | dev = DummyDevice(devid, handle) 132 | 133 | # create pyusb device 134 | device = usb.core.Device(dev, backend) 135 | device._ctx.handle = dev 136 | 137 | # device.set_configuration() 138 | 139 | return device 140 | 141 | 142 | def shell_usbdevice(fd, device): 143 | # interactive explore 144 | backend = device.backend 145 | lib = backend.lib 146 | ctx = backend.ctx 147 | dev = device._ctx.handle 148 | handle = dev.handle 149 | devid = dev.devid 150 | 151 | # query some information 152 | dev_desc = backend.get_device_descriptor(dev) 153 | config_desc = backend.get_configuration_descriptor(dev, 0) 154 | 155 | from IPython.terminal.embed import InteractiveShellEmbed 156 | from IPython.terminal.ipapp import load_default_config 157 | 158 | InteractiveShellEmbed.clear_instance() 159 | namespace = { 160 | "fd": fd, 161 | "handle": handle, 162 | "devid": devid, 163 | "dev": dev, 164 | "dev_desc": dev_desc, 165 | "config_desc": config_desc, 166 | "device": device, 167 | "libusb1": libusb1, 168 | "backend": backend, 169 | "lib": lib, 170 | "ctx": ctx, 171 | } 172 | banner = ( 173 | "Variables:\n" 174 | + "\n".join(["{:>12}: {}".format(k, repr(v)) for k, v in namespace.items()]) 175 | + "\n" 176 | ) 177 | shell = InteractiveShellEmbed.instance(banner1=banner, user_ns=namespace) 178 | shell() 179 | 180 | 181 | # ---------------------------------------------------------------------------- 182 | 183 | 184 | class Buffer: 185 | # https://stackoverflow.com/a/57748513/9360161 186 | def __init__(self): 187 | self.buf = bytearray() 188 | self.lock = threading.RLock() 189 | self.changed = threading.Condition(self.lock) 190 | # TODO: max size? - dequeue? / ringbuffer 191 | 192 | def clear(self): 193 | with self.lock: 194 | self.buf[:] = b"" 195 | self.changed.notify() 196 | 197 | def write(self, data): 198 | with self.lock: 199 | try: 200 | if isinstance(data, int): 201 | self.buf.append(data) 202 | return 1 203 | else: 204 | self.buf.extend(data) 205 | return len(data) 206 | finally: 207 | self.changed.notify() 208 | 209 | def read(self, size): 210 | with self.lock: 211 | try: 212 | # if size == 1: 213 | # return self.buf.pop(0) 214 | 215 | if not size or size <= 0: 216 | # None, 0, negative 217 | size = len(self) 218 | 219 | data = self.buf[:size] 220 | self.buf[:size] = b"" 221 | return data 222 | finally: 223 | self.changed.notify() 224 | 225 | def read_until(self, expected, size=-1): 226 | try: 227 | elen = len(expected) 228 | except TypeError: 229 | elen = 1 230 | 231 | with self.lock: 232 | pos = self.buf.find(expected) 233 | 234 | # not found, return max 235 | if pos == -1: 236 | return self.read(size) 237 | 238 | # found, compute total length 239 | elen = pos + elen 240 | # if len restriction then until limit 241 | if size > 0 and elen > size: 242 | return self.read(size) 243 | # return normal 244 | return self.read(elen) 245 | 246 | def contains(self, expected): 247 | with self.lock: 248 | return -1 != self.buf.find(expected) 249 | 250 | def peek(self, size): 251 | return self.buf[:size] 252 | 253 | def __len__(self): 254 | return len(self.buf) 255 | 256 | 257 | class Timeout: 258 | """\ 259 | Abstraction for timeout operations. Using time.monotonic(). 260 | The class can also be initialized with 0 or None, in order to support 261 | non-blocking and fully blocking I/O operations. The attributes 262 | is_non_blocking and is_infinite are set accordingly. 263 | """ 264 | 265 | TIME = time.monotonic 266 | 267 | def __init__(self, duration): 268 | """Initialize a timeout with given duration.""" 269 | self.is_infinite = duration is None 270 | self.is_non_blocking = duration == 0 271 | self.duration = duration 272 | if duration is not None: 273 | self.target_time = self.TIME() + duration 274 | else: 275 | self.target_time = None 276 | 277 | def expired(self): 278 | """Return a boolean, telling if the timeout has expired.""" 279 | return self.target_time is not None and self.time_left() <= 0 280 | 281 | def time_left(self): 282 | """Return how many seconds are left until the timeout expires.""" 283 | if self.is_non_blocking: 284 | return 0 285 | elif self.is_infinite: 286 | return None 287 | else: 288 | delta = self.target_time - self.TIME() 289 | if delta > self.duration: 290 | # clock jumped, recalculate 291 | self.target_time = self.TIME() + self.duration 292 | return self.duration 293 | else: 294 | return max(0, delta) 295 | 296 | def restart(self, duration): 297 | """\ 298 | Restart a timeout, only supported if a timeout was already set up 299 | before. 300 | """ 301 | self.is_infinite = duration is None 302 | self.is_non_blocking = duration == 0 303 | self.duration = duration 304 | self.target_time = self.TIME() + duration 305 | 306 | # -------------------------------- 307 | 308 | def __enter__(self): 309 | return self 310 | 311 | def __exit__(self, *args, **kwargs): 312 | pass 313 | 314 | 315 | class AbstractStoppableThread(threading.Thread): 316 | def __init__(self, serial, *args, **kwargs): 317 | super(AbstractStoppableThread, self).__init__(*args, **kwargs) 318 | self.serial = serial 319 | self.should_stop = False 320 | 321 | def stop(self): 322 | self.should_stop = True 323 | 324 | def shouldRun(self): 325 | if self.should_stop: 326 | return False 327 | 328 | if not self.serial.is_open: 329 | return False 330 | 331 | return True 332 | 333 | def run(self): 334 | while self.shouldRun(): 335 | self.runOne() 336 | 337 | def runOne(self): 338 | raise NotImplementedError 339 | 340 | 341 | # TODO: r/w may not need to happen in chunks? 342 | 343 | 344 | class SerialBufferReadThread(AbstractStoppableThread): 345 | def __init__(self, serial, endpoint, buffer, timeout=None, *args, **kwargs): 346 | super(SerialBufferReadThread, self).__init__(serial, *args, **kwargs) 347 | self.endpoint = endpoint 348 | self.buffer = buffer 349 | self.timeout = timeout 350 | 351 | def runOne(self): 352 | ser = self.serial 353 | device = ser.device 354 | endp = self.endpoint 355 | buf = self.buffer 356 | 357 | data = None 358 | try: 359 | data = device.read(endp.bEndpointAddress, endp.wMaxPacketSize, self.timeout) 360 | RXTXLOGGER.debug("[RX] %s", hexline(data)) 361 | except libusb1.USBError as ue: 362 | # 110/-7 for timeout 363 | if ue.errno != 110: 364 | raise 365 | RXTXLOGGER.debug( 366 | "RX Timeout: errno: %s, backend_error_code: %s", 367 | ue.errno, 368 | ue.backend_error_code, 369 | ) 370 | if data is not None: 371 | buf.write(data) 372 | # TODO: event 373 | 374 | 375 | class SerialBufferWriteThread(AbstractStoppableThread): 376 | def __init__( 377 | self, serial, endpoint, buffer, timeout=DEFAUL_TIMEOUT, *args, **kwargs 378 | ): 379 | super(SerialBufferWriteThread, self).__init__(serial, *args, **kwargs) 380 | self.endpoint = endpoint 381 | self.buffer = buffer 382 | self.timeout = timeout 383 | 384 | def runOne(self): 385 | ser = self.serial 386 | device = ser.device 387 | endp = self.endpoint 388 | buf = self.buffer 389 | 390 | if not buf: 391 | with buf.changed: 392 | buf.changed.wait(self.timeout / 1000.0) 393 | 394 | data = buf.read(endp.wMaxPacketSize) 395 | if not data: 396 | return 397 | 398 | RXTXLOGGER.debug("[TX] %s", hexline(data)) 399 | num = device.write(endp.bEndpointAddress, data, self.timeout) 400 | 401 | if num < len(data): 402 | RXTXLOGGER.error( 403 | "TX data loss: wrote %s of %s bytes! data: %s", 404 | num, 405 | len(data), 406 | hexline(data), 407 | ) 408 | 409 | # TODO: event 410 | 411 | 412 | class CP210xSerial: 413 | def __init__(self, device, baudRate=DEFAULT_BAUDRATE): 414 | assert self.is_usb_cp210x(device), "Unknown CP210x device!" 415 | self._device = device 416 | self._intf = 0 417 | 418 | self._baudRate = baudRate 419 | self._rtsCts_enabled = False 420 | self._dtrDsr_enabled = False 421 | self._cts_state = False 422 | self._dsr_state = False 423 | self._thrd_flowControl = None 424 | 425 | self._is_open = False 426 | self._is_async = False 427 | self._buf_in = Buffer() 428 | self._buf_out = Buffer() 429 | self._thrd_buf_in = None 430 | self._thrd_buf_out = None 431 | 432 | @staticmethod 433 | def is_usb_cp210x(device): 434 | # https://github.com/felHR85/UsbSerial/blob/master/usbserial/src/main/java/com/felhr/deviceids/CP210xIds.java 435 | idVendor = device.idVendor 436 | idProduct = device.idProduct 437 | 438 | abbrev_cp210x_ids = [(0x10C4, 0xEA60)] 439 | 440 | return (idVendor, idProduct) in abbrev_cp210x_ids 441 | 442 | @staticmethod 443 | def is_endpoint_dir_in(endpoint): 444 | address = endpoint.bEndpointAddress 445 | endp_dir = usb.util.endpoint_direction(address) 446 | return endp_dir == usb.util.ENDPOINT_IN 447 | 448 | @staticmethod 449 | def get_endpoints(device): 450 | configuration = device.configurations()[0] 451 | interface = configuration.interfaces()[0] 452 | endpoints = interface.endpoints() 453 | 454 | # https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/hardware/usb/UsbEndpoint.java 455 | # https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/hardware/usb/UsbConstants.java 456 | 457 | endp_in, endp_out = endpoints 458 | if not CP210xSerial.is_endpoint_dir_in(endp_in): 459 | endp_in, endp_out = endp_out, endp_in 460 | 461 | return endp_in, endp_out 462 | 463 | # -------------------------------- 464 | 465 | def send_ctrl_cmd(self, request, value=0, data=None, intf=0): 466 | ret = self._device.ctrl_transfer( 467 | CP210x_REQTYPE_HOST2DEVICE, 468 | request, 469 | wValue=value, 470 | wIndex=intf, 471 | data_or_wLength=data, 472 | timeout=None, 473 | ) 474 | return ret 475 | 476 | def recv_ctrl_cmd(self, request, blen, value=0, intf=0): 477 | buf = usb.util.create_buffer(blen) 478 | ret = self._device.ctrl_transfer( 479 | CP210x_REQTYPE_DEVICE2HOST, 480 | request, 481 | wValue=value, 482 | wIndex=intf, 483 | data_or_wLength=buf, 484 | timeout=None, 485 | ) 486 | print("recv:", ret, buf) 487 | return buf 488 | 489 | # -------------------------------- 490 | 491 | def set_baudRate(self, baudRate): 492 | data = struct.unpack("4B", struct.pack("= 0: 495 | self._baudRate = baudRate 496 | return ret 497 | 498 | def set_flowControl(self, flowControl): 499 | assert flowControl == FLOW_CONTROL_OFF, "Others not implemented!" 500 | 501 | if flowControl == FLOW_CONTROL_OFF: 502 | dataOff = [ 503 | 0x01, 504 | 0x00, 505 | 0x00, 506 | 0x00, 507 | 0x40, 508 | 0x00, 509 | 0x00, 510 | 0x00, 511 | 0x00, 512 | 0x80, 513 | 0x00, 514 | 0x00, 515 | 0x00, 516 | 0x20, 517 | 0x00, 518 | 0x00, 519 | ] 520 | self._rtsCts_enabled = False 521 | self._dtrDsr_enabled = False 522 | self._stop_thread_flowControl() 523 | return self.send_ctrl_cmd(CP210x_SET_FLOW, 0, dataOff) 524 | elif flowControl == FLOW_CONTROL_RTS_CTS: 525 | dataRtsCts = [ 526 | 0x09, 527 | 0x00, 528 | 0x00, 529 | 0x00, 530 | 0x40, 531 | 0x00, 532 | 0x00, 533 | 0x00, 534 | 0x00, 535 | 0x80, 536 | 0x00, 537 | 0x00, 538 | 0x00, 539 | 0x20, 540 | 0x00, 541 | 0x00, 542 | ] 543 | self._rtsCts_enabled = True 544 | self._dtrDsr_enabled = False 545 | _ = self.send_ctrl_cmd(CP210x_SET_FLOW, 0, dataRtsCts) 546 | _ = self.send_ctrl_cmd(CP210x_SET_MHS, CP210x_MHS_RTS_ON, None) 547 | commStatusCTS = self.get_comm_status() 548 | self._cts_state = (commStatusCTS[4] & 0x01) == 0x00 549 | self._start_thread_flowControl() 550 | elif flowControl == FLOW_CONTROL_DSR_DTR: 551 | dataDsrDtr = [ 552 | 0x11, 553 | 0x00, 554 | 0x00, 555 | 0x00, 556 | 0x40, 557 | 0x00, 558 | 0x00, 559 | 0x00, 560 | 0x00, 561 | 0x80, 562 | 0x00, 563 | 0x00, 564 | 0x00, 565 | 0x20, 566 | 0x00, 567 | 0x00, 568 | ] 569 | self._rtsCts_enabled = False 570 | self._dtrDsr_enabled = True 571 | _ = self.send_ctrl_cmd(CP210x_SET_FLOW, 0, dataDsrDtr) 572 | _ = self.send_ctrl_cmd(CP210x_SET_MHS, CP210x_MHS_DTR_ON, None) 573 | commStatusDSR = self.get_comm_status() 574 | self._dsr_state = (commStatusDSR[4] & 0x02) == 0x00 575 | self._start_thread_flowControl() 576 | elif flowControl == FLOW_CONTROL_XON_XOFF: 577 | dataXonXoff = [ 578 | 0x01, 579 | 0x00, 580 | 0x00, 581 | 0x00, 582 | 0x43, 583 | 0x00, 584 | 0x00, 585 | 0x00, 586 | 0x00, 587 | 0x80, 588 | 0x00, 589 | 0x00, 590 | 0x00, 591 | 0x20, 592 | 0x00, 593 | 0x00, 594 | ] 595 | dataChars = [ 596 | 0x00, 597 | 0x00, 598 | 0x00, 599 | 0x00, 600 | 0x11, 601 | 0x13, 602 | ] 603 | _ = self.send_ctrl_cmd(CP210x_SET_CHARS, 0, dataChars) 604 | _ = self.send_ctrl_cmd(CP210x_SET_FLOW, 0, dataXonXoff) 605 | # self._stop_thread_flowControl() # ? 606 | 607 | return 0 608 | 609 | def set_dataBits(self, dataBits): 610 | val = self.get_CTL() 611 | val &= ~0x0F00 612 | 613 | if dataBits not in (DATA_BITS_5, DATA_BITS_6, DATA_BITS_7, DATA_BITS_8): 614 | return 615 | 616 | val |= dataBits << 8 617 | 618 | self.send_ctrl_cmd(CP210x_SET_LINE_CTL, val, None) 619 | 620 | def set_stopBits(self, stopBits): 621 | val = self.get_CTL() 622 | val &= ~0x0003 623 | 624 | if stopBits == STOP_BITS_1: 625 | val |= 0x0000 626 | elif stopBits == STOP_BITS_15: 627 | val |= 0x0001 628 | elif stopBits == STOP_BITS_2: 629 | val |= 0x0002 630 | else: 631 | return 632 | 633 | self.send_ctrl_cmd(CP210x_SET_LINE_CTL, val, None) 634 | 635 | def set_parity(self, parity): 636 | val = self.get_CTL() 637 | val &= ~0x00F0 638 | 639 | if parity not in ( 640 | PARITY_NONE, 641 | PARITY_ODD, 642 | PARITY_EVEN, 643 | PARITY_MARK, 644 | PARITY_SPACE, 645 | ): 646 | return 647 | 648 | val |= parity << 4 649 | 650 | self.send_ctrl_cmd(CP210x_SET_LINE_CTL, val, None) 651 | 652 | def set_break(self, on): 653 | if on: 654 | self.send_ctrl_cmd(CP210X_SET_BREAK, CP210x_BREAK_ON, None) 655 | else: 656 | self.send_ctrl_cmd(CP210X_SET_BREAK, CP210x_BREAK_OFF, None) 657 | 658 | def set_RTS(self, on): 659 | if on: 660 | self.send_ctrl_cmd(CP210x_SET_MHS, CP210x_MHS_RTS_ON, None) 661 | else: 662 | self.send_ctrl_cmd(CP210x_SET_MHS, CP210x_MHS_RTS_OFF, None) 663 | 664 | def set_DTR(self, on): 665 | if on: 666 | self.send_ctrl_cmd(CP210x_SET_MHS, CP210x_MHS_DTR_ON, None) 667 | else: 668 | self.send_ctrl_cmd(CP210x_SET_MHS, CP210x_MHS_DTR_OFF, None) 669 | 670 | def get_modem_state(self): 671 | buf = self.recv_ctrl_cmd(CP210x_GET_MDMSTS, 1) 672 | return buf 673 | 674 | def get_comm_status(self): 675 | buf = self.recv_ctrl_cmd(CP210x_GET_COMM_STATUS, 19) 676 | return buf 677 | 678 | def get_CTL(self): 679 | buf = self.recv_ctrl_cmd(CP210x_GET_LINE_CTL, 2) 680 | val = struct.unpack(" len(data): 740 | if not buf: 741 | # wait for more, delay 742 | delay = to.time_left() 743 | if delay is None: 744 | delay = 1000 745 | with buf.changed: 746 | buf.changed.wait(delay / 1000.0) 747 | rlen = size - len(data) 748 | chunk = buf.read(rlen) 749 | data += chunk 750 | 751 | # TODO: convert to single byte if array len is 1? 752 | 753 | return data 754 | 755 | # while buf: 756 | # frag = buf.read(1024) 757 | # if not len(frag): 758 | # break 759 | # data.extend(frag) 760 | # return data 761 | 762 | def read_until(self, expected=b"\n", size=None, timeout=None): 763 | """Read from RX buffer until chars found. 764 | 765 | This method may be helpful to read lines from a buffer, etc. 766 | 767 | expected is a single byte or a sequence of bytes (byte 768 | string, array, list, ...) that Python can use for 769 | bytearray.find(expected) . 770 | 771 | size gives an upper limit of how much bytes before the search 772 | string are to be read. A not positive number means infinite. 773 | 774 | timeout limits the time until the search string is found. A 775 | timeout of zero returns after the frist read regardless if 776 | search is successful. None means to block until found or size 777 | limit is reached. 778 | 779 | Note that a unlimited size (-1/None) and a blocking timeout 780 | (None) may never return if the search pattern is never found! 781 | """ 782 | if not size or size <= 0: 783 | size = -1 784 | 785 | # isinstance(expected, (tuple, list, array.array, bytes, bytearray)) 786 | try: 787 | expected_last = expected[-1] 788 | except: 789 | # TypeError, IndexError 790 | expected_last = expected 791 | 792 | buf = self._buf_in 793 | with Timeout(timeout) as to: 794 | data = bytearray() 795 | data += buf.read_until(expected, size) 796 | 797 | # read in loop, blocking 798 | while not to.expired() and data.find(expected) == -1: 799 | if size > 0 and size <= len(data): 800 | break 801 | 802 | if not buf: 803 | # wait for more, delay 804 | delay = to.time_left() 805 | if delay is None: 806 | delay = DEFAUL_TIMEOUT 807 | with buf.changed: 808 | buf.changed.wait(delay / 1000.0) 809 | if size > 0: 810 | rlen = size - len(data) 811 | else: 812 | rlen = size 813 | chunk = buf.read_until(expected_last, rlen) 814 | data += chunk 815 | 816 | return data 817 | 818 | def read_until_or_none(self, expected=b"\n", size=None, timeout=None): 819 | """Read from RX buffer until chars found, return None if not found. 820 | 821 | This method may be helpful to read lines from a buffer, etc. 822 | It will return None if the chars are not in the size 823 | restriction or if the operation timed out. 824 | 825 | expected is a single byte or a sequence of bytes (byte 826 | string, array, list, ...) that Python can use for 827 | bytearray.find(expected) . 828 | 829 | size gives an upper limit of how much bytes before the search 830 | string are to be read. A not positive number means infinite. 831 | 832 | timeout limits the time until the search string is found. A 833 | timeout of zero means an immediate result regardless if the 834 | search is successful. None means to block until found or size 835 | limit is reached. 836 | 837 | Note that a unlimited size (-1/None) and a blocking timeout 838 | (None) may never return if the search pattern is never found! 839 | """ 840 | if not size or size <= 0: 841 | size = -1 842 | 843 | buf = self._buf_in 844 | with Timeout(timeout) as to: 845 | while not to.expired() and not buf.contains(expected): 846 | # check if in size limit 847 | if size > 0 and size < len(buf): 848 | break 849 | 850 | # wait for more, delay 851 | delay = to.time_left() 852 | if delay is None: 853 | delay = DEFAUL_TIMEOUT 854 | with buf.changed: 855 | buf.changed.wait(delay / 1000.0) 856 | 857 | if not buf.contains(expected): 858 | return None 859 | 860 | # lock if parallel 861 | with buf.lock: 862 | # check if needle in size limit 863 | if size > 0 and size < len(buf): 864 | data_peek = buf.peek(size) 865 | if -1 == data_peek.find(expected): 866 | return None 867 | 868 | # needle should be in limit 869 | data = bytearray() 870 | data += buf.read_until(expected, size) 871 | return data 872 | 873 | def wait_on_read_buffer(self, duration): 874 | """Wait for RX buffer to contain data. 875 | 876 | If RX buffer contains data return True. 877 | Wait on buffer until changed, if timeout return False, else 878 | True (on update, cut timeout short).""" 879 | if self._buf_in: 880 | return True 881 | 882 | with self._buf_in.changed: 883 | return self._buf_in.changed.wait(duration) 884 | 885 | def wait_on_write_buffer(self, duration): 886 | """Wait for TX buffer to empty. 887 | 888 | If buffer empty return True immediately. 889 | If TX buffer contains data after timeout return False, else 890 | True if empty. 891 | """ 892 | if not self._buf_out: 893 | return True 894 | 895 | with self._buf_out.changed: 896 | self._buf_out.changed.wait(duration) 897 | 898 | return not self._buf_out 899 | 900 | def write(self, data): 901 | # TODO: check async 902 | 903 | self._buf_out.write(data) 904 | 905 | # - sync 906 | # note: better to use buffers above? 907 | 908 | # TODO: r/w may not need to happen in chunks? 909 | 910 | def read_sync_chunked(self, size): 911 | device = self._device 912 | endp_in = CP210xSerial.get_endpoints(device)[0] 913 | 914 | if not size or size <= 0: 915 | return None 916 | 917 | data = bytearray() 918 | while size > len(data): 919 | rlen = min(endp_in.wMaxPacketSize, size - len(data)) 920 | chunk = device.read(endp_in.bEndpointAddress, rlen) 921 | if not chunk: 922 | break 923 | 924 | data += chunk 925 | 926 | return data 927 | 928 | def write_sync_chunked(self, data): 929 | device = self._device 930 | endp_out = CP210xSerial.get_endpoints(device)[1] 931 | 932 | if isinstance(data, int): 933 | data = bytearray([data]) 934 | elif not data: 935 | return 0 936 | else: 937 | data = bytearray(data) 938 | 939 | total = len(data) 940 | 941 | while data: 942 | slen = min(endp_out.wMaxPacketSize, len(data)) 943 | chunk = data[:slen] 944 | sent = device.write(endp_out.bEndpointAddress, chunk) 945 | if not sent: 946 | break 947 | 948 | data[:sent] = b"" 949 | 950 | return total - len(data) 951 | 952 | def read_sync(self, size): 953 | device = self._device 954 | endp_in = CP210xSerial.get_endpoints(device)[0] 955 | 956 | if not size or size <= 0: 957 | return None 958 | 959 | # may time out and raise USBError ... 960 | return device.read(endp_in.bEndpointAddress, size) 961 | 962 | def write_sync(self, data): 963 | device = self._device 964 | endp_out = CP210xSerial.get_endpoints(device)[1] 965 | 966 | if isinstance(data, int): 967 | data = bytearray([data]) 968 | elif not data: 969 | return 0 970 | else: 971 | data = bytearray(data) 972 | 973 | return device.write(endp_out.bEndpointAddress, data) 974 | 975 | # -------------------------------- 976 | 977 | # TODO: threads + buffers 978 | 979 | class FlowControlThread(AbstractStoppableThread): 980 | def __init__(self, serial, delay=40, *args, **kwargs): 981 | super(CP210xSerial.FlowControlThread, self).__init__( 982 | serial, *args, **kwargs 983 | ) 984 | # msec 985 | self.delay = delay 986 | 987 | def runOne(self): 988 | # wait delay 989 | 990 | ser = self.serial 991 | modemState = ser.get_modem_state() 992 | commStatus = ser.get_comm_status() 993 | 994 | if ser._rtsCts_enabled: 995 | new_cts_state = (modemState[0] & 0x10) == 0x10 996 | if ser._cts_state != new_cts_state: 997 | ser._cts_state = new_cts_state 998 | # TODO: cts callback 999 | 1000 | if ser._dtrDsr_enabled: 1001 | new_dsr_state = (modemState[0] & 0x20) == 0x20 1002 | if ser._dsr_state != new_dsr_state: 1003 | ser._dsr_state = new_dsr_state 1004 | # TODO: dsr callback 1005 | 1006 | has_parity_error = (commStatus[0] & 0x10) == 0x10 1007 | has_framinh_error = (commStatus[0] & 0x02) == 0x02 1008 | is_break_interrupt = (commStatus[0] & 0x01) == 0x01 1009 | has_overrun_error = ((commStatus[0] & 0x04) == 0x04) or ( 1010 | (commStatus[0] & 0x8) == 0x08 1011 | ) 1012 | # TODO: callbacks 1013 | 1014 | def _start_thread_flowControl(self): 1015 | if self._thrd_flowControl: 1016 | if self._thrd_flowControl.is_alive(): 1017 | return 1018 | self._stop_thread_flowControl() 1019 | 1020 | self._thrd_flowControl = CP210xSerial.FlowControlThread(self) 1021 | self._thrd_flowControl.start() 1022 | 1023 | def _stop_thread_flowControl(self): 1024 | if self._thrd_flowControl: 1025 | self._thrd_flowControl.stop() 1026 | self._thrd_flowControl = None 1027 | 1028 | def _start_threads_buffer_rw(self): 1029 | start_in = start_out = True 1030 | if self._thrd_buf_in: 1031 | if self._thrd_buf_in.is_alive(): 1032 | start_in = False 1033 | else: 1034 | self._thrd_buf_in.stop() 1035 | self._thrd_buf_in = None 1036 | if self._thrd_buf_out: 1037 | if self._thrd_buf_out.is_alive(): 1038 | start_out = False 1039 | else: 1040 | self._thrd_buf_out.stop() 1041 | self._thrd_buf_out = None 1042 | 1043 | # TODO: stop anyway and join? 1044 | 1045 | endp_in, endp_out = CP210xSerial.get_endpoints(self.device) 1046 | 1047 | if start_in: 1048 | self._thrd_buf_in = SerialBufferReadThread(self, endp_in, self._buf_in) 1049 | self._thrd_buf_in.start() 1050 | 1051 | if start_out: 1052 | self._thrd_buf_out = SerialBufferWriteThread(self, endp_out, self._buf_out) 1053 | self._thrd_buf_out.start() 1054 | 1055 | def _stop_threads_buffer_rw(self): 1056 | if self._thrd_buf_in: 1057 | self._thrd_buf_in.stop() 1058 | self._thrd_buf_in.join() 1059 | self._thrd_buf_in = None 1060 | if self._thrd_buf_out: 1061 | self._thrd_buf_out.stop() 1062 | self._thrd_buf_out.join() 1063 | self._thrd_buf_out = None 1064 | 1065 | # -------------------------------- 1066 | 1067 | def __enter__(self): 1068 | if not self._is_open: 1069 | self.open() 1070 | return self 1071 | 1072 | def __exit__(self, *args, **kwargs): 1073 | self.close() 1074 | 1075 | # -------------------------------- 1076 | 1077 | def prepare_usb_cp210x(self, intf=0, baudRate=DEFAULT_BAUDRATE): 1078 | backend = self._device.backend 1079 | dev = self._device._ctx.handle 1080 | 1081 | # https://github.com/felHR85/UsbSerial/blob/master/usbserial/src/main/java/com/felhr/usbserial/CP2102SerialDevice.java 1082 | 1083 | backend.claim_interface(dev, intf) 1084 | self._intf = intf 1085 | 1086 | # set defaults 1087 | ret = self.send_ctrl_cmd(CP210x_IFC_ENABLE, CP210x_UART_ENABLE, None) 1088 | if ret < 0: 1089 | return False 1090 | 1091 | ret = self.set_baudRate(baudRate) 1092 | if ret < 0: 1093 | return False 1094 | 1095 | ret = self.send_ctrl_cmd(CP210x_SET_LINE_CTL, CP210x_LINE_CTL_DEFAULT, None) 1096 | if ret < 0: 1097 | return False 1098 | 1099 | ret = self.set_flowControl(FLOW_CONTROL_OFF) 1100 | if ret < 0: 1101 | return False 1102 | 1103 | ret = self.send_ctrl_cmd(CP210x_SET_MHS, CP210x_MHS_DEFAULT, None) 1104 | if ret < 0: 1105 | return False 1106 | 1107 | return True 1108 | 1109 | def open(self, _async=True): 1110 | if self._is_open: 1111 | return 1112 | 1113 | assert self.prepare_usb_cp210x( 1114 | intf=self._intf, baudRate=self._baudRate 1115 | ), "Error setting up defaults" 1116 | self._is_open = True 1117 | 1118 | if _async: 1119 | self._is_async = _async 1120 | self._start_threads_buffer_rw() 1121 | 1122 | def read_dump_forever(self): 1123 | device = self._device 1124 | endp_in = CP210xSerial.get_endpoints(device)[0] 1125 | 1126 | while True: 1127 | try: 1128 | data = device.read(endp_in.bEndpointAddress, endp_in.wMaxPacketSize) 1129 | text = "".join([chr(v) for v in data]) 1130 | print(text, end="", flush=True) 1131 | except libusb1.USBError: 1132 | pass 1133 | except KeyboardInterrupt: 1134 | break 1135 | 1136 | def close(self): 1137 | if self._is_async: 1138 | self._stop_threads_buffer_rw() 1139 | self._stop_thread_flowControl() 1140 | 1141 | self.send_ctrl_cmd(CP210x_PURGE, CP210x_PURGE_ALL, None) 1142 | self.send_ctrl_cmd(CP210x_IFC_ENABLE, CP210x_UART_DISABLE, None) 1143 | 1144 | backend = self._device.backend 1145 | dev = self._device._ctx.handle 1146 | backend.claim_interface(dev, self._intf) 1147 | 1148 | self._is_open = False 1149 | 1150 | 1151 | # ---------------------------------------------------------------------------- 1152 | 1153 | 1154 | def main(fd, debug=True): 1155 | if hasattr(usb.core, "device_from_fd"): 1156 | device = usb.core.device_from_fd(fd) 1157 | else: 1158 | LOGGER.warning("Patch: device_from_fd") 1159 | device = device_from_fd(fd) 1160 | 1161 | if debug: 1162 | shell_usbdevice(fd, device) 1163 | 1164 | print("\n", "#" * 40, "\n") 1165 | print(device) 1166 | 1167 | assert device.idVendor == 0x10C4 and device.idProduct == 0xEA60 1168 | 1169 | ser = CP210xSerial(device, baudRate=115200) 1170 | try: 1171 | # mainly for baud rate 1172 | ser.open(_async=False) 1173 | # just poll read forever 1174 | ser.read_dump_forever() 1175 | finally: 1176 | ser.close() 1177 | 1178 | 1179 | if __name__ == "__main__": 1180 | # https://wiki.termux.com/wiki/Termux-usb 1181 | logging.basicConfig( 1182 | level=logging.INFO, format="[%(levelname).1s] %(name)s: %(message)s" 1183 | ) 1184 | logging.getLogger(__name__).setLevel(logging.DEBUG) 1185 | logging.getLogger("usblib").setLevel(logging.DEBUG) 1186 | logging.getLogger("{}.RXTX".format(__name__)).setLevel(logging.INFO) 1187 | 1188 | # grab fd number from args 1189 | # (from termux wrapper) 1190 | import sys 1191 | 1192 | LOGGER.debug("args: %s", sys.argv) 1193 | 1194 | fd = int(sys.argv[1]) 1195 | main(fd) 1196 | -------------------------------------------------------------------------------- /usblib.py.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /data/data/com.termux/files/home/tools/usb 4 | source venv/bin/activate 5 | 6 | python3 usblib.py "$@" 7 | 8 | deactivate 9 | -------------------------------------------------------------------------------- /usbtest_read4ever.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import threading 5 | 6 | from usblib import device_from_fd 7 | from usblib import shell_usbdevice 8 | from usblib import CP210xSerial 9 | 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | # ---------------------------------------------------------------------------- 15 | 16 | 17 | def main(fd, debug=False): 18 | device = device_from_fd(fd) 19 | 20 | if debug: 21 | shell_usbdevice(fd, device) 22 | 23 | print("\n", "#" * 40, "\n") 24 | print(device) 25 | 26 | assert device.idVendor == 0x10C4 and device.idProduct == 0xEA60 27 | 28 | ser = CP210xSerial(device, baudRate=115200) 29 | try: 30 | # mainly for baud rate 31 | ser.open(_async=True) 32 | # just poll read forever 33 | ser.read_dump_forever() 34 | finally: 35 | ser.close() 36 | 37 | 38 | if __name__ == "__main__": 39 | # https://wiki.termux.com/wiki/Termux-usb 40 | logging.basicConfig( 41 | level=logging.INFO, format="[%(levelname).1s] %(name)s: %(message)s" 42 | ) 43 | logging.getLogger(__name__).setLevel(logging.DEBUG) 44 | logging.getLogger("usblib").setLevel(logging.DEBUG) 45 | logging.getLogger("usb").setLevel(logging.DEBUG) 46 | 47 | # grab fd number from args 48 | # (from termux wrapper) 49 | import sys 50 | 51 | LOGGER.debug("args: %s", sys.argv) 52 | 53 | fd = int(sys.argv[1]) 54 | main(fd) 55 | -------------------------------------------------------------------------------- /usbtest_read4ever.py.sh: -------------------------------------------------------------------------------- 1 | _usbtest.sh -------------------------------------------------------------------------------- /usbtest_rw1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import threading 5 | 6 | from usblib import device_from_fd 7 | from usblib import shell_usbdevice 8 | from usblib import CP210xSerial 9 | 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | # ---------------------------------------------------------------------------- 15 | 16 | 17 | def main(fd, debug=False): 18 | device = device_from_fd(fd) 19 | 20 | if debug: 21 | shell_usbdevice(fd, device) 22 | 23 | print("\n", "#" * 40, "\n") 24 | print(device) 25 | 26 | assert device.idVendor == 0x10C4 and device.idProduct == 0xEA60 27 | 28 | ser = CP210xSerial(device, baudRate=115200) 29 | try: 30 | # mainly for baud rate 31 | ser.open(_async=True) 32 | # just poll read forever 33 | # ser.read_dump_forever() 34 | 35 | # testing ------ 36 | dump_test(ser) 37 | finally: 38 | ser.close() 39 | 40 | 41 | def dump_test(ser): 42 | stop = False 43 | 44 | def dumper(): 45 | while not stop: 46 | data = ser._buf_in.read(100) 47 | text = "".join([chr(v) for v in data]) 48 | print(text, end="", flush=True) 49 | 50 | t = threading.Thread(target=dumper) 51 | t.start() 52 | 53 | import time 54 | 55 | ser._buf_out.write(b"test\n") 56 | time.sleep(1) 57 | ser._buf_out.write(b"test 1\n") 58 | time.sleep(1) 59 | ser._buf_out.write(b"test 2\n") 60 | time.sleep(2) 61 | 62 | endp_in, endp_out = CP210xSerial.get_endpoints(ser.device) 63 | print(ser.device.write(endp_out, bytearray(100))) 64 | print(ser.device.read(endp_in, 100)) 65 | # print(ser.device.read(endp_in, 100)) 66 | # print(ser.device.read(endp_in, 100)) 67 | time.sleep(2) 68 | time.sleep(1) 69 | stop = True 70 | t.join() 71 | print("test done") 72 | 73 | 74 | if __name__ == "__main__": 75 | # https://wiki.termux.com/wiki/Termux-usb 76 | logging.basicConfig( 77 | level=logging.INFO, format="[%(levelname).1s] %(name)s: %(message)s" 78 | ) 79 | logging.getLogger(__name__).setLevel(logging.DEBUG) 80 | logging.getLogger("usblib").setLevel(logging.DEBUG) 81 | logging.getLogger("usb").setLevel(logging.DEBUG) 82 | 83 | # grab fd number from args 84 | # (from termux wrapper) 85 | import sys 86 | 87 | LOGGER.debug("args: %s", sys.argv) 88 | 89 | fd = int(sys.argv[1]) 90 | main(fd) 91 | -------------------------------------------------------------------------------- /usbtest_rw1.py.sh: -------------------------------------------------------------------------------- 1 | _usbtest.sh -------------------------------------------------------------------------------- /usbtest_rw_buf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import threading 5 | import time 6 | 7 | from usblib import device_from_fd 8 | from usblib import shell_usbdevice 9 | from usblib import CP210xSerial 10 | 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | # ---------------------------------------------------------------------------- 16 | 17 | 18 | def main(fd, debug=False): 19 | device = device_from_fd(fd) 20 | 21 | if debug: 22 | shell_usbdevice(fd, device) 23 | 24 | print("\n", "#" * 40, "\n") 25 | print(device) 26 | 27 | assert device.idVendor == 0x10C4 and device.idProduct == 0xEA60 28 | 29 | ser = CP210xSerial(device, baudRate=115200) 30 | try: 31 | # mainly for baud rate 32 | ser.open(_async=True) 33 | # just poll read forever 34 | # ser.read_dump_forever() 35 | 36 | # testing ------ 37 | buf_test(ser) 38 | finally: 39 | ser.close() 40 | 41 | 42 | def buf_test(ser): 43 | stop = False 44 | 45 | def writer(delay): 46 | i = 0 47 | abc = "abcdefghijklmnopqrstuvwxyz" 48 | while not stop: 49 | ch = abc[i % len(abc)] 50 | nl = "\n" if i % 10 == 0 else " " 51 | data = "{}={}{}".format(ch, i, nl).encode("ascii") 52 | i += 1 53 | 54 | ser._buf_in.write(data) 55 | time.sleep(delay) 56 | print("write stopped") 57 | 58 | def dumper(): 59 | delay = 400.0 / 1000.0 60 | chunk_size = 100 61 | while not stop: 62 | if not ser._buf_in: 63 | print("wait ...") 64 | with ser._buf_in.changed: 65 | notified = ser._buf_in.changed.wait(delay) 66 | print(" notify:", notified) 67 | 68 | data = ser._buf_in.read(chunk_size) 69 | if not data: 70 | print(" no data") 71 | continue 72 | text = "".join([chr(v) for v in data]) 73 | print(" data:", text, end="\n", flush=True) 74 | print("read stopped") 75 | 76 | def buf_reads(delay, chunk_size, timeout): 77 | while not stop: 78 | if not ser._buf_in: 79 | with ser._buf_in.changed: 80 | notified = ser._buf_in.changed.wait(200 / 1000.0) 81 | 82 | data = ser.read(chunk_size, timeout) 83 | print( 84 | "read({}, {}): len:{}, data:{}".format( 85 | chunk_size, timeout, len(data), bytes(data) 86 | ) 87 | ) 88 | time.sleep(delay) 89 | print("read stopped") 90 | 91 | def buf_read_util(delay, expected, chunk_size, timeout): 92 | while not stop: 93 | if not ser._buf_in: 94 | with ser._buf_in.changed: 95 | notified = ser._buf_in.changed.wait(200 / 1000.0) 96 | 97 | data = ser.read_until(expected, chunk_size, timeout) 98 | print( 99 | "read_until({}, {}, {}): len:{}, data:{}".format( 100 | expected, chunk_size, timeout, len(data), bytes(data) 101 | ) 102 | ) 103 | time.sleep(delay) 104 | print("read stopped") 105 | 106 | def buf_read_util2(delay, expected, chunk_size, timeout): 107 | while not stop: 108 | if not ser._buf_in: 109 | with ser._buf_in.changed: 110 | notified = ser._buf_in.changed.wait(200 / 1000.0) 111 | 112 | data = ser.read_until_or_none(expected, chunk_size, timeout) 113 | if data is None: 114 | print( 115 | "read_until({}, {}, {}): {}".format( 116 | expected, chunk_size, timeout, data 117 | ) 118 | ) 119 | else: 120 | print( 121 | "read_until({}, {}, {}): len:{}, data:{}".format( 122 | expected, chunk_size, timeout, len(data), bytes(data) 123 | ) 124 | ) 125 | time.sleep(delay) 126 | print("read stopped") 127 | 128 | # dump all, shows locking+notificationd 129 | tr = threading.Thread(target=dumper) 130 | 131 | # buf reads, no-block 132 | # tr = threading.Thread(target=buf_reads, args=(10.0 / 1000.0, 100, 0)) 133 | 134 | # buf reads, block, hangs on Stop ... 135 | # tr = threading.Thread(target=buf_reads, args=(10.0 / 1000.0, 25, None)) 136 | 137 | # buf reads, timeout 3 sec, 25 chars 138 | # tr = threading.Thread(target=buf_reads, args=(10.0 / 1000.0, 25, 3000.0 / 1000.0)) 139 | 140 | # buf reads, timeout 3 sec, any char len, mostly empty? 141 | # tr = threading.Thread(target=buf_reads, args=(10.0 / 1000.0, None, 3000.0 / 1000.0)) 142 | 143 | # until char \n, 3 sec, any size 144 | # tr = threading.Thread(target=buf_read_util, args=(10.0 / 1000.0, b"\n", None, 3000.0 / 1000.0)) 145 | 146 | # until char \n, block, any size 147 | # tr = threading.Thread(target=buf_read_util, args=(10.0 / 1000.0, b"\n", None, None)) 148 | 149 | # until char \n, block, 17 chars 150 | # tr = threading.Thread(target=buf_read_util, args=(10.0 / 1000.0, b"\n", 17, None)) 151 | 152 | # until char \n, no-block, 17 chars 153 | # tr = threading.Thread(target=buf_read_util, args=(10.0 / 1000.0, b"\n", 17, 0)) 154 | 155 | # until no char, 4 sec, 17 chars - always returns 156 | # tr = threading.Thread(target=buf_read_util, args=(10.0 / 1000.0, b"", 17, 4000.0 / 1000.0)) 157 | 158 | # until char "0\n", 4 sec, 17 chars 159 | # tr = threading.Thread(target=buf_read_util, args=(10.0 / 1000.0, b"0\n", 17, 4000.0 / 1000.0)) 160 | 161 | # until char \n or none, 3 sec, any size 162 | # tr = threading.Thread(target=buf_read_util2, args=(10.0 / 1000.0, b"\n", None, 3000.0 / 1000.0)) 163 | 164 | # until char \n or none, no-block, 17 chars - will always return if too 165 | # much data and immediate 166 | # tr = threading.Thread(target=buf_read_util2, args=(10.0 / 1000.0, b"\n", 17, 0)) 167 | 168 | # until char \n or none, 3 sec, 33 chars, 169 | # fast return if size limit reached 170 | # tr = threading.Thread(target=buf_read_util2, args=(10.0 / 1000.0, b"\n", 33, 3000.0 / 1000.0)) 171 | 172 | # until char \n or none, no-block, any size 173 | # fast return with None but sometimes result 174 | # tr = threading.Thread(target=buf_read_util2, args=(10.0 / 1000.0, b"\n", None, 0)) 175 | 176 | # until char \n or none, block, any size 177 | # block until result or forever 178 | # tr = threading.Thread(target=buf_read_util2, args=(10.0 / 1000.0, b"\n", None, None)) 179 | 180 | tw = threading.Thread(target=writer, args=(1000.0 / 1000.0,)) 181 | 182 | tr.start() 183 | tw.start() 184 | 185 | # ser._buf_out.write(b"test\n") 186 | # time.sleep(1) 187 | # ser._buf_out.write(b"test 1\n") 188 | # time.sleep(1) 189 | # ser._buf_out.write(b"test 2\n") 190 | # time.sleep(1) 191 | 192 | try: 193 | time.sleep(30) 194 | except KeyboardInterrupt: 195 | pass 196 | 197 | stop = True 198 | tw.join() 199 | tr.join() 200 | print("test done") 201 | 202 | 203 | if __name__ == "__main__": 204 | # https://wiki.termux.com/wiki/Termux-usb 205 | logging.basicConfig( 206 | level=logging.INFO, format="[%(levelname).1s] %(name)s: %(message)s" 207 | ) 208 | logging.getLogger(__name__).setLevel(logging.DEBUG) 209 | logging.getLogger("usblib").setLevel(logging.DEBUG) 210 | logging.getLogger("usblib.RXTX").setLevel(logging.INFO) 211 | logging.getLogger("usb").setLevel(logging.DEBUG) 212 | 213 | # grab fd number from args 214 | # (from termux wrapper) 215 | import sys 216 | 217 | LOGGER.debug("args: %s", sys.argv) 218 | 219 | fd = int(sys.argv[1]) 220 | main(fd) 221 | -------------------------------------------------------------------------------- /usbtest_rw_buf.py.sh: -------------------------------------------------------------------------------- 1 | _usbtest.sh -------------------------------------------------------------------------------- /usbtest_shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | 5 | from usblib import device_from_fd 6 | from usblib import shell_usbdevice 7 | 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | # ---------------------------------------------------------------------------- 13 | 14 | 15 | def main(fd): 16 | device = device_from_fd(fd) 17 | 18 | shell_usbdevice(fd, device) 19 | 20 | print("\n", "#" * 40, "\n") 21 | print(device) 22 | 23 | 24 | if __name__ == "__main__": 25 | # https://wiki.termux.com/wiki/Termux-usb 26 | logging.basicConfig( 27 | level=logging.INFO, format="[%(levelname).1s] %(name)s: %(message)s" 28 | ) 29 | logging.getLogger(__name__).setLevel(logging.DEBUG) 30 | logging.getLogger("usblib").setLevel(logging.DEBUG) 31 | 32 | # grab fd number from args 33 | # (from termux wrapper) 34 | import sys 35 | 36 | LOGGER.debug("args: %s", sys.argv) 37 | 38 | fd = int(sys.argv[1]) 39 | main(fd) 40 | -------------------------------------------------------------------------------- /usbtest_shell.py.sh: -------------------------------------------------------------------------------- 1 | _usbtest.sh --------------------------------------------------------------------------------