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

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

11
.idea/go.imports.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/razer.iml" filepath="$PROJECT_DIR$/.idea/razer.iml" />
</modules>
</component>
</project>

9
.idea/razer.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

10
go.mod Normal file
View File

@@ -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

6
go.sum Normal file
View File

@@ -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=

84
main.go Normal file
View File

@@ -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)
//}

120
utils/color.go Normal file
View File

@@ -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("设置双色呼吸失败")
}
}

62
utils/const.go Normal file
View File

@@ -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 // 键盘数据包的有效负载长度

147
utils/menu.go Normal file
View File

@@ -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)
}

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