init
This commit is contained in:
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal 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
11
.idea/go.imports.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
9
.idea/razer.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
10
go.mod
Normal 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
6
go.sum
Normal 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
84
main.go
Normal 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
120
utils/color.go
Normal 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
62
utils/const.go
Normal 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
147
utils/menu.go
Normal 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
283
utils/tools.go
Normal 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
|
||||
Reference in New Issue
Block a user