Add PowerStrip device family, PwrUSBDevice driver, and HIDPort transport#115
Merged
Conversation
dccote
force-pushed
the
pwrusb-power-strip
branch
3 times, most recently
from
July 22, 2026 22:38
c0da0bd to
fc49ed3
Compare
Problem: A PwrUSB / PowerUSB power bar (USB HID 04d8:003f, enumerates as "Simple HID Device Demo") with three switchable outlets was attached, but PyHardwareLibrary had no power-strip family or driver for it, and no way to reach an HID device the OS claims (on macOS, IOHIDFamily owns the interface, so neither SerialPort nor USBPort/libusb can open it). Solution: Add hardwarelibrary/powerstrips/ following the interface-segregated capability-mixin pattern of sources/ and daq/. PowerStripDevice is a thin marker base over PhysicalDevice exposing capabilities()/hasCapability(); capabilities.py defines OutletSwitchingControl, DefaultOutletControl, and CurrentMeteringControl, each a public method over a do* hook. PwrUSBDevice implements all three, speaking the device's single-byte HID report protocol through a HIDPort. Outlet state is cached on write because live readback is unreliable on this firmware. DebugPwrUSBPort decodes the real command bytes so DebugPwrUSBDevice exercises the encoding end to end without hardware. Add HIDPort, a CommunicationPort backed by hidapi (IOKit on macOS), alongside SerialPort and USBPort. It is required because the OS claims the HID interface; hidapi is optional, installed via the new pwrusb extra (pip install -e .[pwrusb]), whose package is hidapi (cython-hidapi, imports as hid), not the different hid package. HIDPort.flush drains with a strictly positive per-read timeout, since hidapi blocks on timeout_ms=0. The single-byte protocol was reverse-engineered publicly and cross-checked against aarossig/pwrusbctl (Apache-2.0) and pwrusb.com; the implementation is our own (attribution in the module docstring and CHANGELOG). Verified end to end on the lab unit (a PowerUSB "Basic"): opens over HID and switches outlets 1/2/3 on and off. Metering is only meaningful on a "Smart" unit. Documents the new family in the pyhardwarelibrary skill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dccote
force-pushed
the
pwrusb-power-strip
branch
from
July 22, 2026 22:41
fc49ed3 to
2467a24
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new PowerStrip device family and its first driver,
PwrUSBDevice, for the PwrUSB / PowerUSB controllable power strip (USB HID04d8:003f, enumerates as "Simple HID Device Demo") with three switchable outlets. Also addsHIDPort, aCommunicationPortover hidapi, so HID-only devices the OS claims are reachable (notably on macOS).Verified end to end on the lab unit (a PowerUSB "Basic"): auto-selects the HID transport, opens, and switches outlets 1/2/3 on and off.
Design
Follows the interface-segregated capability-mixin pattern of
sources/anddaq/:PowerStripDevice(PhysicalDevice)— thin family marker exposingcapabilities()/hasCapability().powerstrips/capabilities.py), each a public method over ado*hook:OutletSwitchingControl—turnOutletOn/turnOutletOff/setOutletState/isOutletOn/outletCount(outlets 1-based).DefaultOutletControl— per-outlet power-on (boot) state.CurrentMeteringControl—current()(A),accumulatedCharge()(Ah),resetAccumulatedCharge().PwrUSBDevice(PowerStripDevice, OutletSwitchingControl, DefaultOutletControl, CurrentMeteringControl)implements the device's single-byte HID report protocol, talking to aCommunicationPort. Outlet state is cached on write because live readback is unreliable on this firmware.DebugPwrUSBPortdecodes the real command bytes soDebugPwrUSBDeviceexercises the encoding end to end without hardware.Transport
PwrUSBDevice(transport="auto"|"hid"|"usb"), defaultauto(HID when hidapi is importable, else USB).HIDPort(communication/hidport.py) — hidapi/IOKit backend. Required on macOS, whereIOHIDFamilyclaims the HID interface soSerialPort(no/devnode) andUSBPort/libusb (USBError [Errno 13] Access denied) both fail.pwrusbextra:pip install -e .[pwrusb]. The package ishidapi(cython-hidapi, imports ashid), not the differenthidpackage.Notes / caveats
HIDPort.flush()drains with a strictly positive per-read timeout: hidapi blocks ontimeout_ms=0rather than returning.current()reflects firmware buffer bytes, not a real measurement. The capability is kept for future Smart units.Testing
python3 -m pytest hardwarelibrary/tests/testPwrUSB.py -v— 22 passed. The debug and transport-selection classes always run; the hardware class runs live when a strip is attached and hidapi is installed, and skips cleanly otherwise (per the no-hardware policy).🤖 Generated with Claude Code