commit 85ed0946c8129312cf6404be7b7e8e54e59e7c2c Author: iuu <2167162990@qq.com> Date: Mon Feb 9 00:25:07 2026 +0800 init diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b6b1ecf --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..d7202f0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..905dfab --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/razer.iml b/.idea/razer.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/razer.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0bcd015 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module razer + +go 1.25 + +require ( + github.com/karalabe/hid v0.0.0-00010101000000-000000000000 + github.com/progrium/darwinkit v0.5.0 +) + +replace github.com/karalabe/hid => github.com/chaodada/hid v0.0.0-20240919124526-821c38d2678e diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c24c008 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/chaodada/hid v0.0.0-20240919124526-821c38d2678e h1:o/0aazfYg5RT8T+kPlncyJubSXBGxGJyO3tRn3YkTvw= +github.com/chaodada/hid v0.0.0-20240919124526-821c38d2678e/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/progrium/darwinkit v0.5.0 h1:SwchcMbTOG1py3CQsINmGlsRmYKdlFrbnv3dE4aXA0s= +github.com/progrium/darwinkit v0.5.0/go.mod h1:PxQhZuftnALLkCVaR8LaHtUOfoo4pm8qUDG+3C/sXNs= diff --git a/main.go b/main.go new file mode 100644 index 0000000..78207b3 --- /dev/null +++ b/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "log" + "razer/utils" + + "github.com/karalabe/hid" + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/objc" +) + +func main() { + if !hid.Supported() { + log.Fatal("HID 不支持") + } + + objc.WithAutoreleasePool(func() { + app := appkit.Application_SharedApplication() + app.SetActivationPolicy(appkit.ApplicationActivationPolicyAccessory) + + utils.SetupStatusBar(app) + + app.ActivateIgnoringOtherApps(true) + app.Run() + }) +} + +//// --- UI 逻辑:支持动态刷新 --- +//func updateMenu(menu appkit.Menu, app appkit.Application) { +// // 1. 清空旧菜单 +// menu.RemoveAllItems() +// +// // 2. 重新扫描设备 +// devices := utils.ScanDevices() +// +// // 3. 添加刷新按钮 (放在最上面) +// refreshItem := appkit.NewMenuItemWithAction("🔄 刷新设备列表", "r", func(sender objc.Object) { +// updateMenu(menu, app) // 递归调用刷新自身 +// }) +// menu.AddItem(refreshItem) +// menu.AddItem(appkit.MenuItem_SeparatorItem()) +// +// // 4. 构建设备列表 +// if len(devices) == 0 { +// none := appkit.NewMenuItemWithAction("未发现雷蛇设备", "", func(sender objc.Object) {}) +// none.SetEnabled(false) +// menu.AddItem(none) +// } else { +// for _, dev := range devices { +// devTitle := appkit.NewMenuItemWithAction("设备: "+dev.Name, "", func(sender objc.Object) {}) +// devTitle.SetEnabled(false) +// menu.AddItem(devTitle) +// +// menu.AddItem(appkit.NewMenuItemWithAction(" 🔴 设为红色", "", func(sender objc.Object) { +// //dev.ApplyStaticColor(0xFF, 0x00, 0x00) +// })) +// +// menu.AddItem(appkit.NewMenuItemWithAction(" 🟢 设为绿色", "", func(sender objc.Object) { +// //dev.ApplyStaticColor(0x00, 0xFF, 0x00) +// })) +// +// menu.AddItem(appkit.MenuItem_SeparatorItem()) +// } +// } +// +// // 5. 退出按钮 +// menu.AddItem(appkit.NewMenuItemWithAction("退出", "q", func(sender objc.Object) { +// app.Terminate(nil) +// })) +//} + +//func setupStatusBar(app appkit.Application) { +// // 创建状态栏项 +// item := appkit.StatusBar_SystemStatusBar().StatusItemWithLength(appkit.VariableStatusItemLength) +// objc.Retain(&item) +// +// img := appkit.Image_ImageWithSystemSymbolNameAccessibilityDescription("keyboard", "Razer Tool") +// item.Button().SetImage(img) +// +// // 创建初始菜单 +// menu := appkit.NewMenuWithTitle("Razer") +// updateMenu(menu, app) // 初始填充菜单项 +// item.SetMenu(menu) +//} diff --git a/utils/color.go b/utils/color.go new file mode 100644 index 0000000..c975524 --- /dev/null +++ b/utils/color.go @@ -0,0 +1,120 @@ +package utils + +import "fmt" + +// PRESET_COLORS 预设颜色 +var PRESET_COLORS = []struct { + Name string + R, G, B byte +}{ + {"红色", 0xFF, 0x00, 0x00}, + {"绿色", 0x00, 0xFF, 0x00}, + {"蓝色", 0x00, 0x00, 0xFF}, + {"白色", 0xFF, 0xFF, 0xFF}, + {"黄色", 0xFF, 0xFF, 0x00}, + {"橙色", 0xFF, 0xA5, 0x00}, + {"紫色", 0x80, 0x00, 0x80}, + + //{"熄灭", 0x00, 0x00, 0x00}, +} + +// 雷蛇绿 (标志性的 Razer Green) +//razerGreen := []byte{0x00, 0xFF, 0x00} // 或者稍微深一点的 0x44, 0xFF, 0x00 + +// 熄灭 (关闭灯光) +//off := []byte{0x00, 0x00, 0x00} + +//// SetStaticColor 设置常亮颜色 +//func (rd *RazerDevice) SetStaticColor(color []byte) { +// args := buildArguments(KBD_EFFECT_STATIC, KBD_BACKLIGHT_LED, color) +// // 3. 构建 90 字节报告 (Class 0x0F, ID 0x02) +// report, err := constructRazerReport(rd.TransactionID, KBD_CMD_CLASS, KBD_CMD_ID, KBD_DATA_SIZE, args) +// if err != nil { +// fmt.Println("报告构造失败:", err.Error()) +// return +// } +// if rd.SendReportToDevice(report) { +// fmt.Println("设置常亮颜色成功") +// return +// } +// fmt.Println("设置常亮颜色失败") +//} + +// SetStaticColor 设置常亮颜色 +func (rd *RazerDevice) SetStaticColor(color []byte) { + // 关键修改:在颜色 [R, G, B] 前面补上开关位 0x01 + // 这样 buildArguments 拼接后的第 6 位就是 0x01,后面跟着 RGB + staticParams := append([]byte{0x01}, color...) + + args := buildArguments(KBD_EFFECT_STATIC, KBD_BACKLIGHT_LED, staticParams) + + // 3. 构建 90 字节报告 (注意:KBD_DATA_SIZE 对于常亮通常是 9) + report, err := constructRazerReport(rd.TransactionID, KBD_CMD_CLASS, KBD_CMD_ID, KBD_DATA_SIZE, args) + if err != nil { + fmt.Println("报告构造失败:", err.Error()) + return + } + + if rd.SendReportToDevice(report) { + fmt.Println("设置常亮颜色成功") + return + } + fmt.Println("设置常亮颜色失败") +} + +// SetOffLightsColor 设置常亮颜色 +func (rd *RazerDevice) SetOffLightsColor(color []byte) { + // 关键修改:在颜色 [R, G, B] 前面补上开关位 0x01 + // 这样 buildArguments 拼接后的第 6 位就是 0x01,后面跟着 RGB + staticParams := append([]byte{0x01}, color...) + + args := buildArguments(KBD_EFFECT_STATIC, KBD_BACKLIGHT_LED, staticParams) + + // 3. 构建 90 字节报告 (注意:KBD_DATA_SIZE 对于常亮通常是 9) + report, err := constructRazerReport(rd.TransactionID, KBD_CMD_CLASS, KBD_CMD_ID, KBD_DATA_SIZE, args) + if err != nil { + fmt.Println("报告构造失败:", err.Error()) + return + } + + if rd.SendReportToDevice(report) { + fmt.Println("设置常亮颜色成功") + return + } + fmt.Println("设置常亮颜色失败") +} + +// SetBreathingColor 设置双色呼吸 +func (rd *RazerDevice) SetBreathingColor() { + // 1. 定义呼吸子模式 + subMode := byte(0x02) // 0x02 为双色切换呼吸 + rgb1 := []byte{0xFF, 0x00, 0x00} // 颜色1: 红色 + rgb2 := []byte{0x00, 0x00, 0xFF} // 颜色2: 蓝色 + speed := byte(0x01) // 速度: 1(快) - 3(慢), 200可能过大,建议先用1测试 + + // 2. 构造参数包 (总长 8 字节) + // 结构: [SubMode, R1, G1, B1, R2, G2, B2, Speed] + breathingParams := append([]byte{subMode}, append(rgb1, append(rgb2, speed)...)...) + + // 3. 调用新版 buildArguments (内部不再强制加 0x01) + args := buildArguments(KBD_EFFECT_BREATHING, KBD_BACKLIGHT_LED, breathingParams) + + // 4. 重要:计算正确的数据长度 (Data Size) + // 5字节(prefix) + 8字节(breathingParams) = 13字节 + breathingDataSize := byte(13) + + // 5. 构建 90 字节报告 + // 注意:这里传入的是自定义的 breathingDataSize 而不是全局固定的 KBD_DATA_SIZE + report, err := constructRazerReport(rd.TransactionID, KBD_CMD_CLASS, KBD_CMD_ID, breathingDataSize, args) + if err != nil { + fmt.Println("报告构造失败:", err.Error()) + return + } + + // 6. 发送指令 + if rd.SendReportToDevice(report) { + fmt.Println("设置双色呼吸成功") + } else { + fmt.Println("设置双色呼吸失败") + } +} diff --git a/utils/const.go b/utils/const.go new file mode 100644 index 0000000..2a25218 --- /dev/null +++ b/utils/const.go @@ -0,0 +1,62 @@ +package utils + +// --- 硬件识别常量 --- + +// RAZER_VID (Vendor ID) 是雷蛇在 USB 协会注册的厂商唯一识别码 +const RAZER_VID = 0x1532 + +// RAZER_DEVICES 将产品 ID (PID) 映射到具体的设备型号名称 +var RAZER_DEVICES = map[uint16]string{ + 0x0A24: "雷蛇猎魂光轮 V3 竞技版 (BlackWidow V3 TKL)", + 0x0013: "Razer Orochi 2011 (八岐大蛇)", +} + +// RAZER_DEVICE_TYPES 定义设备的分类,用于后续逻辑区分键盘或鼠标 +var RAZER_DEVICE_TYPES = map[uint16]string{ + 0x0A24: "keyboard", + 0x0013: "mouse", +} + +// RAZER_TRANSACTION_IDS 事务 ID:用于协议握手,确保指令被正确识别 +var RAZER_TRANSACTION_IDS = map[uint16]uint8{ + 0x0A24: 0x1F, + 0x0013: 0xFF, +} + +// --- 协议报文配置 --- + +// REPORT_LEN 定义了发送给 USB 设备的标准报文长度(通常为 90 字节) +const REPORT_LEN = 90 + +// VARSTORE 指向设备的非易失性存储区域,用于断电后保存配置 +const VARSTORE = 0x01 + +// --- 灯效模式常量 (鼠标) --- + +const MOUSE_EFFECT_STATIC = 0x01 // 鼠标:常亮模式 +const MOUSE_EFFECT_BREATHING = 0x02 // 鼠标:呼吸模式 +const MOUSE_EFFECT_WAVE = 0x03 // 鼠标:波浪模式 +const MOUSE_EFFECT_REACTIVE = 0x04 // 鼠标:响应模式(点击触发) + +// --- 灯效模式常量 (键盘) --- + +const KBD_EFFECT_STATIC = 0x01 // 键盘:常亮模式 +const KBD_EFFECT_BREATHING = 0x02 // 键盘:呼吸模式 +const KBD_EFFECT_WAVE = 0x03 // 键盘:波浪模式 +const KBD_EFFECT_REACTIVE = 0x04 // 键盘:响应模式 + +const KBD_EFFECT_OFFLIGHTS = 0x099 // 键盘:关灯模式 + +// --- 指令集参数 (鼠标) --- + +const MOUSE_SCROLL_WHEEL_LED = 0x01 // 鼠标滚轮灯光控制索引 +const MOUSE_CMD_CLASS = 0x0F // 鼠标指令分类 (Command Class) +const MOUSE_CMD_ID = 0x02 // 鼠标指令标识 (Command ID) +const MOUSE_DATA_SIZE = 9 // 鼠标数据包的有效负载长度 + +// --- 指令集参数 (键盘) --- + +const KBD_BACKLIGHT_LED = 0x05 // 键盘背光灯光控制索引 +const KBD_CMD_CLASS = 0x0F // 键盘指令分类 (Command Class) +const KBD_CMD_ID = 0x02 // 键盘指令标识 (Command ID) +const KBD_DATA_SIZE = 9 // 键盘数据包的有效负载长度 diff --git a/utils/menu.go b/utils/menu.go new file mode 100644 index 0000000..f3a9fc7 --- /dev/null +++ b/utils/menu.go @@ -0,0 +1,147 @@ +package utils + +import ( + "fmt" + + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/objc" +) + +var deviceStateCache = make(map[string]State) + +type State struct { + Effect int + Color string +} + +func UpdateMenu(menu appkit.Menu, app appkit.Application, statusBarItem appkit.StatusItem) { + menu.RemoveAllItems() + + // 1. 刷新按钮 + refreshItem := appkit.NewMenuItemWithAction("🔄 刷新设备列表", "r", func(sender objc.Object) { + UpdateMenu(menu, app, statusBarItem) + }) + menu.AddItem(refreshItem) + menu.AddItem(appkit.MenuItem_SeparatorItem()) + + devices := ScanDevices() + + // 更新 ToolTip 显示当前连接数 + statusBarItem.Button().SetToolTip(fmt.Sprintf("雷蛇控制器 - 已连接设备: %d", len(devices))) + + if len(devices) == 0 { + none := appkit.NewMenuItemWithAction("未发现雷蛇设备", "", func(sender objc.Object) {}) + none.SetEnabled(false) + menu.AddItem(none) + } else { + for _, dev := range devices { + // 设备标题 (不可点击) + devTitle := appkit.NewMenuItemWithAction("⌨️ "+dev.Name, "", func(sender objc.Object) {}) + devTitle.SetEnabled(false) + menu.AddItem(devTitle) + + // 切换灯效模式 (二级菜单) + //modeMenuItem := appkit.NewMenuItemWithAction("⚙️ 切换灯效模式", "", func(sender objc.Object) {}) + //modeSubMenu := appkit.NewMenuWithTitle("Select Mode") + + // --- A. 常亮模式 --- + staticItem := appkit.NewMenuItemWithAction("☀️ 常亮模式", "", func(sender objc.Object) {}) + if dev.CurrentEffect == KBD_EFFECT_STATIC { + staticItem.SetState(appkit.ControlStateValueOn) + } + + colorMenu := appkit.NewMenuWithTitle("Colors") + for _, c := range PRESET_COLORS { + colorItem := appkit.NewMenuItemWithAction(c.Name, "", func(sender objc.Object) { + //dev.ApplyEffect(KBD_EFFECT_STATIC, c.R, c.G, c.B) + dev.SetStaticColor([]byte{c.R, c.G, c.B}) + // 更新状态与缓存 + key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + deviceStateCache[key] = State{Effect: KBD_EFFECT_STATIC, Color: c.Name} + UpdateMenu(menu, app, statusBarItem) + }) + if dev.CurrentEffect == KBD_EFFECT_STATIC && dev.CurrentColor == c.Name { + colorItem.SetState(appkit.ControlStateValueOn) + } + colorMenu.AddItem(colorItem) + } + staticItem.SetSubmenu(colorMenu) + menu.AddItem(staticItem) + + // --- B. 呼吸模式 --- + breathItem := appkit.NewMenuItemWithAction("🌬️ 呼吸模式", "", func(sender objc.Object) { + dev.SetBreathingColor() // 默认绿色 + key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + deviceStateCache[key] = State{Effect: KBD_EFFECT_BREATHING, Color: ""} + UpdateMenu(menu, app, statusBarItem) + }) + if dev.CurrentEffect == KBD_EFFECT_BREATHING { + breathItem.SetState(appkit.ControlStateValueOn) + } + menu.AddItem(breathItem) + + // --- C. 波浪模式 --- + waveItem := appkit.NewMenuItemWithAction("🌊 波浪模式", "", func(sender objc.Object) { + //dev.ApplyEffect(KBD_EFFECT_WAVE, 0x01, 0x00, 0x00) + key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + deviceStateCache[key] = State{Effect: KBD_EFFECT_WAVE, Color: ""} + UpdateMenu(menu, app, statusBarItem) + }) + if dev.CurrentEffect == KBD_EFFECT_WAVE { + waveItem.SetState(appkit.ControlStateValueOn) + } + menu.AddItem(waveItem) + + // --- D. 响应模式 --- + reactiveItem := appkit.NewMenuItemWithAction("🌊 响应模式", "", func(sender objc.Object) { + //dev.ApplyEffect(KBD_EFFECT_WAVE, 0x01, 0x00, 0x00) + key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + deviceStateCache[key] = State{Effect: KBD_EFFECT_REACTIVE, Color: ""} + UpdateMenu(menu, app, statusBarItem) + }) + if dev.CurrentEffect == KBD_EFFECT_REACTIVE { + reactiveItem.SetState(appkit.ControlStateValueOn) + } + menu.AddItem(reactiveItem) + + // --- E. 关灯模式 --- 关灯 + offLightsItem := appkit.NewMenuItemWithAction("关灯模式", "", func(sender objc.Object) { + dev.SetOffLightsColor([]byte{0x00, 0x00, 0x00}) + key := fmt.Sprintf("%s|%s|%d", dev.Serial, dev.Product, dev.ProductID) + deviceStateCache[key] = State{Effect: KBD_EFFECT_OFFLIGHTS, Color: ""} + UpdateMenu(menu, app, statusBarItem) + }) + if dev.CurrentEffect == KBD_EFFECT_OFFLIGHTS { + offLightsItem.SetState(appkit.ControlStateValueOn) + } + menu.AddItem(offLightsItem) + + //modeMenuItem.SetSubmenu(modeSubMenu) + //menu.AddItem(modeMenuItem) + + menu.AddItem(appkit.MenuItem_SeparatorItem()) + } + } + + menu.AddItem(appkit.NewMenuItemWithAction("退出", "q", func(sender objc.Object) { + app.Terminate(nil) + })) +} + +func SetupStatusBar(app appkit.Application) { + // 创建状态栏项 + item := appkit.StatusBar_SystemStatusBar().StatusItemWithLength(appkit.VariableStatusItemLength) + objc.Retain(&item) + + // 设置图标 + img := appkit.Image_ImageWithSystemSymbolNameAccessibilityDescription("antenna.radiowaves.left.and.right", "Razer Tool") + item.Button().SetImage(img) + + // 设置描述 + item.Button().SetToolTip("雷蛇设备控制器 (Razer Controller)") + + // 创建初始菜单 + menu := appkit.NewMenuWithTitle("Razer") + UpdateMenu(menu, app, item) + item.SetMenu(menu) +} diff --git a/utils/tools.go b/utils/tools.go new file mode 100644 index 0000000..aeb8352 --- /dev/null +++ b/utils/tools.go @@ -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