Skip to content

Add PowerStrip device family, PwrUSBDevice driver, and HIDPort transport#115

Merged
dccote merged 1 commit into
masterfrom
pwrusb-power-strip
Jul 22, 2026
Merged

Add PowerStrip device family, PwrUSBDevice driver, and HIDPort transport#115
dccote merged 1 commit into
masterfrom
pwrusb-power-strip

Conversation

@dccote

@dccote dccote commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a new PowerStrip device family and its first driver, PwrUSBDevice, for the PwrUSB / PowerUSB controllable power strip (USB HID 04d8:003f, enumerates as "Simple HID Device Demo") with three switchable outlets. Also adds HIDPort, a CommunicationPort over 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/ and daq/:

  • PowerStripDevice(PhysicalDevice) — thin family marker exposing capabilities() / hasCapability().
  • Capability mixins (powerstrips/capabilities.py), each a public method over a do* hook:
    • OutletSwitchingControlturnOutletOn/turnOutletOff/setOutletState/isOutletOn/outletCount (outlets 1-based).
    • DefaultOutletControl — per-outlet power-on (boot) state.
    • CurrentMeteringControlcurrent() (A), accumulatedCharge() (Ah), resetAccumulatedCharge().
  • PwrUSBDevice(PowerStripDevice, OutletSwitchingControl, DefaultOutletControl, CurrentMeteringControl) implements the device's single-byte HID report protocol, talking to a CommunicationPort. 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.

Transport

PwrUSBDevice(transport="auto"|"hid"|"usb"), default auto (HID when hidapi is importable, else USB).

  • HIDPort (communication/hidport.py) — hidapi/IOKit backend. Required on macOS, where IOHIDFamily claims the HID interface so SerialPort (no /dev node) and USBPort/libusb (USBError [Errno 13] Access denied) both fail.
  • hidapi is optional; install via the new pwrusb extra: pip install -e .[pwrusb]. The package is hidapi (cython-hidapi, imports as hid), not the different hid package.

Notes / caveats

  • The protocol was reverse-engineered publicly and cross-checked against aarossig/pwrusbctl (Apache-2.0) and pwrusb.com; the implementation here is our own (attribution in the module docstring and CHANGELOG).
  • HIDPort.flush() drains with a strictly positive per-read timeout: hidapi blocks on timeout_ms=0 rather than returning.
  • Metering values are only meaningful on a metering-capable ("Smart") unit; the lab strip is a Basic, so its 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 -v22 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

@dccote
dccote force-pushed the pwrusb-power-strip branch 3 times, most recently from c0da0bd to fc49ed3 Compare July 22, 2026 22:38
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
dccote force-pushed the pwrusb-power-strip branch from fc49ed3 to 2467a24 Compare July 22, 2026 22:41
@dccote
dccote merged commit df99465 into master Jul 22, 2026
14 checks passed
@dccote
dccote deleted the pwrusb-power-strip branch July 22, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant