Initial release

This commit is contained in:
2026-05-27 07:09:37 +02:00
commit 3ea341a969
5 changed files with 1357 additions and 0 deletions
+787
View File
@@ -0,0 +1,787 @@
#!/usr/bin/env python3
"""Experimental HID RGB control for PH18-class laptops.
This script targets models where WMI RGB calls succeed but do not change LEDs.
It uses hidraw devices directly:
- Main keyboard baseline via 05af:867b ff02 path
- Rear/logo/bar zones via 0d62:a21a / 0d62:a01a short packets
"""
from __future__ import annotations
import argparse
import dataclasses
import fcntl
import os
import time
from pathlib import Path
FF02_HID_IDS = {"0003:000005AF:0000867B", "0003:000005AF:0000866A"}
DARFON_HID_IDS = {"0003:00000D62:0000A21A", "0003:00000D62:0000A01A", "0003:00000D62:0000BA51"}
DEFAULT_REPORT_DELAY = 0.02
DEFAULT_STATIC_BRIGHTNESS = 0x19
KEYBOARD_INDEX_COUNT = 102
ZONE_INDEX_GROUPS: tuple[range, range, range, range] = (
range(0, 26),
range(26, 52),
range(52, 77),
range(77, KEYBOARD_INDEX_COUNT),
)
IOC_NRBITS = 8
IOC_TYPEBITS = 8
IOC_SIZEBITS = 14
IOC_DIRBITS = 2
IOC_NRSHIFT = 0
IOC_TYPESHIFT = IOC_NRSHIFT + IOC_NRBITS
IOC_SIZESHIFT = IOC_TYPESHIFT + IOC_TYPEBITS
IOC_DIRSHIFT = IOC_SIZESHIFT + IOC_SIZEBITS
IOC_WRITE = 1
IOC_READ = 2
def _ioc(direction: int, type_: int, number: int, size: int) -> int:
return (
(direction << IOC_DIRSHIFT)
| (type_ << IOC_TYPESHIFT)
| (number << IOC_NRSHIFT)
| (size << IOC_SIZESHIFT)
)
def hidiocsfeature(length: int) -> int:
return _ioc(IOC_WRITE | IOC_READ, ord("H"), 0x06, length)
@dataclasses.dataclass(frozen=True)
class HidrawInfo:
node: str
hid_id: str | None
interface: int | None
product: str | None
hid_name: str | None
@property
def label(self) -> str:
parts = [self.node]
if self.hid_id:
parts.append(self.hid_id)
if self.interface is not None:
parts.append(f"if={self.interface}")
if self.product:
parts.append(self.product)
elif self.hid_name:
parts.append(self.hid_name)
return " ".join(parts)
@property
def lowered_name(self) -> str:
return " ".join(part for part in [self.product, self.hid_name] if part).lower()
def _read_first_existing(base: Path, names: list[str]) -> str | None:
for name in names:
candidate = base / name
if not candidate.exists():
continue
try:
value = candidate.read_text(encoding="utf-8", errors="replace").strip()
except OSError:
continue
if value:
return value
return None
def collect_hidraw_info() -> list[HidrawInfo]:
devices: list[HidrawInfo] = []
for class_path in sorted(Path("/sys/class/hidraw").glob("hidraw*")):
device_path = (class_path / "device").resolve()
hid_id: str | None = None
interface: int | None = None
product: str | None = None
hid_name: str | None = None
uevent_fields: dict[str, str] = {}
for parent in [device_path, *device_path.parents]:
uevent = parent / "uevent"
if uevent.exists():
try:
for line in uevent.read_text(encoding="utf-8", errors="replace").splitlines():
if "=" not in line:
continue
key, value = line.split("=", 1)
uevent_fields.setdefault(key, value)
except OSError:
pass
if interface is None and (parent / "bInterfaceNumber").exists():
try:
interface = int((parent / "bInterfaceNumber").read_text().strip(), 16)
except (OSError, ValueError):
pass
if product is None:
product = _read_first_existing(parent, ["product"])
hid_id = uevent_fields.get("HID_ID")
hid_name = uevent_fields.get("HID_NAME")
devices.append(
HidrawInfo(
node=f"/dev/{class_path.name}",
hid_id=hid_id,
interface=interface,
product=product,
hid_name=hid_name,
)
)
return devices
def checksum8(prefix: list[int]) -> int:
return (~(sum(prefix) & 0xFF)) & 0xFF
def build_feature(prefix: list[int]) -> bytes:
if len(prefix) != 7:
raise ValueError("feature prefix must be exactly 7 bytes")
return bytes(prefix + [checksum8(prefix)])
def parse_rgb(raw: str) -> tuple[int, int, int]:
text = raw.strip().lower().replace("#", "")
if "," in text:
parts = [part.strip() for part in text.split(",")]
if len(parts) != 3:
raise ValueError("CSV RGB must be R,G,B")
values = tuple(int(part, 10) for part in parts)
if any(value < 0 or value > 255 for value in values):
raise ValueError("RGB components must be in range 0..255")
return values[0], values[1], values[2]
if len(text) != 6:
raise ValueError("RGB must be 6 hex chars, e.g. ff00aa")
return int(text[0:2], 16), int(text[2:4], 16), int(text[4:6], 16)
def format_hexdump(payload: bytes, *, indent: str = " ", width: int = 16) -> str:
if not payload:
return f"{indent}0000"
lines: list[str] = []
hex_width = width * 3 - 1
for offset in range(0, len(payload), width):
chunk = payload[offset:offset + width]
hex_part = " ".join(f"{byte:02x}" for byte in chunk).ljust(hex_width)
ascii_part = "".join(chr(byte) if 32 <= byte <= 126 else "." for byte in chunk)
lines.append(f"{indent}{offset:04x} {hex_part} |{ascii_part}|")
return "\n".join(lines)
def ioctl_feature(node: str, payload: bytes) -> None:
buffer = bytearray(payload)
with open(node, "r+b", buffering=0) as handle:
fcntl.ioctl(handle.fileno(), hidiocsfeature(len(buffer)), buffer, True)
def write_output(node: str, payload: bytes) -> None:
with open(node, "r+b", buffering=0) as handle:
os.write(handle.fileno(), payload)
def select_ff02_nodes(devices: list[HidrawInfo], explicit: str | None) -> list[HidrawInfo]:
if explicit:
for info in devices:
if info.node == explicit:
return [info]
raise SystemExit(f"ff02 device not found: {explicit}")
candidates = [
info
for info in devices
if info.hid_id in FF02_HID_IDS and info.interface is not None and info.interface >= 0
]
if not candidates:
raise SystemExit("no ff02 keyboard HID node found (expected 05af:867b)")
descriptor_candidates: list[HidrawInfo] = []
for info in candidates:
descriptor_path = Path("/sys/class/hidraw") / Path(info.node).name / "device" / "report_descriptor"
try:
descriptor = descriptor_path.read_bytes()
except OSError:
continue
if descriptor.startswith(b"\x06\x02\xff"):
descriptor_candidates.append(info)
# Keep descriptor-matched nodes first, but also include the other
# interfaces for the same keyboard HID ID. Some PH18 units appear to
# split the board across interfaces.
ordered = sorted(
candidates,
key=lambda info: (
0 if info in descriptor_candidates else 1,
info.interface if info.interface is not None else 99,
info.node,
),
)
return ordered
def select_darfon_nodes(devices: list[HidrawInfo]) -> dict[str, HidrawInfo]:
candidates = [info for info in devices if info.hid_id in DARFON_HID_IDS and info.interface == 0]
if not candidates:
raise SystemExit("no Darfon HID nodes found (expected 0d62:a21a / 0d62:a01a)")
rear = next((dev for dev in candidates if "infinitering" in dev.lowered_name), None)
logo = next((dev for dev in candidates if "logo" in dev.lowered_name), None)
if rear is None:
rear = candidates[0]
if logo is None:
logo = next((dev for dev in candidates if dev.node != rear.node), rear)
return {"rear": rear, "logo": logo, "bar": logo}
def _descriptor_contains(descriptor: bytes, pattern: bytes) -> bool:
return any(descriptor[i:i + len(pattern)] == pattern for i in range(len(descriptor) - len(pattern) + 1))
def select_vendor_keyboard_nodes(devices: list[HidrawInfo], explicit: str | None) -> list[HidrawInfo]:
if explicit:
for info in devices:
if info.node == explicit:
return [info]
raise SystemExit(f"vendor keyboard device not found: {explicit}")
candidates = [info for info in devices if info.hid_id in FF02_HID_IDS]
if not candidates:
raise SystemExit("no keyboard HID candidates found for per-key zone writes")
vendor_nodes: list[HidrawInfo] = []
for info in candidates:
descriptor_path = Path("/sys/class/hidraw") / Path(info.node).name / "device" / "report_descriptor"
try:
descriptor = descriptor_path.read_bytes()
except OSError:
continue
has_report82 = _descriptor_contains(descriptor, b"\x85\x82")
has_report83 = _descriptor_contains(descriptor, b"\x85\x83")
if has_report82 and has_report83:
vendor_nodes.append(info)
if vendor_nodes:
return sorted(
vendor_nodes,
key=lambda info: (info.interface if info.interface is not None else 99, info.node),
)
return sorted(
candidates,
key=lambda info: (info.interface if info.interface is not None else 99, info.node),
)
def select_keyboard_family_nodes(devices: list[HidrawInfo], explicit: str | None) -> list[HidrawInfo]:
if explicit:
for info in devices:
if info.node == explicit:
return [info]
raise SystemExit(f"keyboard device not found: {explicit}")
nodes = [info for info in devices if info.hid_id in FF02_HID_IDS]
if not nodes:
raise SystemExit("no 05af keyboard HID nodes found")
return sorted(nodes, key=lambda info: (info.interface if info.interface is not None else 99, info.node))
def build_report84_single_index(
index: int,
red: int,
green: int,
blue: int,
*,
mode_selector: int = 1,
brightness_level: int = 8,
) -> bytes:
payload = bytearray()
payload.extend([0x84, mode_selector & 0xFF, min(8, brightness_level) & 0xFF])
index_le = index.to_bytes(2, byteorder="little", signed=False)
for _ in range(8):
payload.extend(index_le)
for _ in range(8):
payload.extend([red & 0xFF, green & 0xFF, blue & 0xFF, 0x00])
return bytes(payload)
def paint_index_via_report84(node: str, index: int, rgb: tuple[int, int, int], *, dry_run: bool) -> None:
report84 = build_report84_single_index(index, rgb[0], rgb[1], rgb[2])
commit86 = bytes([0x86, 0x01])
if dry_run:
print(f"{node} report84 index={index} rgb={rgb[0]},{rgb[1]},{rgb[2]}")
return
ioctl_feature(node, report84)
ioctl_feature(node, commit86)
def run_keyboard_baseline(
node: str,
rgb: tuple[int, int, int],
*,
dry_run: bool,
report_delay: float,
passes: int,
banks: int,
include_magkeys: bool,
) -> None:
frame_word = bytes([0xFF, rgb[0], rgb[1], rgb[2]])
frame = frame_word * 16
prelude = (
bytes([0xB1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E]),
bytes([0x08, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF5]),
bytes([0x08, 0x02, 0x4F, 0x0A, 0x32, 0x00, 0x00, 0x6A]),
bytes([0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xEA]),
bytes([0x13, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xE4]),
bytes([0x08, 0x02, 0x4F, 0x05, 0x32, 0x08, 0x01, 0x66]),
)
pre_a = bytes([0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77])
pre_b = bytes([0x12, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xE5])
commit33 = bytes([0x08, 0x02, 0x33, 0x05, 0x32, 0x08, 0x01, 0x82])
print(f"keyboard node: {node}")
print(f"word: ff:{rgb[0]:02x}:{rgb[1]:02x}:{rgb[2]:02x}")
print(f"passes={passes}, banks={banks}")
for packet in prelude:
prefixed = b"\x00" + packet
print("feature ff02 prelude")
print(format_hexdump(prefixed))
if not dry_run:
ioctl_feature(node, prefixed)
if report_delay > 0:
time.sleep(report_delay)
for _ in range(passes):
for packet in (pre_a, pre_b):
prefixed = b"\x00" + packet
print("feature ff02 sync")
print(format_hexdump(prefixed))
if not dry_run:
ioctl_feature(node, prefixed)
if report_delay > 0:
time.sleep(report_delay)
for _ in range(banks):
print("output frame")
print(format_hexdump(frame))
if not dry_run:
write_output(node, frame)
if report_delay > 0:
time.sleep(report_delay)
prefixed_commit = b"\x00" + commit33
print("feature ff02 commit33")
print(format_hexdump(prefixed_commit))
if not dry_run:
ioctl_feature(node, prefixed_commit)
if report_delay > 0:
time.sleep(report_delay)
if include_magkeys:
run_magkey_overlay(node, rgb, dry_run=dry_run, report_delay=report_delay)
def build_magkey_payload(rgb: tuple[int, int, int]) -> bytes:
# Emitters:
# 0..2 = W, 3..5 = A, 6..8 = S, 9..11 = D
# Mapping observed from PH18 HID captures:
# frame[N*4+2] = R, frame[N*4+3] = G, frame[(N+1)*4] = B
frame = [0x00] * 64
r, g, b = rgb
for emitter in range(12):
frame[emitter * 4 + 2] = r
frame[emitter * 4 + 3] = g
frame[(emitter + 1) * 4] = b
return bytes(frame)
def run_magkey_overlay(
node: str,
rgb: tuple[int, int, int],
*,
dry_run: bool,
report_delay: float,
) -> None:
prelude = (
bytes([0xB1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E]),
bytes([0x08, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF5]),
bytes([0x08, 0x02, 0x4F, 0x0A, 0x32, 0x00, 0x00, 0x6A]),
bytes([0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xEA]),
bytes([0x13, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xE4]),
bytes([0x08, 0x02, 0x4F, 0x05, 0x32, 0x08, 0x01, 0x66]),
)
commit = bytes([0x08, 0x02, 0x4F, 0x05, 0x32, 0x08, 0x01, 0x66])
payload = build_magkey_payload(rgb)
print("applying MagKey/WASD overlay")
for packet in prelude:
prefixed = b"\x00" + packet
print("feature ff02 magkey prelude")
print(format_hexdump(prefixed))
if not dry_run:
ioctl_feature(node, prefixed)
if report_delay > 0:
time.sleep(report_delay)
print("output magkey frame")
print(format_hexdump(payload))
if not dry_run:
write_output(node, payload)
if report_delay > 0:
time.sleep(report_delay)
prefixed_commit = b"\x00" + commit
print("feature ff02 magkey commit")
print(format_hexdump(prefixed_commit))
if not dry_run:
ioctl_feature(node, prefixed_commit)
if report_delay > 0:
time.sleep(report_delay)
def run_keyboard_baseline_all(
nodes: list[str],
rgb: tuple[int, int, int],
*,
dry_run: bool,
report_delay: float,
passes: int,
banks: int,
include_magkeys: bool,
) -> None:
success_count = 0
for node in nodes:
try:
run_keyboard_baseline(
node,
rgb,
dry_run=dry_run,
report_delay=report_delay,
passes=passes,
banks=banks,
include_magkeys=include_magkeys,
)
success_count += 1
except OSError as exc:
print(f"warning: {node} failed: {exc}")
if success_count == 0:
raise SystemExit("failed to write keyboard baseline on all candidate ff02 nodes")
def run_keyboard_zones(
ff02_nodes: list[str],
vendor_nodes: list[str],
zone_colors: tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]],
*,
dry_run: bool,
report_delay: float,
passes: int,
banks: int,
include_magkeys: bool,
) -> None:
# Anchor keyboard in static mode and clear previous state before per-zone paint.
run_keyboard_baseline_all(
ff02_nodes,
(0, 0, 0),
dry_run=dry_run,
report_delay=report_delay,
passes=passes,
banks=banks,
include_magkeys=False,
)
print("applying keyboard 4-zone colors via report84")
for zone_index, key_indexes in enumerate(ZONE_INDEX_GROUPS):
rgb = zone_colors[zone_index]
print(f"zone{zone_index + 1}: rgb={rgb[0]},{rgb[1]},{rgb[2]} indexes={key_indexes.start}-{key_indexes.stop - 1}")
for node in vendor_nodes:
for index in key_indexes:
paint_index_via_report84(node, index, rgb, dry_run=dry_run)
if report_delay > 0:
time.sleep(report_delay)
if include_magkeys:
for node in ff02_nodes:
run_magkey_overlay(node, zone_colors[0], dry_run=dry_run, report_delay=report_delay)
def run_keyboard_dynamic(
nodes: list[str],
*,
dry_run: bool,
repeats: int,
report_delay: float,
) -> None:
# Working sequence confirmed on PH18-73:
# legacy-86-with-blackout
payloads = (
("blackout-raw", bytes([0x86, 0x00])),
("dynamic-raw", bytes([0x86, 0x01])),
("dynamic-prefixed", bytes([0x00, 0x86, 0x01])),
)
success_count = 0
for node in nodes:
for _ in range(max(1, repeats)):
for encoding, payload in payloads:
print(f"{node} feature return-to-dynamic ({encoding})")
print(format_hexdump(payload))
if not dry_run:
try:
ioctl_feature(node, payload)
success_count += 1
except OSError as exc:
print(f"warning: {node} {encoding} failed: {exc}")
if report_delay > 0:
time.sleep(report_delay)
if not dry_run and success_count == 0:
raise SystemExit("failed to switch keyboard to dynamic mode on all keyboard nodes")
def zone_packets(zone: str, mode: str, rgb: tuple[int, int, int], brightness: int) -> list[bytes]:
selector = 0x00 if zone in {"rear", "logo"} else 0x05
if mode == "static":
return [
build_feature([0x14, 0x01, selector, rgb[0], rgb[1], rgb[2], 0x00]),
build_feature([0x08, selector, 0x01, 0x05, brightness, 0x00, 0x01]),
]
if mode == "off":
return [build_feature([0x08, selector, 0x01, 0x05, 0x00, 0x00, 0x01])]
if mode == "progressbar":
effect = 0x03 if zone == "logo" else 0x5D
return [build_feature([0x08, selector, effect, 0x05, 0x64, 0x08, 0x01])]
raise ValueError(f"unsupported mode '{mode}' for zone '{zone}'")
def run_zone_apply(
devices: dict[str, HidrawInfo],
zone: str,
mode: str,
rgb: tuple[int, int, int],
*,
brightness: int,
dry_run: bool,
init: bool,
report_delay: float,
) -> None:
zones = [zone] if zone != "all" else ["rear", "logo", "bar"]
init_payload = bytes([0x41, 0x01] + [0x00] * 62)
init_done: set[str] = set()
for name in zones:
target = devices[name].node
if init and target not in init_done:
print(f"{target} output init")
print(format_hexdump(init_payload))
if not dry_run:
write_output(target, init_payload)
init_done.add(target)
if report_delay > 0:
time.sleep(report_delay)
for packet in zone_packets(name, mode, rgb, brightness):
print(f"{target} feature {name}/{mode}")
print(format_hexdump(packet))
if not dry_run:
ioctl_feature(target, b"\x00" + packet)
if report_delay > 0:
time.sleep(report_delay)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="PH18 HID RGB utility (experimental)")
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("list", help="list hidraw devices and guessed RGB mapping")
keyboard = sub.add_parser("keyboard", help="set full-keyboard static baseline via ff02")
keyboard.add_argument("--color", default="00c8ff", help="R,G,B or hex RRGGBB")
keyboard.add_argument("--ff02-device", help="override ff02 hidraw node (e.g. /dev/hidraw5)")
keyboard.add_argument("--dry-run", action="store_true", help="print packets without sending")
keyboard.add_argument(
"--no-magkeys",
action="store_true",
help="do not apply the MagKey/WASD overlay frame after keyboard baseline",
)
keyboard.add_argument(
"--passes",
type=int,
default=20,
help="anchor loop count (default: 20, improves full-board coverage on some units)",
)
keyboard.add_argument(
"--banks",
type=int,
default=8,
help="frame writes per pass (default: 8, improves full-board coverage on some units)",
)
keyboard.add_argument(
"--report-delay", type=float, default=DEFAULT_REPORT_DELAY, help="sleep between writes"
)
keyboard_zones = sub.add_parser(
"keyboard-zones",
help="set 4 keyboard zones with distinct colors (experimental report84 overlay)",
)
keyboard_zones.add_argument("--z1", required=True, help="zone1 color (left), R,G,B or RRGGBB")
keyboard_zones.add_argument("--z2", required=True, help="zone2 color, R,G,B or RRGGBB")
keyboard_zones.add_argument("--z3", required=True, help="zone3 color, R,G,B or RRGGBB")
keyboard_zones.add_argument("--z4", required=True, help="zone4 color (right), R,G,B or RRGGBB")
keyboard_zones.add_argument("--ff02-device", help="override ff02 hidraw node")
keyboard_zones.add_argument("--vendor-device", help="override vendor report84 hidraw node")
keyboard_zones.add_argument("--dry-run", action="store_true", help="print writes without sending")
keyboard_zones.add_argument(
"--no-magkeys",
action="store_true",
help="do not apply the MagKey/WASD overlay (defaults to zone1 color)",
)
keyboard_zones.add_argument(
"--passes",
type=int,
default=20,
help="anchor loop count before per-zone writes (default: 20)",
)
keyboard_zones.add_argument(
"--banks",
type=int,
default=8,
help="frame writes per pass before per-zone writes (default: 8)",
)
keyboard_zones.add_argument(
"--report-delay",
type=float,
default=0.0,
help="sleep between per-key writes (default: 0.0)",
)
keyboard_dynamic = sub.add_parser(
"keyboard-dynamic",
help="switch keyboard back to firmware dynamic animation",
)
keyboard_dynamic.add_argument("--vendor-device", help="override one keyboard hidraw node")
keyboard_dynamic.add_argument("--dry-run", action="store_true", help="print writes without sending")
keyboard_dynamic.add_argument("--repeats", type=int, default=2, help="number of report86 writes per node")
keyboard_dynamic.add_argument(
"--report-delay", type=float, default=DEFAULT_REPORT_DELAY, help="sleep between writes"
)
zone = sub.add_parser("zone", help="control rear/logo/bar HID zones")
zone.add_argument("target", choices=["rear", "logo", "bar", "all"])
zone.add_argument("mode", choices=["static", "off", "progressbar"])
zone.add_argument("--color", default="00c8ff", help="R,G,B or hex RRGGBB")
zone.add_argument("--brightness", type=int, default=DEFAULT_STATIC_BRIGHTNESS)
zone.add_argument("--dry-run", action="store_true", help="print packets without sending")
zone.add_argument("--no-init", action="store_true", help="skip 41 01 init packet")
zone.add_argument(
"--report-delay", type=float, default=DEFAULT_REPORT_DELAY, help="sleep between writes"
)
return parser.parse_args()
def main() -> int:
args = parse_args()
devices = collect_hidraw_info()
if args.command == "list":
print("Detected hidraw devices:")
for info in devices:
print(f" {info.label}")
try:
ff02_nodes = select_ff02_nodes(devices, None)
print("ff02 keyboard candidates:")
for info in ff02_nodes:
print(f" {info.label}")
except SystemExit as exc:
print(f"ff02 keyboard candidates: {exc}")
try:
vendor_nodes = select_vendor_keyboard_nodes(devices, None)
print("vendor report84 candidates:")
for info in vendor_nodes:
print(f" {info.label}")
except SystemExit as exc:
print(f"vendor report84 candidates: {exc}")
try:
zones = select_darfon_nodes(devices)
print(f"rear candidate: {zones['rear'].label}")
print(f"logo candidate: {zones['logo'].label}")
print(f"bar candidate: {zones['bar'].label}")
except SystemExit as exc:
print(f"Darfon candidates: {exc}")
return 0
if args.command == "keyboard":
rgb = parse_rgb(args.color)
ff02_nodes = select_ff02_nodes(devices, args.ff02_device)
run_keyboard_baseline_all(
[info.node for info in ff02_nodes],
rgb,
dry_run=args.dry_run,
report_delay=args.report_delay,
passes=max(1, args.passes),
banks=max(1, args.banks),
include_magkeys=not args.no_magkeys,
)
return 0
if args.command == "keyboard-zones":
zone_colors = (
parse_rgb(args.z1),
parse_rgb(args.z2),
parse_rgb(args.z3),
parse_rgb(args.z4),
)
ff02_nodes = select_ff02_nodes(devices, args.ff02_device)
vendor_nodes = select_vendor_keyboard_nodes(devices, args.vendor_device)
run_keyboard_zones(
[info.node for info in ff02_nodes],
[info.node for info in vendor_nodes],
zone_colors,
dry_run=args.dry_run,
report_delay=max(0.0, args.report_delay),
passes=max(1, args.passes),
banks=max(1, args.banks),
include_magkeys=not args.no_magkeys,
)
return 0
if args.command == "keyboard-dynamic":
keyboard_nodes = select_keyboard_family_nodes(devices, args.vendor_device)
run_keyboard_dynamic(
[info.node for info in keyboard_nodes],
dry_run=args.dry_run,
repeats=max(1, args.repeats),
report_delay=max(0.0, args.report_delay),
)
return 0
if args.command == "zone":
if not 0 <= args.brightness <= 255:
raise SystemExit("--brightness must be in range 0..255")
rgb = parse_rgb(args.color)
zone_devices = select_darfon_nodes(devices)
run_zone_apply(
zone_devices,
args.target,
args.mode,
rgb,
brightness=args.brightness,
dry_run=args.dry_run,
init=not args.no_init,
report_delay=args.report_delay,
)
return 0
raise SystemExit("invalid command")
if __name__ == "__main__":
raise SystemExit(main())