如何在Mac上调整外星人鼠标AW720M的灯光颜色

OpenRGB可以调整部分外星人外设的灯光,目前不支持AW720M,在Mac上又不能安装AWCC,这一点戴尔做的属实垃圾,目前亲测成功的是,使用 Hidapi 向外设发送指令来更改灯光颜色。

整体流程:

# AW720M 灯光控制脚本(Mac)

通过 HID Feature Report 控制 Alienware AW720M 等设备的 RGB 灯光。

## 遇到 externally-managed-environment 时(Homebrew Python)

系统禁止直接 `pip3 install` 时,可用下面任一方式,**无需手动建虚拟环境**。

### 方式一:pipx run(推荐,零安装)

每次用 `pipx run` 自动带好 hidapi,不用激活 venv、不用改系统 Python:

```bash
cd Hidapi
# 首次使用需安装 pipx:brew install pipx && pipx ensurepath
pipx run --spec hidapi python3 AW720M.py --list
pipx run --spec hidapi python3 AW720M.py --r 255 --g 0 --b 0
```

### 方式二:允许 pip 安装到系统(一次安装,之后直接 python3)

接受 PEP 668 的覆盖方式,以后可直接 `python3 AW720M.py`:

```bash
pip3 install --break-system-packages hidapi
cd Hidapi
python3 AW720M.py --list
python3 AW720M.py --r 255 --g 0 --b 0
```

### 方式三:虚拟环境(传统做法)

```bash
cd Hidapi
python3 -m venv .venv
source .venv/bin/activate
pip install hidapi
python3 AW720M.py --list
python3 AW720M.py --r 255 --g 0 --b 0
```

## 使用

```bash
# 枚举本机 HID 设备,确认鼠标的 VID/PID
python3 AW720M.py --list

# 设置灯光颜色(不写死 VID/PID,按设备名称自动发现;或 --list 后用 --vid/--pid 指定)
python3 AW720M.py --r 255 --g 0 --b 0
python3 AW720M.py --r 0 --g 255 --b 0
python3 AW720M.py --r 0 --g 0 --b 255
```

(若用方式一,上述命令前加:`pipx run --spec hidapi`。)

## 提示「无法打开设备 / open failed」时

- 脚本**不写死 VID/PID**,设备 ID 从枚举结果动态获取;名称含 Alienware/AW720 等即会匹配。
1. **macOS 输入监控权限**:系统设置 → 隐私与安全性 → 输入监控 → 勾选「终端」(或你运行脚本的 App)。
2. **用 sudo 试一次**:`sudo python3 AW720M.py --r 255 --g 0 --b 0`(若这样能成功,多半是权限问题)。
3. 关闭 **Alienware Command Center、Dell 外设管理** 等可能占用 HID 的软件后再试。
4. **AW720M 请用 USB 线连接**(不要仅用蓝牙)再运行脚本。
5. **macOS 限制**:系统可能独占鼠标的 HID 接口,导致即使用 sudo 仍「open failed」。此时只能在 Windows 或 Linux 下用本脚本控制灯光。
6. 脚本会遍历所有匹配接口并尝试按 path / 按 vid:pid 打开;若仍失败,可用 `--list` 查看本机 VID:PID 后以 `--vid/--pid` 明确指定。

## 参数说明

| 参数 | 说明 |
|------|------|
| `--list` | 列出所有 HID 设备(用于查看 VID/PID) |
| `--vid 0xXXXX` | USB Vendor ID(不指定则按设备名称自动发现) |
| `--pid 0xXXXX` | USB Product ID(不指定则按设备名称自动发现) |
| `--r`, `--g`, `--b` | 红/绿/蓝 0–255 |
| `--zone N` | 灯区索引,默认 0 |

协议参考 OpenRGB 的 Alienware 控制器实现。

