#!/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())