This commit is contained in:
2026-02-09 00:25:07 +08:00
commit 85ed0946c8
12 changed files with 756 additions and 0 deletions

283
utils/tools.go Normal file
View File

@@ -0,0 +1,283 @@
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