serialport-tool 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/dist/index.js ADDED
@@ -0,0 +1,1284 @@
1
+ // src/app-config.ts
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ var DEFAULT_CONFIG = {
6
+ baudRate: 115200,
7
+ dataBits: 8,
8
+ stopBits: 1,
9
+ parity: "none",
10
+ flowControl: "none",
11
+ lastDevicePath: null,
12
+ presetPanelVisible: true,
13
+ sessionPanelVisible: true,
14
+ receiveDisplayMode: "text",
15
+ receiveShowMixed: true,
16
+ receiveShowTimestamp: true,
17
+ receiveAutoScroll: true,
18
+ sendMode: "ascii",
19
+ sendAppendCRLF: true,
20
+ localEcho: true,
21
+ sendInputText: "",
22
+ autoConnect: false
23
+ };
24
+ var CONFIG_DIR = join(homedir(), ".serial_tool");
25
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
26
+ var AppConfig = class {
27
+ data;
28
+ constructor() {
29
+ this.data = { ...DEFAULT_CONFIG };
30
+ this.load();
31
+ }
32
+ load() {
33
+ try {
34
+ if (existsSync(CONFIG_FILE)) {
35
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
36
+ const saved = JSON.parse(raw);
37
+ this.data = { ...DEFAULT_CONFIG, ...saved };
38
+ }
39
+ } catch {
40
+ }
41
+ this.ensureDir();
42
+ }
43
+ save() {
44
+ this.ensureDir();
45
+ try {
46
+ writeFileSync(CONFIG_FILE, JSON.stringify(this.data, null, 2), "utf-8");
47
+ } catch (e) {
48
+ console.error("[AppConfig] save failed:", e);
49
+ }
50
+ }
51
+ ensureDir() {
52
+ if (!existsSync(CONFIG_DIR)) {
53
+ mkdirSync(CONFIG_DIR, { recursive: true });
54
+ }
55
+ }
56
+ /** 转换为 SerialConfig */
57
+ toSerialConfig() {
58
+ return {
59
+ baudRate: this.data.baudRate,
60
+ dataBits: this.data.dataBits,
61
+ stopBits: this.data.stopBits,
62
+ parity: this.data.parity,
63
+ flowControl: this.data.flowControl,
64
+ lineEnding: this.data.sendAppendCRLF ? "crlf" : "none",
65
+ localEcho: this.data.localEcho,
66
+ showTimestamp: this.data.receiveShowTimestamp
67
+ };
68
+ }
69
+ /** 从 SerialConfig 更新 */
70
+ updateFromSerialConfig(cfg) {
71
+ this.data.baudRate = cfg.baudRate;
72
+ this.data.dataBits = cfg.dataBits;
73
+ this.data.stopBits = cfg.stopBits;
74
+ this.data.parity = cfg.parity;
75
+ this.data.flowControl = cfg.flowControl;
76
+ }
77
+ /** 重置为默认值 */
78
+ resetAll() {
79
+ this.data = { ...DEFAULT_CONFIG };
80
+ this.save();
81
+ }
82
+ /** 获取存储目录路径 */
83
+ get configDir() {
84
+ return CONFIG_DIR;
85
+ }
86
+ /** 获取会话存储目录 */
87
+ get sessionsDir() {
88
+ return join(CONFIG_DIR, "sessions");
89
+ }
90
+ };
91
+
92
+ // src/preset-store.ts
93
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
94
+ import { join as join2 } from "path";
95
+ import { homedir as homedir2 } from "os";
96
+ function uuid() {
97
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
98
+ const r = Math.random() * 16 | 0;
99
+ const v = c === "x" ? r : r & 3 | 8;
100
+ return v.toString(16);
101
+ });
102
+ }
103
+ var CONFIG_DIR2 = join2(homedir2(), ".serial_tool");
104
+ var PRESETS_FILE = join2(CONFIG_DIR2, "presets.json");
105
+ function createDefaultPresetGroups() {
106
+ return [
107
+ {
108
+ id: uuid(),
109
+ name: "AT",
110
+ commands: [
111
+ { id: uuid(), name: "AT \u6D4B\u8BD5", payload: "AT", displayMode: "ascii", hotkey: 1, appendNewline: true },
112
+ { id: uuid(), name: "\u5173\u95ED\u56DE\u663E", payload: "ATE0", displayMode: "ascii", hotkey: 2, appendNewline: true },
113
+ { id: uuid(), name: "\u6A21\u5757\u4FE1\u606F", payload: "ATI", displayMode: "ascii", hotkey: 3, appendNewline: true },
114
+ { id: uuid(), name: "\u67E5\u8BE2\u7248\u672C", payload: "AT+GMR", displayMode: "ascii", hotkey: 4, appendNewline: true },
115
+ { id: uuid(), name: "\u67E5\u8BE2\u4FE1\u53F7", payload: "AT+CSQ", displayMode: "ascii", hotkey: null, appendNewline: true },
116
+ { id: uuid(), name: "\u6CE8\u518C\u72B6\u6001", payload: "AT+CREG?", displayMode: "ascii", hotkey: null, appendNewline: true },
117
+ { id: uuid(), name: "\u9644\u7740\u72B6\u6001", payload: "AT+CGATT?", displayMode: "ascii", hotkey: null, appendNewline: true },
118
+ { id: uuid(), name: "\u67E5\u8BE2 IMEI", payload: "AT+CGSN", displayMode: "ascii", hotkey: null, appendNewline: true },
119
+ { id: uuid(), name: "\u91CD\u542F\u6A21\u5757", payload: "AT+CFUN=1,1", displayMode: "ascii", hotkey: null, appendNewline: true }
120
+ ]
121
+ },
122
+ {
123
+ id: uuid(),
124
+ name: "Modbus",
125
+ commands: [
126
+ { id: uuid(), name: "\u8BFB\u4FDD\u6301\u5BC4\u5B58\u5668 0x0001", payload: "01 03 00 01 00 01 D5 CA", displayMode: "hex", hotkey: 5, appendNewline: false },
127
+ { id: uuid(), name: "\u5199\u5355\u4E2A\u5BC4\u5B58\u5668", payload: "01 06 00 01 00 0A 9A 0B", displayMode: "hex", hotkey: 6, appendNewline: false }
128
+ ]
129
+ }
130
+ ];
131
+ }
132
+ var PresetStore = class {
133
+ groups = [];
134
+ constructor() {
135
+ this.load();
136
+ if (this.groups.length === 0) {
137
+ this.groups = createDefaultPresetGroups();
138
+ this.save();
139
+ }
140
+ }
141
+ ensureDir() {
142
+ if (!existsSync2(CONFIG_DIR2)) {
143
+ mkdirSync2(CONFIG_DIR2, { recursive: true });
144
+ }
145
+ }
146
+ load() {
147
+ try {
148
+ if (existsSync2(PRESETS_FILE)) {
149
+ const raw = readFileSync2(PRESETS_FILE, "utf-8");
150
+ this.groups = JSON.parse(raw);
151
+ }
152
+ } catch {
153
+ }
154
+ }
155
+ save() {
156
+ this.ensureDir();
157
+ try {
158
+ writeFileSync2(PRESETS_FILE, JSON.stringify(this.groups, null, 2), "utf-8");
159
+ } catch (e) {
160
+ console.error("[PresetStore] save failed:", e);
161
+ }
162
+ }
163
+ addGroup(name) {
164
+ const g = { id: uuid(), name, commands: [] };
165
+ this.groups.push(g);
166
+ this.save();
167
+ return g;
168
+ }
169
+ removeGroup(id) {
170
+ this.groups = this.groups.filter((g) => g.id !== id);
171
+ this.save();
172
+ }
173
+ addCommand(cmd, groupId) {
174
+ const g = this.groups.find((g2) => g2.id === groupId);
175
+ if (!g) return null;
176
+ const c = { ...cmd, id: uuid() };
177
+ g.commands.push(c);
178
+ this.save();
179
+ return c;
180
+ }
181
+ updateCommand(cmd, groupId) {
182
+ const g = this.groups.find((g2) => g2.id === groupId);
183
+ if (!g) return false;
184
+ const idx = g.commands.findIndex((c) => c.id === cmd.id);
185
+ if (idx === -1) return false;
186
+ g.commands[idx] = cmd;
187
+ this.save();
188
+ return true;
189
+ }
190
+ removeCommand(cmdId, groupId) {
191
+ const g = this.groups.find((g2) => g2.id === groupId);
192
+ if (!g) return;
193
+ g.commands = g.commands.filter((c) => c.id !== cmdId);
194
+ this.save();
195
+ }
196
+ /** 在所有分组中查找 hotkey == n 的命令 */
197
+ findByHotkey(n) {
198
+ for (const g of this.groups) {
199
+ const cmd = g.commands.find((c) => c.hotkey === n);
200
+ if (cmd) return cmd;
201
+ }
202
+ return null;
203
+ }
204
+ };
205
+
206
+ // src/session-store.ts
207
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, statSync, renameSync, rmSync } from "fs";
208
+ import { join as join3, basename, dirname, extname } from "path";
209
+ import { homedir as homedir3 } from "os";
210
+ function uuid2() {
211
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
212
+ const r = Math.random() * 16 | 0;
213
+ const v = c === "x" ? r : r & 3 | 8;
214
+ return v.toString(16);
215
+ });
216
+ }
217
+ var CONFIG_DIR3 = join3(homedir3(), ".serial_tool");
218
+ var TEMPLATE_FILE = join3(CONFIG_DIR3, "template.json");
219
+ var SessionStore = class {
220
+ baseURL;
221
+ constructor() {
222
+ this.baseURL = join3(CONFIG_DIR3, "sessions");
223
+ if (!existsSync3(this.baseURL)) {
224
+ mkdirSync3(this.baseURL, { recursive: true });
225
+ }
226
+ }
227
+ get storagePath() {
228
+ return this.baseURL;
229
+ }
230
+ ensureDir() {
231
+ if (!existsSync3(CONFIG_DIR3)) {
232
+ mkdirSync3(CONFIG_DIR3, { recursive: true });
233
+ }
234
+ if (!existsSync3(this.baseURL)) {
235
+ mkdirSync3(this.baseURL, { recursive: true });
236
+ }
237
+ }
238
+ /** 递归扫描构建树 */
239
+ buildTree() {
240
+ return this._buildTree(this.baseURL);
241
+ }
242
+ _buildTree(dir) {
243
+ const nodes = [];
244
+ let entries;
245
+ try {
246
+ entries = readdirSync(dir).sort();
247
+ } catch {
248
+ return nodes;
249
+ }
250
+ for (const name of entries) {
251
+ const fullPath = join3(dir, name);
252
+ let st;
253
+ try {
254
+ st = statSync(fullPath);
255
+ } catch {
256
+ continue;
257
+ }
258
+ if (st.isDirectory()) {
259
+ const folder = {
260
+ id: uuid2(),
261
+ name,
262
+ kind: "folder",
263
+ filePath: fullPath,
264
+ children: this._buildTree(fullPath),
265
+ expanded: true
266
+ };
267
+ nodes.push(folder);
268
+ } else {
269
+ const parsed = this._parseFile(fullPath);
270
+ if (parsed) {
271
+ const session = {
272
+ id: uuid2(),
273
+ name: basename(name, extname(name)),
274
+ kind: "session",
275
+ sessionId: parsed.id,
276
+ filePath: fullPath,
277
+ children: [],
278
+ expanded: true
279
+ };
280
+ nodes.push(session);
281
+ } else {
282
+ const file = {
283
+ id: uuid2(),
284
+ name,
285
+ kind: "file",
286
+ filePath: fullPath,
287
+ children: [],
288
+ expanded: true
289
+ };
290
+ nodes.push(file);
291
+ }
292
+ }
293
+ }
294
+ return this.sortTree(nodes);
295
+ }
296
+ /** 按类型和名称排序: 目录 -> 会话 -> 普通文件 */
297
+ sortTree(tree) {
298
+ const kindOrder = { folder: 0, session: 1, file: 2 };
299
+ for (const node of tree) {
300
+ if (node.children.length > 0) node.children = this.sortTree(node.children);
301
+ }
302
+ return tree.sort((a, b) => {
303
+ const byKind = kindOrder[a.kind] - kindOrder[b.kind];
304
+ if (byKind !== 0) return byKind;
305
+ return a.name.localeCompare(b.name, "zh-CN", { numeric: true, sensitivity: "base" });
306
+ });
307
+ }
308
+ /** 在树中查找节点 */
309
+ findNode(tree, id) {
310
+ for (const n of tree) {
311
+ if (n.id === id) return n;
312
+ const r = this.findNode(n.children, id);
313
+ if (r) return r;
314
+ }
315
+ return null;
316
+ }
317
+ /** 在树中查找会话节点 */
318
+ findBySessionId(tree, sid) {
319
+ for (const n of tree) {
320
+ if (n.kind === "session" && n.sessionId === sid) return n;
321
+ const r = this.findBySessionId(n.children, sid);
322
+ if (r) return r;
323
+ }
324
+ return null;
325
+ }
326
+ /** 按文件路径查找节点 */
327
+ findByFilePath(tree, filePath) {
328
+ for (const n of tree) {
329
+ if (n.filePath === filePath) return n;
330
+ const r = this.findByFilePath(n.children, filePath);
331
+ if (r) return r;
332
+ }
333
+ return null;
334
+ }
335
+ /** 查找父节点 */
336
+ findParent(tree, id) {
337
+ for (const n of tree) {
338
+ if (n.children.some((c) => c.id === id)) return n;
339
+ const r = this.findParent(n.children, id);
340
+ if (r) return r;
341
+ }
342
+ return null;
343
+ }
344
+ // ---- 新建 ----
345
+ /** 新建会话 */
346
+ newSession(name, parentId, config, _presetGroups, tree) {
347
+ const parentDir = parentId ? this.findNode(tree, parentId)?.filePath ?? this.baseURL : this.baseURL;
348
+ const sessionId = uuid2();
349
+ const fileName = `${name}.json`;
350
+ let filePath = join3(parentDir, fileName);
351
+ if (existsSync3(filePath)) {
352
+ let idx = 2;
353
+ while (true) {
354
+ filePath = join3(parentDir, `${name} ${idx}.json`);
355
+ if (!existsSync3(filePath)) break;
356
+ idx++;
357
+ }
358
+ }
359
+ const displayName = basename(filePath, extname(filePath));
360
+ const template = this._loadTemplate(config);
361
+ const data = {
362
+ version: "2.0",
363
+ id: sessionId,
364
+ name: displayName,
365
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
366
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
367
+ config: JSON.parse(JSON.stringify(template.config)),
368
+ presetGroups: JSON.parse(JSON.stringify(template.presetGroups)),
369
+ entries: JSON.parse(JSON.stringify(template.entries)),
370
+ __serial_session: (/* @__PURE__ */ new Date()).toISOString()
371
+ };
372
+ this._writeFile(filePath, data);
373
+ const node = {
374
+ id: uuid2(),
375
+ name: displayName,
376
+ kind: "session",
377
+ sessionId,
378
+ filePath,
379
+ children: [],
380
+ expanded: true
381
+ };
382
+ if (parentId) {
383
+ const parent = this.findNode(tree, parentId);
384
+ if (parent) {
385
+ parent.children.push(node);
386
+ parent.expanded = true;
387
+ }
388
+ } else {
389
+ tree.push(node);
390
+ }
391
+ return { node, tree: this.sortTree(tree) };
392
+ }
393
+ /** 新建目录 */
394
+ newFolder(name, parentId, tree) {
395
+ const parentDir = parentId ? this.findNode(tree, parentId)?.filePath ?? this.baseURL : this.baseURL;
396
+ let dirPath = join3(parentDir, name);
397
+ if (existsSync3(dirPath)) {
398
+ let idx = 2;
399
+ while (true) {
400
+ dirPath = join3(parentDir, `${name} ${idx}`);
401
+ if (!existsSync3(dirPath)) break;
402
+ idx++;
403
+ }
404
+ }
405
+ mkdirSync3(dirPath, { recursive: true });
406
+ const node = {
407
+ id: uuid2(),
408
+ name: basename(dirPath),
409
+ kind: "folder",
410
+ filePath: dirPath,
411
+ children: [],
412
+ expanded: true
413
+ };
414
+ if (parentId) {
415
+ const parent = this.findNode(tree, parentId);
416
+ if (parent) {
417
+ parent.children.push(node);
418
+ parent.expanded = true;
419
+ }
420
+ } else {
421
+ tree.push(node);
422
+ }
423
+ return { node, tree: this.sortTree(tree) };
424
+ }
425
+ // ---- 加载会话 ----
426
+ loadSession(node) {
427
+ if (node.kind !== "session") return null;
428
+ const data = this._readFile(node.filePath);
429
+ if (!data) return null;
430
+ return { session: data };
431
+ }
432
+ /** 从会话数据恢复配置和预设 */
433
+ applySession(config, presetGroups, session) {
434
+ const s = session.config;
435
+ config.baudRate = s.baudRate ?? config.baudRate;
436
+ config.dataBits = s.dataBits ?? config.dataBits;
437
+ config.stopBits = s.stopBits ?? config.stopBits;
438
+ config.parity = s.parity ?? config.parity;
439
+ config.flowControl = s.flowControl ?? config.flowControl;
440
+ config.lastDevicePath = s.lastDevicePath ?? null;
441
+ config.presetPanelVisible = s.presetPanelVisible ?? true;
442
+ config.sessionPanelVisible = s.sessionPanelVisible ?? true;
443
+ config.receiveDisplayMode = s.receiveDisplayMode ?? "text";
444
+ config.receiveShowMixed = s.receiveShowMixed ?? true;
445
+ config.receiveShowTimestamp = s.receiveShowTimestamp ?? true;
446
+ config.receiveAutoScroll = s.receiveAutoScroll ?? true;
447
+ config.sendMode = s.sendMode ?? "ascii";
448
+ config.sendAppendCRLF = s.sendAppendCRLF ?? true;
449
+ config.localEcho = s.localEcho ?? true;
450
+ config.sendInputText = s.sendInputText ?? "";
451
+ config.autoConnect = s.autoConnect ?? false;
452
+ presetGroups.length = 0;
453
+ for (const g of session.presetGroups) {
454
+ presetGroups.push(JSON.parse(JSON.stringify(g)));
455
+ }
456
+ }
457
+ // ---- 保存 ----
458
+ saveSession(node, config, presetGroups, entries) {
459
+ if (node.kind !== "session" || !node.sessionId) return false;
460
+ const existing = this._readFile(node.filePath);
461
+ const data = {
462
+ version: "2.0",
463
+ id: node.sessionId,
464
+ name: node.name,
465
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
466
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
467
+ config: this._captureConfig(config),
468
+ presetGroups: JSON.parse(JSON.stringify(presetGroups)),
469
+ entries: entries ?? existing?.entries ?? [],
470
+ __serial_session: existing?.__serial_session ?? (/* @__PURE__ */ new Date()).toISOString()
471
+ };
472
+ this._writeFile(node.filePath, data);
473
+ return true;
474
+ }
475
+ // ---- 删除 ----
476
+ deleteNode(node, tree) {
477
+ if (node.kind === "folder") {
478
+ try {
479
+ rmSync(node.filePath, { recursive: true, force: true });
480
+ } catch {
481
+ }
482
+ } else {
483
+ try {
484
+ rmSync(node.filePath);
485
+ } catch {
486
+ }
487
+ }
488
+ return this.sortTree(this._removeFromTree(tree, node.id));
489
+ }
490
+ // ---- 重命名 ----
491
+ renameNode(node, newName, tree) {
492
+ const trimmed = newName.trim();
493
+ if (!trimmed) return tree;
494
+ const parentDir = dirname(node.filePath);
495
+ let newPath;
496
+ if (node.kind === "folder") {
497
+ newPath = join3(parentDir, trimmed);
498
+ } else {
499
+ const ext = extname(node.filePath);
500
+ newPath = join3(parentDir, ext ? `${trimmed}${ext}` : trimmed);
501
+ }
502
+ try {
503
+ renameSync(node.filePath, newPath);
504
+ } catch (e) {
505
+ console.error("[SessionStore] rename failed:", e);
506
+ return tree;
507
+ }
508
+ if (node.kind === "session") {
509
+ const data = this._readFile(newPath);
510
+ if (data) {
511
+ data.name = trimmed;
512
+ data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
513
+ this._writeFile(newPath, data);
514
+ }
515
+ }
516
+ if (node.kind === "folder") {
517
+ this._updateChildPaths(node, node.filePath, newPath);
518
+ }
519
+ node.name = trimmed;
520
+ node.filePath = newPath;
521
+ return this.sortTree(tree);
522
+ }
523
+ // ---- 移动(拖拽) ----
524
+ moveNode(node, targetId, tree) {
525
+ const target = targetId ? this.findNode(tree, targetId) : null;
526
+ if (target && target.kind !== "folder") return tree;
527
+ if (target && target.id === node.id) return tree;
528
+ if (target && node.kind === "folder" && this._containsNode(node, target.id)) return tree;
529
+ const destDir = target?.filePath ?? this.baseURL;
530
+ const destPath = join3(destDir, basename(node.filePath));
531
+ try {
532
+ renameSync(node.filePath, destPath);
533
+ } catch (e) {
534
+ console.error("[SessionStore] move failed:", e);
535
+ return tree;
536
+ }
537
+ if (node.kind === "folder") {
538
+ this._updateChildPaths(node, node.filePath, destPath);
539
+ }
540
+ node.filePath = destPath;
541
+ tree = this._removeFromTree(tree, node.id);
542
+ if (target) {
543
+ target.children.push(node);
544
+ } else {
545
+ tree.push(node);
546
+ }
547
+ return this.sortTree(tree);
548
+ }
549
+ // ---- 文件操作 ----
550
+ readFileContent(filePath) {
551
+ try {
552
+ return readFileSync3(filePath, "utf-8");
553
+ } catch {
554
+ return "";
555
+ }
556
+ }
557
+ writeFileContent(filePath, content) {
558
+ try {
559
+ writeFileSync3(filePath, content, "utf-8");
560
+ } catch (e) {
561
+ console.error("[SessionStore] write file failed:", e);
562
+ }
563
+ }
564
+ // ---- 内部方法 ----
565
+ _captureConfig(config) {
566
+ return {
567
+ baudRate: config.baudRate,
568
+ dataBits: config.dataBits,
569
+ stopBits: config.stopBits,
570
+ parity: config.parity,
571
+ flowControl: config.flowControl,
572
+ lastDevicePath: config.lastDevicePath,
573
+ presetPanelVisible: config.presetPanelVisible,
574
+ sessionPanelVisible: config.sessionPanelVisible,
575
+ receiveDisplayMode: config.receiveDisplayMode,
576
+ receiveShowMixed: config.receiveShowMixed,
577
+ receiveShowTimestamp: config.receiveShowTimestamp,
578
+ receiveAutoScroll: config.receiveAutoScroll,
579
+ sendMode: config.sendMode,
580
+ sendAppendCRLF: config.sendAppendCRLF,
581
+ localEcho: config.localEcho,
582
+ sendInputText: config.sendInputText,
583
+ autoConnect: config.autoConnect
584
+ };
585
+ }
586
+ _loadTemplate(config) {
587
+ const defaults = {
588
+ version: "2.0",
589
+ config: this._captureConfig(config),
590
+ presetGroups: createDefaultPresetGroups(),
591
+ entries: []
592
+ };
593
+ this.ensureDir();
594
+ if (!existsSync3(TEMPLATE_FILE)) {
595
+ try {
596
+ writeFileSync3(TEMPLATE_FILE, JSON.stringify(defaults, null, 2), "utf-8");
597
+ } catch (e) {
598
+ console.error("[SessionStore] template save failed:", e);
599
+ }
600
+ return defaults;
601
+ }
602
+ try {
603
+ const raw = readFileSync3(TEMPLATE_FILE, "utf-8");
604
+ const parsed = JSON.parse(raw);
605
+ return {
606
+ config: { ...defaults.config, ...parsed.config ?? {} },
607
+ presetGroups: Array.isArray(parsed.presetGroups) ? parsed.presetGroups : defaults.presetGroups,
608
+ entries: Array.isArray(parsed.entries) ? parsed.entries : defaults.entries
609
+ };
610
+ } catch (e) {
611
+ console.error("[SessionStore] template load failed:", e);
612
+ return defaults;
613
+ }
614
+ }
615
+ _readFile(path) {
616
+ try {
617
+ const raw = readFileSync3(path, "utf-8");
618
+ return JSON.parse(raw);
619
+ } catch {
620
+ return null;
621
+ }
622
+ }
623
+ _writeFile(path, data) {
624
+ try {
625
+ writeFileSync3(path, JSON.stringify(data, null, 2), "utf-8");
626
+ } catch (e) {
627
+ console.error("[SessionStore] write failed:", e);
628
+ }
629
+ }
630
+ _parseFile(path) {
631
+ try {
632
+ const raw = readFileSync3(path, "utf-8");
633
+ const data = JSON.parse(raw);
634
+ if (data && data.version && data.id && data.config) {
635
+ return data;
636
+ }
637
+ return null;
638
+ } catch {
639
+ return null;
640
+ }
641
+ }
642
+ _removeFromTree(tree, id) {
643
+ return tree.filter((n) => {
644
+ if (n.id === id) return false;
645
+ n.children = this._removeFromTree(n.children, id);
646
+ return true;
647
+ });
648
+ }
649
+ _updateChildPaths(node, oldParent, newParent) {
650
+ for (const child of node.children) {
651
+ const relative = child.filePath.slice(oldParent.length);
652
+ child.filePath = newParent + relative;
653
+ if (child.kind === "folder") {
654
+ this._updateChildPaths(child, oldParent, newParent);
655
+ }
656
+ }
657
+ }
658
+ _containsNode(node, id) {
659
+ for (const child of node.children) {
660
+ if (child.id === id || this._containsNode(child, id)) return true;
661
+ }
662
+ return false;
663
+ }
664
+ };
665
+
666
+ // src/serial-port.ts
667
+ import { EventEmitter } from "events";
668
+ var LINE_ENDING_BYTES = {
669
+ none: [],
670
+ lf: [10],
671
+ cr: [13],
672
+ crlf: [13, 10]
673
+ };
674
+ var serialportModule = null;
675
+ var serialportAvailable = false;
676
+ var serialportReason = "";
677
+ async function getSerialPort() {
678
+ if (serialportModule) return serialportModule;
679
+ if (!serialportAvailable && serialportReason) return null;
680
+ try {
681
+ serialportModule = await import("serialport");
682
+ serialportAvailable = true;
683
+ console.log("[serial] serialport \u6A21\u5757\u52A0\u8F7D\u6210\u529F");
684
+ return serialportModule;
685
+ } catch (e) {
686
+ serialportReason = "serialport \u6A21\u5757\u52A0\u8F7D\u5931\u8D25: " + (e.message || e);
687
+ console.warn("[serial]", serialportReason);
688
+ return null;
689
+ }
690
+ }
691
+ function isSerialPortAvailable() {
692
+ return serialportAvailable;
693
+ }
694
+ var SerialPortManager = class extends EventEmitter {
695
+ port = null;
696
+ _isOpen = false;
697
+ _currentDevice = null;
698
+ _currentConfig = null;
699
+ _lastError = null;
700
+ get isOpen() {
701
+ return this._isOpen;
702
+ }
703
+ get currentDevice() {
704
+ return this._currentDevice;
705
+ }
706
+ get currentConfig() {
707
+ return this._currentConfig;
708
+ }
709
+ get lastError() {
710
+ return this._lastError;
711
+ }
712
+ /** 枚举可用串口设备 */
713
+ static async listDevices() {
714
+ const sp = await getSerialPort();
715
+ if (!sp) return [];
716
+ try {
717
+ const ports = await sp.SerialPort.list();
718
+ return ports.map((p) => ({
719
+ path: p.path,
720
+ manufacturer: p.manufacturer,
721
+ serialNumber: p.serialNumber,
722
+ pnpId: p.pnpId
723
+ }));
724
+ } catch {
725
+ return [];
726
+ }
727
+ }
728
+ /** 返回串口不可用的原因 */
729
+ static unavailableReason() {
730
+ return serialportReason;
731
+ }
732
+ /** 打开串口 */
733
+ async open(device, config) {
734
+ const sp = await getSerialPort();
735
+ if (!sp) throw new Error(serialportReason || "serialport \u6A21\u5757\u4E0D\u53EF\u7528\uFF0C\u8BF7\u5728 Node.js \u73AF\u5883\u4E0B\u8FD0\u884C");
736
+ if (this._isOpen) throw new Error("\u4E32\u53E3\u5DF2\u6253\u5F00");
737
+ this._lastError = null;
738
+ return new Promise((resolve, reject) => {
739
+ const port = new sp.SerialPort({
740
+ path: device.path,
741
+ baudRate: config.baudRate,
742
+ dataBits: config.dataBits,
743
+ stopBits: config.stopBits,
744
+ parity: config.parity,
745
+ rtscts: config.flowControl === "rtscts",
746
+ xon: config.flowControl === "xonxoff",
747
+ xoff: config.flowControl === "xonxoff"
748
+ }, (err) => {
749
+ if (err) {
750
+ this._lastError = `\u6253\u5F00\u4E32\u53E3\u5931\u8D25: ${device.path} (${err.message})`;
751
+ reject(err);
752
+ return;
753
+ }
754
+ this.port = port;
755
+ this._isOpen = true;
756
+ this._currentDevice = device;
757
+ this._currentConfig = config;
758
+ port.on("data", (data) => {
759
+ this.emit("data", { direction: "RX", data: Array.from(data), timestamp: (/* @__PURE__ */ new Date()).toISOString() });
760
+ });
761
+ port.on("error", (err2) => {
762
+ this._lastError = err2.message;
763
+ this.emit("error", err2.message);
764
+ });
765
+ port.on("close", () => {
766
+ this._isOpen = false;
767
+ this._currentDevice = null;
768
+ this.port = null;
769
+ this.emit("close");
770
+ });
771
+ this.emit("open", device);
772
+ resolve();
773
+ });
774
+ });
775
+ }
776
+ /** 关闭串口 */
777
+ async close() {
778
+ if (!this._isOpen || !this.port) return;
779
+ return new Promise((resolve) => {
780
+ this.port.close(() => {
781
+ this._isOpen = false;
782
+ this._currentDevice = null;
783
+ this.port = null;
784
+ this.emit("close");
785
+ resolve();
786
+ });
787
+ });
788
+ }
789
+ /** 发送数据 */
790
+ async send(data) {
791
+ if (!this._isOpen || !this.port) {
792
+ this._lastError = "\u4E32\u53E3\u672A\u6253\u5F00";
793
+ return false;
794
+ }
795
+ return new Promise((resolve) => {
796
+ this.port.write(Buffer.from(data), (err) => {
797
+ if (err) {
798
+ this._lastError = `\u5199\u5165\u5931\u8D25: ${err.message}`;
799
+ resolve(false);
800
+ } else this.port.drain(() => resolve(true));
801
+ });
802
+ });
803
+ }
804
+ /** 发送字符串 + 行尾符 */
805
+ async sendString(text, lineEnding) {
806
+ const bytes = Array.from(new TextEncoder().encode(text));
807
+ bytes.push(...LINE_ENDING_BYTES[lineEnding]);
808
+ return this.send(bytes);
809
+ }
810
+ /** 编译预设指令为字节数组 */
811
+ compilePayload(payload, mode) {
812
+ if (mode === "ascii") return Array.from(new TextEncoder().encode(payload));
813
+ const bytes = [];
814
+ const tokens = payload.split(/[\s,\n\t]+/).filter(Boolean);
815
+ for (const tok of tokens) {
816
+ const b = parseInt(tok, 16);
817
+ if (isNaN(b) || b < 0 || b > 255) return null;
818
+ bytes.push(b);
819
+ }
820
+ return bytes;
821
+ }
822
+ };
823
+
824
+ // src/index.ts
825
+ import http from "http";
826
+ import { readFileSync as readFileSync4 } from "fs";
827
+ import { WebSocketServer } from "ws";
828
+ var appConfig = new AppConfig();
829
+ var presetStore = new PresetStore();
830
+ var sessionStore = new SessionStore();
831
+ var serialPort = new SerialPortManager();
832
+ var currentTree = sessionStore.buildTree();
833
+ var activeSessionId = null;
834
+ var activeSessionPath = null;
835
+ var activeFilePath = null;
836
+ var logEntries = [];
837
+ function uuid3() {
838
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
839
+ const r = Math.random() * 16 | 0;
840
+ const v = c === "x" ? r : r & 3 | 8;
841
+ return v.toString(16);
842
+ });
843
+ }
844
+ async function handleMessage(ws, msg) {
845
+ const { id, method, params } = msg;
846
+ try {
847
+ let result;
848
+ switch (method) {
849
+ // ---- 串口操作 ----
850
+ case "list_devices": {
851
+ const devices = await SerialPortManager.listDevices();
852
+ result = {
853
+ devices,
854
+ serialPortStatus: { available: isSerialPortAvailable(), reason: SerialPortManager.unavailableReason() }
855
+ };
856
+ break;
857
+ }
858
+ case "open_port": {
859
+ const { device, config } = params;
860
+ await serialPort.open(device, config);
861
+ appConfig.data.lastDevicePath = device.path;
862
+ appConfig.updateFromSerialConfig(config);
863
+ appConfig.save();
864
+ result = { success: true, device };
865
+ break;
866
+ }
867
+ case "close_port": {
868
+ await serialPort.close();
869
+ result = { success: true };
870
+ break;
871
+ }
872
+ case "send_data": {
873
+ const { text, mode, appendNewline, localEcho } = params;
874
+ const le = appendNewline ? "crlf" : "none";
875
+ let bytes;
876
+ if (mode === "hex") {
877
+ const compiled = serialPort.compilePayload(text, "hex");
878
+ if (!compiled) {
879
+ result = { error: "HEX \u683C\u5F0F\u9519\u8BEF" };
880
+ break;
881
+ }
882
+ bytes = compiled;
883
+ if (appendNewline) bytes.push(13, 10);
884
+ } else {
885
+ bytes = Array.from(new TextEncoder().encode(text));
886
+ if (appendNewline) bytes.push(13, 10);
887
+ }
888
+ const ok = await serialPort.send(bytes);
889
+ if (ok && localEcho) {
890
+ const entry = {
891
+ id: uuid3(),
892
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
893
+ direction: "TX",
894
+ data: bytes
895
+ };
896
+ logEntries.push(entry);
897
+ broadcast({ type: "log_entry", entry });
898
+ }
899
+ result = { success: ok };
900
+ break;
901
+ }
902
+ case "send_preset": {
903
+ const { command, appendNewline } = params;
904
+ const compiled = serialPort.compilePayload(command.payload, command.displayMode);
905
+ if (!compiled) {
906
+ result = { error: "HEX \u683C\u5F0F\u9519\u8BEF" };
907
+ break;
908
+ }
909
+ let bytes = compiled;
910
+ if (appendNewline !== void 0 ? appendNewline : command.appendNewline) {
911
+ bytes = [...bytes, 13, 10];
912
+ }
913
+ const ok = await serialPort.send(bytes);
914
+ if (ok && appConfig.data.localEcho) {
915
+ const entry = {
916
+ id: uuid3(),
917
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
918
+ direction: "TX",
919
+ data: bytes
920
+ };
921
+ logEntries.push(entry);
922
+ broadcast({ type: "log_entry", entry });
923
+ }
924
+ result = { success: ok };
925
+ break;
926
+ }
927
+ case "get_state": {
928
+ result = getFullState();
929
+ break;
930
+ }
931
+ // ---- 配置操作 ----
932
+ case "update_config": {
933
+ Object.assign(appConfig.data, params);
934
+ appConfig.save();
935
+ if (activeSessionId) {
936
+ const node = activeSessionPath ? sessionStore.findByFilePath(currentTree, activeSessionPath) : sessionStore.findBySessionId(currentTree, activeSessionId);
937
+ if (node) {
938
+ sessionStore.saveSession(node, appConfig.data, presetStore.groups);
939
+ }
940
+ }
941
+ broadcast({ type: "config_updated", config: appConfig.data });
942
+ result = { success: true };
943
+ break;
944
+ }
945
+ case "reset_config": {
946
+ appConfig.resetAll();
947
+ broadcast({ type: "config_updated", config: appConfig.data });
948
+ result = { success: true };
949
+ break;
950
+ }
951
+ // ---- 预设操作 ----
952
+ case "add_group": {
953
+ const g = presetStore.addGroup(params.name);
954
+ broadcast({ type: "presets_updated", groups: presetStore.groups });
955
+ result = { group: g };
956
+ break;
957
+ }
958
+ case "remove_group": {
959
+ presetStore.removeGroup(params.id);
960
+ broadcast({ type: "presets_updated", groups: presetStore.groups });
961
+ result = { success: true };
962
+ break;
963
+ }
964
+ case "add_command": {
965
+ const c = presetStore.addCommand(params.command, params.groupId);
966
+ broadcast({ type: "presets_updated", groups: presetStore.groups });
967
+ result = { command: c };
968
+ break;
969
+ }
970
+ case "update_command": {
971
+ presetStore.updateCommand(params.command, params.groupId);
972
+ broadcast({ type: "presets_updated", groups: presetStore.groups });
973
+ result = { success: true };
974
+ break;
975
+ }
976
+ case "remove_command": {
977
+ presetStore.removeCommand(params.cmdId, params.groupId);
978
+ broadcast({ type: "presets_updated", groups: presetStore.groups });
979
+ result = { success: true };
980
+ break;
981
+ }
982
+ // ---- 会话操作 ----
983
+ case "get_tree": {
984
+ currentTree = sessionStore.buildTree();
985
+ if (activeSessionPath && !sessionStore.findByFilePath(currentTree, activeSessionPath)) {
986
+ activeSessionId = null;
987
+ activeSessionPath = null;
988
+ logEntries = [];
989
+ }
990
+ if (activeFilePath && !sessionStore.findByFilePath(currentTree, activeFilePath)) {
991
+ activeFilePath = null;
992
+ }
993
+ result = { tree: currentTree, activeSessionId, activeFilePath, logEntries };
994
+ break;
995
+ }
996
+ case "new_session": {
997
+ const { node, tree } = sessionStore.newSession(
998
+ params.name,
999
+ params.parentId ?? null,
1000
+ appConfig.data,
1001
+ presetStore.groups,
1002
+ currentTree
1003
+ );
1004
+ currentTree = sessionStore.buildTree();
1005
+ if (node.sessionId) {
1006
+ activeSessionId = node.sessionId;
1007
+ activeSessionPath = node.filePath;
1008
+ activeFilePath = null;
1009
+ logEntries = [];
1010
+ const sessionData = sessionStore.loadSession(node);
1011
+ if (sessionData) {
1012
+ sessionStore.applySession(appConfig.data, presetStore.groups, sessionData.session);
1013
+ }
1014
+ }
1015
+ result = { node, tree: currentTree, activeSessionId };
1016
+ broadcast({
1017
+ type: "session_loaded",
1018
+ config: appConfig.data,
1019
+ presets: presetStore.groups,
1020
+ logEntries,
1021
+ activeSessionId,
1022
+ activeFilePath: null,
1023
+ tree: currentTree,
1024
+ serialOpen: serialPort.isOpen,
1025
+ serialDevice: serialPort.currentDevice,
1026
+ serialConfig: serialPort.currentConfig,
1027
+ serialError: serialPort.lastError
1028
+ });
1029
+ break;
1030
+ }
1031
+ case "new_folder": {
1032
+ const { node, tree } = sessionStore.newFolder(
1033
+ params.name,
1034
+ params.parentId ?? null,
1035
+ currentTree
1036
+ );
1037
+ currentTree = sessionStore.buildTree();
1038
+ result = { node, tree: currentTree };
1039
+ broadcast({ type: "tree_updated", tree: currentTree });
1040
+ break;
1041
+ }
1042
+ case "load_session": {
1043
+ const node = sessionStore.findNode(currentTree, params.nodeId);
1044
+ if (!node || node.kind !== "session") {
1045
+ result = { error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728" };
1046
+ break;
1047
+ }
1048
+ if (activeSessionId) {
1049
+ const prevNode = activeSessionPath ? sessionStore.findByFilePath(currentTree, activeSessionPath) : sessionStore.findBySessionId(currentTree, activeSessionId);
1050
+ if (prevNode) {
1051
+ sessionStore.saveSession(prevNode, appConfig.data, presetStore.groups, logEntries);
1052
+ }
1053
+ }
1054
+ const data = sessionStore.loadSession(node);
1055
+ if (!data) {
1056
+ result = { error: "\u52A0\u8F7D\u4F1A\u8BDD\u5931\u8D25" };
1057
+ break;
1058
+ }
1059
+ sessionStore.applySession(appConfig.data, presetStore.groups, data.session);
1060
+ logEntries = data.session.entries ?? [];
1061
+ activeSessionId = node.sessionId;
1062
+ activeSessionPath = node.filePath;
1063
+ activeFilePath = null;
1064
+ sessionStore.saveSession(node, appConfig.data, presetStore.groups, logEntries);
1065
+ currentTree = sessionStore.buildTree();
1066
+ if (serialPort.isOpen) {
1067
+ await serialPort.close();
1068
+ }
1069
+ let autoOpenError = null;
1070
+ if (appConfig.data.autoConnect && appConfig.data.lastDevicePath) {
1071
+ try {
1072
+ await serialPort.open(
1073
+ { path: appConfig.data.lastDevicePath },
1074
+ appConfig.toSerialConfig()
1075
+ );
1076
+ } catch (err) {
1077
+ autoOpenError = err.message || String(err);
1078
+ broadcast({ type: "serial_error", message: `\u81EA\u52A8\u6253\u5F00\u4E32\u53E3\u5931\u8D25: ${autoOpenError}` });
1079
+ }
1080
+ }
1081
+ result = {
1082
+ session: data.session,
1083
+ config: appConfig.data,
1084
+ presets: presetStore.groups,
1085
+ logEntries,
1086
+ activeSessionId,
1087
+ activeFilePath: null,
1088
+ tree: currentTree,
1089
+ serialOpen: serialPort.isOpen,
1090
+ serialDevice: serialPort.currentDevice,
1091
+ serialConfig: serialPort.currentConfig,
1092
+ serialError: serialPort.lastError,
1093
+ autoOpenError
1094
+ };
1095
+ broadcast({ type: "session_loaded", ...result });
1096
+ break;
1097
+ }
1098
+ case "save_session": {
1099
+ const node = activeSessionId ? activeSessionPath ? sessionStore.findByFilePath(currentTree, activeSessionPath) : sessionStore.findBySessionId(currentTree, activeSessionId) : null;
1100
+ if (node) {
1101
+ sessionStore.saveSession(node, appConfig.data, presetStore.groups, logEntries);
1102
+ }
1103
+ result = { success: true };
1104
+ break;
1105
+ }
1106
+ case "delete_node": {
1107
+ const node = sessionStore.findNode(currentTree, params.nodeId);
1108
+ if (node) {
1109
+ if (activeSessionPath === node.filePath || !activeSessionPath && activeSessionId === node.sessionId) {
1110
+ activeSessionId = null;
1111
+ activeSessionPath = null;
1112
+ }
1113
+ if (activeFilePath === node.filePath) {
1114
+ activeFilePath = null;
1115
+ }
1116
+ currentTree = sessionStore.deleteNode(node, currentTree);
1117
+ currentTree = sessionStore.buildTree();
1118
+ }
1119
+ result = { tree: currentTree };
1120
+ broadcast({ type: "tree_updated", tree: currentTree });
1121
+ break;
1122
+ }
1123
+ case "rename_node": {
1124
+ const node = sessionStore.findNode(currentTree, params.nodeId);
1125
+ if (node) {
1126
+ const oldPath = node.filePath;
1127
+ currentTree = sessionStore.renameNode(node, params.newName, currentTree);
1128
+ if (activeSessionPath === oldPath) activeSessionPath = node.filePath;
1129
+ currentTree = sessionStore.buildTree();
1130
+ }
1131
+ result = { tree: currentTree };
1132
+ broadcast({ type: "tree_updated", tree: currentTree });
1133
+ break;
1134
+ }
1135
+ case "move_node": {
1136
+ const node = sessionStore.findNode(currentTree, params.nodeId);
1137
+ if (node) {
1138
+ const oldPath = node.filePath;
1139
+ currentTree = sessionStore.moveNode(node, params.targetId ?? null, currentTree);
1140
+ if (activeSessionPath === oldPath) activeSessionPath = node.filePath;
1141
+ currentTree = sessionStore.buildTree();
1142
+ }
1143
+ result = { tree: currentTree };
1144
+ broadcast({ type: "tree_updated", tree: currentTree });
1145
+ break;
1146
+ }
1147
+ // ---- 文件操作 ----
1148
+ case "open_file": {
1149
+ const node = sessionStore.findNode(currentTree, params.nodeId);
1150
+ if (!node || node.kind !== "file") {
1151
+ result = { error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
1152
+ break;
1153
+ }
1154
+ activeFilePath = node.filePath;
1155
+ activeSessionId = null;
1156
+ activeSessionPath = null;
1157
+ const content = sessionStore.readFileContent(node.filePath);
1158
+ result = {
1159
+ filePath: node.filePath,
1160
+ fileName: node.name,
1161
+ content,
1162
+ activeFilePath,
1163
+ activeSessionId: null
1164
+ };
1165
+ break;
1166
+ }
1167
+ case "save_file": {
1168
+ if (!activeFilePath) {
1169
+ result = { error: "\u6CA1\u6709\u6253\u5F00\u7684\u6587\u4EF6" };
1170
+ break;
1171
+ }
1172
+ sessionStore.writeFileContent(activeFilePath, params.content);
1173
+ result = { success: true };
1174
+ break;
1175
+ }
1176
+ // ---- 日志操作 ----
1177
+ case "clear_logs": {
1178
+ logEntries = [];
1179
+ broadcast({ type: "logs_cleared" });
1180
+ result = { success: true };
1181
+ break;
1182
+ }
1183
+ case "export_logs": {
1184
+ result = { entries: logEntries };
1185
+ break;
1186
+ }
1187
+ // ---- 获取存储路径 ----
1188
+ case "open_config_dir": {
1189
+ result = { path: appConfig.configDir, sessionsPath: sessionStore.storagePath };
1190
+ break;
1191
+ }
1192
+ default:
1193
+ result = { error: `\u672A\u77E5\u65B9\u6CD5: ${method}` };
1194
+ }
1195
+ if (id !== void 0) {
1196
+ ws.send(JSON.stringify({ id, result }));
1197
+ }
1198
+ } catch (err) {
1199
+ if (id !== void 0) {
1200
+ ws.send(JSON.stringify({ id, error: err.message }));
1201
+ }
1202
+ }
1203
+ }
1204
+ function getFullState() {
1205
+ return {
1206
+ config: appConfig.data,
1207
+ presets: presetStore.groups,
1208
+ tree: currentTree,
1209
+ logEntries,
1210
+ activeSessionId,
1211
+ activeFilePath,
1212
+ serialOpen: serialPort.isOpen,
1213
+ serialDevice: serialPort.currentDevice,
1214
+ serialConfig: serialPort.currentConfig,
1215
+ serialError: serialPort.lastError,
1216
+ serialPortStatus: { available: isSerialPortAvailable(), reason: SerialPortManager.unavailableReason() }
1217
+ };
1218
+ }
1219
+ var clients = /* @__PURE__ */ new Set();
1220
+ function broadcast(data) {
1221
+ const msg = JSON.stringify(data);
1222
+ for (const ws of clients) {
1223
+ try {
1224
+ ws.send(msg);
1225
+ } catch {
1226
+ }
1227
+ }
1228
+ }
1229
+ var HTML_PATH = new URL("./public/index.html", import.meta.url).pathname;
1230
+ var HTML_CONTENT = readFileSync4(HTML_PATH, "utf-8");
1231
+ serialPort.on("data", (evt) => {
1232
+ const entry = {
1233
+ id: uuid3(),
1234
+ timestamp: evt.timestamp,
1235
+ direction: evt.direction,
1236
+ data: evt.data
1237
+ };
1238
+ logEntries.push(entry);
1239
+ if (logEntries.length > 5e3) logEntries = logEntries.slice(-5e3);
1240
+ broadcast({ type: "log_entry", entry });
1241
+ });
1242
+ serialPort.on("error", (err) => broadcast({ type: "serial_error", message: err }));
1243
+ serialPort.on("close", () => broadcast({ type: "serial_closed" }));
1244
+ serialPort.on("open", (device) => broadcast({ type: "serial_opened", device }));
1245
+ var PORT = 8765;
1246
+ var ADDR = `http://localhost:${PORT}`;
1247
+ var httpServer = http.createServer((req, res) => {
1248
+ if (req.method === "GET" && req.url !== "/ws") {
1249
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1250
+ res.end(HTML_CONTENT);
1251
+ } else {
1252
+ res.writeHead(404);
1253
+ res.end("Not Found");
1254
+ }
1255
+ });
1256
+ var wss = new WebSocketServer({ server: httpServer, path: "/ws" });
1257
+ wss.on("connection", (ws) => {
1258
+ clients.add(ws);
1259
+ ws.send(JSON.stringify({ type: "initial_state", ...getFullState() }));
1260
+ ws.on("message", async (data) => {
1261
+ try {
1262
+ const msg = JSON.parse(data.toString());
1263
+ await handleMessage(ws, msg);
1264
+ } catch (e) {
1265
+ console.error("[WS] message error:", e);
1266
+ }
1267
+ });
1268
+ ws.on("close", () => clients.delete(ws));
1269
+ });
1270
+ httpServer.listen(PORT, () => {
1271
+ console.log("");
1272
+ console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
1273
+ console.log("\u2551 \u{1F50C} Serialport Tool - \u4E32\u53E3\u8C03\u8BD5\u52A9\u624B \u2551");
1274
+ console.log("\u2551\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2551");
1275
+ console.log(`\u2551 \u5730\u5740: ${ADDR} \u2551`);
1276
+ console.log("\u2551 \u6309 Ctrl+C \u9000\u51FA \u2551");
1277
+ console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
1278
+ console.log("");
1279
+ import("child_process").then((cp) => {
1280
+ const platform = process.platform;
1281
+ const openCmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
1282
+ cp.exec(`${openCmd} ${ADDR}`);
1283
+ });
1284
+ });