diff options
author | gustavo panizzo <gfa@zumbi.com.ar> | 2021-04-05 16:33:37 +0200 |
---|---|---|
committer | gustavo panizzo <gfa@zumbi.com.ar> | 2021-04-05 16:33:37 +0200 |
commit | 9d4a4f75c9bdee68a523e8ba68054a4b80e6ee0d (patch) | |
tree | 3227583f45bb2ac3ddf31fe8f331b3d11f886de7 | |
parent | 81c15da78edbc7cd0fb847752eae221fa5e0c10d (diff) | |
parent | 296cf882a95f18582cb13da31b6b5bd61bcac6f1 (diff) |
Record uhubctl (2.4.0-1) in archive suite sid
-rw-r--r-- | Formula/uhubctl.rb | 10 | ||||
-rw-r--r-- | Makefile | 29 | ||||
-rw-r--r-- | README.md | 102 | ||||
-rw-r--r-- | VERSION | 2 | ||||
-rw-r--r-- | debian/changelog | 17 | ||||
-rw-r--r-- | debian/control | 3 | ||||
-rw-r--r-- | debian/patches/debian-changes | 18 | ||||
-rw-r--r-- | debian/watch | 6 | ||||
-rw-r--r-- | uhubctl.c | 289 |
9 files changed, 310 insertions, 166 deletions
diff --git a/Formula/uhubctl.rb b/Formula/uhubctl.rb index 440b1cd..dbe6010 100644 --- a/Formula/uhubctl.rb +++ b/Formula/uhubctl.rb @@ -1,9 +1,17 @@ class Uhubctl < Formula - desc "control USB hubs powering per-port" + desc "USB hub per-port power control" homepage "https://github.com/mvp/uhubctl" head "https://github.com/mvp/uhubctl.git" + url "https://github.com/mvp/uhubctl/archive/v2.3.0.tar.gz" + sha256 "714f733592d3cb6ba8efc84fbc03b1beed2323918ff33aef01cdb956755be7b7" + license "GPL-2.0" depends_on "libusb" + depends_on "pkg-config" => :build + + livecheck do + url :stable + end def install system "make" @@ -10,6 +10,7 @@ INSTALL := install INSTALL_DIR := $(INSTALL) -m 755 -d INSTALL_PROGRAM := $(INSTALL) -m 755 RM := rm -rf +PKG_CONFIG ?= pkg-config CC ?= gcc CFLAGS ?= -g -O0 @@ -21,29 +22,25 @@ DEB_NOEPOCH_VERSION := $(shell echo $(DEB_VERSION) | cut -d: -f2-) DEB_UPSTREAM_VERSION := $(shell echo $(DEB_NOEPOCH_VERSION) | sed 's/-[^-]*$$//') CFLAGS += -DPROGRAM_VERSION=\"$(DEB_VERSION)\" +GIT_VERSION := $(shell git describe --match "v[0-9]*" --abbrev=8 --dirty --tags | cut -c2-) +ifeq ($(GIT_VERSION),) + GIT_VERSION := $(shell cat VERSION) +endif +# Use hardening options on Linux ifeq ($(UNAME_S),Linux) - LDFLAGS += -Wl,-zrelro,-znow -lusb-1.0 + LDFLAGS += -Wl,-zrelro,-znow endif -ifeq ($(UNAME_S),Darwin) -ifneq ($(wildcard /opt/local/include),) - # MacPorts - CFLAGS += -I/opt/local/include - LDFLAGS += -L/opt/local/lib -endif +# Use pkg-config if available +ifneq (,$(shell which $(PKG_CONFIG))) + CFLAGS += $(shell $(PKG_CONFIG) --cflags libusb-1.0) + LDFLAGS += $(shell $(PKG_CONFIG) --libs libusb-1.0) +else +# But it should still build if pkg-config is not available (e.g. Linux or Mac homebrew) LDFLAGS += -lusb-1.0 endif -ifeq ($(UNAME_S),FreeBSD) - LDFLAGS += -lusb -endif - -ifeq ($(UNAME_S),NetBSD) - CFLAGS += $(shell pkg-config --cflags libusb-1.0) - LDFLAGS += $(shell pkg-config --libs libusb-1.0) -endif - PROGRAM = uhubctl $(PROGRAM): $(PROGRAM).c @@ -20,30 +20,34 @@ This is list of known compatible USB hubs: |:-------------------|:-----------------------------------------------------|:------|:----|:----------|:--------|:-----| | AmazonBasics | HU3641V1 ([RPi issue](https://goo.gl/CLt46M)) | 4 | 3.0 |`2109:2811`| 2013 | | | AmazonBasics | HU3770V1 ([RPi issue](https://goo.gl/CLt46M)) | 7 | 3.0 |`2109:2811`| 2013 | | -| AmazonBasics | HU9002V1SBL ([RPi issue](https://goo.gl/CLt46M)) | 10 | 3.1 |`2109:2817`| 2018 | | -| AmazonBasics | HUC9002V1ESL ([RPi issue](https://goo.gl/CLt46M)) | 10 | 3.1 |`2109:2817`| 2018 | | +| AmazonBasics | HU9003V1EBL | 7 | 3.1 |`2109:2817`| 2018 | | +| AmazonBasics | HU9002V1SBL, HU9002V1ESL | 10 | 3.1 |`2109:2817`| 2018 | | +| AmazonBasics | HUC9002V1SBL, HUC9002V1EBL, HUC9002V1ESL | 10 | 3.1 |`2109:2817`| 2018 | | +| Anker | AK-68ANHUB-BV7A-0004 ([note](https://git.io/JLnZb)) | 7 | 3.0 |`2109:0812`| 2014 | | | Apple | Thunderbolt Display 27" (internal USB hub) | 6 | 2.0 | | 2011 | 2016 | | Apple | USB Keyboard With Numeric Pad (internal USB hub) | 3 | 2.0 | | 2011 | | | Asus | Z87-PLUS Motherboard (onboard USB hub) | 4 | 3.0 | | 2013 | 2016 | +| Aukey | CB-C59 | 4 | 3.1 |`2109:2813`| 2017 | | | B+B SmartWorx | UHR204 | 4 | 2.0 |`0856:DB00`| 2013 | | | B+B SmartWorx | USH304 | 4 | 3.0 |`04B4:6506`| 2017 | | | Basler | 2000036234 | 4 | 3.0 |`0451:8046`| 2016 | | | Belkin | F5U101 | 4 | 2.0 |`0451:2046`| 2005 | 2010 | -| Belkin | F5U701-BLK | 7 | 2.0 | | 2008 | 2012 | | Buffalo | BSH4A05U3BK | 4 | 3.0 |`05E3:0610`| 2015 | | | Bytecc | BT-UH340 | 4 | 3.0 |`2109:8110`| 2010 | | | Circuitco | Beagleboard-xM (internal USB hub) | 4 | 2.0 |`0424:9514`| 2010 | | | Club3D | CSV-3242HD Dual Display Docking Station | 4 | 3.0 |`2109:2811`| 2015 | | | CyberPower | CP-H420P | 4 | 2.0 |`0409:0059`| 2004 | | | Cypress | CY4608 HX2VL development kit | 4 | 2.0 |`04B4:6570`| 2012 | | -| D-Link | DUB-H4 rev B (silver) | 4 | 2.0 |`05E3:0605`| 2005 | 2010 | -| D-Link | DUB-H4 rev D,E (black). Note: rev A,C not supported | 4 | 2.0 |`05E3:0608`| 2012 | | +| D-Link | DUB-H4 rev B (silver). Note: rev B7+ not supported | 4 | 2.0 |`05E3:0605`| 2005 | 2010 | +| D-Link | DUB-H4 rev D,E (black). Note: rev A,C,F not supported| 4 | 2.0 |`05E3:0608`| 2012 | | | D-Link | DUB-H7 rev A (silver) | 7 | 2.0 |`2001:F103`| 2005 | 2010 | -| D-Link | DUB-H7 rev D (black). Note: rev B,C not supported | 7 | 2.0 |`05E3:0608`| 2012 | | -| Dell | P2416D 24" QHD Monitor | 4 | 2.0 | | 2017 | | +| D-Link | DUB-H7 rev D,E (black). Note: rev B,C,F not supported| 7 | 2.0 |`05E3:0608`| 2012 | | +| Dell | P2416D 24" QHD Monitor ([note](https://git.io/JUAu8))| 4 | 2.0 | | 2017 | | +| Dell | S2719DGF 27" WQHD Gaming-Monitor | 5 | 3.1 |`0424:5734`| 2018 | | | Dell | UltraSharp 1704FPT 17" LCD Monitor | 4 | 2.0 |`0424:A700`| 2005 | 2015 | | Dell | UltraSharp U2415 24" LCD Monitor | 5 | 3.0 | | 2014 | | | Elecom | U2H-G4S | 4 | 2.0 | | 2006 | 2011 | +| ExSys | EX-1113HMS | 16 | 3.1 | | 2018 | | | GlobalScale | ESPRESSObin SBUD102 V5 | 1 | 3.0 |`1D6B:0003`| 2017 | | | Hawking Technology | UH214 | 4 | 2.0 | | 2003 | 2008 | | IOI | U3H415E1 | 4 | 3.0 | | 2012 | | @@ -58,21 +62,24 @@ This is list of known compatible USB hubs: | Linksys | USB2HUB4 | 4 | 2.0 | | 2004 | 2010 | | Maplin | A08CQ | 7 | 2.0 |`0409:0059`| 2008 | 2011 | | Microchip | EVB-USB2517 | 7 | 2.0 | | 2008 | | +| Microchip | EVB-USB2534BC | 4 | 2.0 | | 2013 | | | Moxa | Uport-407 | 7 | 2.0 |`110A:0407`| 2009 | | +| NVidia | Jetson Nano B01 ([details](https://git.io/JJaFR)) | 4 | 3.0 | | 2019 | | | Phidgets | HUB0003_0 | 7 | 2.0 |`1A40:0201`| 2017 | | | Plugable | USB3-HUB7BC | 7 | 3.0 |`2109:0813`| 2015 | | | Plugable | USB3-HUB7C | 7 | 3.0 |`2109:0813`| 2015 | | | Plugable | USB3-HUB7-81X | 7 | 3.0 |`2109:0813`| 2012 | | +| Port Inc | NWUSB01 | 4 | 1.1 |`0451:1446`| 1999 | 2003 | | Raspberry Pi | B+, 2B, 3B ([see below](#raspberry-pi-b2b3b)) | 4 | 2.0 | | 2011 | | | Raspberry Pi | 3B+ ([see below](#raspberry-pi-3b)) | 4 | 2.0 |`0424:2514`| 2018 | | | Raspberry Pi | 4B ([see below](#raspberry-pi-4b)) | 4 | 3.0 |`2109:3431`| 2019 | | | Renesas | uPD720202 PCIe USB 3.0 host controller | 2 | 3.0 | | 2013 | | | Rosewill | RHUB-210 | 4 | 2.0 |`0409:005A`| 2011 | 2014 | +| Rosonway | RSH-A16 ([note](https://git.io/JTawg)) | 16 | 3.2 |`0bda:0411`| 2020 | | | Sanwa Supply | USB-HUB14GPH | 4 | 1.1 | | 2001 | 2003 | | Seagate | Backup Plus Hub STEL8000100 | 2 | 3.0 |`0BC2:AB44`| 2016 | | -| StarTech | ST4200USBM | 4 | 2.0 |`0409:005A`| 2004 | | | Sunix | SHB4200MA | 4 | 2.0 |`0409:0058`| 2006 | 2009 | -| Targus | PAUH212U | 7 | 2.0 | | 2004 | 2009 | +| Targus | PAUH212/PAUH212U | 7 | 2.0 | | 2004 | 2009 | | Texas Instruments | TUSB4041PAPEVM | 4 | 2.1 |`0451:8142`| 2015 | | This table is by no means complete. @@ -103,52 +110,63 @@ Compiling ========= This utility was tested to compile and work on Linux -(Ubuntu/Debian, Redhat/Fedora/CentOS, Arch Linux, Gentoo, openSUSE, Buildroot), FreeBSD, NetBSD and Mac OS X. +(Ubuntu/Debian, Redhat/Fedora/CentOS, Arch Linux, Gentoo, openSUSE, Buildroot), FreeBSD, NetBSD, SunOS and MacOS. While `uhubctl` compiles on Windows, USB power switching does not work on Windows because `libusb` is using `winusb.sys` driver, which according to Microsoft does not support [necessary USB control requests](https://social.msdn.microsoft.com/Forums/sqlserver/en-US/f680b63f-ca4f-4e52-baa9-9e64f8eee101). This may be fixed if `libusb` starts supporting different driver on Windows. +Note that it is highly recommended to have `pkg-config` installed (many platforms provide it by default). + First, you need to install library libusb-1.0 (version 1.0.12 or later, 1.0.16 or later is recommended): * Ubuntu: `sudo apt-get install libusb-1.0-0-dev` * Redhat: `sudo yum install libusb1-devel` -* MacOSX: `brew install libusb`, or `sudo port install libusb-devel` - > :warning: `libusb-1.0.23` is [broken](https://github.com/libusb/libusb/issues/707) on MacOS Catalina! - You have to install `libusb-1.0.22` until [libusb issue 707](https://github.com/libusb/libusb/issues/707) is fixed, - or use this workaround to force use of older Mojave build: - - brew uninstall --ignore-dependencies libusb - brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/5314f1d/Formula/libusb.rb - +* MacOS: `brew install libusb`, or `sudo port install libusb-devel` * FreeBSD: libusb is included by default * NetBSD: `sudo pkgin install libusb1 gmake pkg-config` * Windows: TBD? -To fetch uhubctl source: +To fetch uhubctl source and compile it: git clone https://github.com/mvp/uhubctl + cd uhubctl + make + +This should generate `uhubctl` binary. +You can install it in your system as `/usr/sbin/uhubctl` using: + + sudo make install -To compile, simply run `make` - this will generate `uhubctl` binary. Note that on some OS (e.g. FreeBSD/NetBSD) you need to use `gmake` instead to build. -Also, for Mac OS X you can install `uhubctl` with Homebrew custom tap: +Also, on MacOS you can install `uhubctl` with all necessary dependencies in one shot using Homebrew tap: ``` brew tap mvp/uhubctl https://github.com/mvp/uhubctl -brew install --HEAD uhubctl +brew install uhubctl +``` +To build/install from master branch, use `--HEAD`: +``` +brew install uhubctl --HEAD ``` Usage ===== +> :warning: On Linux, use `sudo` or configure USB permissions as described below! + +To list all supported hubs: + + uhubctl + You can control the power on a USB port(s) like this: uhubctl -a off -p 2 This means operate on default smart hub and turn power off (`-a off`, or `-a 0`) -on port 2 (`-p 2`). Supported actions are `off`/`on`/`cycle` (or `0`/`1`/`2`). +on port 2 (`-p 2`). Supported actions are `off`/`on`/`cycle`/`toggle` (or `0`/`1`/`2`/`3`). `cycle` means turn power off, wait some delay (configurable with `-d`) and turn it back on. Ports can be comma separated list, and may use `-` for ranges e.g. `2`, or `2,4`, or `2-5`, or `1-2,5-8`. @@ -169,10 +187,16 @@ Linux USB permissions On Linux, you should configure `udev` USB permissions (otherwise you will have to run it as root using `sudo uhubctl`). To fix USB permissions, first run `sudo uhubctl` and note all `vid:pid` for hubs you need to control. -Then, add one or more udev rules like below to file `/etc/udev/rules.d/52-usb.rules` (replace with your vendor id): +Then, add one or more udev rules like below to file `/etc/udev/rules.d/52-usb.rules` (replace 2001 with your vendor id): SUBSYSTEM=="usb", ATTR{idVendor}=="2001", MODE="0666" +Note that for USB3 hubs, some hubs use different vendor ID for USB2 vs USB3 components of the same chip, +and both need permissions to make uhubctl work properly. E.g. for Raspberry Pi 4B, you need to add these 2 lines: + + SUBSYSTEM=="usb", ATTR{idVendor}=="2109", MODE="0666" + SUBSYSTEM=="usb", ATTR{idVendor}=="1d6b", MODE="0666" + If you don't like wide open mode `0666`, you can restrict access by group like this: SUBSYSTEM=="usb", ATTR{idVendor}=="2001", MODE="0664", GROUP="dialout" @@ -194,7 +218,8 @@ FAQ According to USB 2.0 specification, USB hubs can advertise no power switching, ganged (all ports at once) power switching or per-port (individual) power switching. -Note that `uhubctl` will only detect USB hubs which support per-port power switching. +Note that by default `uhubctl` will only detect USB hubs which support per-port power switching +(but you can force it to try operating on unsupported hubs with option `-f`). You can find what kind of power switching your hardware supports by using `sudo lsusb -v`: No power switching: @@ -233,6 +258,8 @@ Per-port power switching: 3. If tests above were successful, please report your hub by opening new issue at https://github.com/mvp/uhubctl/issues, so we can add it to list of supported devices. + Please do not report unsupported hubs, unless it is different + hardware revision of some already listed supported model. #### _USB devices are not removed after port power down on Linux_ @@ -261,8 +288,11 @@ You can use option `-r N` where N is some number from 10 to 1000 to fix this - `uhubctl` will try to turn power off many times in quick succession, and it should suppress that. This may be eventually fixed in kernel, see more discussion [here](https://bit.ly/2JzczjZ). -If your device is USB mass storage, invoking `udisksctl` before calling `uhubctl` -might help to mitigate this issue: +Disabling USB authorization for device in question before turning power off with `uhubctl` should help: + + echo 0 > sudo tee /sys/bus/usb/devices/${location}.${port}/authorized + +If your device is USB mass storage, invoking `udisksctl` before calling `uhubctl` should help too: sudo udisksctl power-off --block-device /dev/disk/...` sudo uhubctl -a off ... @@ -281,7 +311,9 @@ Doing so will confuse internal hub circuitry and will cause unpredictable behavi #### _Raspberry Pi turns power off on all ports, not just the one I specified_ -This is limitation of Raspberry Pi hardware design. +This is the limitation of Raspberry Pi hardware design. +As a workaround, you can buy any external USB hub from supported list above, +attach it to any USB port of Raspberry Pi, and control power on its ports independently. For reference, supported Raspberry Pi models have following internal USB topology: @@ -307,8 +339,9 @@ For reference, supported Raspberry Pi models have following internal USB topolog ##### Raspberry Pi 4B - > :warning: You may need to [update firmware](https://www.raspberrypi.org/documentation/hardware/raspberrypi/booteeprom.md) -to vl805 00137ac or later to make power switching work on RPi 4B. + > :warning: If your VL805 firmware is older than `00137ad` (check with `sudo rpi-eeprom-update`), +you have to [update firmware](https://www.raspberrypi.org/documentation/hardware/raspberrypi/booteeprom.md) +to make power switching work on RPi 4B. * USB2 hub `1`, 1 port, only connects hub `1-1` below. @@ -320,13 +353,7 @@ to vl805 00137ac or later to make power switching work on RPi 4B. uhubctl -l 2 -a 0 - * USB2 hub `3`, 1 port, OTG controller: - - uhubctl -l 3 -p 1 -a 0 - - -As a workaround, you can buy any external USB hub from supported list, -attach it to any USB port of Raspberry Pi, and control power on its ports independently. + * USB2 hub `3`, 1 port, OTG controller. Power switching is [not supported](https://git.io/JUc5Q). @@ -350,6 +377,7 @@ Notable projects using uhubctl | [USB fan for Raspberry Pi](https://bit.ly/2TRV6sM) | Control USB fan to avoid Raspberry Pi overheating | | [Raspberry Pi Reboot Router](https://bit.ly/3aNbQqs) | Automatically reboot router if internet isn't working | | [Control USB Lamp With Voice](https://bit.ly/2VtW2SX) | Voice Control of USB Lamp using Siri and Raspberry Pi | +| [Control USB LED Strip](https://bit.ly/3oVWfeZ) | Controlling USB powered LED Light Strip | Copyright @@ -1 +1 @@ -v2.2.0-dev +2.4.0 diff --git a/debian/changelog b/debian/changelog index dea8544..db41eae 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,20 @@ +uhubctl (2.4.0-1) experimental; urgency=medium + + * New upstream release. + + -- gustavo panizzo <gfa@zumbi.com.ar> Mon, 05 Apr 2021 14:33:37 +0000 + +uhubctl (2.3.0-1) unstable; urgency=medium + + [ gustavo panizzo ] + * New upstream release + * [30ee12] Standards version 4.5.1 (no changes) + * [579aa3] Use a version 4 of the watch file + * [7239e9] Set Rules-Requires-Requires no + * [d28e61] Refresh patches + + -- gustavo panizzo <gfa@zumbi.com.ar> Tue, 15 Dec 2020 20:46:37 +0000 + uhubctl (2.2.0-1) unstable; urgency=medium * New upstream release. diff --git a/debian/control b/debian/control index cefb16b..b5e14d5 100644 --- a/debian/control +++ b/debian/control @@ -3,10 +3,11 @@ Section: electronics Priority: optional Maintainer: gustavo panizzo <gfa@zumbi.com.ar> Build-Depends: debhelper-compat (= 13), libusb-1.0-0-dev, autopkgtest, help2man -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Homepage: https://github.com/mvp/uhubctl Vcs-Browser: https://salsa.debian.org/debian/uhubctl Vcs-Git: https://salsa.debian.org/debian/uhubctl.git +Rules-Requires-Root: no Package: uhubctl Architecture: any diff --git a/debian/patches/debian-changes b/debian/patches/debian-changes index 6afc77d..6bb409f 100644 --- a/debian/patches/debian-changes +++ b/debian/patches/debian-changes @@ -10,17 +10,12 @@ the first upload of upstream version 1.2.3, you could use: (If you have dgit, use `dgit clone uhubctl`, rather than plain `git clone`.) A single combined diff, containing all the changes, follows. ---- uhubctl-2.2.0.orig/Makefile -+++ uhubctl-2.2.0/Makefile -@@ -14,11 +14,13 @@ RM := rm -rf +--- uhubctl-2.4.0.orig/Makefile ++++ uhubctl-2.4.0/Makefile +@@ -15,11 +15,17 @@ PKG_CONFIG ?= pkg-config CC ?= gcc CFLAGS ?= -g -O0 CFLAGS += -Wall -Wextra -std=c99 -pedantic --GIT_VERSION := $(shell git describe --match "v[0-9]*" --abbrev=8 --dirty --always --tags | cut -c2-) --ifeq ($(GIT_VERSION),) -- GIT_VERSION := $(shell cat VERSION) --endif --CFLAGS += -DPROGRAM_VERSION=\"$(GIT_VERSION)\" +# Gets the full version of the source package including debian version +DEB_VERSION := $(shell dpkg-parsechangelog | egrep '^Version:' | cut -f 2 -d ' ') +DEB_NOEPOCH_VERSION := $(shell echo $(DEB_VERSION) | cut -d: -f2-) @@ -28,6 +23,11 @@ A single combined diff, containing all the changes, follows. +DEB_UPSTREAM_VERSION := $(shell echo $(DEB_NOEPOCH_VERSION) | sed 's/-[^-]*$$//') + +CFLAGS += -DPROGRAM_VERSION=\"$(DEB_VERSION)\" + GIT_VERSION := $(shell git describe --match "v[0-9]*" --abbrev=8 --dirty --tags | cut -c2-) + ifeq ($(GIT_VERSION),) + GIT_VERSION := $(shell cat VERSION) + endif +-CFLAGS += -DPROGRAM_VERSION=\"$(GIT_VERSION)\" + # Use hardening options on Linux ifeq ($(UNAME_S),Linux) - LDFLAGS += -Wl,-zrelro,-znow -lusb-1.0 diff --git a/debian/watch b/debian/watch index 11ee23f..5a57406 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,3 @@ -version=3 -opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/uhubctl-$1\.tar\.gz/ \ - https://github.com/mvp/uhubctl/releases .*/v?(\d\S*)\.tar\.gz +version=4 +opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/uhubctl-$1\.tar\.gz/ \ + https://github.com/mvp/uhubctl/releases .*/v?(\d\S+)\.tar\.gz @@ -75,6 +75,7 @@ void sleep_ms(int milliseconds) #define POWER_OFF 0 #define POWER_ON 1 #define POWER_CYCLE 2 +#define POWER_TOGGLE 3 #define MAX_HUB_CHAIN 8 /* Per USB 3.0 spec max hub chain is 7 */ @@ -188,13 +189,16 @@ struct descriptor_strings { struct hub_info { struct libusb_device *dev; int bcd_usb; + int super_speed; /* 1 if super speed hub, and 0 otherwise */ int nports; - int ppps; + int lpsm; /* logical power switching mode */ int actionable; /* true if this hub is subject to action */ char container_id[33]; /* container ID as hex string */ char vendor[16]; char location[32]; - int level; + uint8_t bus; + uint8_t port_numbers[MAX_HUB_CHAIN]; + int pn_len; /* length of port numbers */ struct descriptor_strings ds; }; @@ -206,6 +210,7 @@ static int hub_phys_count = 0; /* default options */ static char opt_vendor[16] = ""; +static char opt_search[64] = ""; /* Search by attached device description */ static char opt_location[32] = ""; /* Hub location a-b.c.d */ static int opt_level = 0; /* Hub location level (e.g., a-b is level 2, a-b.c is level 3)*/ static int opt_ports = ALL_HUB_PORTS; /* Bitmask of ports to operate on */ @@ -215,10 +220,12 @@ static int opt_repeat = 1; static int opt_wait = 20; /* wait before repeating in ms */ static int opt_exact = 0; /* exact location match - disable USB3 duality handling */ static int opt_reset = 0; /* reset hub after operation(s) */ +static int opt_force = 0; /* force operation even on unsupported hubs */ static const struct option long_options[] = { { "location", required_argument, NULL, 'l' }, { "vendor", required_argument, NULL, 'n' }, + { "search", required_argument, NULL, 's' }, { "level", required_argument, NULL, 'L' }, { "ports", required_argument, NULL, 'p' }, { "action", required_argument, NULL, 'a' }, @@ -226,6 +233,7 @@ static const struct option long_options[] = { { "repeat", required_argument, NULL, 'r' }, { "wait", required_argument, NULL, 'w' }, { "exact", no_argument, NULL, 'e' }, + { "force", no_argument, NULL, 'f' }, { "reset", no_argument, NULL, 'R' }, { "version", no_argument, NULL, 'v' }, { "help", no_argument, NULL, 'h' }, @@ -236,30 +244,33 @@ static const struct option long_options[] = { static int print_usage() { printf( - "uhubctl %s: utility to control USB port power for smart hubs.\n" + "uhubctl: utility to control USB port power for smart hubs.\n" "Usage: uhubctl [options]\n" "Without options, show status for all smart hubs.\n" "\n" "Options [defaults in brackets]:\n" - "--action, -a - action to off/on/cycle (0/1/2) for affected ports.\n" + "--action, -a - action to off/on/cycle/toggle (0/1/2/3) for affected ports.\n" "--ports, -p - ports to operate on [all hub ports].\n" "--location, -l - limit hub by location [all smart hubs].\n" "--level -L - limit hub by location level (e.g. a-b.c is level 3).\n" "--vendor, -n - limit hub by vendor id [%s] (partial ok).\n" + "--search, -s - limit hub by attached device description.\n" "--delay, -d - delay for cycle action [%g sec].\n" "--repeat, -r - repeat power off count [%d] (some devices need it to turn off).\n" "--exact, -e - exact location (no USB3 duality handling).\n" + "--force, -f - force operation even on unsupported hubs.\n" "--reset, -R - reset hub after each power-on action, causing all devices to reassociate.\n" "--wait, -w - wait before repeat power off [%d ms].\n" "--version, -v - print program version.\n" "--help, -h - print this text.\n" "\n" - "Send bugs and requests to: https://github.com/mvp/uhubctl\n", - PROGRAM_VERSION, + "Send bugs and requests to: https://github.com/mvp/uhubctl\n" + "version: %s\n", strlen(opt_vendor) ? opt_vendor : "any", opt_delay, opt_repeat, - opt_wait + opt_wait, + PROGRAM_VERSION ); return 0; } @@ -385,9 +396,9 @@ static int get_hub_info(struct libusb_device *dev, struct hub_info *info) ); if (len >= minlen) { - unsigned char port_numbers[MAX_HUB_CHAIN] = {0}; info->dev = dev; info->bcd_usb = bcd_usb; + info->super_speed = (bcd_usb >= USB_SS_BCD); info->nports = uhd->bNbrPorts; snprintf( info->vendor, sizeof(info->vendor), @@ -397,14 +408,13 @@ static int get_hub_info(struct libusb_device *dev, struct hub_info *info) ); /* Convert bus and ports array into USB location string */ - int bus = libusb_get_bus_number(dev); - snprintf(info->location, sizeof(info->location), "%d", bus); - int pcount = get_port_numbers(dev, port_numbers, MAX_HUB_CHAIN); - info->level = pcount + 1; + info->bus = libusb_get_bus_number(dev); + snprintf(info->location, sizeof(info->location), "%d", info->bus); + info->pn_len = get_port_numbers(dev, info->port_numbers, sizeof(info->port_numbers)); int k; - for (k=0; k<pcount; k++) { + for (k=0; k < info->pn_len; k++) { char s[8]; - snprintf(s, sizeof(s), "%s%d", k==0 ? "-" : ".", port_numbers[k]); + snprintf(s, sizeof(s), "%s%d", k==0 ? "-" : ".", info->port_numbers[k]); strcat(info->location, s); } @@ -434,10 +444,10 @@ static int get_hub_info(struct libusb_device *dev, struct hub_info *info) } libusb_free_bos_descriptor(bos); - /* Raspberry Pi 4 hack for USB3 root hub: */ + /* Raspberry Pi 4B hack for USB3 root hub: */ if (strlen(info->container_id)==0 && strcasecmp(info->vendor, "1d6b:0003")==0 && - info->level==1 && + info->pn_len==0 && info->nports==4 && bcd_usb==USB_SS_BCD) { @@ -445,26 +455,18 @@ static int get_hub_info(struct libusb_device *dev, struct hub_info *info) } } - info->ppps = 0; /* Logical Power Switching Mode */ int lpsm = uhd->wHubCharacteristics[0] & HUB_CHAR_LPSM; if (lpsm == HUB_CHAR_COMMON_LPSM && info->nports == 1) { /* For 1 port hubs, ganged power switching is the same as per-port: */ lpsm = HUB_CHAR_INDV_PORT_LPSM; } - /* Raspberry Pi 4 reports inconsistent descriptors, override: */ + /* Raspberry Pi 4B reports inconsistent descriptors, override: */ if (lpsm == HUB_CHAR_COMMON_LPSM && strcasecmp(info->vendor, "2109:3431")==0) { lpsm = HUB_CHAR_INDV_PORT_LPSM; } - /* Over-Current Protection Mode */ - int ocpm = uhd->wHubCharacteristics[0] & HUB_CHAR_OCPM; - /* LPSM must be supported per-port, and OCPM per port or ganged */ - if ((lpsm == HUB_CHAR_INDV_PORT_LPSM) && - (ocpm == HUB_CHAR_INDV_PORT_OCPM || - ocpm == HUB_CHAR_COMMON_OCPM)) - { - info->ppps = 1; - } + info->lpsm = lpsm; + rc = 0; } else { rc = len; } @@ -522,7 +524,7 @@ static int get_device_description(struct libusb_device * dev, struct descriptor_ int rc; int id_vendor = 0; int id_product = 0; - char ports[64] = ""; + char hub_specific[64] = ""; struct libusb_device_descriptor desc; struct libusb_device_handle *devh = NULL; rc = libusb_get_device_descriptor(dev, &desc); @@ -552,8 +554,16 @@ static int get_device_description(struct libusb_device * dev, struct descriptor_ struct hub_info info; rc = get_hub_info(dev, &info); if (rc == 0) { - snprintf(ports, sizeof(ports), ", USB %x.%02x, %d ports", - info.bcd_usb >> 8, info.bcd_usb & 0xFF, info.nports); + const char * lpsm_type; + if (info.lpsm == HUB_CHAR_INDV_PORT_LPSM) { + lpsm_type = "ppps"; + } else if (info.lpsm == HUB_CHAR_COMMON_LPSM) { + lpsm_type = "ganged"; + } else { + lpsm_type = "nops"; + } + snprintf(hub_specific, sizeof(hub_specific), ", USB %x.%02x, %d ports, %s", + info.bcd_usb >> 8, info.bcd_usb & 0xFF, info.nports, lpsm_type); } } libusb_close(devh); @@ -564,7 +574,7 @@ static int get_device_description(struct libusb_device * dev, struct descriptor_ ds->vendor[0] ? " " : "", ds->vendor, ds->product[0] ? " " : "", ds->product, ds->serial[0] ? " " : "", ds->serial, - ports + hub_specific ); return 0; } @@ -581,17 +591,9 @@ static int print_port_status(struct hub_info * hub, int portmask) int port_status; struct libusb_device_handle * devh = NULL; int rc = 0; - int hub_bus; - int dev_bus; - unsigned char hub_pn[MAX_HUB_CHAIN]; - unsigned char dev_pn[MAX_HUB_CHAIN]; - int hub_plen; - int dev_plen; struct libusb_device *dev = hub->dev; rc = libusb_open(dev, &devh); if (rc == 0) { - hub_bus = libusb_get_bus_number(dev); - hub_plen = get_port_numbers(dev, hub_pn, sizeof(hub_pn)); int port; for (port = 1; port <= hub->nports; port++) { if (portmask > 0 && (portmask & (1 << (port-1))) == 0) continue; @@ -611,12 +613,15 @@ static int print_port_status(struct hub_info * hub, int portmask) struct libusb_device * udev; int i = 0; while ((udev = usb_devs[i++]) != NULL) { + uint8_t dev_bus; + uint8_t dev_pn[MAX_HUB_CHAIN]; + int dev_plen; dev_bus = libusb_get_bus_number(udev); /* only match devices on the same bus: */ - if (dev_bus != hub_bus) continue; + if (dev_bus != hub->bus) continue; dev_plen = get_port_numbers(udev, dev_pn, sizeof(dev_pn)); - if ((dev_plen == hub_plen + 1) && - (memcmp(hub_pn, dev_pn, hub_plen) == 0) && + if ((dev_plen == hub->pn_len + 1) && + (memcmp(hub->port_numbers, dev_pn, hub->pn_len) == 0) && libusb_get_port_number(udev) == port) { rc = get_device_description(udev, &ds); @@ -625,7 +630,7 @@ static int print_port_status(struct hub_info * hub, int portmask) } } - if (hub->bcd_usb < USB_SS_BCD) { + if (!hub->super_speed) { if (port_status == 0) { printf(" off"); } else { @@ -703,30 +708,63 @@ static int usb_find_hubs() rc = get_hub_info(dev, &info); if (rc) { perm_ok = 0; /* USB permission issue? */ + continue; } get_device_description(dev, &info.ds); - if (info.ppps) { /* PPPS is supported */ - if (hub_count < MAX_HUBS) { - info.actionable = 1; - if (strlen(opt_location) > 0) { - if (strcasecmp(opt_location, info.location)) { - info.actionable = 0; - } - } - if (opt_level > 0) { - if (opt_level != info.level) { - info.actionable = 0; - } - } - if (strlen(opt_vendor) > 0) { - if (strncasecmp(opt_vendor, info.vendor, strlen(opt_vendor))) { - info.actionable = 0; + if (info.lpsm != HUB_CHAR_INDV_PORT_LPSM && !opt_force) { + continue; + } + info.actionable = 1; + if (strlen(opt_search) > 0) { + /* Search by attached device description */ + info.actionable = 0; + struct libusb_device * udev; + int k = 0; + while ((udev = usb_devs[k++]) != NULL) { + uint8_t dev_pn[MAX_HUB_CHAIN]; + uint8_t dev_bus = libusb_get_bus_number(udev); + /* only match devices on the same bus: */ + if (dev_bus != info.bus) continue; + int dev_plen = get_port_numbers(udev, dev_pn, sizeof(dev_pn)); + if ((dev_plen == info.pn_len + 1) && + (memcmp(info.port_numbers, dev_pn, info.pn_len) == 0)) + { + struct descriptor_strings ds; + bzero(&ds, sizeof(ds)); + rc = get_device_description(udev, &ds); + if (rc != 0) + break; + if (strstr(ds.description, opt_search)) { + info.actionable = 1; + opt_ports &= 1 << (dev_pn[dev_plen-1] - 1); + break; } } - memcpy(&hubs[hub_count], &info, sizeof(info)); - hub_count++; } } + if (strlen(opt_location) > 0) { + if (strcasecmp(opt_location, info.location)) { + info.actionable = 0; + } + } + if (opt_level > 0) { + if (opt_level != info.pn_len + 1) { + info.actionable = 0; + } + } + if (strlen(opt_vendor) > 0) { + if (strncasecmp(opt_vendor, info.vendor, strlen(opt_vendor))) { + info.actionable = 0; + } + } + memcpy(&hubs[hub_count], &info, sizeof(info)); + if (hub_count < MAX_HUBS) { + hub_count++; + } else { + /* That should be impossible - but we don't want to crash! */ + fprintf(stderr, "Too many hubs!"); + break; + } } if (!opt_exact) { /* Handle USB2/3 duality: */ @@ -737,7 +775,8 @@ static int usb_find_hubs() /* Must have non empty container ID: */ if (strlen(hubs[i].container_id) == 0) continue; - int match = -1; + int best_match = -1; + int best_score = -1; for (j=0; j<hub_count; j++) { if (i==j) continue; @@ -745,8 +784,7 @@ static int usb_find_hubs() /* Find hub which is USB2/3 dual to the hub above */ /* Hub and its dual must be different types: one USB2, another USB3: */ - if ((hubs[i].bcd_usb < USB_SS_BCD) == - (hubs[j].bcd_usb < USB_SS_BCD)) + if (hubs[i].super_speed == hubs[j].super_speed) continue; /* Must have non empty container ID: */ @@ -762,6 +800,14 @@ static int usb_find_hubs() * We should do few more checks below if multiple such devices are present. */ + /* Hubs should have the same number of ports */ + if (hubs[i].nports != hubs[j].nports) { + /* Except for some weird hubs like Apple mini-dock (has 2 usb2 + 1 usb3 ports) */ + if (hubs[i].nports + hubs[j].nports > 3) { + continue; + } + } + /* If serial numbers are both present, they must match: */ if ((strlen(hubs[i].ds.serial) > 0 && strlen(hubs[j].ds.serial) > 0) && strcmp(hubs[i].ds.serial, hubs[j].ds.serial) != 0) @@ -769,18 +815,56 @@ static int usb_find_hubs() continue; } - /* Hubs should have the same number of ports: */ - if (hubs[i].nports != hubs[j].nports) - continue; + /* We have first possible candidate, but need to keep looking for better one */ - /* Finally, we claim a match: */ - match = j; - break; + if (best_score < 1) { + best_score = 1; + best_match = j; + } + + /* Checks for various levels of USB2 vs USB3 path similarity... */ + + uint8_t* p1 = hubs[i].port_numbers; + uint8_t* p2 = hubs[j].port_numbers; + int l1 = hubs[i].pn_len; + int l2 = hubs[j].pn_len; + int s1 = hubs[i].super_speed; + int s2 = hubs[j].super_speed; + + /* Check if port path is the same after removing top level (needed for M1 Macs): */ + if (l1 >= 1 && l1 == l2 && memcmp(p1 + 1, p2 + 1, l1 - 1)==0) { + if (best_score < 2) { + best_score = 2; + best_match = j; + } + } + + /* Raspberry Pi 4B hack (USB2 hub is one level deeper than USB3): */ + if (l1 + s1 == l2 + s2 && l1 >= s2 && memcmp(p1 + s2, p2 + s1, l1 - s2)==0) { + if (best_score < 3) { + best_score = 3; + best_match = j; + } + } + /* Check if port path is exactly the same: */ + if (l1 == l2 && memcmp(p1, p2, l1)==0) { + if (best_score < 4) { + best_score = 4; + best_match = j; + } + /* Give even higher priority if `usb2bus + 1 == usb3bus` (Linux specific): */ + if (hubs[i].bus - s1 == hubs[j].bus - s2) { + if (best_score < 5) { + best_score = 5; + best_match = j; + } + } + } } - if (match >= 0) { - if (!hubs[match].actionable) { + if (best_match >= 0) { + if (!hubs[best_match].actionable) { /* Use 2 to signify that this is derived dual device */ - hubs[match].actionable = 2; + hubs[best_match].actionable = 2; } } } @@ -789,11 +873,19 @@ static int usb_find_hubs() for (i=0; i<hub_count; i++) { if (!hubs[i].actionable) continue; - if (hubs[i].bcd_usb < USB_SS_BCD || opt_exact) { + if (!hubs[i].super_speed || opt_exact) { hub_phys_count++; } } if (perm_ok == 0 && hub_phys_count == 0) { +#ifdef __gnu_linux__ + if (geteuid() != 0) { + fprintf(stderr, + "There were permission problems while accessing USB.\n" + "Follow https://git.io/JIB2Z for a fix!\n" + ); + } +#endif return LIBUSB_ERROR_ACCESS; } return hub_phys_count; @@ -807,7 +899,7 @@ int main(int argc, char *argv[]) int option_index = 0; for (;;) { - c = getopt_long(argc, argv, "l:L:n:a:p:d:r:w:hveR", + c = getopt_long(argc, argv, "l:L:n:a:p:d:r:w:s:hvefR", long_options, &option_index); if (c == -1) break; /* no more options left */ @@ -830,6 +922,9 @@ int main(int argc, char *argv[]) case 'n': strncpy(opt_vendor, optarg, sizeof(opt_vendor)); break; + case 's': + strncpy(opt_search, optarg, sizeof(opt_search)); + break; case 'p': if (!strcasecmp(optarg, "all")) { /* all ports is the default */ break; @@ -848,6 +943,9 @@ int main(int argc, char *argv[]) if (!strcasecmp(optarg, "cycle") || !strcasecmp(optarg, "2")) { opt_action = POWER_CYCLE; } + if (!strcasecmp(optarg, "toggle") || !strcasecmp(optarg, "3")) { + opt_action = POWER_TOGGLE; + } break; case 'd': opt_delay = atof(optarg); @@ -855,6 +953,9 @@ int main(int argc, char *argv[]) case 'r': opt_repeat = atoi(optarg); break; + case 'f': + opt_force = 1; + break; case 'e': opt_exact = 1; break; @@ -908,23 +1009,11 @@ int main(int argc, char *argv[]) rc = usb_find_hubs(); if (rc <= 0) { fprintf(stderr, - "No compatible smart hubs detected%s%s!\n" + "No compatible devices detected%s%s!\n" "Run with -h to get usage info.\n", strlen(opt_location) ? " at location " : "", opt_location ); -#ifdef __gnu_linux__ - if (rc < 0 && geteuid() != 0) { - fprintf(stderr, - "There were permission problems while accessing USB.\n" - "To fix this, run this tool as root using 'sudo uhubctl',\n" - "or add one or more udev rules like below\n" - "to file '/etc/udev/rules.d/52-usb.rules':\n" - "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"2001\", MODE=\"0666\"\n" - "then run 'sudo udevadm trigger --attr-match=subsystem=usb'\n" - ); - } -#endif rc = 1; goto cleanup; } @@ -944,6 +1033,9 @@ int main(int argc, char *argv[]) continue; if (k == 1 && opt_action == POWER_KEEP) continue; + // if toggle requested, do it only once when `k == 0` + if (k == 1 && opt_action == POWER_TOGGLE) + continue; int i; for (i=0; i<hub_count; i++) { if (hubs[i].actionable == 0) @@ -966,17 +1058,18 @@ int main(int argc, char *argv[]) for (port=1; port <= hubs[i].nports; port++) { if ((1 << (port-1)) & ports) { int port_status = get_port_status(devh, port); - int power_mask = hubs[i].bcd_usb < USB_SS_BCD ? USB_PORT_STAT_POWER - : USB_SS_PORT_STAT_POWER; - if (k == 0 && !(port_status & power_mask)) + int power_mask = hubs[i].super_speed ? USB_SS_PORT_STAT_POWER + : USB_PORT_STAT_POWER; + int powered_on = port_status & power_mask; + if (opt_action == POWER_TOGGLE) { + request = powered_on ? LIBUSB_REQUEST_CLEAR_FEATURE + : LIBUSB_REQUEST_SET_FEATURE; + } + if (k == 0 && !powered_on && opt_action != POWER_TOGGLE) continue; - if (k == 1 && (port_status & power_mask)) + if (k == 1 && powered_on) continue; - int repeat = 1; - if (k == 0) - repeat = opt_repeat; - if (!(port_status & ~power_mask)) - repeat = 1; + int repeat = powered_on ? opt_repeat : 1; while (repeat-- > 0) { rc = libusb_control_transfer(devh, LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_OTHER, @@ -993,7 +1086,7 @@ int main(int argc, char *argv[]) } } /* USB3 hubs need extra delay to actually turn off: */ - if (k==0 && hubs[i].bcd_usb >= USB_SS_BCD) + if (k==0 && hubs[i].super_speed) sleep_ms(150); printf("Sent power %s request\n", request == LIBUSB_REQUEST_CLEAR_FEATURE ? "off" : "on" |