284 lines
8.8 KiB
Go
284 lines
8.8 KiB
Go
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
|