elemousedriver 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +329 -0
- package/main.js +68 -0
- package/package.json +17 -0
- package/preload.js +5 -0
- package/renderer.js +211 -0
- package/style.css +0 -0
- package/usbCmd.exe +0 -0
package/index.html
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<title>Mouse Driver (OMM Clone)</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root {
|
|
8
|
+
--bg-color: #101010;
|
|
9
|
+
--card-bg: #101010;
|
|
10
|
+
/* OMM 其实是很平的黑色 */
|
|
11
|
+
--text-main: #FFFFFF;
|
|
12
|
+
--text-sub: #AAAAAA;
|
|
13
|
+
--accent: #00B8FC;
|
|
14
|
+
/* 罗技蓝 */
|
|
15
|
+
--border: #333333;
|
|
16
|
+
--input-bg: #222222;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 20px;
|
|
22
|
+
background-color: var(--bg-color);
|
|
23
|
+
color: var(--text-main);
|
|
24
|
+
font-family: 'Segoe UI', sans-serif;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
height: 100vh;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Header */
|
|
32
|
+
header {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
align-items: center;
|
|
36
|
+
margin-bottom: 30px;
|
|
37
|
+
border-bottom: 1px solid var(--border);
|
|
38
|
+
padding-bottom: 10px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
h1 {
|
|
42
|
+
font-size: 24px;
|
|
43
|
+
font-weight: 300;
|
|
44
|
+
margin: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.profile-select {
|
|
48
|
+
background: var(--input-bg);
|
|
49
|
+
color: #fff;
|
|
50
|
+
border: 1px solid var(--border);
|
|
51
|
+
padding: 5px 10px;
|
|
52
|
+
border-radius: 4px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Main Grid Layout */
|
|
56
|
+
.container {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: 1fr 1fr;
|
|
59
|
+
/* 左侧 DPI/Rate, 右侧按键 */
|
|
60
|
+
gap: 40px;
|
|
61
|
+
flex-grow: 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.section-title {
|
|
65
|
+
color: var(--text-main);
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
margin-bottom: 15px;
|
|
68
|
+
font-size: 16px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* DPI Section */
|
|
72
|
+
.dpi-box {
|
|
73
|
+
display: grid;
|
|
74
|
+
/* repeat(4, 60px) 意思是:重复 4 次,每列宽度固定为 60px */
|
|
75
|
+
grid-template-columns: repeat(4, 120px);
|
|
76
|
+
gap: 10px;
|
|
77
|
+
/* 行与列之间的间距 */
|
|
78
|
+
margin-bottom: 30px;
|
|
79
|
+
|
|
80
|
+
/* 可选:如果希望整体靠左对齐,不占满整行 */
|
|
81
|
+
justify-content: start;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* 确保每个输入框的宽度与 Grid 列宽一致 */
|
|
85
|
+
.dpi-slot {
|
|
86
|
+
background: var(--input-bg);
|
|
87
|
+
border: 1px solid var(--border);
|
|
88
|
+
color: var(--text-sub);
|
|
89
|
+
padding: 10px 0;
|
|
90
|
+
/* 上下内边距,左右设为0以防撑大 */
|
|
91
|
+
|
|
92
|
+
width: 100%;
|
|
93
|
+
/* 让 input 填满它的格子 (即 60px) */
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
/* 确保 padding 不会撑大宽度 */
|
|
96
|
+
|
|
97
|
+
text-align: center;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
transition: all 0.2s;
|
|
101
|
+
|
|
102
|
+
/* 去掉输入框默认的箭头 (Chrome/Safari) */
|
|
103
|
+
-moz-appearance: textfield;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.dpi-slot::-webkit-outer-spin-button,
|
|
107
|
+
.dpi-slot::-webkit-inner-spin-button {
|
|
108
|
+
-webkit-appearance: none;
|
|
109
|
+
margin: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.dpi-slot.active {
|
|
113
|
+
border-color: var(--accent);
|
|
114
|
+
color: var(--accent);
|
|
115
|
+
font-weight: bold;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.dpi-slot:hover {
|
|
119
|
+
border-color: #666;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* 右键菜单样式 */
|
|
123
|
+
.context-menu {
|
|
124
|
+
position: absolute;
|
|
125
|
+
background-color: #1e1e1e;
|
|
126
|
+
border: 1px solid #333;
|
|
127
|
+
border-radius: 4px;
|
|
128
|
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
|
129
|
+
padding: 5px 0;
|
|
130
|
+
z-index: 1000;
|
|
131
|
+
display: none;
|
|
132
|
+
/* 默认隐藏 */
|
|
133
|
+
min-width: 120px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.menu-item {
|
|
137
|
+
padding: 8px 15px;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
color: #ff4d4d;
|
|
140
|
+
/* 删除通常用红色示警 */
|
|
141
|
+
font-size: 13px;
|
|
142
|
+
transition: background 0.2s;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.menu-item:hover {
|
|
146
|
+
background-color: #333;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* 新增:New 输入框的样式 */
|
|
150
|
+
.dpi-slot.add-new {
|
|
151
|
+
border: 1px dashed #444;
|
|
152
|
+
color: #888;
|
|
153
|
+
background: transparent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.dpi-slot.add-new:focus {
|
|
157
|
+
border-style: solid;
|
|
158
|
+
color: var(--text-main);
|
|
159
|
+
border-color: var(--accent);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.dpi-slot.add-new::placeholder {
|
|
163
|
+
color: #555;
|
|
164
|
+
font-size: 12px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Report Rate */
|
|
168
|
+
.rate-options {
|
|
169
|
+
display: flex;
|
|
170
|
+
gap: 20px;
|
|
171
|
+
margin-bottom: 30px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.radio-label {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
align-items: center;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
color: var(--text-sub);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.radio-label input {
|
|
183
|
+
margin-top: 5px;
|
|
184
|
+
accent-color: var(--accent);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Assignments (Right Column) */
|
|
188
|
+
.assignment-row {
|
|
189
|
+
display: flex;
|
|
190
|
+
justify-content: space-between;
|
|
191
|
+
align-items: center;
|
|
192
|
+
margin-bottom: 15px;
|
|
193
|
+
border-bottom: 1px solid #222;
|
|
194
|
+
padding-bottom: 5px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.key-label {
|
|
198
|
+
color: var(--text-sub);
|
|
199
|
+
font-size: 12px;
|
|
200
|
+
text-transform: uppercase;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.key-select {
|
|
204
|
+
background: var(--input-bg);
|
|
205
|
+
color: white;
|
|
206
|
+
border: 1px solid var(--border);
|
|
207
|
+
padding: 5px;
|
|
208
|
+
border-radius: 4px;
|
|
209
|
+
width: 200px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Footer */
|
|
213
|
+
footer {
|
|
214
|
+
margin-top: auto;
|
|
215
|
+
display: flex;
|
|
216
|
+
justify-content: flex-end;
|
|
217
|
+
gap: 10px;
|
|
218
|
+
padding-top: 20px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
button {
|
|
222
|
+
padding: 10px 30px;
|
|
223
|
+
border: none;
|
|
224
|
+
border-radius: 2px;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
font-weight: bold;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.btn-reset {
|
|
230
|
+
background: #333;
|
|
231
|
+
color: #fff;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.btn-save {
|
|
235
|
+
background: var(--accent);
|
|
236
|
+
color: #000;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.btn-save:hover {
|
|
240
|
+
filter: brightness(1.1);
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
243
|
+
</head>
|
|
244
|
+
|
|
245
|
+
<body>
|
|
246
|
+
<header>
|
|
247
|
+
<h1>G102 / G203 Clone</h1>
|
|
248
|
+
<select class="profile-select">
|
|
249
|
+
<option>Onboard Profile 1</option>
|
|
250
|
+
</select>
|
|
251
|
+
</header>
|
|
252
|
+
|
|
253
|
+
<div class="container">
|
|
254
|
+
<div class="left-panel">
|
|
255
|
+
|
|
256
|
+
<div class="section-title">DPI (Sensitivity)</div>
|
|
257
|
+
<div class="dpi-box" id="dpi-container">
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div id="ctx-menu" class="context-menu">
|
|
261
|
+
<div class="menu-item" id="btn-delete-dpi">Delete Level</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div class="section-title">Report Rate (Hz)</div>
|
|
265
|
+
<div class="rate-options">
|
|
266
|
+
<label class="radio-label">125<input type="radio" name="rate" value="01"></label>
|
|
267
|
+
<label class="radio-label">250<input type="radio" name="rate" value="02"></label>
|
|
268
|
+
<label class="radio-label">500<input type="radio" name="rate" value="03"></label>
|
|
269
|
+
<label class="radio-label">1000<input type="radio" name="rate" value="04" checked></label>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="section-title" style="margin-top: 30px;">Lighting</div>
|
|
273
|
+
<select class="key-select" style="width: 100%">
|
|
274
|
+
<option>Fixed</option>
|
|
275
|
+
<option>Cycle</option>
|
|
276
|
+
<option>Off</option>
|
|
277
|
+
</select>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div class="right-panel">
|
|
281
|
+
<div class="section-title">Assignments</div>
|
|
282
|
+
|
|
283
|
+
<div id="assignments-list">
|
|
284
|
+
<div class="assignment-row">
|
|
285
|
+
<span class="key-label">Button 1 (Left)</span>
|
|
286
|
+
<select class="key-select" disabled>
|
|
287
|
+
<option>Primary Click</option>
|
|
288
|
+
</select>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="assignment-row">
|
|
291
|
+
<span class="key-label">Button 2 (Right)</span>
|
|
292
|
+
<select class="key-select" disabled>
|
|
293
|
+
<option>Secondary Click</option>
|
|
294
|
+
</select>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="assignment-row">
|
|
297
|
+
<span class="key-label">Button 3 (Middle)</span>
|
|
298
|
+
<select class="key-select map-select" data-btn-id="02">
|
|
299
|
+
<option value="04">Key 'A'</option>
|
|
300
|
+
<option value="05">Key 'B'</option>
|
|
301
|
+
<option value="84" selected>Middle Click</option>
|
|
302
|
+
</select>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="assignment-row">
|
|
305
|
+
<span class="key-label">Button 4 (Side Back)</span>
|
|
306
|
+
<select class="key-select map-select" data-btn-id="04">
|
|
307
|
+
<option value="50">Key 'Left Arrow'</option>
|
|
308
|
+
<option value="80">Key 'V'</option>
|
|
309
|
+
</select>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="assignment-row">
|
|
312
|
+
<span class="key-label">Button 5 (Side Fwd)</span>
|
|
313
|
+
<select class="key-select map-select" data-btn-id="05">
|
|
314
|
+
<option value="51">Key 'Right Arrow'</option>
|
|
315
|
+
</select>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<footer>
|
|
322
|
+
<button class="btn-reset">RESET</button>
|
|
323
|
+
<button class="btn-save" id="btn-save">SAVE</button>
|
|
324
|
+
</footer>
|
|
325
|
+
|
|
326
|
+
<script src="renderer.js"></script>
|
|
327
|
+
</body>
|
|
328
|
+
|
|
329
|
+
</html>
|
package/main.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { app, BrowserWindow, ipcMain } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// 硬件常量 (根据你的实际情况修改)
|
|
6
|
+
const VID = '3151';
|
|
7
|
+
const PID = '3020';
|
|
8
|
+
const USB_CMD_PATH = 'usbCmd.exe'; // 假设 exe 在根目录
|
|
9
|
+
|
|
10
|
+
function createWindow() {
|
|
11
|
+
const win = new BrowserWindow({
|
|
12
|
+
width: 1000,
|
|
13
|
+
height: 700,
|
|
14
|
+
backgroundColor: '#101010', // 罗技深色背景
|
|
15
|
+
webPreferences: {
|
|
16
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
17
|
+
nodeIntegration: false,
|
|
18
|
+
contextIsolation: true
|
|
19
|
+
},
|
|
20
|
+
autoHideMenuBar: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
win.loadFile('index.html');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 封装调用 usbCmd 的函数
|
|
27
|
+
const sendUsbCommand = (reportId, payloadHex) => {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
// 计算 payload 长度 (字节数)
|
|
30
|
+
const byteLen = payloadHex.length / 2 + 1; // +1 是因为 Len 包含 ReportID 字节
|
|
31
|
+
|
|
32
|
+
// 构造命令: usbCmd.exe -V 1234 -P 5678 -R 03 -L [Len] -H [Payload]
|
|
33
|
+
// 注意: 你的协议说明中 -R 是ReportID(03), -H是后续载荷(Command+Len+Data)
|
|
34
|
+
const cmd = `${USB_CMD_PATH} -V ${VID} -P ${PID} -R ${reportId} -L ${byteLen} -H ${payloadHex}`;
|
|
35
|
+
|
|
36
|
+
console.log("Executing:", cmd);
|
|
37
|
+
|
|
38
|
+
exec(cmd, (error, stdout, stderr) => {
|
|
39
|
+
if (error) {
|
|
40
|
+
console.error(`Exec error: ${error}`);
|
|
41
|
+
// 模拟开发环境没有硬件时不崩溃,返回空或模拟数据
|
|
42
|
+
// reject(error);
|
|
43
|
+
resolve("02010100"); // 模拟返回
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// 假设 usbCmd.exe 在 stdout 输出返回的十六进制字符串 (例如 "02010501...")
|
|
47
|
+
resolve(stdout.trim());
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
app.whenReady().then(() => {
|
|
53
|
+
createWindow();
|
|
54
|
+
|
|
55
|
+
// IPC 监听:处理前端发来的指令请求
|
|
56
|
+
ipcMain.handle('send-command', async (event, { reportId, payload }) => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await sendUsbCommand(reportId, payload);
|
|
59
|
+
return { success: true, data: result };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return { success: false, error: err.message };
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
app.on('window-all-closed', () => {
|
|
67
|
+
if (process.platform !== 'darwin') app.quit();
|
|
68
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "elemousedriver",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Driver for phy6235/6 based MCU mouse.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Raymond@SiliconWin",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"main": "main.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "electron .",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@electron-forge/publisher-github": "^7.10.2",
|
|
15
|
+
"electron": "^39.2.7"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/preload.js
ADDED
package/renderer.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// ================= STATE MANAGEMENT =================
|
|
2
|
+
// 维护当前 DPI 列表的状态 (默认值)
|
|
3
|
+
let dpiList = [400, 800, 1600, 3200];
|
|
4
|
+
let activeDpiIndex = 1; // 默认选中第2个 (索引从0开始)
|
|
5
|
+
let targetDeleteIndex = -1; // 用于记录右键点击了哪个
|
|
6
|
+
|
|
7
|
+
const dpiContainer = document.getElementById('dpi-container');
|
|
8
|
+
const ctxMenu = document.getElementById('ctx-menu');
|
|
9
|
+
const deleteBtn = document.getElementById('btn-delete-dpi');
|
|
10
|
+
|
|
11
|
+
// 辅助:数字转2位Hex
|
|
12
|
+
const toHex = (num) => {
|
|
13
|
+
let hex = parseInt(num).toString(16).toUpperCase();
|
|
14
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ================= RENDER LOGIC =================
|
|
18
|
+
|
|
19
|
+
function renderDPI() {
|
|
20
|
+
dpiContainer.innerHTML = ''; // 清空容器
|
|
21
|
+
|
|
22
|
+
// 1. 渲染现有的 DPI 列表
|
|
23
|
+
dpiList.forEach((val, index) => {
|
|
24
|
+
const input = document.createElement('input');
|
|
25
|
+
input.type = 'number';
|
|
26
|
+
input.className = 'dpi-slot';
|
|
27
|
+
input.value = val;
|
|
28
|
+
|
|
29
|
+
// 如果是当前激活的 DPI
|
|
30
|
+
if (index === activeDpiIndex) {
|
|
31
|
+
input.classList.add('active');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 左键点击:激活该档位
|
|
35
|
+
input.addEventListener('click', () => {
|
|
36
|
+
activeDpiIndex = index;
|
|
37
|
+
renderDPI(); // 重绘以更新 active 类
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 修改数值:更新数组
|
|
41
|
+
input.addEventListener('change', (e) => {
|
|
42
|
+
let newVal = parseInt(e.target.value);
|
|
43
|
+
// 简单的校验,防止负数或过大
|
|
44
|
+
if (newVal > 0 && newVal < 20000) {
|
|
45
|
+
dpiList[index] = newVal;
|
|
46
|
+
} else {
|
|
47
|
+
e.target.value = dpiList[index]; // 恢复原值
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 右键点击:显示菜单
|
|
52
|
+
input.addEventListener('contextmenu', (e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
showContextMenu(e, index);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
dpiContainer.appendChild(input);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// 2. 如果少于 8 个,渲染 "New..." 输入框
|
|
61
|
+
if (dpiList.length < 8) {
|
|
62
|
+
const addInput = document.createElement('input');
|
|
63
|
+
addInput.type = 'number'; // 或者 text
|
|
64
|
+
addInput.className = 'dpi-slot add-new';
|
|
65
|
+
addInput.placeholder = 'New...';
|
|
66
|
+
|
|
67
|
+
// 监听输入完成 (回车或失焦)
|
|
68
|
+
addInput.addEventListener('change', (e) => {
|
|
69
|
+
const newVal = parseInt(e.target.value);
|
|
70
|
+
if (!isNaN(newVal) && newVal > 0) {
|
|
71
|
+
dpiList.push(newVal); // 添加到数组
|
|
72
|
+
// 如果是第一个添加的,自动设为激活
|
|
73
|
+
if (dpiList.length === 1) activeDpiIndex = 0;
|
|
74
|
+
renderDPI(); // 重绘
|
|
75
|
+
} else {
|
|
76
|
+
e.target.value = ''; // 无效输入清空
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
dpiContainer.appendChild(addInput);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ================= CONTEXT MENU LOGIC =================
|
|
85
|
+
|
|
86
|
+
function showContextMenu(e, index) {
|
|
87
|
+
targetDeleteIndex = index; // 记住要删哪个
|
|
88
|
+
|
|
89
|
+
// 设置位置 (基于鼠标点击坐标)
|
|
90
|
+
ctxMenu.style.left = `${e.pageX}px`;
|
|
91
|
+
ctxMenu.style.top = `${e.pageY}px`;
|
|
92
|
+
ctxMenu.style.display = 'block';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hideContextMenu() {
|
|
96
|
+
ctxMenu.style.display = 'none';
|
|
97
|
+
targetDeleteIndex = -1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 点击 "Delete"
|
|
101
|
+
deleteBtn.addEventListener('click', () => {
|
|
102
|
+
if (targetDeleteIndex > -1) {
|
|
103
|
+
// 从数组中删除
|
|
104
|
+
dpiList.splice(targetDeleteIndex, 1);
|
|
105
|
+
|
|
106
|
+
// 修正 activeIndex (防止索引越界)
|
|
107
|
+
if (activeDpiIndex >= dpiList.length) {
|
|
108
|
+
activeDpiIndex = Math.max(0, dpiList.length - 1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
renderDPI();
|
|
112
|
+
hideContextMenu();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 点击页面任何其他地方隐藏菜单
|
|
117
|
+
document.addEventListener('click', (e) => {
|
|
118
|
+
if (e.target.closest('#ctx-menu')) return; // 点菜单内部不关闭
|
|
119
|
+
hideContextMenu();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
// ================= PROTOCOL LOGIC =================
|
|
124
|
+
|
|
125
|
+
async function saveDPI() {
|
|
126
|
+
let dpiDataHex = "";
|
|
127
|
+
|
|
128
|
+
// 遍历状态数组构造 HEX
|
|
129
|
+
dpiList.forEach(val => {
|
|
130
|
+
const byteVal = Math.floor(val / 100); // 协议要求:值/100
|
|
131
|
+
dpiDataHex += toHex(byteVal);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 协议说明:DPI档位数据可能是固定长度的(例如7档或8档)
|
|
135
|
+
// 此处假设最大支持8档位 (8 bytes),不足补 00
|
|
136
|
+
// 如果固件严格要求7个档位,请将 i < 8 改为 i < 7
|
|
137
|
+
const maxSlots = 8;
|
|
138
|
+
const currentLen = dpiList.length;
|
|
139
|
+
|
|
140
|
+
for (let i = currentLen; i < maxSlots; i++) {
|
|
141
|
+
dpiDataHex += "00";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 构造 Payload
|
|
145
|
+
// 假设:Length = 1(FeatureID) + 8(Data) = 9 (0x09)
|
|
146
|
+
// 注意:请根据固件实际解析的 Length 修改这里的 '09'
|
|
147
|
+
const payloadLen = toHex(1 + maxSlots);
|
|
148
|
+
const fullPayload = `01${payloadLen}01${dpiDataHex}`;
|
|
149
|
+
|
|
150
|
+
console.log("Sending DPI:", fullPayload, "Raw List:", dpiList);
|
|
151
|
+
|
|
152
|
+
// 发送指令 (调用 preload 定义的 API)
|
|
153
|
+
if (window.api) {
|
|
154
|
+
await window.api.sendCommand('03', fullPayload);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2. 设置回报率 (Polling Rate)
|
|
159
|
+
// 协议: 03 [01 02 02 RateID]
|
|
160
|
+
async function savePollingRate() {
|
|
161
|
+
const selected = document.querySelector('input[name="rate"]:checked');
|
|
162
|
+
if (!selected) return;
|
|
163
|
+
|
|
164
|
+
const rateId = selected.value; // 01, 02, 03, 04
|
|
165
|
+
|
|
166
|
+
// Cmd(01) + Len(02) + Feature(02) + Data
|
|
167
|
+
const fullPayload = `010202${rateId}`;
|
|
168
|
+
|
|
169
|
+
console.log("Saving Rate Payload:", fullPayload);
|
|
170
|
+
await window.api.sendCommand('03', fullPayload);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 3. 设置按键映射 (Key Remap)
|
|
174
|
+
// 协议: 03 [01 03 03 BtnID KeyCode]
|
|
175
|
+
// 注意:这可能需要循环发送,因为你的指令一次只能改一个键
|
|
176
|
+
async function saveKeyMaps() {
|
|
177
|
+
const selects = document.querySelectorAll('.map-select');
|
|
178
|
+
|
|
179
|
+
for (const select of selects) {
|
|
180
|
+
const btnId = select.getAttribute('data-btn-id'); // e.g., '04'
|
|
181
|
+
const keyCode = select.value; // e.g., '80'
|
|
182
|
+
|
|
183
|
+
// Cmd(01) + Len(03) + Feature(03) + Btn + Key
|
|
184
|
+
const fullPayload = `010303${btnId}${keyCode}`;
|
|
185
|
+
|
|
186
|
+
console.log(`Saving Key ${btnId} -> ${keyCode}:`, fullPayload);
|
|
187
|
+
// 这里最好加一点延迟,防止USB堵塞
|
|
188
|
+
await window.api.sendCommand('03', fullPayload);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// ================= INITIALIZATION =================
|
|
192
|
+
|
|
193
|
+
// 初始化渲染
|
|
194
|
+
renderDPI();
|
|
195
|
+
|
|
196
|
+
// 绑定保存按钮 (假设 HTML 中有 id="btn-save")
|
|
197
|
+
const saveBtn = document.getElementById('btn-save');
|
|
198
|
+
if (saveBtn) {
|
|
199
|
+
saveBtn.addEventListener('click', async () => {
|
|
200
|
+
saveBtn.innerText = "SAVING...";
|
|
201
|
+
try {
|
|
202
|
+
await saveDPI();
|
|
203
|
+
await savePollingRate();
|
|
204
|
+
await saveKeyMaps();
|
|
205
|
+
setTimeout(() => saveBtn.innerText = "SAVE", 500);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error(e);
|
|
208
|
+
saveBtn.innerText = "ERROR";
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
package/style.css
ADDED
|
File without changes
|
package/usbCmd.exe
ADDED
|
Binary file
|