Files
Razer-led-mac/utils/tools.go
2026-02-09 00:25:07 +08:00

284 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"errors"
"fmt"
"time"
"github.com/karalabe/hid"
)
// GetDeviceType 根据产品 ID (PID) 获取设备类型名称
// 如果 PID 不在映射表中,则返回 "unknown"
func GetDeviceType(pid uint16) string {
if deviceType, exists := RAZER_DEVICE_TYPES[pid]; exists {
return deviceType
}
return "unknown"
}
// IsMouseDevice 判断指定 PID 是否为鼠标设备
func IsMouseDevice(pid uint16) bool {
return GetDeviceType(pid) == "mouse"
}
// IsKeyboardDevice 判断指定 PID 是否为键盘设备
func IsKeyboardDevice(pid uint16) bool {
return GetDeviceType(pid) == "keyboard"
}
type RazerDevice struct {
Name string
Product string
ProductID uint16 // PID
Serial string
TransactionID uint8
Devices []hid.DeviceInfo
CurrentEffect int
CurrentColor string
}
// calculateCRC 计算雷蛇协议校验和
// 逻辑:对报告中第 2 字节索引2到第 88 字节索引87进行异或运算
func calculateCRC(reportData []byte) byte {
var crc byte = 0
// 按照 Python 逻辑range(2, 88)
for i := 2; i < 88; i++ {
if i < len(reportData) {
crc ^= reportData[i]
}
}
return crc
}
//// buildArguments 构造灯效参数数组
//// effectCode: 效果代码 (如静态、呼吸)
//// ledID: 控制区域 (如键盘背光、滚轮)
//// extraParams: 额外的参数 (通常是 RGB 颜色值)
//func buildArguments(effectCode byte, ledID byte, extraParams []byte) []byte {
// // 构造固定格式前缀:[存储区, LED_ID, 效果代码, 0x00, 0x00, 启用开关(0x01)]
// prefix := []byte{VARSTORE, ledID, effectCode, 0x00, 0x00, 0x01}
//
// // 使用 append 将 extraParams 拼接在 prefix 后面
// // 注意extraParams... 是 Go 的展开语法,相当于把切片里的元素一个个传给 append
// return append(prefix, extraParams...)
//}
// buildArguments 构造雷蛇 HID 报告的参数部分Arguments Section
// 该函数负责生成 90 字节报告中从第 8 字节开始的数据载荷。
//
// 参数说明:
// 1. effectCode: 灯效类型。例如 0x01(常亮), 0x02(呼吸), 0x03(波浪)。
// 2. ledID: 控制的硬件区域。例如 0x05 通常代表键盘主背光。
// 3. extraParams: 模式特定的扩展参数。
// - 对于常亮:通常是 [启用位(0x01), R, G, B]
// - 对于呼吸:通常是 [子模式, R1, G1, B1, R2, G2, B2, 速度]
func buildArguments(effectCode byte, ledID byte, extraParams []byte) []byte {
// 构造前 5 位固定协议头
// [0] VARSTORE: 数据存储区,通常固定为 0x01 (Flash/RAM)
// [1] ledID: 目标 LED 标识0x05 是键盘背光
// [2] effectCode: 告诉硬件现在要执行哪种灯效逻辑
// [3] 0x00: 预留字节 (Reserved)
// [4] 0x00: 预留字节 (Reserved)
prefix := []byte{VARSTORE, ledID, effectCode, 0x00, 0x00}
// 将前缀与动态扩展参数合并
// 注意:从第 6 个字节 (索引 5) 开始,数据含义取决于具体的 effectCode
return append(prefix, extraParams...)
}
// constructRazerReport 构造标准 90 字节的雷蛇 HID 报告
func constructRazerReport(transactionID byte, commandClass byte, commandID byte, dataSize byte, arguments []byte) ([]byte, error) {
// 参数长度检查
if len(arguments) > 80 {
return nil, errors.New("参数列表过长 (最大支持 80 字节)")
}
// 初始化一个全为 0 的 90 字节切片
report := make([]byte, REPORT_LEN)
// 填充固定格式
report[0] = 0x00 // Report ID (通常为 0)
report[1] = transactionID // 事务 ID (Transaction ID)
report[2] = 0x00 // 状态字节 1
report[3] = 0x00 // 状态字节 2
report[4] = 0x00 // 状态字节 3
report[5] = dataSize // 数据部分长度
report[6] = commandClass // 命令类 (Command Class)
report[7] = commandID // 命令 ID (Command ID)
// 填充参数部分 (从第 8 字节开始)
// 使用 copy 函数更安全高效
copy(report[8:], arguments)
// 计算校验和并填入第 88 字节 (索引 88)
report[88] = calculateCRC(report)
// 结束字节 (索引 89) 默认为 0x00
report[89] = 0x00
return report, nil
}
// ScanDevices --- 扫描逻辑抽离 ---
func ScanDevices() map[string]*RazerDevice {
rawDevices, _ := hid.Enumerate(RAZER_VID, 0)
grouped := make(map[string]*RazerDevice)
for _, dev := range rawDevices {
//// 打印 128 个横杠作为视觉分隔线
//fmt.Println(strings.Repeat("-", 128))
//fmt.Printf("HID 设备索引 #%d\n", i)
//
//// 系统路径该接口在操作系统中的唯一标识符macOS 下通常是一串 IOKit 路径)
//fmt.Printf(" 系统路径: %s\n", dev.Path)
//// 厂商 ID (Vendor ID)
//fmt.Printf(" 厂商 ID: %#04x\n", dev.VendorID)
//// 产品 ID (Product ID)
//fmt.Printf(" 产品 ID: %#04x\n", dev.ProductID)
//// 设备固件版本号
//fmt.Printf(" 版本号: %d\n", dev.Release)
//// 设备唯一序列号(有些雷蛇设备在不开启雷云时可能返回为空)
//fmt.Printf(" 序列号: %s\n", dev.Serial)
//// 制造商名称,通常显示 "Razer"
//fmt.Printf(" 制造商: %s\n", dev.Manufacturer)
//// 产品原始名称(固件中定义的名称)
//fmt.Printf(" 产品名称: %s\n", dev.Product)
//
//// 使用页 (Usage Page): 定义了设备的功能类别 (例如 0x01 是通用桌面控制)
//fmt.Printf(" 使用页 (UP): %#04x\n", dev.UsagePage)
//// 使用 ID (Usage): 在使用页下的具体分类 (例如 0x06 是键盘)
//fmt.Printf(" 使用 ID (ID): %d\n", dev.Usage)
//// 接口编号 (Interface): 区分设备内不同逻辑功能的索引
//fmt.Printf(" 接口编号: %d\n", dev.Interface)
name, ok := RAZER_DEVICES[dev.ProductID]
if !ok {
continue
}
key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID)
if _, exists := grouped[key]; !exists {
grouped[key] = &RazerDevice{
Product: dev.Product,
Name: name,
ProductID: dev.ProductID,
TransactionID: RAZER_TRANSACTION_IDS[dev.ProductID],
Serial: dev.Serial,
}
// 从缓存恢复状态
if s, found := deviceStateCache[key]; found {
grouped[key].CurrentEffect = s.Effect
grouped[key].CurrentColor = s.Color
}
}
grouped[key].Devices = append(grouped[key].Devices, dev)
}
return grouped
}
//
//func (rd *RazerDevice) SendReportToDevice(report []byte) bool {
// // 补 0x00 作为 HID Report ID
// msg := append([]byte{0x00}, report...)
//
// overallSuccess := false
//
// for _, devInfo := range rd.Devices {
// dev, err := devInfo.Open()
// if err != nil {
// // 这里你可以打印详细错误,看是不是 "device is busy"
// fmt.Printf("[跳过] 无法打开接口 %s: %v\n", devInfo.Path, err)
// continue
// }
//
// // 必须在每次打开后确保关闭,否则下次无法再次 Open
// success := func() bool {
// defer dev.Close() // 👈 必须取消注释!
//
// time.Sleep(50 * time.Millisecond)
//
// // 发送 Feature Report
// n, err := dev.SendFeatureReport(msg)
// if err == nil && n == len(msg) {
// fmt.Printf("[成功] 接口 %d 发送成功\n", devInfo.Interface)
// return true
// }
//
// if err != nil {
// fmt.Printf("[失败] 接口 %d 错误: %v\n", devInfo.Interface, err)
// }
// return false
// }()
//
// if success {
// overallSuccess = true
// break
// }
// }
//
// return overallSuccess
//}
func (rd *RazerDevice) SendReportToDevice(report []byte) bool {
// 补 0x00 作为 HID Report ID
msg := append([]byte{0x00}, report...)
// 记录最终是否有一个接口发送成功
overallSuccess := false
for _, devInfo := range rd.Devices { // 假设你的 RazerDevice 结构体里存储的是 Info []hid.DeviceInfo
dev, err := devInfo.Open()
if err != nil {
fmt.Printf("[跳过] 无法打开接口 %s: %v\n", devInfo.Path, err)
continue
}
success := false
// 稍微等待设备响应,提高稳定性
time.Sleep(500 * time.Millisecond)
//n, err := dev.Write(msg)
//if err == nil && n == len(msg) {
// fmt.Println("[成功] 通过 Write 发送成功")
// success = true
//} else {
// 策略 2: 尝试 SendFeatureReport (Control Transfer)
n, err := dev.SendFeatureReport(msg)
if err == nil && n == len(msg) {
fmt.Println("[成功] 通过 FeatureReport 发送成功")
success = true
} else if err != nil {
fmt.Printf("[失败] 写入错误: %v\n", err)
}
//}
dev.Close()
if success {
overallSuccess = true
continue
}
}
return overallSuccess
}
//
//report_with_id = b'\x00' + report
//success = False
//for iface in selected_device.get('interfaces', []):
//path = iface['path']
//try:
//dev = hid.device()
//dev.open_path(path)
//time.sleep(0.05)
//bytes_written = dev.send_feature_report(report_with_id)
//if bytes_written == len(report_with_id):
//success = True
//dev.close()
//except Exception as e:
//print(f"Error on interface {path}: {e}")
//return success