├── requirements.txt ├── images └── logo-usbhub-header.jpg ├── capablerobot_usbhub ├── windows │ ├── 32bit │ │ ├── libusb-1.0.dll │ │ ├── libusb-1.0.lib │ │ └── libusb-1.0.pdb │ ├── 64bit │ │ ├── libusb-1.0.dll │ │ ├── libusb-1.0.lib │ │ └── libusb-1.0.pdb │ └── README.md ├── formats │ ├── usb4715_mcu.ksy │ ├── usb4715_gpio.ksy │ ├── usb4715.ksy │ ├── usb4715_port.ksy │ └── usb4715_main.ksy ├── __init__.py ├── registers │ ├── gpio.py │ ├── __init__.py │ ├── port.py │ └── main.py ├── util.py ├── spi.py ├── gpio.py ├── config.py ├── i2c.py ├── console.py ├── main.py ├── power.py └── device.py ├── 50-capablerobot-usbhub.rules ├── examples ├── registers.py ├── i2c_current.py ├── power.py ├── gpio.py └── status.py ├── pyproject.toml ├── LICENSE ├── .gitignore ├── README.md └── CODE_OF_CONDUCT.md /requirements.txt: -------------------------------------------------------------------------------- 1 | pyusb >=1.0.2,<2.0 2 | construct >=2.9,<3.0 3 | pyyaml >=5.3,<6.0 4 | click >=7.0,<8.0 -------------------------------------------------------------------------------- /images/logo-usbhub-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/HEAD/images/logo-usbhub-header.jpg -------------------------------------------------------------------------------- /capablerobot_usbhub/windows/32bit/libusb-1.0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/HEAD/capablerobot_usbhub/windows/32bit/libusb-1.0.dll -------------------------------------------------------------------------------- /capablerobot_usbhub/windows/32bit/libusb-1.0.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/HEAD/capablerobot_usbhub/windows/32bit/libusb-1.0.lib -------------------------------------------------------------------------------- /capablerobot_usbhub/windows/32bit/libusb-1.0.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/HEAD/capablerobot_usbhub/windows/32bit/libusb-1.0.pdb -------------------------------------------------------------------------------- /capablerobot_usbhub/windows/64bit/libusb-1.0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/HEAD/capablerobot_usbhub/windows/64bit/libusb-1.0.dll -------------------------------------------------------------------------------- /capablerobot_usbhub/windows/64bit/libusb-1.0.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/HEAD/capablerobot_usbhub/windows/64bit/libusb-1.0.lib -------------------------------------------------------------------------------- /capablerobot_usbhub/windows/64bit/libusb-1.0.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/HEAD/capablerobot_usbhub/windows/64bit/libusb-1.0.pdb -------------------------------------------------------------------------------- /50-capablerobot-usbhub.rules: -------------------------------------------------------------------------------- 1 | # Endpoint on Capable Robot Programmable USB Hub. Access needed 2 | # for I2C, SPI, and GPIO control over the USB bus. 3 | 4 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0424", ATTRS{idProduct}=="494c", MODE:="0666" 5 | 6 | # If you share your linux system with other users, or just don't like the 7 | # idea of write permission for everybody, you can replace MODE:="0666" with 8 | # OWNER:="yourusername" to create the device owned by you, or with 9 | # GROUP:="somegroupname" and mange access using standard unix groups. -------------------------------------------------------------------------------- /examples/registers.py: -------------------------------------------------------------------------------- 1 | import os, sys, inspect 2 | 3 | lib_folder = os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0], '..') 4 | lib_load = os.path.realpath(os.path.abspath(lib_folder)) 5 | 6 | if lib_load not in sys.path: 7 | sys.path.insert(0, lib_load) 8 | 9 | import capablerobot_usbhub 10 | 11 | hub = capablerobot_usbhub.USBHub() 12 | 13 | hub.register_read(name='main::product_id', print=True) 14 | hub.register_read(name='port::connection', print=True) 15 | hub.register_read(name='port::device_speed', print=True) -------------------------------------------------------------------------------- /capablerobot_usbhub/formats/usb4715_mcu.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: usb4715_mcu 3 | title: Structure that allows communication with the MCU inside the Programmable USB Hub 4 | types: 5 | control: 6 | seq: 7 | - id: command 8 | type: b3 9 | - id: name 10 | type: b5 11 | - id: value 12 | type: b16 13 | - id: crc 14 | type: b8 15 | status: 16 | seq: 17 | - id: command 18 | type: b3 19 | - id: name 20 | type: b5 21 | - id: value 22 | type: b16 23 | - id: crc 24 | type: b8 -------------------------------------------------------------------------------- /examples/i2c_current.py: -------------------------------------------------------------------------------- 1 | import os, sys, inspect 2 | import time 3 | 4 | lib_folder = os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0], '..') 5 | lib_load = os.path.realpath(os.path.abspath(lib_folder)) 6 | 7 | if lib_load not in sys.path: 8 | sys.path.insert(0, lib_load) 9 | 10 | import capablerobot_usbhub 11 | 12 | hub = capablerobot_usbhub.USBHub() 13 | last = int(time.time() * 1000) 14 | 15 | while True: 16 | now = int(time.time() * 1000) 17 | print(str(now - last).rjust(3), " ".join([("%.2f" % v).rjust(7) for v in hub.power.measurements()])) 18 | last = now 19 | time.sleep(0.5) -------------------------------------------------------------------------------- /examples/power.py: -------------------------------------------------------------------------------- 1 | import os, sys, inspect, logging, time 2 | 3 | lib_folder = os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0], '..') 4 | lib_load = os.path.realpath(os.path.abspath(lib_folder)) 5 | 6 | if lib_load not in sys.path: 7 | sys.path.insert(0, lib_load) 8 | 9 | FORMAT = '%(levelname)-8s %(message)s' 10 | logging.basicConfig(format=FORMAT) 11 | logger = logging.getLogger('USB Hub Status') 12 | logger.setLevel(logging.DEBUG) 13 | 14 | import capablerobot_usbhub 15 | 16 | hub = capablerobot_usbhub.USBHub() 17 | 18 | ports = [1,2,3,4] 19 | for port in ports: 20 | logger.info("enable port %d" % port) 21 | hub.power.enable([port]) 22 | time.sleep(0.5) 23 | logger.info("disable port %d" % port) 24 | hub.power.disable([port]) 25 | time.sleep(0.5) 26 | -------------------------------------------------------------------------------- /examples/gpio.py: -------------------------------------------------------------------------------- 1 | import os, sys, inspect, logging, time 2 | 3 | lib_folder = os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0], '..') 4 | lib_load = os.path.realpath(os.path.abspath(lib_folder)) 5 | 6 | if lib_load not in sys.path: 7 | sys.path.insert(0, lib_load) 8 | 9 | import capablerobot_usbhub 10 | 11 | hub = capablerobot_usbhub.USBHub() 12 | 13 | ## Input enabled here on the output so that reading the output's current state works 14 | hub.gpio.configure(ios=[0], output=True, input=True) 15 | hub.gpio.configure(ios=[1], input=True, pull_down=True) 16 | 17 | while True: 18 | 19 | hub.gpio.io0 = True 20 | print("IO {} {}".format(*hub.gpio.io)) 21 | time.sleep(1) 22 | 23 | hub.gpio.io0 = False 24 | 25 | print("IO {} {}".format(*hub.gpio.io)) 26 | time.sleep(1) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "capablerobot_usbhub" 3 | version = "0.5.0" 4 | description = "Host side driver for the Capable Robot Programmable USB Hub" 5 | authors = ["Chris Osterwood "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/CapableRobot/CapableRobot_USBHub_Driver" 9 | keywords = ["usb", "circuitpython", "hardware"] 10 | include = ["examples", "LICENSE", "CODE_OF_CONDUCT.md", "50-capablerobot-usbhub.rules", "requirements.txt"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.6" 14 | pyusb = "^1.0.2" 15 | construct = "^2.9.51" 16 | pyyaml = "^5.3" 17 | click = "^7.0" 18 | 19 | [tool.poetry.dev-dependencies] 20 | 21 | [tool.poetry.scripts] 22 | usbhub = "capablerobot_usbhub.console:main" 23 | 24 | [build-system] 25 | requires = ["poetry>=0.12"] 26 | build-backend = "poetry.masonry.api" 27 | -------------------------------------------------------------------------------- /capablerobot_usbhub/windows/README.md: -------------------------------------------------------------------------------- 1 | # libusb Binaries for Windows 2 | 3 | ## Overview 4 | 5 | libusb is a C library that provides generic access to USB devices. It is intended to be used by developers to facilitate the production of applications that communicate with USB hardware. 6 | 7 | - It is portable: Using a single cross-platform API, it provides access to USB devices on Linux, macOS, Windows, etc. 8 | - It is user-mode: No special privilege or elevation is required for the application to communicate with a device. 9 | - It is version-agnostic: All versions of the USB protocol, from 1.0 to 3.1 (latest), are supported. 10 | 11 | ## This Directory 12 | 13 | This directory contains Windows binaries (32 and 64 bit) of libusb. They are packaged with this driver so that end-users do not need to install them into their Windows installations. The driver detects when it is running on Windows and specifies these DLLs as the libusb backend for the pyusb library. 14 | 15 | DLLs were downloaded from https://libusb.info and are version 1.0.23. -------------------------------------------------------------------------------- /examples/status.py: -------------------------------------------------------------------------------- 1 | import os, sys, inspect 2 | import time 3 | import logging 4 | 5 | lib_folder = os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0], '..') 6 | lib_load = os.path.realpath(os.path.abspath(lib_folder)) 7 | 8 | if lib_load not in sys.path: 9 | sys.path.insert(0, lib_load) 10 | 11 | FORMAT = '%(levelname)-8s %(message)s' 12 | logging.basicConfig(format=FORMAT) 13 | logger = logging.getLogger('USB Hub Status') 14 | logger.setLevel(logging.DEBUG) 15 | 16 | import capablerobot_usbhub 17 | 18 | hub = capablerobot_usbhub.USBHub() 19 | hub.i2c.enable() 20 | 21 | 22 | logger.info("Port Connections : {}".format(hub.connections())) 23 | logger.info("Port Speeds : {}".format(hub.speeds())) 24 | 25 | logger.info("Port Currents (mA)") 26 | logger.info(" Measured : " + " ".join([("%.2f" % v).rjust(7) for v in hub.power.measurements()])) 27 | logger.info(" Limit : {}".format(hub.power.limits())) 28 | logger.info(" Alerts : {}".format(" ".join(hub.power.alerts()))) 29 | logger.info(" State : {}".format(hub.power.state())) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Capable Robot 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 | -------------------------------------------------------------------------------- /capablerobot_usbhub/formats/usb4715_gpio.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: usb4715_gpio 3 | title: Microchip USB4715 GPIO Registers 4 | types: 5 | output_enable: 6 | doc-ref: Table 3-4 7 | seq: 8 | - id: value 9 | type: b32 10 | input_enable: 11 | doc-ref: Table 3-8 12 | seq: 13 | - id: value 14 | type: b32 15 | output: 16 | doc-ref: Table 3-11 17 | seq: 18 | - id: value 19 | type: b32 20 | input: 21 | doc-ref: Table 3-14 22 | seq: 23 | - id: value 24 | type: b32 25 | pull_up: 26 | doc-ref: Table 3-17 27 | seq: 28 | - id: value 29 | type: b32 30 | pull_down: 31 | doc-ref: Table 3-20 32 | seq: 33 | - id: value 34 | type: b32 35 | open_drain: 36 | doc-ref: Table 3-23 37 | seq: 38 | - id: value 39 | type: b32 40 | debounce: 41 | doc-ref: Table 3-26 42 | seq: 43 | - id: value 44 | type: b32 45 | debounce_time: 46 | doc-ref: Table 3-29 47 | seq: 48 | - id: reserved 49 | type: b24 50 | - id: value 51 | type: b8 52 | doc: Debounce count in 10-msec increments -------------------------------------------------------------------------------- /capablerobot_usbhub/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | from .main import USBHub -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /capablerobot_usbhub/registers/gpio.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | from construct import * 24 | 25 | gpio_output_enable = BitStruct( 26 | "value" / BitsInteger(32), 27 | ) 28 | 29 | gpio_input_enable = BitStruct( 30 | "value" / BitsInteger(32), 31 | ) 32 | 33 | gpio_output = BitStruct( 34 | "value" / BitsInteger(32), 35 | ) 36 | 37 | gpio_input = BitStruct( 38 | "value" / BitsInteger(32), 39 | ) 40 | 41 | gpio_pull_up = BitStruct( 42 | "value" / BitsInteger(32), 43 | ) 44 | 45 | gpio_pull_down = BitStruct( 46 | "value" / BitsInteger(32), 47 | ) 48 | 49 | gpio_open_drain = BitStruct( 50 | "value" / BitsInteger(32), 51 | ) 52 | 53 | gpio_debounce = BitStruct( 54 | "value" / BitsInteger(32), 55 | ) 56 | 57 | gpio_debounce_time = BitStruct( 58 | Padding(24), 59 | "value" / BitsInteger(8), 60 | ) 61 | 62 | -------------------------------------------------------------------------------- /capablerobot_usbhub/formats/usb4715.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: usb4715 3 | title: Microchip USB4715 Registers 4 | endian: be 5 | imports: 6 | - usb4715_main 7 | - usb4715_port 8 | - usb4715_gpio 9 | - usb4715_mcu 10 | seq: 11 | - id: registers 12 | type: register 13 | repeat: eos 14 | types: 15 | register: 16 | seq: 17 | - id: addr 18 | type: u2 19 | - id: len 20 | type: u1 21 | - id: body 22 | size: len 23 | type: 24 | switch-on: addr 25 | cases: 26 | 0x0000: 'usb4715_main::revision' 27 | 0x0914: 'usb4715_mcu::control' 28 | 0x0924: 'usb4715_mcu::status' 29 | 0x0900: 'usb4715_gpio::output_enable' 30 | 0x0910: 'usb4715_gpio::input_enable' 31 | 0x0920: 'usb4715_gpio::output' 32 | 0x0930: 'usb4715_gpio::input' 33 | 0x0940: 'usb4715_gpio::pull_up' 34 | 0x0950: 'usb4715_gpio::pull_down' 35 | 0x0960: 'usb4715_gpio::open_drain' 36 | 0x09E0: 'usb4715_gpio::debounce' 37 | 0x09F0: 'usb4715_gpio::debounce_time' 38 | 0x3000: 'usb4715_main::vendor_id' 39 | 0x3002: 'usb4715_main::product_id' 40 | 0x3004: 'usb4715_main::device_id' 41 | 0x3006: 'usb4715_main::hub_configuration_1' 42 | 0x3007: 'usb4715_main::hub_configuration_2' 43 | 0x3008: 'usb4715_main::hub_configuration_3' 44 | 0x3006: 'usb4715_main::hub_configuration' 45 | 0x3104: 'usb4715_main::hub_control' 46 | 0x30FA: 'usb4715_main::port_swap' 47 | 0x3197: 'usb4715_main::suspend' 48 | 0x30E5: 'usb4715_port::power_status' 49 | 0x30FB: 'usb4715_port::remap12' 50 | 0x30FC: 'usb4715_port::remap34' 51 | 0x3100: 'usb4715_port::power_state' 52 | 0x3194: 'usb4715_port::connection' 53 | 0x3195: 'usb4715_port::device_speed' 54 | 0x3C00: 'usb4715_port::power_select_1' 55 | 0x3C04: 'usb4715_port::power_select_2' 56 | 0x3C08: 'usb4715_port::power_select_3' 57 | 0x3C0C: 'usb4715_port::power_select_4' 58 | -------------------------------------------------------------------------------- /capablerobot_usbhub/registers/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | from construct import * 24 | 25 | from .main import * 26 | from .port import * 27 | from .gpio import * 28 | 29 | register = Struct( 30 | "addr" / Int16ub, 31 | "len" / Int8ub, 32 | "body" / Switch(this.addr, { 33 | 0: main_revision, 34 | 2304: gpio_output_enable, 35 | 2320: gpio_input_enable, 36 | 2336: gpio_output, 37 | 2352: gpio_input, 38 | 2368: gpio_pull_up, 39 | 2384: gpio_pull_down, 40 | 2400: gpio_open_drain, 41 | 2528: gpio_debounce, 42 | 2544: gpio_debounce_time, 43 | 12288: main_vendor_id, 44 | 12290: main_product_id, 45 | 12292: main_device_id, 46 | 12294: main_hub_configuration, 47 | 12295: main_hub_configuration_2, 48 | 12296: main_hub_configuration_3, 49 | 12548: main_hub_control, 50 | 12538: main_port_swap, 51 | 12695: main_suspend, 52 | 12517: port_power_status, 53 | 12539: port_remap12, 54 | 12540: port_remap34, 55 | 12544: port_power_state, 56 | 12692: port_connection, 57 | 12693: port_device_speed, 58 | 15360: port_power_select_1, 59 | 15364: port_power_select_2, 60 | 15368: port_power_select_3, 61 | 15372: port_power_select_4, 62 | }), 63 | ) 64 | 65 | registers = GreedyRange(register) 66 | -------------------------------------------------------------------------------- /capablerobot_usbhub/registers/port.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | from construct import * 24 | 25 | port_power_status = BitStruct( 26 | Padding(3), 27 | "port4" / BitsInteger(1), 28 | "port3" / BitsInteger(1), 29 | "port2" / BitsInteger(1), 30 | "port1" / BitsInteger(1), 31 | Padding(1), 32 | ) 33 | 34 | port_remap12 = BitStruct( 35 | "port2" / BitsInteger(4), 36 | "port1" / BitsInteger(4), 37 | ) 38 | 39 | port_remap34 = BitStruct( 40 | "port4" / BitsInteger(4), 41 | "port3" / BitsInteger(4), 42 | ) 43 | 44 | port_power_state = BitStruct( 45 | "port3" / BitsInteger(2), 46 | "port2" / BitsInteger(2), 47 | "port1" / BitsInteger(2), 48 | "port0" / BitsInteger(2), 49 | Padding(6), 50 | "port4" / BitsInteger(2), 51 | ) 52 | 53 | port_connection = BitStruct( 54 | Padding(3), 55 | "port4" / BitsInteger(1), 56 | "port3" / BitsInteger(1), 57 | "port2" / BitsInteger(1), 58 | "port1" / BitsInteger(1), 59 | "port0" / BitsInteger(1), 60 | ) 61 | 62 | port_device_speed = BitStruct( 63 | "port4" / BitsInteger(2), 64 | "port3" / BitsInteger(2), 65 | "port2" / BitsInteger(2), 66 | "port1" / BitsInteger(2), 67 | ) 68 | 69 | port_power_select_1 = BitStruct( 70 | "combined_mode" / BitsInteger(1), 71 | "gang_mode" / BitsInteger(1), 72 | Padding(2), 73 | "source" / BitsInteger(4), 74 | ) 75 | 76 | port_power_select_2 = BitStruct( 77 | "combined_mode" / BitsInteger(1), 78 | "gang_mode" / BitsInteger(1), 79 | Padding(2), 80 | "source" / BitsInteger(4), 81 | ) 82 | 83 | port_power_select_3 = BitStruct( 84 | "combined_mode" / BitsInteger(1), 85 | "gang_mode" / BitsInteger(1), 86 | Padding(2), 87 | "source" / BitsInteger(4), 88 | ) 89 | 90 | port_power_select_4 = BitStruct( 91 | "combined_mode" / BitsInteger(1), 92 | "gang_mode" / BitsInteger(1), 93 | Padding(2), 94 | "source" / BitsInteger(4), 95 | ) 96 | 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capable Robot Programmable USB Hub Driver 2 | 3 | This package has two functions: 4 | 5 | - It provides access to internal state of the Capable Robot USB Hub, allowing you to monitor and control the Hub from an upstream computer. 6 | - It creates a transparent CircuitPython Bridge, allowing unmodified CircuitPython code to run on the host computer and interact with I2C & SPI devices attached to the USB Hub. 7 | 8 | ![Capable Robot logo & image of Programmable USB Hub](https://raw.githubusercontent.com/CapableRobot/CapableRobot_USBHub_Driver/master/images/logo-usbhub-header.jpg "Capable Robot logo & image of Programmable USB Hub") 9 | 10 | ## Installing 11 | 12 | Install and update using [pip](https://pip.pypa.io/en/stable/quickstart/): 13 | 14 | pip install capablerobot_usbhub --upgrade 15 | 16 | This driver requires Python 3.6 or newer. 17 | 18 | **Additional Linux Instructions** 19 | 20 | On Linux, the the udev permission system will likely prevent normal users from accessing the USB Hub's endpoint which allows for Hub Monitoring, Control, and I2C Bridging. To resolve this, install the provided udev rule: 21 | 22 | ``` 23 | sudo cp 50-capablerobot-usbhub.rules /etc/udev/rules.d/ 24 | sudo udevadm control --reload 25 | sudo udevadm trigger 26 | ``` 27 | 28 | Then unplug and replug your USB Hub. Note, the provided udev rule allows all system users to access the Hub, but can be changed to a specific user or user group. 29 | 30 | **Additional Windows Instructions** 31 | 32 | On Windows, the low-level USB driver for the Hub's VID/PID must be re-assigned from the default one to the libusbk driver. This can be done with the [Zadig](https://zadig.akeo.ie) GUI, or by executing the following commands: 33 | 34 | ``` 35 | pip install capablerobot_usbregister 36 | usbregister device usbhub 37 | ``` 38 | 39 | For additional details on how the `usbregister` command-line utility work, please vist the [CapableRobot_USBRegister](https://github.com/CapableRobot/CapableRobot_USBRegister) repository. 40 | 41 | ## Usage & Examples 42 | 43 | The [examples](https://github.com/CapableRobot/CapableRobot_USBHub_Driver/tree/master/examples) folder has some code samples of how to use this Python API to control the Programmable USB Hub. There is also another [example repository](https://github.com/CapableRobot/CapableRobot_USBHub_CircuitPython_Examples) which includes additional host-side code as well as examples of customizing behavior via changing the Hub's firmware. 44 | 45 | ## Working Functionality 46 | 47 | - Reading USB Hub registers over USB and decoding of register data. 48 | - Writing USB Hub registers over USB. 49 | - Reading & writing I2C data thru the Hub. 50 | - Python API to control and read the two GPIO pins. 51 | - CircuitPython I2C Bridge to the rear I2C1 port. 52 | - CircuitPython SPI Bridge to the internal mikroBUS header. 53 | 54 | ## Not Working / Not Implemented Yet 55 | 56 | _No known errata at this time_ 57 | 58 | ## Contributing 59 | 60 | Contributions are welcome! Please read our 61 | [Code of Conduct](https://github.com/capablerobot/CapableRobot_CircuitPython_USBHub_Bridge/blob/master/CODE_OF_CONDUCT.md) 62 | before contributing to help this project stay welcoming. -------------------------------------------------------------------------------- /capablerobot_usbhub/registers/main.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | 24 | from construct import * 25 | 26 | main_revision = BitStruct( 27 | "device_id" / BitsInteger(16), 28 | Padding(8), 29 | "revision_id" / BitsInteger(8), 30 | ) 31 | 32 | main_vendor_id = BitStruct( 33 | "value" / BitsInteger(16), 34 | ) 35 | 36 | main_product_id = BitStruct( 37 | "value" / BitsInteger(16), 38 | ) 39 | 40 | main_device_id = BitStruct( 41 | "value" / BitsInteger(16), 42 | ) 43 | 44 | main_hub_configuration = BitStruct( 45 | "self_power" / BitsInteger(1), 46 | "vsm_disable" / BitsInteger(1), 47 | "hs_disable" / BitsInteger(1), 48 | "mtt_enable" / BitsInteger(1), 49 | "eop_disable" / BitsInteger(1), 50 | "current_sense" / BitsInteger(2), 51 | "port_power" / BitsInteger(1), 52 | Padding(2), 53 | "oc_timer" / BitsInteger(2), 54 | "compound" / BitsInteger(1), 55 | Padding(3), 56 | Padding(4), 57 | "prtmap_enable" / BitsInteger(1), 58 | Padding(2), 59 | "string_enable" / BitsInteger(1), 60 | ) 61 | 62 | main_hub_configuration_1 = BitStruct( 63 | "self_power" / BitsInteger(1), 64 | "vsm_disable" / BitsInteger(1), 65 | "hs_disable" / BitsInteger(1), 66 | "mtt_enable" / BitsInteger(1), 67 | "eop_disable" / BitsInteger(1), 68 | "current_sense" / BitsInteger(2), 69 | "port_power" / BitsInteger(1), 70 | ) 71 | 72 | main_hub_configuration_2 = BitStruct( 73 | Padding(2), 74 | "oc_timer" / BitsInteger(2), 75 | "compound" / BitsInteger(1), 76 | Padding(3), 77 | ) 78 | 79 | main_hub_configuration_3 = BitStruct( 80 | Padding(4), 81 | "prtmap_enable" / BitsInteger(1), 82 | Padding(2), 83 | "string_enable" / BitsInteger(1), 84 | ) 85 | 86 | main_port_swap = BitStruct( 87 | Padding(3), 88 | "port4" / BitsInteger(1), 89 | "port3" / BitsInteger(1), 90 | "port2" / BitsInteger(1), 91 | "port1" / BitsInteger(1), 92 | "port0" / BitsInteger(1), 93 | ) 94 | 95 | main_hub_control = BitStruct( 96 | Padding(6), 97 | "lpm_disable" / BitsInteger(1), 98 | "reset" / BitsInteger(1), 99 | ) 100 | 101 | main_suspend = BitStruct( 102 | Padding(7), 103 | "suspend" / BitsInteger(1), 104 | ) 105 | 106 | -------------------------------------------------------------------------------- /capablerobot_usbhub/formats/usb4715_port.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: usb4715_port 3 | title: Microchip USB4715 Port Status Registers 4 | types: 5 | power_status: 6 | doc-ref: Table 3-52 7 | doc: Read only register. Returns State of Port Power Enables. 8 | seq: 9 | - id: reserved1 10 | type: b3 11 | - id: port4 12 | type: b1 13 | - id: port3 14 | type: b1 15 | - id: port2 16 | type: b1 17 | - id: port1 18 | type: b1 19 | - id: reserved2 20 | type: b1 21 | remap12: 22 | doc-ref: Table 3-56 23 | doc: Writes to this register are disabled unless PRTMAP_EN bit in HUB_CFG_3 is set. 24 | seq: 25 | - id: port2 26 | type: b4 27 | - id: port1 28 | type: b4 29 | remap34: 30 | doc-ref: Table 3-57 31 | doc: Writes to this register are disabled unless PRTMAP_EN bit in HUB_CFG_3 is set. 32 | seq: 33 | - id: port4 34 | type: b4 35 | - id: port3 36 | type: b4 37 | power_state: 38 | doc-ref: Table 3-60, 3-61 39 | doc: Read only register. Returns state of downstream ports (normal, sleep, suspend, off). 40 | seq: 41 | - id: port3 42 | type: b2 43 | enum: state 44 | - id: port2 45 | type: b2 46 | enum: state 47 | - id: port1 48 | type: b2 49 | enum: state 50 | - id: port0 51 | type: b2 52 | enum: state 53 | - id: reserved 54 | type: b6 55 | - id: port4 56 | type: b2 57 | enum: state 58 | enums: 59 | state: 60 | 0: normal 61 | 1: sleep 62 | 2: suspend 63 | 3: off 64 | connection: 65 | doc-ref: Table 3-66 66 | seq: 67 | - id: reserved 68 | type: b3 69 | - id: port4 70 | type: b1 71 | - id: port3 72 | type: b1 73 | - id: port2 74 | type: b1 75 | - id: port1 76 | type: b1 77 | - id: port0 78 | type: b1 79 | device_speed: 80 | doc-ref: Table 3-67 81 | seq: 82 | - id: port4 83 | type: b2 84 | enum: state 85 | - id: port3 86 | type: b2 87 | enum: state 88 | - id: port2 89 | type: b2 90 | enum: state 91 | - id: port1 92 | type: b2 93 | enum: state 94 | enums: 95 | state: 96 | 0: none 97 | 1: low 98 | 2: full 99 | 3: high 100 | power_select_1: 101 | doc-ref: Table 3-72 102 | seq: 103 | - id: combined_mode 104 | type: b1 105 | - id: gang_mode 106 | type: b1 107 | - id: reserved 108 | type: b2 109 | - id: source 110 | type: b4 111 | enum: source 112 | enums: 113 | source: 114 | 0: disabled 115 | 1: software 116 | 8: gpio 117 | power_select_2: 118 | doc-ref: Table 3-73 119 | seq: 120 | - id: combined_mode 121 | type: b1 122 | - id: gang_mode 123 | type: b1 124 | - id: reserved 125 | type: b2 126 | - id: source 127 | type: b4 128 | enum: source 129 | enums: 130 | source: 131 | 0: disabled 132 | 1: software 133 | 8: gpio 134 | power_select_3: 135 | doc-ref: Table 3-74 136 | seq: 137 | - id: combined_mode 138 | type: b1 139 | - id: gang_mode 140 | type: b1 141 | - id: reserved 142 | type: b2 143 | - id: source 144 | type: b4 145 | enum: source 146 | enums: 147 | source: 148 | 0: disabled 149 | 1: software 150 | 8: gpio 151 | power_select_4: 152 | doc-ref: Table 3-75 153 | seq: 154 | - id: combined_mode 155 | type: b1 156 | - id: gang_mode 157 | type: b1 158 | - id: reserved 159 | type: b2 160 | - id: source 161 | type: b4 162 | enum: source 163 | enums: 164 | source: 165 | 0: disabled 166 | 1: software 167 | 8: gpio -------------------------------------------------------------------------------- /capablerobot_usbhub/formats/usb4715_main.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: usb4715_main 3 | title: Microchip USB4715 General Registers 4 | types: 5 | revision: 6 | doc-ref: Table 3-4 7 | meta: 8 | endian: le 9 | seq: 10 | - id: device_id 11 | type: b16 12 | - id: reserved 13 | type: b8 14 | - id: revision_id 15 | type: b8 16 | vendor_id: 17 | doc-ref: Table 3-30, 3-31 18 | meta: 19 | endian: le 20 | seq: 21 | - id: value 22 | type: b16 23 | product_id: 24 | doc-ref: Table 3-32, 3-33 25 | meta: 26 | endian: le 27 | seq: 28 | - id: value 29 | type: b16 30 | device_id: 31 | doc-ref: Table 3-34, 3-35 32 | meta: 33 | endian: le 34 | seq: 35 | - id: value 36 | type: b16 37 | hub_configuration: 38 | doc-ref: Table 3-36 thru 3-38 39 | seq: 40 | - id: self_power 41 | type: b1 42 | - id: vsm_disable 43 | type: b1 44 | - id: hs_disable 45 | type: b1 46 | - id: mtt_enable 47 | type: b1 48 | - id: eop_disable 49 | type: b1 50 | - id: current_sense 51 | type: b2 52 | enum: current_sense 53 | - id: port_power 54 | type: b1 55 | doc: 0 -> Ganged switching (all ports together). 1 -> Individual (port by port) switching. 56 | - id: reserved1 57 | type: b2 58 | - id: oc_timer 59 | type: b2 60 | enum: oc_timer 61 | - id: compound 62 | type: b1 63 | - id: reserved2 64 | type: b3 65 | - id: reserved3 66 | type: b4 67 | - id: prtmap_enable 68 | type: b1 69 | - id: reserved4 70 | type: b2 71 | - id: string_enable 72 | type: b1 73 | enums: 74 | current_sense: 75 | 0: ganged_sensing 76 | 1: port_by_port 77 | 2: not_supported 78 | 3: not_supported 79 | oc_timer: 80 | 0: ns50 81 | 1: ns100 82 | 2: ns200 83 | 3: ns400 84 | hub_configuration_1: 85 | doc-ref: Table 3-36 86 | seq: 87 | - id: self_power 88 | type: b1 89 | - id: vsm_disable 90 | type: b1 91 | - id: hs_disable 92 | type: b1 93 | - id: mtt_enable 94 | type: b1 95 | - id: eop_disable 96 | type: b1 97 | - id: current_sense 98 | type: b2 99 | enum: current_sense 100 | - id: port_power 101 | type: b1 102 | doc: 0 -> Ganged switching (all ports together). 1 -> Individual (port by port) switching. 103 | enums: 104 | current_sense: 105 | 0: ganged_sensing 106 | 1: port_by_port 107 | 2: not_supported 108 | 3: not_supported 109 | hub_configuration_2: 110 | doc-ref: Table 3-37 111 | seq: 112 | - id: reserved1 113 | type: b2 114 | - id: oc_timer 115 | type: b2 116 | enum: oc_timer 117 | - id: compound 118 | type: b1 119 | - id: reserved2 120 | type: b3 121 | enums: 122 | oc_timer: 123 | 0: ns50 124 | 1: ns100 125 | 2: ns200 126 | 3: ns400 127 | hub_configuration_3: 128 | doc-ref: Table 3-38 3-38 129 | seq: 130 | - id: reserved1 131 | type: b4 132 | - id: prtmap_enable 133 | type: b1 134 | - id: reserved2 135 | type: b2 136 | - id: string_enable 137 | type: b1 138 | port_swap: 139 | doc-ref: Table 3-55 140 | seq: 141 | - id: reserved 142 | type: b3 143 | - id: port4 144 | type: b1 145 | - id: port3 146 | type: b1 147 | - id: port2 148 | type: b1 149 | - id: port1 150 | type: b1 151 | - id: port0 152 | type: b1 153 | hub_control: 154 | doc-ref: Table 3-62 155 | seq: 156 | - id: reserved 157 | type: b6 158 | - id: lpm_disable 159 | type: b1 160 | - id: reset 161 | type: b1 162 | suspend: 163 | doc-ref: Table 3-69 164 | seq: 165 | - id: reserved 166 | type: b7 167 | - id: suspend 168 | type: b1 -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Capable Robot Components Community Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and leaders pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | * Any spamming, flaming, baiting or other attention-stealing behavior 34 | 35 | ## Our Responsibilities 36 | 37 | Project leaders are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project leaders have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Moderation 48 | 49 | Instances of behaviors that violate the Capable Robot Components Community Code of Conduct 50 | may be reported by any member of the community. Community members are 51 | encouraged to report these situations, including situations they witness 52 | involving other community members. 53 | 54 | You may report by send an email to . 55 | 56 | Email and direct message reports will be kept confidential. 57 | 58 | These are the steps for upholding our community's standards of conduct. 59 | 60 | 1. Any member of the community may report any situation that violates the 61 | Capable Robot Components Community Code of Conduct. All reports will be 62 | reviewed and investigated. 63 | 2. If the behavior is an egregious violation, the community member who 64 | committed the violation may be banned immediately, without warning. 65 | 3. Otherwise, moderators will first respond to such behavior with a warning. 66 | 4. Moderators follow a soft "three strikes" policy - the community member may 67 | be given another chance, if they are receptive to the warning and change their 68 | behavior. 69 | 5. If the community member is unreceptive or unreasonable when warned by a 70 | moderator, or the warning goes unheeded, they may be banned for a first or 71 | second offense. Repeated offenses will result in the community member being 72 | banned. 73 | 74 | ## Scope 75 | 76 | This Code of Conduct applies within all project spaces, and it also applies when 77 | an individual is representing the project or its community in public spaces. 78 | Examples of representing a project or community include using an official 79 | project e-mail address, posting via an official social media account, or acting 80 | as an appointed representative at an online or offline event. Representation of 81 | a project may be further defined and clarified by project leaders. 82 | 83 | ## Attribution 84 | 85 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), [version 1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html), and 86 | the [Adafruit Community Code of Conduct](https://github.com/adafruit/Adafruit_Community_Code_of_Conduc) 87 | 88 | For answers to common questions about this code of conduct, see 89 | https://www.contributor-covenant.org/faq 90 | 91 | -------------------------------------------------------------------------------- /capablerobot_usbhub/util.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import usb.util 24 | import threading 25 | 26 | REQ_OUT = usb.util.build_request_type( 27 | usb.util.CTRL_OUT, 28 | usb.util.CTRL_TYPE_VENDOR, 29 | usb.util.CTRL_RECIPIENT_DEVICE) 30 | 31 | REQ_IN = usb.util.build_request_type( 32 | usb.util.CTRL_IN, 33 | usb.util.CTRL_TYPE_VENDOR, 34 | usb.util.CTRL_RECIPIENT_DEVICE) 35 | 36 | 37 | class Lockable(): 38 | _lock = threading.Lock() 39 | 40 | def acquire_lock(self, blocking=True, timeout=-1): 41 | return self._lock.acquire(blocking=blocking, timeout=timeout) 42 | 43 | def release_lock(self): 44 | return self._lock.release() 45 | 46 | ## The following two methods are expected by CircuitPython device drivers to control 47 | ## access to underlying I2C / SPI hardware of a device. 48 | ## 49 | ## Because the Driver I2C / SPI bridges already do locking, these methods are no-ops. 50 | ## If they also tried to lock & unlock, the software would dead-lock. 51 | 52 | def try_lock(self): 53 | """Attempt to grab the lock. Return True on success, False if the lock is already taken.""" 54 | return True 55 | 56 | def unlock(self): 57 | """Release the lock so others may use the resource.""" 58 | pass 59 | 60 | 61 | def bits_to_bytes(bits): 62 | return int(bits / 8) 63 | 64 | def int_from_bytes(xbytes, endian='big'): 65 | return int.from_bytes(xbytes, endian) 66 | 67 | def hexstr(value): 68 | return hex(value).upper().replace("0X","") 69 | 70 | def build_value(start=True, stop=True, nack=True, addr=0): 71 | flags = 0 72 | if nack: 73 | flags += 4 74 | if start: 75 | flags += 2 76 | if stop: 77 | flags += 1 78 | 79 | return (flags << 8) + addr 80 | 81 | def set_bit(value, bit): 82 | return value | (1< 0 89 | 90 | def set_bit_to(value, bit, state): 91 | if state: 92 | return value | (1<>item.start)&(2**(item.stop-item.start+1)-1) 133 | else: 134 | raise TypeError('non-slice indexing not supported') 135 | 136 | def __int__(self): 137 | return self._val 138 | -------------------------------------------------------------------------------- /capablerobot_usbhub/spi.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import logging 24 | 25 | import usb.core 26 | import usb.util 27 | 28 | from .util import * 29 | 30 | class USBHubSPI(Lockable): 31 | 32 | CMD_SPI_ENABLE = 0x60 33 | CMD_SPI_WRITE = 0x61 34 | CMD_SPI_DISABLE = 0x62 35 | 36 | REG_SPI_DATA = 0x2310 37 | 38 | def __init__(self, hub, enable=False, timeout=100): 39 | self.hub = hub 40 | self.enabled = False 41 | self.timeout = timeout 42 | 43 | if enable: 44 | self.enable() 45 | 46 | def __enter__(self): 47 | while not self.try_lock(): 48 | pass 49 | return self 50 | 51 | def __exit__(self, *exc): 52 | self.unlock() 53 | return False 54 | 55 | def enable(self): 56 | if self.enabled: 57 | return True 58 | 59 | self.acquire_lock() 60 | 61 | try: 62 | self.hub.handle.ctrl_transfer(REQ_OUT+1, self.CMD_SPI_ENABLE, 0, 0, 0, timeout=self.timeout) 63 | except usb.core.USBError: 64 | logging.warn("USB Error in SPI Enable") 65 | self.release_lock() 66 | return False 67 | 68 | self.enabled = True 69 | 70 | self.release_lock() 71 | return True 72 | 73 | def disable(self): 74 | if not self.enabled: 75 | return True 76 | 77 | self.acquire_lock() 78 | 79 | try: 80 | self.hub.handle.ctrl_transfer(REQ_OUT+1, self.CMD_SPI_DISABLE, 0, 0, 0, timeout=self.timeout) 81 | except usb.core.USBError: 82 | logging.warn("USB Error in SPI Disable") 83 | self.release_lock() 84 | return False 85 | 86 | self.enabled = False 87 | 88 | self.release_lock() 89 | return True 90 | 91 | def write(self, buf, start=0, end=None): 92 | if not self.enabled: 93 | self.enable() 94 | 95 | if not buf: 96 | return 97 | 98 | if end is None: 99 | end = len(buf) 100 | 101 | if start-end >= 256: 102 | raise ValueError('SPI interface cannot write a buffer longer than 256 elements') 103 | return False 104 | 105 | self.acquire_lock() 106 | 107 | logging.debug("SPI Write : [{}]".format(" ".join([hex(v) for v in list(buf[start:end])]))) 108 | 109 | value = end - start 110 | length = self.hub.handle.ctrl_transfer(REQ_OUT+1, self.CMD_SPI_WRITE, value, 0, buf[start:end], value, timeout=self.timeout) 111 | 112 | self.release_lock() 113 | return length 114 | 115 | def readinto(self, buf, start=0, end=None, addr='', try_lock=True): 116 | if not self.enabled: 117 | self.enable() 118 | 119 | if not buf: 120 | return 121 | 122 | if end is None: 123 | end = len(buf) 124 | 125 | length = end - start 126 | 127 | ## Need to offset the address for USB access 128 | address = self.REG_SPI_DATA + self.hub.REG_BASE_ALT 129 | 130 | ## Split 32 bit register address into the 16 bit value & index fields 131 | value = address & 0xFFFF 132 | index = address >> 16 133 | 134 | if try_lock: 135 | self.acquire_lock() 136 | 137 | data = list(self.hub.handle.ctrl_transfer(REQ_IN, self.hub.CMD_REG_READ, value, index, length, timeout=self.timeout)) 138 | 139 | if length != len(data): 140 | self.release_lock() 141 | raise OSError('Incorrect data length') 142 | 143 | # 'readinto' the given buffer 144 | for i in range(end-start): 145 | buf[start+i] = data[i] 146 | 147 | logging.debug("SPI Read [{}] : [{}]".format(" ".join([hex(v) for v in addr]), " ".join([hex(v) for v in list(data)]))) 148 | self.release_lock() 149 | 150 | 151 | def write_readinto(self, buffer_out, buffer_in, out_start=0, out_end=None, in_start=0, in_end=None): 152 | if not self.enabled: 153 | self.enable() 154 | 155 | if not buffer_out or not buffer_in: 156 | return 157 | 158 | if out_end is None: 159 | out_end = len(buffer_out) 160 | 161 | if in_end is None: 162 | in_end = len(buffer_in) 163 | 164 | in_length = in_end - in_start 165 | out_length = out_end - out_start 166 | value = out_length + in_length 167 | 168 | self.acquire_lock() 169 | 170 | try: 171 | length = self.hub.handle.ctrl_transfer(REQ_OUT+1, self.CMD_SPI_WRITE, value, 0, buffer_out[out_start:out_end], out_length, timeout=self.timeout) 172 | except usb.core.USBError: 173 | self.release_lock() 174 | raise OSError('Unable to setup SPI write_readinto') 175 | 176 | if length != 1: 177 | self.release_lock() 178 | raise OSError('Incorrect response in write_readinto') 179 | 180 | ## readinto will release the lock created here, and 181 | ## we have not release it, so there is no need to grab a new one 182 | self.readinto(buffer_in, start=in_start, end=in_end, addr=list(buffer_out[out_start:out_end]), try_lock=False) 183 | 184 | -------------------------------------------------------------------------------- /capablerobot_usbhub/gpio.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import logging 24 | 25 | import usb.core 26 | import usb.util 27 | 28 | from .util import * 29 | 30 | _OUTPUT_ENABLE = 0x0900 31 | _INPUT_ENABLE = 0x0910 32 | _OUTPUT = 0x0920 33 | _INPUT = 0x0930 34 | _PULL_UP = 0x0940 35 | _PULL_DOWN = 0x0950 36 | _OPEN_DRAIN = 0x0960 37 | _DEBOUNCE = 0x09E0 38 | _DEBOUNCE_TIME = 0x09F0 39 | 40 | ## GPIO 0 is interally GPIO6, so it's on the 6th bit 41 | ## GPIO 1 is interally GPIO11, so it's on the 11th bit 42 | _GPIO0_BIT = [0, 6] 43 | _GPIO1_BIT = [1, 3] 44 | _GPIOS = [_GPIO0_BIT, _GPIO1_BIT] 45 | 46 | class USBHubGPIO: 47 | 48 | def __init__(self, hub): 49 | self.hub = hub 50 | 51 | self._io0_output_config = None 52 | self._io0_input_config = None 53 | 54 | self._io1_output_config = None 55 | self._io1_input_config = None 56 | 57 | def _read(self, addr): 58 | data, _ = self.hub.register_read(addr=addr, length=4) 59 | return data 60 | 61 | def configure(self, ios=[], output=None, input=None, pull_down=None, pull_up=None, open_drain=None): 62 | 63 | if output is not None: 64 | self.configure_output(ios=ios, value=output) 65 | 66 | if input is not None: 67 | self.configure_input(ios=ios, value=input) 68 | 69 | if pull_down is not None: 70 | self.configure_pull_down(ios=ios, value=pull_down) 71 | 72 | if pull_up is not None: 73 | self.configure_pull_up(ios=ios, value=pull_up) 74 | 75 | if open_drain is not None: 76 | self.configure_open_drain(ios=ios, value=open_drain) 77 | 78 | def _generic_configure(self, addr, ios, value): 79 | current = self._read(addr=addr) 80 | desired = current.copy() 81 | 82 | if 0 in ios: 83 | if addr == _OUTPUT_ENABLE: 84 | self._io0_output_config = value 85 | if addr == _INPUT_ENABLE: 86 | self._io0_input_config = value 87 | 88 | desired[_GPIO0_BIT[0]] = set_bit_to(desired[_GPIO0_BIT[0]], _GPIO0_BIT[1], value) 89 | 90 | if 1 in ios: 91 | if addr == _OUTPUT_ENABLE: 92 | self._io1_output_config = value 93 | if addr == _INPUT_ENABLE: 94 | self._io1_input_config = value 95 | 96 | desired[_GPIO1_BIT[0]] = set_bit_to(desired[_GPIO1_BIT[0]], _GPIO1_BIT[1], value) 97 | 98 | if current != desired: 99 | self.hub.register_write(addr=addr, buf=desired) 100 | 101 | def configure_output(self, ios, value): 102 | self._generic_configure(_OUTPUT_ENABLE, ios, value) 103 | 104 | def configure_input(self, ios, value): 105 | self._generic_configure(_INPUT_ENABLE, ios, value) 106 | 107 | def configure_pull_up(self, ios, value): 108 | self._generic_configure(_PULL_UP, ios, value) 109 | 110 | def configure_pull_down(self, ios, value): 111 | self._generic_configure(_PULL_DOWN, ios, value) 112 | 113 | def configure_open_drain(self, ios, value): 114 | self._generic_configure(_OPEN_DRAIN, ios, value) 115 | 116 | @property 117 | def io(self): 118 | if not self._io0_input_config: 119 | logging.warn("IO0 is not configured as an input, but is being read") 120 | 121 | if not self._io1_input_config: 122 | logging.warn("IO1 is not configured as an input, but is being read") 123 | 124 | value = self._read(addr=_INPUT) 125 | io0 = get_bit(value[_GPIO0_BIT[0]], _GPIO0_BIT[1]) 126 | io1 = get_bit(value[_GPIO1_BIT[0]], _GPIO1_BIT[1]) 127 | 128 | return (io0, io1) 129 | 130 | @property 131 | def io0(self): 132 | if not self._io0_input_config: 133 | logging.warn("IO0 is not configured as an input, but is being read") 134 | 135 | value = self._read(addr=_INPUT) 136 | return get_bit(value[_GPIO0_BIT[0]], _GPIO0_BIT[1]) 137 | 138 | @io0.setter 139 | def io0(self, value): 140 | if not self._io0_output_config: 141 | logging.warn("IO0 is not configured as an output, but is being set") 142 | 143 | current = self._read(addr=_OUTPUT) 144 | desired = current.copy() 145 | desired[_GPIO0_BIT[0]] = set_bit_to(desired[_GPIO0_BIT[0]], _GPIO0_BIT[1], value) 146 | 147 | if current != desired: 148 | self.hub.register_write(addr=_OUTPUT, buf=desired) 149 | 150 | @property 151 | def io1(self): 152 | if not self._io1_input_config: 153 | logging.warn("IO1 is not configured as an input, but is being read") 154 | 155 | value = self._read(addr=_INPUT) 156 | return get_bit(value[_GPIO1_BIT[0]], _GPIO1_BIT[1]) 157 | 158 | @io1.setter 159 | def io1(self, value): 160 | if not self._io1_output_config: 161 | logging.warn("IO1 is not configured as an output, but is being set") 162 | 163 | current = self._read(addr=_OUTPUT) 164 | desired = current.copy() 165 | desired[_GPIO1_BIT[0]] = set_bit_to(desired[_GPIO1_BIT[0]], _GPIO1_BIT[1], value) 166 | 167 | if current != desired: 168 | self.hub.register_write(addr=_OUTPUT, buf=desired) 169 | -------------------------------------------------------------------------------- /capablerobot_usbhub/config.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import logging 24 | import time 25 | 26 | import usb.core 27 | import usb.util 28 | 29 | from .util import * 30 | 31 | _MEM_IDENT = 0x0904 32 | _MEM_WRITE = 0x0914 33 | _MEM_READ = 0x0924 34 | 35 | _CRC8_POLYNOMIAL = 0x31 36 | _CRC8_INIT = 0xFF 37 | 38 | _CMD_NOOP = 0b000 39 | _CMD_GET = 0b001 40 | _CMD_SET = 0b010 41 | _CMD_SAVE = 0b100 42 | _CMD_RESET = 0b111 43 | 44 | _WAIT = 0.1 45 | 46 | _NAME_RO = [ 47 | 'power_errors', 48 | 'power_measure_12', 49 | 'power_measure_34', 50 | ] 51 | 52 | _NAME_ADDR = dict( 53 | data_state = 0x05, 54 | # power_errors = 0x06, 55 | power_limits = 0x07, 56 | power_measure_12 = 0x08, 57 | power_measure_34 = 0x09, 58 | highspeed_disable = 0x10, 59 | loop_delay = 0x11, 60 | external_heartbeat = 0x12, 61 | ) 62 | 63 | def _generate_crc(data): 64 | crc = _CRC8_INIT 65 | 66 | for byte in data: 67 | crc ^= byte 68 | for _ in range(8): 69 | if crc & 0x80: 70 | crc = (crc << 1) ^ _CRC8_POLYNOMIAL 71 | else: 72 | crc <<= 1 73 | 74 | return crc & 0xFF 75 | 76 | class USBHubConfig: 77 | 78 | def __init__(self, hub, clear=False): 79 | self.hub = hub 80 | self._version = None 81 | 82 | if clear: 83 | self.clear() 84 | 85 | @property 86 | def version(self): 87 | if self._version is None: 88 | buf, _ = self.hub.register_read(addr=_MEM_IDENT, length=4) 89 | self._version = buf 90 | 91 | return self._version[0] 92 | 93 | @property 94 | def circuitpython_version(self): 95 | if self._version is None: 96 | buf, _ = self.hub.register_read(addr=_MEM_IDENT, length=4) 97 | self._version = buf 98 | 99 | return ".".join([str(v) for v in self._version[1:4]]) 100 | 101 | def _read(self): 102 | buf, _ = self.hub.register_read(addr=_MEM_READ, length=4) 103 | crc = _generate_crc(buf[0:3]) 104 | 105 | if crc == buf[3]: 106 | self.hub.register_write(addr=_MEM_READ, buf=[0,0,0,0]) 107 | return buf[0:3] 108 | 109 | return None 110 | 111 | def _write_okay(self): 112 | buf, _ = self.hub.register_read(addr=_MEM_WRITE, length=4) 113 | 114 | if buf[0] >> 5 == _CMD_NOOP: 115 | return True 116 | 117 | return False 118 | 119 | def _write(self, buf): 120 | crc = _generate_crc(buf[0:3]) 121 | buf = buf[0:3] + [crc] 122 | return self.hub.register_write(addr=_MEM_WRITE, buf=buf) 123 | 124 | def read(self): 125 | buf = self._read() 126 | 127 | if buf is None: 128 | return _CMD_NOOP, None, None 129 | 130 | cmd = buf[0] >> 5 131 | name = buf[0] & 0b11111 132 | value = buf[1] << 8 | buf[2] 133 | 134 | return cmd, name, value 135 | 136 | def write(self, cmd, name=None, value=0): 137 | if name is None: 138 | name_addr = 0 139 | else: 140 | name_addr = _NAME_ADDR[name] 141 | 142 | if name_addr > 0b11111: 143 | logging.error("Address of name '{}' is above 5 bit limit".format(name)) 144 | 145 | while not self._write_okay(): 146 | time.sleep(_WAIT) 147 | 148 | self._write([cmd << 5 | name_addr, (value >> 8) & 0xFF, value & 0xFF]) 149 | 150 | def clear(self): 151 | self.hub.register_write(addr=_MEM_READ, buf=[0,0,0,0]) 152 | self.hub.register_write(addr=_MEM_WRITE, buf=[0,0,0,0]) 153 | 154 | def device_info(self): 155 | return dict( 156 | firmware = self.version, 157 | circuitpython = self.circuitpython_version.split(".") 158 | ) 159 | 160 | def reset(self, target="usb"): 161 | targets = ["usb", "mcu", "bootloader"] 162 | self.write(_CMD_RESET, value=targets.index(target)) 163 | 164 | def save(self): 165 | info = self.device_info() 166 | 167 | if info["circuitpython"][0] == 5 and info["circuitpython"][1] < 2: 168 | logging.error("MCU must be upgraded to CircuitPython 5.2.0 or newer for filesystem saves to work.") 169 | return 170 | 171 | self.write(_CMD_SAVE) 172 | 173 | out = self.read() 174 | while out[0] != _CMD_SAVE: 175 | time.sleep(_WAIT) 176 | out = self.read() 177 | 178 | if out[2] == 0: 179 | logging.error("Save of the config.ini file failed.") 180 | logging.error("Please unmount the CIRCUITPY volume and try again.") 181 | 182 | return out[2] 183 | 184 | def get(self, name): 185 | self.write(_CMD_GET, name=name) 186 | 187 | out = self.read() 188 | while out[0] != _CMD_GET: 189 | time.sleep(_WAIT) 190 | out = self.read() 191 | 192 | return out[2] 193 | 194 | def set(self, name, value): 195 | if name in _NAME_RO: 196 | raise ValueError("Cannot set read-only parameter '{}'".format(name)) 197 | 198 | self.write(_CMD_SET, name=name, value=value) 199 | 200 | out = self.read() 201 | while out[0] != _CMD_SET: 202 | time.sleep(_WAIT) 203 | out = self.read() 204 | 205 | return out[2] 206 | -------------------------------------------------------------------------------- /capablerobot_usbhub/i2c.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import logging 24 | import sys 25 | import time 26 | 27 | import usb.core 28 | import usb.util 29 | 30 | from .util import * 31 | 32 | class USBHubI2C(Lockable): 33 | 34 | CMD_I2C_ENTER = 0x70 35 | CMD_I2C_WRITE = 0x71 36 | CMD_I2C_READ = 0x72 37 | 38 | def __init__(self, hub, timeout=1000, attempts_max=5, attempt_delay=50, fake_probe=True): 39 | self.hub = hub 40 | self.enabled = False 41 | 42 | self.timeout = timeout 43 | 44 | ## Convert from milliseconds to seconds for sleep call 45 | self.attempt_delay = float(attempt_delay)/1000.0 46 | self.attempts_max = attempts_max 47 | 48 | self.fake_probe = fake_probe 49 | 50 | self.enable() 51 | 52 | def enable(self, freq=100): 53 | 54 | if self.enabled: 55 | return True 56 | 57 | value = 0x3131 58 | # if freq == 100: 59 | # value = 0x3131 60 | # elif freq == 400: 61 | # value = 0x0A00 62 | # elif freq == 250: 63 | # value = 0x081B 64 | # elif freq == 200: 65 | # value = 0x1818 66 | # elif freq == 80: 67 | # value = 0x3D3E 68 | # elif freq == 50: 69 | # value = 0x6363 70 | 71 | if freq != 100: 72 | raise ValueError('Currently only 100 kHz I2C operation is supported') 73 | 74 | self.acquire_lock() 75 | 76 | try: 77 | self.hub.handle.ctrl_transfer(REQ_OUT+1, self.CMD_I2C_ENTER, value, 0, 0, timeout=self.timeout) 78 | except usb.core.USBError: 79 | self.release_lock() 80 | return False 81 | 82 | self.enabled = True 83 | 84 | self.release_lock() 85 | return True 86 | 87 | 88 | def write_bytes(self, addr, buf): 89 | """Write many bytes to the specified device. buf is a bytearray""" 90 | 91 | self.acquire_lock() 92 | 93 | # Passed in address is in 7-bit form, so shift it 94 | # and add the start / stop flags 95 | cmd = build_value(addr=(addr << 1)) 96 | 97 | attempts = 0 98 | length = None 99 | 100 | while attempts < self.attempts_max: 101 | attempts += 1 102 | try: 103 | length = self.hub.handle.ctrl_transfer(REQ_OUT+1, self.CMD_I2C_WRITE, cmd, 0, list(buf), timeout=self.timeout) 104 | break 105 | except usb.core.USBError: 106 | time.sleep(self.attempt_delay) 107 | 108 | if attempts >= self.attempts_max: 109 | self.release_lock() 110 | raise OSError('Unable to perform sucessful I2C write') 111 | if attempts == 1: 112 | logging.debug("I2C : Retry Write") 113 | 114 | self.release_lock() 115 | return length 116 | 117 | def read_bytes(self, addr, number, try_lock=True): 118 | """Read many bytes from the specified device.""" 119 | 120 | if try_lock: 121 | self.acquire_lock() 122 | 123 | # Passed in address is in 7-bit form, so shift it 124 | # and add the start / stop flags 125 | cmd = build_value(addr=(addr<<1)+1) 126 | 127 | attempts = 0 128 | data = None 129 | 130 | while attempts < self.attempts_max: 131 | attempts += 1 132 | try: 133 | data = list(self.hub.handle.ctrl_transfer(REQ_IN+1, self.CMD_I2C_READ, cmd, 0, number, timeout=self.timeout)) 134 | break 135 | except usb.core.USBError: 136 | time.sleep(self.attempt_delay) 137 | 138 | if attempts >= self.attempts_max: 139 | self.release_lock() 140 | raise OSError('Unable to perform sucessful I2C read') 141 | if attempts == 1: 142 | logging.debug("I2C : Retry Read") 143 | 144 | self.release_lock() 145 | return data 146 | 147 | def read_i2c_block_data(self, addr, register, number=32): 148 | """Perform a read from the specified cmd register of device. Length number 149 | of bytes (default of 32) will be read and returned as a bytearray. 150 | """ 151 | 152 | self.acquire_lock() 153 | 154 | # Passed in address is in 7-bit form, so shift it 155 | # and add the start / stop flags 156 | i2c_addr = addr << 1 157 | cmd = build_value(addr=i2c_addr, nack=False) 158 | 159 | attempts = 0 160 | 161 | while attempts < self.attempts_max: 162 | attempts += 1 163 | try: 164 | length = self.hub.handle.ctrl_transfer(REQ_OUT+1, self.CMD_I2C_WRITE, cmd, 0, [register], timeout=self.timeout) 165 | break 166 | except usb.core.USBError as e: 167 | if "permission" in str(e): 168 | self.hub.main.print_permission_instructions() 169 | self.release_lock() 170 | sys.exit(0) 171 | 172 | time.sleep(self.attempt_delay) 173 | 174 | if attempts >= self.attempts_max: 175 | self.release_lock() 176 | raise OSError('Unable to perform sucessful I2C read block data') 177 | if attempts == 1: 178 | logging.debug("I2C : Retry Read Block") 179 | 180 | if length != 1: 181 | self.release_lock() 182 | raise OSError('Unable to perform sucessful I2C read block data') 183 | 184 | ## read_bytes will release the lock created here, and 185 | ## we have not release it, so there is no need to grab a new one 186 | return self.read_bytes(addr, number, try_lock=False) 187 | 188 | def writeto(self, address, buffer, *, start=0, end=None, stop=True): 189 | if end is None: 190 | end = len(buffer) 191 | 192 | if self.fake_probe and buffer == b'': 193 | logging.debug("I2C : skipping probe of {}".format(address)) 194 | return 195 | 196 | self.write_bytes(address, buffer[start:end]) 197 | 198 | def readfrom_into(self, address, buffer, *, start=0, end=None, stop=True): 199 | if end is None: 200 | end = len(buffer) 201 | 202 | readin = self.read_bytes(address, end-start) 203 | for i in range(end-start): 204 | buffer[i+start] = readin[i] 205 | 206 | def writeto_then_readfrom(self, address, buffer_out, buffer_in, *, 207 | out_start=0, out_end=None, 208 | in_start=0, in_end=None, stop=False): 209 | if out_end is None: 210 | out_end = len(buffer_out) 211 | if in_end is None: 212 | in_end = len(buffer_in) 213 | if stop: 214 | # To generate a stop in linux, do in two transactions 215 | self.writeto(address, buffer_out, start=out_start, end=out_end, stop=True) 216 | self.readfrom_into(address, buffer_in, start=in_start, end=in_end) 217 | else: 218 | # To generate without a stop, do in one block transaction 219 | if out_end-out_start != 1: 220 | raise NotImplementedError("Currently can only write a single byte in writeto_then_readfrom") 221 | readin = self.read_i2c_block_data(address, buffer_out[out_start:out_end][0], in_end-in_start) 222 | for i in range(in_end-in_start): 223 | buffer_in[i+in_start] = readin[i] -------------------------------------------------------------------------------- /capablerobot_usbhub/console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | 25 | import os, sys, inspect 26 | import time 27 | import argparse 28 | import logging 29 | 30 | import click 31 | import usb 32 | 33 | # This allows this file to be run directly and still be able to import the 34 | # driver library -- as relative imports do not work without running as a package. 35 | # If this library is installed, the else path is followed and relative imports work as expected. 36 | if __name__ == '__main__' and __package__ is None: 37 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 38 | from capablerobot_usbhub.main import USBHub 39 | else: 40 | from .main import USBHub 41 | 42 | 43 | 44 | COL_WIDTH = 12 45 | PORTS = ["Port {}".format(num) for num in [1,2,3,4]] 46 | 47 | 48 | def _print_row(data): 49 | print(*[str(v).rjust(COL_WIDTH) for v in data]) 50 | 51 | def setup_logging(): 52 | fmtstr = '%(asctime)s | %(filename)25s:%(lineno)4d %(funcName)20s() | %(levelname)7s | %(message)s' 53 | formatter = logging.Formatter(fmtstr) 54 | 55 | handler = logging.StreamHandler(sys.stdout) 56 | handler.setLevel(logging.DEBUG) 57 | handler.setFormatter(formatter) 58 | 59 | logger = logging.getLogger() 60 | logger.setLevel(logging.DEBUG) 61 | logger.addHandler(handler) 62 | 63 | @click.group() 64 | @click.option('--hub', 'key', default='0', help='Numeric index or key (last 4 characters of serial number) or USB path (\'bus-address\') of Hub for command to operate on.') 65 | @click.option('--verbose', default=False, is_flag=True, help='Increase logging level.') 66 | @click.option('--disable-i2c', default=False, is_flag=True, help='Disable I2C bus access.') 67 | def cli(key, verbose, disable_i2c): 68 | global hub 69 | 70 | if verbose: 71 | setup_logging() 72 | logging.debug("Logging Setup") 73 | 74 | hub = USBHub(device=dict(disable_i2c=disable_i2c)) 75 | 76 | if len(key) == hub.KEY_LENGTH or "-" in key: 77 | ## CLI parameter is the string key of the hub or the USB path, so directly use it 78 | result = hub.activate(key) 79 | 80 | if result is None: 81 | if "-" in key: 82 | print("Cannot locate Hub at USB path '{}'".format(key)) 83 | else: 84 | print("Cannot locate Hub with serial number ending in '{}'".format(key)) 85 | sys.exit(0) 86 | 87 | else: 88 | ## CLI parameter is the index of the hub, so convert to int 89 | try: 90 | result = hub.activate(int(key)) 91 | except ValueError: 92 | print("Cannot locate Hub at index '{}' as it is not an integer".format(key)) 93 | sys.exit(0) 94 | 95 | if result is None: 96 | print("Can't attach to Hub with index {} as there only {} were detected".format(key, len(hub.devices))) 97 | sys.exit(0) 98 | 99 | @cli.command() 100 | def id(): 101 | """Print serial number for attached hub""" 102 | 103 | for idx, key in enumerate(hub.devices): 104 | hub.activate(key) 105 | device = hub.device 106 | 107 | print("Hub Key : {} ({})".format(device.key, idx)) 108 | print("MPN : {}".format(device.mpn)) 109 | print("Revision : {}".format(device.revision)) 110 | print("Serial : {}".format(device.serial)) 111 | print("USB Path : {}".format(device.usb_path)) 112 | 113 | print() 114 | 115 | @cli.group() 116 | def data(): 117 | """Sub-commands for data control & monitoring""" 118 | pass 119 | 120 | @data.command() 121 | @click.option('--port', default=None, help='Comma separated list of ports (1 thru 4) to act upon.') 122 | @click.option('--on', default=False, is_flag=True, help='Enable data to the listed ports.') 123 | @click.option('--off', default=False, is_flag=True, help='Disable data to the listed ports.') 124 | def state(port, on, off): 125 | """ Get or set per-port data state. With no arguments, will print out if port data is on or off. """ 126 | 127 | if on and off: 128 | print("Error : Please specify '--on' or '--off', not both.") 129 | return 130 | 131 | if on or off: 132 | if port is None: 133 | print("Error : Please specify at least one port with '--port' flag") 134 | return 135 | else: 136 | port = [int(p) for p in port.split(",")] 137 | 138 | if on: 139 | hub.data_enable(ports=port) 140 | elif off: 141 | hub.data_disable(ports=port) 142 | else: 143 | _print_row(PORTS) 144 | _print_row(hub.data_state()) 145 | _print_row(hub.speeds()) 146 | 147 | @cli.group() 148 | def power(): 149 | """Sub-commands for power control & monitoring""" 150 | pass 151 | 152 | @power.command() 153 | @click.option('--loop', default=False, is_flag=True, help='Continue to output data until CTRL-C.') 154 | @click.option('--delay', default=500, help='Delay in ms between current samples.') 155 | def measure(loop, delay): 156 | """ Reports single-shot or continuous power measurements. """ 157 | 158 | if loop: 159 | delay_ms = float(delay)/1000 160 | start = time.time() 161 | 162 | while True: 163 | try: 164 | ellapsed = time.time() - start 165 | data = hub.power.measurements() 166 | 167 | print("%.3f" % ellapsed, " ".join([("%.2f" % v).rjust(7) for v in data])) 168 | except usb.core.USBError: 169 | pass 170 | 171 | time.sleep(delay_ms) 172 | else: 173 | _print_row(PORTS) 174 | _print_row(["%.2f mA" % v for v in hub.power.measurements()]) 175 | 176 | @power.command() 177 | @click.option('--port', default=None, help='Comma separated list of ports (1 thru 4) to act upon.') 178 | @click.option('--ma', default=2670, help='Current limit in mA for specified ports.') 179 | def limits(port, ma): 180 | """ Get or set per-port current limits. With no arguments, will print out active limits. """ 181 | 182 | if port is None: 183 | _print_row(PORTS) 184 | _print_row(["{} mA".format(v) for v in hub.power.limits()]) 185 | else: 186 | try: 187 | port = [int(p) for p in port.split(",")] 188 | hub.power.set_limits(port, ma) 189 | except ValueError as e: 190 | print(e) 191 | 192 | @power.command() 193 | def alerts(): 194 | """ Print any power alerts on the downstream ports. """ 195 | data = hub.power.alerts() 196 | 197 | if len(data) == 0: 198 | print(" -- no alerts --") 199 | 200 | for alert in data: 201 | print(alert) 202 | 203 | @power.command() 204 | @click.option('--port', default=None, help='Comma separated list of ports (1 thru 4) to act upon.') 205 | @click.option('--on', default=False, is_flag=True, help='Enable power to the listed ports.') 206 | @click.option('--off', default=False, is_flag=True, help='Disable power to the listed ports.') 207 | @click.option('--reset', default=False, is_flag=True, help='Reset power to the listed ports (cycles power off & on).') 208 | @click.option('--delay', default=500, help='Delay in ms between off and on states during reset action.') 209 | def state(port, on, off, reset, delay): 210 | """ Get or set per-port power state. With no arguments, will print out if port power is on or off. """ 211 | 212 | if on and off: 213 | print("Error : Please specify '--on' or '--off', not both.") 214 | return 215 | 216 | if on or off or reset: 217 | if port is None: 218 | print("Error : Please specify at least one port with '--port' flag") 219 | return 220 | else: 221 | port = [int(p) for p in port.split(",")] 222 | 223 | if on: 224 | hub.power.enable(ports=port) 225 | elif off: 226 | hub.power.disable(ports=port) 227 | elif reset: 228 | hub.power.disable(ports=port) 229 | time.sleep(float(delay)/1000) 230 | hub.power.enable(ports=port) 231 | else: 232 | _print_row(PORTS) 233 | _print_row(["on" if s else "off" for s in hub.power.state()]) 234 | 235 | 236 | def main(): 237 | cli() 238 | 239 | if __name__ == '__main__': 240 | main() -------------------------------------------------------------------------------- /capablerobot_usbhub/main.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import os 24 | import glob 25 | import yaml 26 | import struct 27 | import time 28 | import logging 29 | import copy 30 | import subprocess 31 | import weakref 32 | import sys 33 | from typing import Dict 34 | 35 | import usb.core 36 | import usb.util 37 | 38 | from .registers import registers 39 | from .device import USBHubDevice 40 | from .util import * 41 | 42 | PORT_MAP = ["port2", "port4", "port1", "port3"] 43 | 44 | REGISTER_NEEDS_PORT_REMAP = [ 45 | 'port::connection', 46 | 'port::device_speed' 47 | ] 48 | 49 | class USBHub: 50 | 51 | ID_PRODUCT = 0x494C 52 | ID_VENDOR = 0x0424 53 | 54 | KEY_LENGTH = 4 55 | 56 | def __init__(self, vendor=None, product=None, device={}): 57 | if vendor == None: 58 | vendor = self.ID_VENDOR 59 | if product == None: 60 | product = self.ID_PRODUCT 61 | 62 | this_dir = os.path.dirname(os.path.abspath(__file__)) 63 | 64 | self._active_device = None 65 | self._device_keys = [] 66 | self._device_paths = [] 67 | 68 | self.backend = None 69 | self.device_kwargs = device 70 | 71 | if sys.platform.startswith('win'): 72 | import usb.backend.libusb1 73 | import platform 74 | 75 | arch = platform.architecture()[0] 76 | dllpath = os.path.abspath(os.path.join( 77 | this_dir, "windows", arch, "libusb-1.0.dll") 78 | ) 79 | 80 | logging.debug("Assigning pyusb {} backend".format(arch)) 81 | 82 | if not os.path.exists(dllpath): 83 | logging.warn("{} was not located".format(os.path.basename(dllpath))) 84 | 85 | self.backend = usb.backend.libusb1.get_backend(find_library=lambda x: dllpath) 86 | 87 | self.devices: Dict[str, USBHubDevice] = {} 88 | self.attach(vendor, product) 89 | 90 | self.definition = {} 91 | 92 | for file in glob.glob("%s/formats/*.ksy" % this_dir): 93 | key = os.path.basename(file).replace(".ksy","") 94 | self.definition[key] = yaml.load(open(file), Loader=yaml.SafeLoader) 95 | 96 | # Extract the dictionary of register addresses to names 97 | # Flip the keys and values (name will now be key) 98 | # Add number of bytes to the mapping table, extracted from the YAML file 99 | # 100 | # Register names (keys) have the 'DEVICE_' prefix removed from them 101 | # but still have the '::' and '_' separators 102 | mapping = self.definition['usb4715']['types']['register']['seq'][-1]['type']['cases'] 103 | mapping = {v:k for k,v in mapping.items()} 104 | self.mapping = {k.replace('usb4715_',''):[v,self.get_register_length(k),self.get_register_endian(k)] for k,v in mapping.items()} 105 | 106 | # Function to extract and sum the number of bits in each register definition. 107 | # For this to function correctly, all lengths MUST be in bit lengths 108 | # Key is split into namespace to correctly locate the right sequence field 109 | def get_register_length(self, key): 110 | key = key.split("::") 111 | seq = self.definition[key[0]]['types'][key[1]]['seq'] 112 | return sum([int(v['type'].replace('b','')) for v in seq]) 113 | 114 | def get_register_endian(self, key): 115 | key = key.split("::") 116 | obj = self.definition[key[0]]['types'][key[1]] 117 | 118 | if 'meta' in obj: 119 | if 'endian' in obj['meta']: 120 | value = obj['meta']['endian'] 121 | if value == 'le': 122 | return 'little' 123 | 124 | return 'big' 125 | 126 | def find_register_name_by_addr(self, register): 127 | for name, value in self.mapping.items(): 128 | if value[0] == register: 129 | return name 130 | 131 | raise ValueError("Unknown register address : %s" % hex(register)) 132 | 133 | def find_register_by_name(self, name): 134 | 135 | if name in self.mapping: 136 | register, bits, endian = self.mapping[name] 137 | 138 | if bits in [8, 16, 24, 32]: 139 | return register, bits, endian 140 | else: 141 | raise ValueError("Register %s has %d bits" % (name, bits)) 142 | else: 143 | raise ValueError("Unknown register name : %s" % name) 144 | 145 | @property 146 | def device(self): 147 | return self.devices[self._active_device] 148 | 149 | def activate(self, selector): 150 | 151 | if isinstance(selector, int): 152 | if selector >= len(self.devices): 153 | key = None 154 | else: 155 | key = self._device_keys[selector] 156 | 157 | else: 158 | if selector in self._device_keys: 159 | key = selector 160 | elif selector in self._device_paths: 161 | key = self._device_keys[self._device_paths.index(selector)] 162 | else: 163 | key = None 164 | 165 | self._active_device = key 166 | return self._active_device 167 | 168 | 169 | def attach(self, vendor=ID_VENDOR, product=ID_PRODUCT): 170 | logging.debug("Looking for USB Hubs") 171 | 172 | kwargs = dict( 173 | idVendor=vendor, idProduct=product, find_all=True 174 | ) 175 | 176 | if self.backend is not None: 177 | kwargs['backend'] = self.backend 178 | 179 | handles = list(usb.core.find(**kwargs)) 180 | 181 | logging.debug("Found {} Hub(s)".format(len(handles))) 182 | 183 | if handles is None or len(handles) == 0: 184 | raise RuntimeError('No USB Hub was found') 185 | 186 | for handle in handles: 187 | device = USBHubDevice(weakref.proxy(self), handle, **self.device_kwargs) 188 | self.devices[device.key] = device 189 | 190 | self._active_device = device.key 191 | self._device_keys.append(device.key) 192 | self._device_paths.append(device.usb_path) 193 | 194 | 195 | def print_register(self, data): 196 | meta = {} 197 | body = data.body 198 | 199 | # Allows for printing of KaiTai and Construct objects 200 | # Construct containers already inherit from dict, but 201 | # KaiTai objects need to be converted via vars call 202 | if not isinstance(body, dict): 203 | body = vars(data.body) 204 | 205 | for key, value in body.items(): 206 | if key.startswith("reserved") or key[0] == "_": 207 | continue 208 | 209 | meta[key] = value 210 | 211 | addr = hex(data.addr).upper().replace("0X","0x") 212 | name = self.find_register_name_by_addr(data.addr) 213 | 214 | print("%s %s" % (addr, name) ) 215 | for key in sorted(meta.keys()): 216 | value = meta[key] 217 | print(" %s : %s" % (key, hex(value))) 218 | 219 | def parse_register(self, name, stream): 220 | parsed = registers.parse(stream)[0] 221 | 222 | if name in REGISTER_NEEDS_PORT_REMAP: 223 | raw = copy.deepcopy(parsed) 224 | 225 | for key, value in raw.body.items(): 226 | if key in PORT_MAP: 227 | port = PORT_MAP[int(key.replace("port",""))-1] 228 | parsed.body[port] = value 229 | 230 | return parsed 231 | 232 | def connections(self): 233 | return self.device.connections() 234 | 235 | def speeds(self): 236 | return self.device.speeds() 237 | 238 | @property 239 | def serial(self): 240 | return self.device.serial 241 | 242 | @property 243 | def sku(self): 244 | return self.device.sku 245 | 246 | @property 247 | def mpn(self): 248 | return self.device.mpn 249 | 250 | @property 251 | def rev(self): 252 | return self.device.rev 253 | 254 | @property 255 | def revision(self): 256 | return self.device.revision 257 | 258 | def data_state(self): 259 | return self.device.data_state() 260 | 261 | def data_enable(self, ports=[]): 262 | return self.device.data_enable(ports) 263 | 264 | def data_disable(self, ports=[]): 265 | return self.device.data_disable(ports) 266 | 267 | def register_read(self, name=None, addr=None, length=1, print=False, endian='big'): 268 | return self.device.register_read(name, addr, length, print, endian) 269 | 270 | def register_write(self, name=None, addr=None, buf=[]): 271 | return self.device.register_write(name, addr, buf) 272 | 273 | @property 274 | def power(self): 275 | return self.device.power 276 | 277 | @property 278 | def gpio(self): 279 | return self.device.gpio 280 | 281 | @property 282 | def i2c(self): 283 | return self.device.i2c 284 | 285 | @property 286 | def spi(self): 287 | return self.device.spi 288 | 289 | @property 290 | def config(self): 291 | return self.device.config 292 | 293 | 294 | def print_permission_instructions(self): 295 | message = ['User has insufficient permissions to access the USB Hub.'] 296 | 297 | ## Check that this linux distro has 'udevadm' before instructing 298 | ## the user on how to install udev rule. 299 | check = subprocess.run(["which", "udevadm"], stdout=subprocess.PIPE) 300 | check = check.stdout.decode("utf-8") 301 | 302 | if len(check) > 0 and check[0] == "/": 303 | folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 304 | 305 | message += [ 306 | 'Please run the following commands, then unplug and re-plug your Hub.', 307 | '', 308 | "sudo cp {}/50-capablerobot-usbhub.rules /etc/udev/rules.d/".format(folder), 309 | 'sudo udevadm control --reload', 310 | 'sudo udevadm trigger' 311 | ] 312 | 313 | print() 314 | for line in message: 315 | print(line) 316 | print() -------------------------------------------------------------------------------- /capablerobot_usbhub/power.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | from .util import * 24 | 25 | ADDR_USC12 = 0x57 26 | ADDR_USC34 = 0x56 27 | 28 | _PORT_CONTROL = 0x3C00 29 | 30 | _PORT1_CURRENT = 0x00 31 | _PORT2_CURRENT = 0x01 32 | _PORT_STATUS = 0x02 33 | _INTERRUPT1 = 0x03 34 | _INTERRUPT2 = 0x04 35 | _CONFIG1 = 0x11 36 | _CONFIG2 = 0x12 37 | _CURRENT_LIMIT = 0x14 38 | 39 | _CURRENT_MAPPING = [ 40 | 530, 41 | 960, 42 | 1070, 43 | 1280, 44 | 1600, 45 | 2130, 46 | 2670, 47 | 3200 48 | ] 49 | 50 | class USBHubPower: 51 | 52 | def __init__(self, hub): 53 | self.hub = hub 54 | self.i2c = hub.i2c 55 | 56 | self._control_registers = None 57 | 58 | def control_register(self, port): 59 | if self._control_registers is None: 60 | # Interconnect between Hub IC power control pins and downstream power control devices 61 | # changed between REV 1 and REV 2. Therefore, we have to look at the hardware revision 62 | # to know which register controls which physical port (even though the logical data 63 | # connection to the Hub IC did not change between REV 1 and REV 2) 64 | # 65 | # If hub revision cannot be querried, then we assume that revision is below 2. This should 66 | # be correct as all revision 2 Hubs shipped with firmware which puts revision into the 67 | # 'deviceid' register upon boot up. All Hubs with recent firmware will put revision 68 | # into the 'deviceid' register, regardless of hardware revision. The revision query will 69 | # fail only when I2C has been set to off, and Hub firmware is old. 70 | 71 | if self.hub.revision is None or self.hub.revision < 2: 72 | self._control_registers = [_PORT_CONTROL+(port-1)*4 for port in [1,2,3,4]] 73 | else: 74 | self._control_registers = [_PORT_CONTROL+(port-1)*4 for port in [3,1,4,2]] 75 | 76 | return self._control_registers[port] 77 | 78 | def state(self, ports=[1,2,3,4]): 79 | out = [] 80 | 81 | for port in ports: 82 | data, _ = self.hub.register_read(addr=self.control_register(port-1)) 83 | out.append(get_bit(data[0], 0)) 84 | 85 | return out 86 | 87 | def disable(self, ports=[]): 88 | for port in ports: 89 | self.hub.register_write(addr=self.control_register(port-1), buf=[0x80]) 90 | 91 | def enable(self, ports=[]): 92 | for port in ports: 93 | self.hub.register_write(addr=self.control_register(port-1), buf=[0x81]) 94 | 95 | def measurements(self, ports=[1,2,3,4]): 96 | TO_MA = 13.3 97 | 98 | out = [] 99 | 100 | if self.hub.config.version > 1: 101 | if 1 in ports or 2 in ports: 102 | data = self.hub.config.get("power_measure_12") 103 | if 1 in ports: 104 | out.append(float(data & 0xFF) * TO_MA) 105 | if 2 in ports: 106 | out.append(float((data >> 8) & 0xFF ) * TO_MA) 107 | 108 | if 3 in ports or 4 in ports: 109 | data = self.hub.config.get("power_measure_34") 110 | if 3 in ports: 111 | out.append(float(data & 0xFF) * TO_MA) 112 | if 4 in ports: 113 | out.append(float((data >> 8) & 0xFF ) * TO_MA) 114 | 115 | return out 116 | 117 | for port in ports: 118 | if port == 1 or port == 2: 119 | i2c_addr = ADDR_USC12 120 | else: 121 | i2c_addr = ADDR_USC34 122 | 123 | if port == 1 or port == 3: 124 | reg_addr = _PORT1_CURRENT 125 | else: 126 | reg_addr = _PORT2_CURRENT 127 | 128 | value = self.i2c.read_i2c_block_data(i2c_addr, reg_addr, number=1)[0] 129 | out.append(float(value) * TO_MA) 130 | 131 | return out 132 | 133 | def limits(self): 134 | out = [] 135 | 136 | if self.hub.config.version > 1: 137 | value = self.hub.config.get("power_limits") 138 | 139 | port12 = value & 0xFF 140 | port34 = (value >> 8) & 0xFF 141 | 142 | out.append(port12 & 0b111) 143 | out.append((port12 >> 3) & 0b111) 144 | 145 | out.append(port34 & 0b111) 146 | out.append((port34 >> 3) & 0b111) 147 | else: 148 | reg_addr = _CURRENT_LIMIT 149 | 150 | for i2c_addr in [ADDR_USC12, ADDR_USC34]: 151 | value = self.i2c.read_i2c_block_data(i2c_addr, reg_addr, number=1)[0] 152 | 153 | ## Extract Port 1 of this chip 154 | out.append(value & 0b111) 155 | 156 | ## Extract Port 2 of this chip 157 | out.append((value >> 3) & 0b111) 158 | 159 | return [_CURRENT_MAPPING[key] for key in out] 160 | 161 | def set_limits(self, ports, limit): 162 | if limit not in _CURRENT_MAPPING: 163 | raise ValueError("Specified current limit of {} is not valid. Limits can be: {}".format(limit, _CURRENT_MAPPING)) 164 | 165 | setting = _CURRENT_MAPPING.index(limit) 166 | 167 | if self.hub.config.version > 1: 168 | value = self.hub.config.get("power_limits") 169 | 170 | port12 = value & 0xFF 171 | port34 = (value >> 8) & 0xFF 172 | 173 | if 1 in ports or 2 in ports: 174 | port12 = BitVector(port12) 175 | 176 | if 1 in ports: 177 | port12[0:3] = setting 178 | 179 | if 2 in ports: 180 | port12[3:6] = setting 181 | 182 | if 3 in ports or 4 in ports: 183 | port34 = BitVector(port34) 184 | 185 | if 3 in ports: 186 | port34[0:3] = setting 187 | 188 | if 4 in ports: 189 | port34[3:6] = setting 190 | 191 | value = int(port12) + (int(port34) << 8) 192 | self.hub.config.set("power_limits", value) 193 | 194 | else: 195 | reg_addr = _CURRENT_LIMIT 196 | 197 | if 1 in ports or 2 in ports: 198 | value = self.i2c.read_i2c_block_data(ADDR_USC12, reg_addr, number=1)[0] 199 | value = BitVector(value) 200 | 201 | if 1 in ports: 202 | value[0:3] = setting 203 | 204 | if 2 in ports: 205 | value[3:6] = setting 206 | 207 | self.i2c.write_bytes(ADDR_USC12, bytes([reg_addr, int(value)])) 208 | 209 | if 3 in ports or 4 in ports: 210 | value = self.i2c.read_i2c_block_data(ADDR_USC34, reg_addr, number=1)[0] 211 | value = BitVector(value) 212 | 213 | if 3 in ports: 214 | value[0:3] = setting 215 | 216 | if 4 in ports: 217 | value[3:6] = setting 218 | 219 | self.i2c.write_bytes(ADDR_USC34, bytes([reg_addr, int(value)])) 220 | 221 | def alerts(self): 222 | out = [] 223 | 224 | for idx, i2c_addr in enumerate([ADDR_USC12, ADDR_USC34]): 225 | 226 | value = self.i2c.read_i2c_block_data(i2c_addr, _PORT_STATUS, number=1)[0] 227 | 228 | if get_bit(value, 7): 229 | out.append("ALERT.{}".format(idx*2+2)) 230 | 231 | if get_bit(value, 6): 232 | out.append("ALERT.{}".format(idx*2+1)) 233 | 234 | if get_bit(value, 5): 235 | out.append("CC_MODE.{}".format(idx*2+2)) 236 | 237 | if get_bit(value, 4): 238 | out.append("CC_MODE.{}".format(idx*2+1)) 239 | 240 | 241 | value = self.i2c.read_i2c_block_data(i2c_addr, _INTERRUPT1, number=1)[0] 242 | 243 | if get_bit(value, 7): 244 | out.append("ERROR.{}".format(idx*2+1)) 245 | 246 | if get_bit(value, 6): 247 | out.append("DISCHARGE.{}".format(idx*2+1)) 248 | 249 | if get_bit(value, 5): 250 | if idx == 0: 251 | out.append("RESET.12") 252 | else: 253 | out.append("RESET.34") 254 | 255 | if get_bit(value, 4): 256 | out.append("KEEP_OUT.{}".format(idx*2+1)) 257 | 258 | if get_bit(value, 3): 259 | if idx == 0: 260 | out.append("DIE_TEMP_HIGH.12") 261 | else: 262 | out.append("DIE_TEMP_HIGH.34") 263 | 264 | if get_bit(value, 2): 265 | if idx == 0: 266 | out.append("OVER_VOLT.12") 267 | else: 268 | out.append("OVER_VOLT.34") 269 | 270 | if get_bit(value, 1): 271 | out.append("BACK_BIAS.{}".format(idx*2+1)) 272 | 273 | if get_bit(value, 0): 274 | out.append("OVER_LIMIT.{}".format(idx*2+1)) 275 | 276 | 277 | value = self.i2c.read_i2c_block_data(i2c_addr, _INTERRUPT2, number=1)[0] 278 | 279 | if get_bit(value, 7): 280 | out.append("ERROR.{}".format(idx*2+2)) 281 | 282 | if get_bit(value, 6): 283 | out.append("DISCHARGE.{}".format(idx*2+2)) 284 | 285 | if get_bit(value, 5): 286 | if idx == 0: 287 | out.append("VS_LOW.12") 288 | else: 289 | out.append("VS_LOW.34") 290 | 291 | if get_bit(value, 4): 292 | out.append("KEEP_OUT.{}".format(idx*2+2)) 293 | 294 | if get_bit(value, 3): 295 | if idx == 0: 296 | out.append("DIE_TEMP_LOW.12") 297 | else: 298 | out.append("DIE_TEMP_LOW.34") 299 | 300 | ## Bit 2 is unimplemented 301 | 302 | if get_bit(value, 1): 303 | out.append("BACK_BIAS.{}".format(idx*2+2)) 304 | 305 | if get_bit(value, 0): 306 | out.append("OVER_LIMIT.{}".format(idx*2+2)) 307 | 308 | return out -------------------------------------------------------------------------------- /capablerobot_usbhub/device.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Chris Osterwood for Capable Robot Components 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import struct 24 | import logging 25 | import weakref 26 | 27 | from .i2c import USBHubI2C 28 | from .spi import USBHubSPI 29 | from .gpio import USBHubGPIO 30 | from .power import USBHubPower 31 | from .config import USBHubConfig 32 | from .util import * 33 | 34 | EEPROM_I2C_ADDR = 0x50 35 | EEPROM_EUI_ADDR = 0xFA 36 | EEPROM_EUI_BYTES = 0xFF - 0xFA + 1 37 | EEPROM_SKU_ADDR = 0x00 38 | EEPROM_SKU_BYTES = 0x06 39 | 40 | MCP_I2C_ADDR = 0x20 41 | MCP_REG_GPIO = 0x09 42 | 43 | class USBHubDevice: 44 | 45 | CMD_REG_WRITE = 0x03 46 | CMD_REG_READ = 0x04 47 | 48 | REG_BASE_DFT = 0xBF800000 49 | REG_BASE_ALT = 0xBFD20000 50 | 51 | def __init__(self, main, handle, 52 | timeout = 100, 53 | i2c_attempts_max = 5, 54 | i2c_attempt_delay = 10, 55 | disable_i2c = False 56 | ): 57 | 58 | self.main = main 59 | self.handle = handle 60 | 61 | self._serial = None 62 | self._sku = None 63 | self._revision = None 64 | self._descriptor = None 65 | 66 | proxy = weakref.proxy(self) 67 | 68 | ## Function to enable the I2C bus 69 | ## 70 | ## This is bound here so that passed-in parameters don't have 71 | ## to be saved as object parameters, and we can still enable later 72 | ## if we need to. 73 | def enable_i2c(): 74 | self.i2c = USBHubI2C(proxy, 75 | timeout = timeout, 76 | attempts_max = i2c_attempts_max, 77 | attempt_delay = i2c_attempt_delay 78 | ) 79 | 80 | if disable_i2c: 81 | ## For now, don't enable the I2C bus, but save the fuction to do so to a lambda. 82 | ## This allows delayed enabling of the I2C bus if it is needed later 83 | ## (e.g. to turn port data on and off). 84 | self.i2c = None 85 | self.enable_i2c = lambda : enable_i2c() 86 | else: 87 | enable_i2c() 88 | 89 | self.spi = USBHubSPI(proxy, timeout=timeout) 90 | self.gpio = USBHubGPIO(proxy) 91 | self.power = USBHubPower(proxy) 92 | self.config = USBHubConfig(proxy) 93 | 94 | logging.debug("Device class created") 95 | logging.debug("Firmware version {} running on {}".format(self.config.version, self.config.circuitpython_version)) 96 | 97 | def register_read(self, name=None, addr=None, length=1, print=False, endian='big', base=REG_BASE_DFT): 98 | if name != None: 99 | addr, bits, endian = self.main.find_register_by_name(name) 100 | length = int(bits / 8) 101 | else: 102 | try: 103 | name = self.main.find_register_name_by_addr(addr) 104 | except : 105 | print = False 106 | 107 | bits = length * 8 108 | 109 | if addr == None: 110 | raise ValueError('Must specify an name or address') 111 | 112 | ## Need to offset the register address for USB access 113 | address = addr + base 114 | 115 | ## Split 32 bit register address into the 16 bit value & index fields 116 | value = address & 0xFFFF 117 | index = address >> 16 118 | 119 | data = list(self.handle.ctrl_transfer(REQ_IN, self.CMD_REG_READ, value, index, length)) 120 | 121 | if length != len(data): 122 | raise ValueError('Incorrect data length') 123 | 124 | shift = 0 125 | 126 | if bits == 8: 127 | code = 'B' 128 | elif bits == 16: 129 | code = 'H' 130 | elif bits == 24: 131 | ## There is no good way to extract a 3 byte number. 132 | ## 133 | ## So we tell pack it's a 4 byte number and shift all the data over 1 byte 134 | ## so it decodes correctly (as the register defn starts from the MSB) 135 | code = 'L' 136 | shift = 8 137 | elif bits == 32: 138 | code = 'L' 139 | 140 | if name is None: 141 | parsed = None 142 | else: 143 | num = bits_to_bytes(bits) 144 | value = int_from_bytes(data, endian) 145 | stream = struct.pack(">HB" + code, *[addr, num, value << shift]) 146 | parsed = self.main.parse_register(name, stream) 147 | 148 | if print: 149 | self.main.print_register(parsed) 150 | 151 | logging.debug("{} [0x{}] read {} [{}]".format(name, hexstr(addr), length, " ".join(["0x"+hexstr(v) for v in data]))) 152 | 153 | return data, parsed 154 | 155 | def register_write(self, name=None, addr=None, buf=[]): 156 | if name != None: 157 | addr, _, _ = self.find_register_by_name(name) 158 | 159 | if addr == None: 160 | raise ValueError('Must specify an name or address') 161 | 162 | ## Need to offset the register address for USB access 163 | address = addr + self.REG_BASE_DFT 164 | 165 | ## Split 32 bit register address into the 16 bit value & index fields 166 | value = address & 0xFFFF 167 | index = address >> 16 168 | 169 | try: 170 | length = self.handle.ctrl_transfer(REQ_OUT, self.CMD_REG_WRITE, value, index, buf) 171 | except usb.core.USBError: 172 | raise OSError('Unable to write to register {}'.format(addr)) 173 | 174 | if length != len(buf): 175 | raise OSError('Number of bytes written to bus was {}, expected {}'.format(length, len(buf))) 176 | 177 | return length 178 | 179 | def connections(self): 180 | _, conn = self.register_read(name='port::connection') 181 | return [conn.body[key] == 1 for key in register_keys(conn)] 182 | 183 | def speeds(self): 184 | _, speed = self.register_read(name='port::device_speed') 185 | speeds = ['none', 'low', 'full', 'high'] 186 | return [speeds[speed.body[key]] for key in register_keys(speed)] 187 | 188 | 189 | def load_descriptor(self): 190 | ## If descriptor has already been loaded, return early 191 | if self._descriptor is not None: 192 | return False 193 | 194 | ## Recent firmware puts a shortened serial number in a register 195 | ## Here we read that and will fall back to I2C-based extraction if needed 196 | desc_len, _ = self.register_read(addr=0x3472, length=1, base=self.REG_BASE_ALT) 197 | desc_bytes = self.register_read(addr=0x3244, length=desc_len[0], base=self.REG_BASE_ALT) 198 | desc = USBHubDevice._utf16le_to_string(desc_bytes[0][2:]) 199 | 200 | if desc.startswith("CRZRYC") or desc.startswith("CRR3C4"): 201 | self._descriptor = desc 202 | sku_rev = desc.split(" ")[0].split(".") 203 | 204 | self._sku = sku_rev[0] 205 | self._revision = int(sku_rev[1]) 206 | self._serial = desc.split(" ")[1] 207 | else: 208 | self._descriptor = False 209 | 210 | def load_revision_from_deviceid(self): 211 | ## If REV has already been loaded, return early 212 | if self._revision is not None: 213 | return False 214 | 215 | ## There was a hardware change between REV 1 and REV 2 which necessites the host-side driver 216 | ## knowing of that change. Data should be correct in EEPROM, but the on-hub firmware puts 217 | ## hardware revision in this register with the format of [REV, 'C']. If 'C' is in the second 218 | ## byte, the first byte has valid hardware information. 219 | data, _ = self.register_read(addr=0x3004, length=2) 220 | 221 | if data[1] == ord('C'): 222 | self._revision = data[0] 223 | 224 | def load_sku_revision_from_eeprom(self): 225 | ## If SKU has already been loaded, return early 226 | if self._sku is not None: 227 | return False 228 | 229 | ## If I2C has been disabled and parsing of descriptor failed, we cannot find out the SKU 230 | if self.i2c is None: 231 | return False 232 | 233 | data = self.i2c.read_i2c_block_data(EEPROM_I2C_ADDR, EEPROM_SKU_ADDR, EEPROM_SKU_BYTES+1) 234 | 235 | if data[0] == 0 or data[0] == 255: 236 | ## Prototype units didn't have the PCB SKU programmed into the EEPROM 237 | ## If EEPROM location is empty, we assume we're interacting with that hardware 238 | self._sku = '......' 239 | 240 | if self._revision is None: 241 | self._revision = 0 242 | else: 243 | ## Cache the SKU and the revision stored in the EEPROM 244 | self._sku = ''.join([chr(char) for char in data[0:EEPROM_SKU_BYTES]]) 245 | 246 | ## Only store EEPROM revision info if no other revision info has been fetched. 247 | ## This allow SKU to be fetched from EEPROM, but hardware revision to come 248 | ## from USB device ID (set by firmware) 249 | if self._revision is None: 250 | self._revision = data[EEPROM_SKU_BYTES] 251 | 252 | return [self._sku, data[EEPROM_SKU_BYTES]] 253 | 254 | def load_serial_from_eeprom(self): 255 | ## If serial has already been loaded, return early 256 | if self._serial is not None: 257 | return False 258 | 259 | ## If I2C has been disabled and parsing of descriptor failed, we cannot find out the serial number 260 | if self.i2c is None: 261 | return False 262 | 263 | data = self.i2c.read_i2c_block_data(EEPROM_I2C_ADDR, EEPROM_EUI_ADDR, EEPROM_EUI_BYTES) 264 | data = [char for char in data] 265 | 266 | if len(data) == 6: 267 | data = data[0:3] + [0xFF, 0xFE] + data[3:6] 268 | 269 | self._serial = ''.join(["%0.2X" % v for v in data]) 270 | 271 | @property 272 | def serial(self): 273 | self.load_descriptor() 274 | 275 | ## Loading data from descriptor did not succeed, try fallback methods 276 | if self._serial is None: 277 | self.load_serial_from_eeprom() 278 | 279 | return self._serial 280 | 281 | @property 282 | def usb_path(self): 283 | return "{}-{}".format(self.handle.bus, self.handle.address) 284 | 285 | @property 286 | def key(self): 287 | if self.serial is None: 288 | return self.usb_path 289 | 290 | return self.serial[-self.main.KEY_LENGTH:] 291 | 292 | @property 293 | def sku(self): 294 | self.load_descriptor() 295 | 296 | ## Loading data from descriptor did not succeed, try fallback methods 297 | if self._sku is None: 298 | ## Revision loaded from device ID as EEPROM [sku, revision] data is lower 299 | ## priority and we don't want EEPROM rev to overwrite device ID revision info 300 | ## 301 | ## Running load_sku_revision_from_eeprom afterwards is okay as it checks to see 302 | ## if self._revision has been set prior to assigning based on EEPROM information. 303 | self.load_revision_from_deviceid() 304 | self.load_sku_revision_from_eeprom() 305 | 306 | return self._sku 307 | 308 | @property 309 | def mpn(self): 310 | return self.sku 311 | 312 | @property 313 | def rev(self): 314 | return self.revision 315 | 316 | @property 317 | def revision(self): 318 | self.load_descriptor() 319 | 320 | if self._revision is None: 321 | self.load_revision_from_deviceid() 322 | 323 | if self._revision is None: 324 | self.load_sku_revision_from_eeprom() 325 | 326 | return self._revision 327 | 328 | def check_hardware_revision(self): 329 | sky, rev = self.load_sku_revision_from_eeprom() 330 | return self.revision == rev 331 | 332 | def _data_state(self): 333 | if self.config.version > 1: 334 | return self.config.get("data_state") 335 | 336 | if self.i2c is None: 337 | self.enable_i2c() 338 | 339 | return self.i2c.read_i2c_block_data(MCP_I2C_ADDR, MCP_REG_GPIO, 1)[0] 340 | 341 | def data_state(self): 342 | value = self._data_state() 343 | return ["off" if get_bit(value, idx) else "on" for idx in [7,6,5,4]] 344 | 345 | def data_enable(self, ports=[]): 346 | value = self._data_state() 347 | 348 | for port in ports: 349 | value = clear_bit(value, 8-port) 350 | 351 | if self.config.version > 1: 352 | self.config.set("data_state", int(value)) 353 | else: 354 | self.i2c.write_bytes(MCP_I2C_ADDR, bytes([MCP_REG_GPIO, int(value)])) 355 | 356 | def data_disable(self, ports=[]): 357 | value = self._data_state() 358 | 359 | for port in ports: 360 | value = set_bit(value, 8-port) 361 | 362 | if self.config.version > 1: 363 | self.config.set("data_state", int(value)) 364 | else: 365 | self.i2c.write_bytes(MCP_I2C_ADDR, bytes([MCP_REG_GPIO, int(value)])) 366 | 367 | def _utf16le_to_string(data): 368 | out = "" 369 | 370 | for idx in range(int(len(data)/2)): 371 | out += chr(data[idx*2]) 372 | 373 | return out --------------------------------------------------------------------------------