-
Notifications
You must be signed in to change notification settings - Fork 19
NOYITO USB Relay Power Driver #268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../../../../packages/jumpstarter-driver-noyito-relay/README.md | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| __pycache__/ | ||
| .coverage | ||
| coverage.xml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| # NoyitoPowerSerial / NoyitoPowerHID Driver | ||
|
|
||
| `jumpstarter-driver-noyito-relay` provides Jumpstarter power drivers for NOYITO | ||
| USB relay boards in 1, 2, 4, and 8-channel variants. | ||
|
|
||
| Two hardware series are supported: | ||
|
|
||
| - **`NoyitoPowerSerial`** — 1/2-channel boards using a CH340 USB-to-serial chip | ||
| (serial port, supports status query) | ||
| - **`NoyitoPowerHID`** — 4/8-channel "HID Drive-free" boards presenting as a | ||
| USB HID device (no serial port, supports all-channels status query) | ||
|
|
||
| Both use the same 4-byte binary command protocol (`A0` + channel + state + | ||
| checksum). | ||
|
|
||
| ## Installation | ||
|
|
||
| ```shell | ||
| pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-noyito-relay | ||
| ``` | ||
|
|
||
| If you are using `NoyitoPowerHID`, the `hid` Python package requires the native | ||
| `hidapi` shared library. Install it for your OS before use: | ||
|
|
||
| | OS | Command | | ||
| |----|---------| | ||
| | macOS | `brew install hidapi` | | ||
| | Debian/Ubuntu | `sudo apt-get install libhidapi-hidraw0` | | ||
| | Fedora/RHEL | `sudo dnf install hidapi` | | ||
|
|
||
| ## Board Detection | ||
|
|
||
| To determine which driver to use, check whether the board appears as a serial | ||
| port or a HID device: | ||
|
|
||
| - **Serial port** (`/dev/ttyUSB*`, `/dev/tty.usbserial-*`): Use `NoyitoPowerSerial` | ||
| (1/2-channel CH340 board) | ||
| - **No serial port / HID only**: Use `NoyitoPowerHID` (4/8-channel HID | ||
| Drive-free board). Confirm with `lsusb` — the NOYITO HID module appears with | ||
| VID `0x1409` / PID `0x07D7` (decimal: 5131 / 2007). | ||
|
|
||
| ## `NoyitoPowerSerial` (1/2-Channel Serial) | ||
|
|
||
| ### Hardware Notes | ||
|
|
||
| - **Purchase**: [NOYITO 2-Channel USB Relay Module (Amazon)](https://www.amazon.com/NOYITO-2-Channel-Module-Control-Intelligent/dp/B081RM7PMY/) | ||
| - **Chip**: CH340 USB-to-serial | ||
| - **Baud rate**: 9600 | ||
| - **Default port**: `/dev/ttyUSB0` (Linux) — may appear as `/dev/tty.usbserial-*` on macOS | ||
| - **Channels**: 1 or 2 independent relay channels on one USB port | ||
| - **Supply voltage**: 5 V via USB | ||
|
|
||
| ### Configuration | ||
|
|
||
| | Parameter | Type | Default | Description | | ||
| |-----------|------|---------|-------------| | ||
| | `port` | `str` | *(required)* | Serial port path, e.g. `/dev/ttyUSB0` | | ||
| | `channel` | `int` | `1` | Relay channel to control (`1` or `2`) | | ||
| | `all_channels` | `bool` | `false` | Switch both channels simultaneously | | ||
|
|
||
| Example configuration controlling both channels independently: | ||
|
|
||
| ```yaml | ||
| export: | ||
| relay1: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerSerial | ||
| config: | ||
| port: "/dev/ttyUSB0" | ||
| channel: 1 | ||
| relay2: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerSerial | ||
| config: | ||
| port: "/dev/ttyUSB0" | ||
| channel: 2 | ||
| ``` | ||
|
|
||
| ### API Reference | ||
|
|
||
| Implements `PowerInterface` (provides `on`, `off`, `read`, and `cycle` via | ||
| `PowerClient`). | ||
|
|
||
| | Method | Description | | ||
| |--------|-------------| | ||
| | `on()` | Energise the configured relay channel | | ||
| | `off()` | De-energise the configured relay channel | | ||
| | `read()` | Yields a single `PowerReading(voltage=0.0, current=0.0)` | | ||
| | `status()` | Returns the channel state string, e.g. `"on"`, `"off"`, or `"partial"` | | ||
|
|
||
| ### CLI Usage | ||
|
|
||
| Inside a `jmp exporter shell`: | ||
|
|
||
| ```shell | ||
| # Power on relay 1 | ||
| j relay1 on | ||
|
|
||
| # Query state of relay 1 | ||
| j relay1 status | ||
| # on | ||
|
|
||
| # Power cycle relay 2 with a 3-second wait | ||
| j relay2 cycle --wait 3 | ||
|
|
||
| # Power off relay 1 | ||
| j relay1 off | ||
| ``` | ||
|
|
||
| ## `NoyitoPowerHID` (4/8-Channel HID Drive-free) | ||
|
|
||
| ### Hardware Notes | ||
|
|
||
| - **Purchase (4-channel)**: [NOYITO 4-Channel HID Drive-free USB Relay (Amazon)](https://www.amazon.com/NOYITO-Drive-Free-Computer-2-Channel-Micro-USB/dp/B0B538N95Q) | ||
| - **Purchase (8-channel)**: [NOYITO 8-Channel HID Drive-free USB Relay (Amazon)](https://www.amazon.com/NOYITO-Drive-Free-Computer-2-Channel-Micro-USB/dp/B0B536M5MH) | ||
| - **Interface**: USB HID (no serial port) | ||
| - **Default VID/PID**: `5131` / `2007` (0x1409 / 0x07D7) | ||
| - **Channels**: 4 or 8 independent relay channels | ||
| - **Supply voltage**: 5 V via USB | ||
|
|
||
| ### Configuration | ||
|
|
||
| | Parameter | Type | Default | Description | | ||
| |-----------|------|---------|-------------| | ||
| | `num_channels` | `int` | `4` | Number of relay channels on the board (`4` or `8`) | | ||
| | `channel` | `int` | `1` | Relay channel to control (`1`..`num_channels`) | | ||
| | `all_channels` | `bool` | `false` | Fire every channel simultaneously | | ||
| | `vendor_id` | `int` | `5131` | USB vendor ID (override if needed) | | ||
| | `product_id` | `int` | `2007` | USB product ID (override if needed) | | ||
|
|
||
| Example configuration for a 4-channel board (channel 1) and an 8-channel board | ||
| (all channels simultaneously): | ||
|
|
||
| ```yaml | ||
| export: | ||
| relay_4ch_ch1: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerHID | ||
| config: | ||
| num_channels: 4 | ||
| channel: 1 | ||
| relay_8ch_all: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerHID | ||
| config: | ||
| num_channels: 8 | ||
| channel: 1 | ||
| all_channels: true | ||
| ``` | ||
|
|
||
| ### API Reference | ||
|
|
||
| Implements `PowerInterface` (provides `on`, `off`, `read`, and `cycle` via | ||
| `PowerClient`). | ||
|
|
||
| | Method | Description | | ||
| |--------|-------------| | ||
| | `on()` | Energise the configured relay channel(s) | | ||
| | `off()` | De-energise the configured relay channel(s) | | ||
| | `read()` | Yields a single `PowerReading(voltage=0.0, current=0.0)` | | ||
| | `status()` | Returns the channel state string, e.g. `"on"`, `"off"`, or `"partial"` | | ||
|
|
||
| ### CLI Usage | ||
|
|
||
| Inside a `jmp exporter shell`: | ||
|
|
||
| ```shell | ||
| # Power on relay channel 1 of the 4-ch board | ||
| j relay_4ch_ch1 on | ||
|
|
||
| # Power cycle with a 1-second wait | ||
| j relay_4ch_ch1 cycle --wait 1 | ||
|
|
||
| # Power off | ||
| j relay_4ch_ch1 off | ||
|
|
||
| # Power on all 8 channels simultaneously | ||
| j relay_8ch_all on | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| apiVersion: jumpstarter.dev/v1alpha1 | ||
| kind: ExporterConfig | ||
| metadata: | ||
| namespace: default | ||
| name: noyito-relay-demo | ||
| endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082 | ||
| token: "<token>" | ||
| export: | ||
| relay1: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerSerial | ||
| config: | ||
| port: "/dev/cu.usbserial-9120" | ||
| channel: 1 | ||
| relay2: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerSerial | ||
| config: | ||
| port: "/dev/cu.usbserial-9120" | ||
| channel: 2 | ||
| # all_channels=true switches both channels simultaneously | ||
| relay_serial_all: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerSerial | ||
| config: | ||
| port: "/dev/cu.usbserial-9120" | ||
| all_channels: true | ||
| # 4-channel HID board — individual channel | ||
| relay_4ch_ch1: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerHID | ||
| config: | ||
| num_channels: 4 | ||
| channel: 1 | ||
| # 4-channel HID board — all_channels=true fires all 4 channels simultaneously | ||
| relay_4ch_all: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerHID | ||
| config: | ||
| num_channels: 4 | ||
| all_channels: true | ||
| # 8-channel HID board — individual channel | ||
| relay_8ch_ch1: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerHID | ||
| config: | ||
| num_channels: 8 | ||
| channel: 1 | ||
| # 8-channel HID board — all_channels=true fires all 8 channels simultaneously | ||
| relay_8ch_all: | ||
| type: jumpstarter_driver_noyito_relay.driver.NoyitoPowerHID | ||
| config: | ||
| num_channels: 8 | ||
| all_channels: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import click | ||
| from jumpstarter_driver_power.client import PowerClient | ||
|
|
||
|
|
||
| class NoyitoPowerClient(PowerClient): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would go ahead and add status and cli to all PowerClients, defaulting to "not implemented", then we can add it to others as well. WDYT?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Saying because we have been talking about that sometimes. ... anyway, we can do that later TBH, not really this PR, please ignore :D
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mangelajo yeah, I think this makes sense, let's just throw NotImplemented for this driver for now. |
||
| def status(self) -> str: | ||
| """Query the configured relay channel state.""" | ||
| return self.call("status") | ||
|
|
||
| def cli(self): | ||
| base = super().cli() | ||
|
|
||
| @base.command() | ||
| def status(): | ||
| """Query relay channel state""" | ||
| click.echo(self.status()) | ||
|
|
||
| return base | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import sys | ||
| from unittest.mock import MagicMock | ||
|
|
||
| # Stub the hid module so tests run without the native hidapi shared library. | ||
| # NoyitoPowerHID._send_command defers `import hid` to call time; this stub | ||
| # ensures that deferred import returns a mock rather than attempting to load | ||
| # the native library. Tests that verify HID commands patch hid.Device | ||
| # explicitly on top of this stub. | ||
| if "hid" not in sys.modules: | ||
| sys.modules["hid"] = MagicMock() | ||
|
|
||
| import pytest | ||
| import serial | ||
|
|
||
|
|
||
| def pytest_runtest_call(item): | ||
| try: | ||
| item.runtest() | ||
| except serial.SerialException: | ||
| pytest.skip("Serial device not available") # ty: ignore[call-non-callable] | ||
|
Comment on lines
+16
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's examine the actual conftest.py file
cat -n python/packages/jumpstarter-driver-noyito-relay/jumpstarter_driver_noyito_relay/conftest.pyRepository: jumpstarter-dev/jumpstarter Length of output: 871 🏁 Script executed: # Run the verification script from the review comment
#!/bin/bash
set -euo pipefail
echo "Custom pytest_runtest_call hooks:"
rg -n --type=py 'def pytest_runtest_call\('
echo
echo "Direct calls to item.runtest() (should be absent in custom hooks):"
rg -n --type=py 'item\.runtest\s*\('
echo
echo "All pytest hook declarations:"
rg -n --type=py 'def pytest_' python/packages/jumpstarter-driver-noyito-relay/Repository: jumpstarter-dev/jumpstarter Length of output: 1013 🌐 Web query:
💡 Result:
Where it fits in the per-item runtest protocol:
Implementation/usage notes:
Docs: see the pytest “API Reference” entry for Sources: Citations:
Implement The current implementation directly calls 💡 Proposed fix+@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
- try:
- item.runtest()
- except serial.SerialException:
- pytest.skip("Serial device not available") # ty: ignore[call-non-callable]
+ outcome = yield
+ if outcome.excinfo and issubclass(outcome.excinfo[0], serial.SerialException):
+ pytest.skip("Serial device not available")🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs page is currently orphaned from Sphinx navigation.
The docs pipeline warning shows this document is not included in any toctree, so it won’t be discoverable in generated reference docs. Please add it to the appropriate drivers toctree index.
🧰 Tools
🪛 GitHub Actions: Documentation
[warning] 1-1: Sphinx warning: document isn't included in any toctree
🤖 Prompt for AI Agents