From 85ed0946c8129312cf6404be7b7e8e54e59e7c2c Mon Sep 17 00:00:00 2001
From: iuu <2167162990@qq.com>
Date: Mon, 9 Feb 2026 00:25:07 +0800
Subject: [PATCH] init
---
.idea/.gitignore | 10 ++
.idea/go.imports.xml | 11 ++
.idea/modules.xml | 8 ++
.idea/razer.iml | 9 ++
.idea/vcs.xml | 6 +
go.mod | 10 ++
go.sum | 6 +
main.go | 84 +++++++++++++
utils/color.go | 120 ++++++++++++++++++
utils/const.go | 62 ++++++++++
utils/menu.go | 147 ++++++++++++++++++++++
utils/tools.go | 283 +++++++++++++++++++++++++++++++++++++++++++
12 files changed, 756 insertions(+)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/go.imports.xml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/razer.iml
create mode 100644 .idea/vcs.xml
create mode 100644 go.mod
create mode 100644 go.sum
create mode 100644 main.go
create mode 100644 utils/color.go
create mode 100644 utils/const.go
create mode 100644 utils/menu.go
create mode 100644 utils/tools.go
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