Python3 脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AW720M 灯光控制(Mac,可不使用虚拟环境)
通过 HID Feature Report 设置 Alienware 鼠标/设备的 RGB 灯光。
依赖:pip3 install --user hidapi  或  brew install hidapi 后 pip 安装 hidapi
"""

from __future__ import print_function

import argparse
import sys
import time

try:
    import hid
except ImportError:
    print("请先安装 hidapi:", file=sys.stderr)
    print("  方式一(推荐,无需改系统):pipx run --spec hidapi python3 AW720M.py --list", file=sys.stderr)
    print("  方式二(一次安装):pip3 install --break-system-packages hidapi", file=sys.stderr)
    print("  方式三:python3 -m venv .venv && source .venv/bin/activate && pip install hidapi", file=sys.stderr)
    sys.exit(1)

# 协议常量
# APIv4:34 字节 Feature Report(OpenRGB Alienware 笔记本/台式)
REPORT_SIZE = 33
HIDAPI_REPORT_SIZE = REPORT_SIZE + 1
CMD_SET_COLOR = 0x27
# APIv7:65 字节 USB Interrupt OUT(Alienware 鼠标,如 AW610M/AW720M,见 alienfx-tools)
APIv7_REPORT_SIZE = 65
APIv7_CMD_CONTROL = (0x40, 0x10, 0x0c, 0x00, 0x01)  # COMMV7_control
APIv7_EFFECT_COLOR = 1  # v7OpCodes[AlienFX_A_Color]
# COMMV7_update:设色后发送以“提交”,否则后续设色可能不生效
APIv7_CMD_UPDATE = (0x40, 0x60, 0x07, 0x00, 0xc0, 0x4e, 0x00, 0x01)

# 自动发现时仅按设备名称匹配,VID/PID 完全从枚举结果动态获取,不写死
NAME_KEYWORDS = ("alienware", "aw720", "g series")


def _path_str(d):
    p = d.get("path")
    if p is None:
        return ""
    return p.decode("utf-8", errors="replace") if isinstance(p, bytes) else p


def _str_lower(d, key):
    s = (d.get(key) or "").strip() or ""
    if isinstance(s, bytes):
        s = s.decode("utf-8", errors="replace")
    return s.lower()


def _device_name_matches(d, keywords=NAME_KEYWORDS):
    """设备厂商或产品名包含任一关键词即视为匹配,用于动态发现。"""
    product = _str_lower(d, "product_string")
    manufacturer = _str_lower(d, "manufacturer_string")
    combined = product + " " + manufacturer
    return any(kw in combined for kw in keywords)


def list_hid_devices():
    """枚举所有 HID 设备,便于确认 AW720M 的 VID/PID。"""
    print("HID 设备列表(VID:PID 路径 厂商 产品):")
    print("-" * 60)
    for d in hid.enumerate():
        print("  {:04x}:{:04x}  {}  {}  {}".format(
            d["vendor_id"],
            d["product_id"],
            _path_str(d),
            (d.get("manufacturer_string") or "").strip() or "-",
            (d.get("product_string") or "").strip() or "-",
        ))
    print("-" * 60)
    print("未指定 --vid/--pid 时会按设备名称自动匹配(名称含 Alienware/AW720 等);VID:PID 从枚举动态获取。")


def find_target_devices(vid=None, pid=None, name_keywords=None):
    """
    动态查找目标设备,不写死 VID/PID。
    - 若指定了 vid+pid:枚举该 VID:PID 下所有 HID 接口,返回 [(path, vid, pid), ...]。
    - 未指定:枚举全部设备,按厂商/产品名包含关键词匹配,返回匹配项的 path 与枚举得到的 vid/pid。
    返回 [(path, vid, pid), ...],vid/pid 均来自枚举结果。
    """
    out = []
    for d in hid.enumerate(vid if vid is not None else 0, pid if pid is not None else 0):
        v, p = d["vendor_id"], d["product_id"]
        if vid is not None and pid is not None:
            if v != vid or p != pid:
                continue
        else:
            if not _device_name_matches(d, name_keywords or NAME_KEYWORDS):
                continue
        out.append((d["path"], v, p))
    return out


def send_feature_report(dev, buf):
    """发送 HID Feature Report,并做短暂延时避免设备忙。"""
    dev.send_feature_report(bytes(buf))
    time.sleep(0.06)


def get_feature_report(dev, report_id=0x00, size=HIDAPI_REPORT_SIZE):
    """读取 HID Feature Report。"""
    return dev.get_feature_report(report_id, size)


def set_color_apiv7(dev, r, g, b, zone=0, brightness=0x64, retries=5):
    """
    APIv7:65 字节 USB Interrupt OUT,用于 Alienware 鼠标(AW610M/AW720M 等)。
    先发 control 设色,再发 update 提交;重复 retries 次并加延时,提高“有时有效有时无效”时的成功率。
    """
    # 预建 control 与 update 包,避免重复逻辑
    buf = bytearray(APIv7_REPORT_SIZE)
    buf[0] = 0x00
    for i, byte_val in enumerate(APIv7_CMD_CONTROL):
        buf[1 + i] = byte_val
    buf[5] = APIv7_EFFECT_COLOR
    buf[6] = brightness & 0xFF
    buf[7] = zone & 0xFF
    buf[8] = r & 0xFF
    buf[9] = g & 0xFF
    buf[10] = b & 0xFF
    control_packet = bytes(buf)

    upd = bytearray(APIv7_REPORT_SIZE)
    upd[0] = 0x00
    for i, byte_val in enumerate(APIv7_CMD_UPDATE):
        upd[1 + i] = byte_val
    update_packet = bytes(upd)

    time.sleep(0.05)  # 打开设备后稍等再发,避免首包被丢
    for _ in range(retries):
        dev.write(control_packet)
        time.sleep(0.12)
        dev.write(update_packet)
        time.sleep(0.12)


def set_color_direct(dev, r, g, b, zones=(0,)):
    """
    APIv4:34 字节 HID Feature Report,用于部分 Alienware 笔记本/台式。
    部分设备不回复读取,发送后若读回失败则忽略。
    """
    buf = bytearray(HIDAPI_REPORT_SIZE)
    buf[0] = 0x00
    buf[1] = 0x03
    buf[2] = CMD_SET_COLOR
    buf[3] = r & 0xFF
    buf[4] = g & 0xFF
    buf[5] = b & 0xFF
    n = len(zones)
    buf[6] = (n >> 8) & 0xFF
    buf[7] = n & 0xFF
    for i, z in enumerate(zones):
        if 8 + i < len(buf):
            buf[8 + i] = z & 0xFF
    send_feature_report(dev, buf)
    try:
        get_feature_report(dev)
    except OSError:
        pass


def open_device(vid, pid, path=None):
    """
    打开 HID 设备,兼容 hid.Device(新)与 hid.device()(旧)两种 API。
    path 优先;否则用 vid/pid。
    """
    def open_by_path(p):
        if hasattr(hid, "Device"):
            return hid.Device(path=p)
        dev = hid.device()
        dev.open_path(p)
        return dev

    def open_by_vid_pid(v, p):
        if hasattr(hid, "Device"):
            return hid.Device(vid=v, pid=p)
        dev = hid.device()
        dev.open(v, p)
        return dev

    if path is not None:
        return open_by_path(path)
    return open_by_vid_pid(vid, pid)


def main():
    ap = argparse.ArgumentParser(description="AW720M / Alienware HID 灯光控制(可不使用虚拟环境)")
    ap.add_argument("--list", action="store_true", help="枚举 HID 设备并退出,用于查看 VID/PID")
    ap.add_argument("--vid", type=lambda x: int(x, 0), default=None, help="USB Vendor ID(不指定则按设备名称自动发现)")
    ap.add_argument("--pid", type=lambda x: int(x, 0), default=None, help="USB Product ID(不指定则按设备名称自动发现)")
    ap.add_argument("--no-report-id", action="store_true", help="部分设备不需要 Report ID,当前实现仍带 1 字节")
    ap.add_argument("--r", type=int, default=None, metavar="0-255", help="红色")
    ap.add_argument("--g", type=int, default=None, metavar="0-255", help="绿色")
    ap.add_argument("--b", type=int, default=None, metavar="0-255", help="蓝色")
    ap.add_argument("--zone", type=int, default=0, help="灯区索引,默认 0")
    args = ap.parse_args()

    if args.list:
        list_hid_devices()
        return

    r = args.r if args.r is not None else 255
    g = args.g if args.g is not None else 0
    b = args.b if args.b is not None else 0
    r = max(0, min(255, r))
    g = max(0, min(255, g))
    b = max(0, min(255, b))

    # 动态查找目标设备:未指定 vid/pid 时按名称匹配,vid/pid 一律从枚举结果取
    candidates = find_target_devices(args.vid, args.pid)
    if not candidates:
        # 仅当用户同时指定了 vid 和 pid 时,再试一次直接按该 ID 打开
        if args.vid is not None and args.pid is not None:
            candidates = [(None, args.vid, args.pid)]
        else:
            print("未找到名称匹配的设备(Alienware/AW720 等)。请运行 python3 AW720M.py --list 查看设备列表,并用 --vid/--pid 指定。", file=sys.stderr)
            sys.exit(2)

    # 对每个候选接口都打开并发送设色命令(不只看第一个能打开的),避免枚举顺序导致有时命中 LED 有时没命中
    last_error = None
    any_open = False
    any_sent = False
    for path, vid_open, pid_open in candidates:
        dev = None
        try:
            dev = open_device(vid_open, pid_open, path=path)
            any_open = True
        except Exception as e:
            last_error = e
            continue
        try:
            try:
                set_color_apiv7(dev, r, g, b, zone=args.zone)
                any_sent = True
            except OSError:
                try:
                    set_color_direct(dev, r, g, b, zones=(args.zone,))
                    any_sent = True
                except OSError:
                    pass
        finally:
            if dev is not None:
                dev.close()

    # 若按 path 一个都没打开,再试一次仅用 vid/pid 打开
    if not any_open and candidates:
        vid_open, pid_open = candidates[0][1], candidates[0][2]
        try:
            dev = open_device(vid_open, pid_open, path=None)
            any_open = True
            try:
                set_color_apiv7(dev, r, g, b, zone=args.zone)
                any_sent = True
            except OSError:
                try:
                    set_color_direct(dev, r, g, b, zones=(args.zone,))
                    any_sent = True
                except OSError:
                    pass
            finally:
                dev.close()
        except Exception as e:
            last_error = e

    if not any_open:
        print("无法打开 Alienware 设备:{}".format(last_error), file=sys.stderr)
        print("", file=sys.stderr)
        print("常见原因与处理:", file=sys.stderr)
        print("  1. macOS 权限:系统设置 → 隐私与安全性 → 输入监控 → 勾选「终端」或你使用的 App。", file=sys.stderr)
        print("  2. 尝试用 sudo 运行:sudo python3 AW720M.py --r 255 --g 0 --b 0", file=sys.stderr)
        print("  3. 关闭 Alienware Command Center、Dell 外设管理等可能占用设备的软件后重试。", file=sys.stderr)
        print("  4. AW720M 请用 USB 线连接(不要仅用蓝牙),再试。", file=sys.stderr)
        print("  5. macOS 可能独占鼠标的 HID 接口,导致无法打开;可尝试在 Windows 或 Linux 下用本脚本。", file=sys.stderr)
        if not candidates or candidates[0][0] is None:
            print("  6. 运行 python3 AW720M.py --list 查看本机 HID 设备,用 --vid/--pid 指定你的设备。", file=sys.stderr)
        sys.exit(2)

    if any_sent:
        print("已设置灯光 R={} G={} B={}".format(r, g, b))
    else:
        print("已向设备各接口发送命令但均写失败;若灯光无变化可尝试关闭 Dell/Alienware 驱动或换 USB 口。", file=sys.stderr)


if __name__ == "__main__":
    main()

使用:

sudo python3 AW720M.py --r 255 --g 255 --b 255
来更改RGB颜色

Github:整外星人AW720M鼠标颜色

posted @ 2026-02-25 14:22  SKILL·NULL  阅读(21)  评论(0)    收藏  举报