@sstar/embedlink_agent 0.1.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/README.md +107 -0
- package/dist/.platform +1 -0
- package/dist/board/docs.js +59 -0
- package/dist/board/notes.js +11 -0
- package/dist/board_uart/history.js +81 -0
- package/dist/board_uart/index.js +66 -0
- package/dist/board_uart/manager.js +313 -0
- package/dist/board_uart/resource.js +578 -0
- package/dist/board_uart/sessions.js +559 -0
- package/dist/config/index.js +341 -0
- package/dist/core/activity.js +7 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/log_stream.js +26 -0
- package/dist/files/__tests__/files_manager.test.js +209 -0
- package/dist/files/artifact_manager.js +68 -0
- package/dist/files/file_operation_logger.js +271 -0
- package/dist/files/files_manager.js +511 -0
- package/dist/files/index.js +87 -0
- package/dist/files/types.js +5 -0
- package/dist/firmware/burn_recover.js +733 -0
- package/dist/firmware/prepare_images.js +184 -0
- package/dist/firmware/user_guide.js +43 -0
- package/dist/index.js +449 -0
- package/dist/logger.js +245 -0
- package/dist/macro/index.js +241 -0
- package/dist/macro/runner.js +168 -0
- package/dist/nfs/index.js +105 -0
- package/dist/plugins/loader.js +30 -0
- package/dist/proto/agent.proto +473 -0
- package/dist/resources/docs/board-interaction.md +115 -0
- package/dist/resources/docs/firmware-upgrade.md +404 -0
- package/dist/resources/docs/nfs-mount-guide.md +78 -0
- package/dist/resources/docs/tftp-transfer-guide.md +81 -0
- package/dist/secrets/index.js +9 -0
- package/dist/server/grpc.js +1069 -0
- package/dist/server/web.js +2284 -0
- package/dist/ssh/adapter.js +126 -0
- package/dist/ssh/candidates.js +85 -0
- package/dist/ssh/index.js +3 -0
- package/dist/ssh/paircheck.js +35 -0
- package/dist/ssh/tunnel.js +111 -0
- package/dist/tftp/client.js +345 -0
- package/dist/tftp/index.js +284 -0
- package/dist/tftp/server.js +731 -0
- package/dist/uboot/index.js +45 -0
- package/dist/ui/assets/index-BlnLVmbt.js +374 -0
- package/dist/ui/assets/index-xMbarYXA.css +32 -0
- package/dist/ui/index.html +21 -0
- package/dist/utils/network.js +150 -0
- package/dist/utils/platform.js +83 -0
- package/dist/utils/port-check.js +153 -0
- package/dist/utils/user-prompt.js +139 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @sstar/embedlink_agent
|
|
2
|
+
|
|
3
|
+
EmbedLink Agent - 为嵌入式开发提供板卡硬件访问服务的 gRPC 服务器。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **板卡 UART 通信**: 支持多会话串口管理和数据传输
|
|
8
|
+
- **文件传输**: 支持 Host 与 Agent 之间的双向文件传输
|
|
9
|
+
- **固件烧录**: 支持 ISP 方式的固件恢复和升级
|
|
10
|
+
- **SSH 隧道**: 支持 SSH 远程访问和端口转发
|
|
11
|
+
- **TFTP 服务**: 内置 TFTP 服务器用于镜像传输
|
|
12
|
+
- **NFS 同步**: 支持 NFS 文件系统同步
|
|
13
|
+
- **Web 管理界面**: 内置 Web UI 用于配置和监控
|
|
14
|
+
- **插件系统**: 支持通过插件扩展功能
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 全局安装
|
|
20
|
+
npm install -g @sstar/embedlink_agent
|
|
21
|
+
|
|
22
|
+
# 或使用 pnpm
|
|
23
|
+
pnpm add -g @sstar/embedlink_agent
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 使用
|
|
27
|
+
|
|
28
|
+
### 启动 Agent
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 直接启动
|
|
32
|
+
embedlink
|
|
33
|
+
|
|
34
|
+
# 带调试信息启动
|
|
35
|
+
embedlink -d
|
|
36
|
+
|
|
37
|
+
# 启动但不自动打开浏览器
|
|
38
|
+
embedlink --no-open
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 环境变量配置
|
|
42
|
+
|
|
43
|
+
Agent 支持通过环境变量进行配置:
|
|
44
|
+
|
|
45
|
+
| 变量名 | 说明 | 默认值 |
|
|
46
|
+
|--------|------|--------|
|
|
47
|
+
| `EMBEDLINK_GRPC_PORT` | gRPC 服务器端口 | `10000` |
|
|
48
|
+
| `EMBEDLINK_WEB_HOST` | Web 服务器主机地址 | `0.0.0.0` |
|
|
49
|
+
| `EMBEDLINK_WEB_PORT` | Web 服务器端口 | `8080` |
|
|
50
|
+
| `EMBEDLINK_TFTP_DIR` | TFTP 文件服务目录 | `~/.local/embed_link/agent_files` |
|
|
51
|
+
| `EMBEDLINK_TFTP_TTL_HOURS` | TFTP 文件保存时间(小时) | `24` |
|
|
52
|
+
| `EMBEDLINK_BOARD_UART_PORT` | 板卡串口设备路径 | 自动检测 |
|
|
53
|
+
| `EMBEDLINK_BOARD_UART_BAUD` | 板卡串口波特率 | `115200` |
|
|
54
|
+
| `EMBEDLINK_LOG_LEVEL` | 日志级别 | `DEBUG` |
|
|
55
|
+
|
|
56
|
+
### 配置文件
|
|
57
|
+
|
|
58
|
+
Agent 启动后会创建配置文件:`~/.config/embed_link/agent.json`
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"agent": {
|
|
63
|
+
"id": "agent",
|
|
64
|
+
"version": "0.1.0",
|
|
65
|
+
"pairCode": "xxx-xxx-xxx",
|
|
66
|
+
"startupTime": "2024-01-01T00:00:00.000Z"
|
|
67
|
+
},
|
|
68
|
+
"endpoint": {
|
|
69
|
+
"grpc": {
|
|
70
|
+
"host": "127.0.0.1",
|
|
71
|
+
"port": 10000,
|
|
72
|
+
"scheme": "grpc"
|
|
73
|
+
},
|
|
74
|
+
"web": {
|
|
75
|
+
"host": "127.0.0.1",
|
|
76
|
+
"port": 8080
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Web 管理界面
|
|
83
|
+
|
|
84
|
+
Agent 启动后会自动打开浏览器访问 Web 管理界面(可通过 `--no-open` 禁用)。
|
|
85
|
+
|
|
86
|
+
默认地址:`http://127.0.0.1:8080`
|
|
87
|
+
|
|
88
|
+
## 与 Host 配合使用
|
|
89
|
+
|
|
90
|
+
Agent 需要与 [@sstar/embedlink_host](../host/) 配合使用才能完整发挥功能:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# 终端 1: 启动 Agent
|
|
94
|
+
embedlink
|
|
95
|
+
|
|
96
|
+
# 终端 2: 启动 Host
|
|
97
|
+
npx @sstar/embedlink_host
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 系统要求
|
|
101
|
+
|
|
102
|
+
- Node.js >= 18.0.0
|
|
103
|
+
- 支持 macOS、Linux、Windows
|
|
104
|
+
|
|
105
|
+
## 许可证
|
|
106
|
+
|
|
107
|
+
MIT
|
package/dist/.platform
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linux-x64
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'embed_link');
|
|
6
|
+
const DOC_DIR = process.env.EMBEDLINK_DOCS_DIR || CONFIG_DIR;
|
|
7
|
+
const DEFAULT_DOC_DIR = process.env.EMBEDLINK_DOCS_DEFAULT_DIR ||
|
|
8
|
+
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../resources/docs');
|
|
9
|
+
const DOC_FILES = {
|
|
10
|
+
'board-interaction': 'board-interaction.md',
|
|
11
|
+
'tftp-transfer': 'tftp-transfer-guide.md',
|
|
12
|
+
'nfs-mount': 'nfs-mount-guide.md',
|
|
13
|
+
'firmware-upgrade': 'firmware-upgrade.md',
|
|
14
|
+
};
|
|
15
|
+
export function getDocsDir() {
|
|
16
|
+
return DOC_DIR;
|
|
17
|
+
}
|
|
18
|
+
export function getDefaultDocsDir() {
|
|
19
|
+
return DEFAULT_DOC_DIR;
|
|
20
|
+
}
|
|
21
|
+
async function resolveDefaultDoc(kind, fallback) {
|
|
22
|
+
const bundledPath = path.join(DEFAULT_DOC_DIR, DOC_FILES[kind]);
|
|
23
|
+
try {
|
|
24
|
+
const buf = await fs.readFile(bundledPath, 'utf8');
|
|
25
|
+
return { content: buf, format: 'markdown' };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// ignore and fallback to built-in template
|
|
29
|
+
}
|
|
30
|
+
if (fallback)
|
|
31
|
+
return { content: fallback.content, format: fallback.format };
|
|
32
|
+
return { content: '', format: 'markdown' };
|
|
33
|
+
}
|
|
34
|
+
export async function readDoc(kind, fallback) {
|
|
35
|
+
const file = path.join(DOC_DIR, DOC_FILES[kind]);
|
|
36
|
+
try {
|
|
37
|
+
const buf = await fs.readFile(file, 'utf8');
|
|
38
|
+
return { content: buf, format: 'markdown', path: file };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const def = await resolveDefaultDoc(kind, fallback);
|
|
42
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
43
|
+
await fs.writeFile(file, def.content ?? '', 'utf8');
|
|
44
|
+
return { content: def.content, format: def.format, path: file };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function writeDoc(kind, content) {
|
|
48
|
+
const file = path.join(DOC_DIR, DOC_FILES[kind]);
|
|
49
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
50
|
+
await fs.writeFile(file, content ?? '', 'utf8');
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
export async function resetDoc(kind, fallback) {
|
|
54
|
+
const file = path.join(DOC_DIR, DOC_FILES[kind]);
|
|
55
|
+
const def = await resolveDefaultDoc(kind, fallback);
|
|
56
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
57
|
+
await fs.writeFile(file, def.content ?? '', 'utf8');
|
|
58
|
+
return file;
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readDoc, writeDoc } from './docs.js';
|
|
2
|
+
export async function loadBoardNotes(_cfg, _board, _section) {
|
|
3
|
+
const doc = await readDoc('board-interaction');
|
|
4
|
+
return { content: doc.content, format: doc.format };
|
|
5
|
+
}
|
|
6
|
+
export async function loadBoardNotesFromFile() {
|
|
7
|
+
return await readDoc('board-interaction');
|
|
8
|
+
}
|
|
9
|
+
export async function saveBoardNotes(content) {
|
|
10
|
+
return writeDoc('board-interaction', content);
|
|
11
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const DEFAULT_MAX_LINES = 5000;
|
|
2
|
+
const HARD_MAX_LINES = 50000;
|
|
3
|
+
const histories = new Map();
|
|
4
|
+
function normalizeSessionId(sessionId) {
|
|
5
|
+
return (sessionId || '').trim();
|
|
6
|
+
}
|
|
7
|
+
function clampMaxLines(maxLines) {
|
|
8
|
+
if (!Number.isFinite(maxLines))
|
|
9
|
+
return DEFAULT_MAX_LINES;
|
|
10
|
+
const n = Math.floor(maxLines);
|
|
11
|
+
if (n <= 0)
|
|
12
|
+
return 0;
|
|
13
|
+
return Math.min(n, HARD_MAX_LINES);
|
|
14
|
+
}
|
|
15
|
+
function getOrCreate(sessionId) {
|
|
16
|
+
const id = normalizeSessionId(sessionId);
|
|
17
|
+
let st = histories.get(id);
|
|
18
|
+
if (!st) {
|
|
19
|
+
st = {
|
|
20
|
+
seq: 0,
|
|
21
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
22
|
+
lineBuffer: '',
|
|
23
|
+
lines: [],
|
|
24
|
+
};
|
|
25
|
+
histories.set(id, st);
|
|
26
|
+
}
|
|
27
|
+
return st;
|
|
28
|
+
}
|
|
29
|
+
export function setBoardUartHistoryMaxLines(sessionId, maxLines) {
|
|
30
|
+
const id = normalizeSessionId(sessionId);
|
|
31
|
+
if (!id)
|
|
32
|
+
return;
|
|
33
|
+
const target = clampMaxLines(maxLines);
|
|
34
|
+
if (target <= 0)
|
|
35
|
+
return;
|
|
36
|
+
const st = getOrCreate(id);
|
|
37
|
+
st.maxLines = target;
|
|
38
|
+
while (st.lines.length > st.maxLines) {
|
|
39
|
+
st.lines.shift();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function appendBoardUartHistoryChunk(sessionId, chunk) {
|
|
43
|
+
const id = normalizeSessionId(sessionId);
|
|
44
|
+
if (!id)
|
|
45
|
+
return;
|
|
46
|
+
if (!Buffer.isBuffer(chunk) || chunk.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const st = getOrCreate(id);
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
let text = '';
|
|
51
|
+
try {
|
|
52
|
+
text = chunk.toString('utf8');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
text = '';
|
|
56
|
+
}
|
|
57
|
+
if (!text)
|
|
58
|
+
return;
|
|
59
|
+
const parts = (st.lineBuffer + text).split('\n');
|
|
60
|
+
st.lineBuffer = parts.pop() || '';
|
|
61
|
+
for (const line of parts) {
|
|
62
|
+
st.seq += 1;
|
|
63
|
+
st.lines.push({ seq: st.seq, receivedAtMs: now, text: line });
|
|
64
|
+
}
|
|
65
|
+
while (st.lines.length > st.maxLines) {
|
|
66
|
+
st.lines.shift();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function getBoardUartHistoryLines(sessionId, maxLines) {
|
|
70
|
+
const id = normalizeSessionId(sessionId);
|
|
71
|
+
if (!id)
|
|
72
|
+
return [];
|
|
73
|
+
const st = histories.get(id);
|
|
74
|
+
if (!st)
|
|
75
|
+
return [];
|
|
76
|
+
const limit = clampMaxLines(maxLines);
|
|
77
|
+
if (limit <= 0)
|
|
78
|
+
return [];
|
|
79
|
+
const start = Math.max(0, st.lines.length - limit);
|
|
80
|
+
return st.lines.slice(start).map((it) => ({ receivedAtMs: it.receivedAtMs, text: it.text }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getSharedBoardUartManager } from './manager.js';
|
|
2
|
+
import { addSessionConsumer, buildSessionId, closeSession, hasSession, openManualSession, removeSessionConsumer, writeToSession, } from './sessions.js';
|
|
3
|
+
const manager = getSharedBoardUartManager();
|
|
4
|
+
export async function boardUartWrite(params) {
|
|
5
|
+
if (params.mock) {
|
|
6
|
+
return manager.writeOnce({
|
|
7
|
+
port: params.port,
|
|
8
|
+
baud: params.baud,
|
|
9
|
+
data: params.data,
|
|
10
|
+
mock: params.mock,
|
|
11
|
+
timeoutMs: params.timeoutMs,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const sessionId = buildSessionId(params.port, params.baud);
|
|
15
|
+
if (hasSession(sessionId)) {
|
|
16
|
+
return writeToSession(sessionId, params.data);
|
|
17
|
+
}
|
|
18
|
+
// 对尚未存在的会话使用临时会话写入,写完即关闭,避免长时间占用端口锁
|
|
19
|
+
const session = await openManualSession({ port: params.port, baud: params.baud });
|
|
20
|
+
try {
|
|
21
|
+
return await writeToSession(session.id, params.data);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
try {
|
|
25
|
+
await closeSession(session.id);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// ignore close failure
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function listBoardUartPorts() {
|
|
33
|
+
return manager.list();
|
|
34
|
+
}
|
|
35
|
+
export async function boardUartStatus() {
|
|
36
|
+
const s = await manager.status();
|
|
37
|
+
return {
|
|
38
|
+
disabled: s.disabled,
|
|
39
|
+
hasModule: s.hasModule,
|
|
40
|
+
ports: s.ports,
|
|
41
|
+
attachedTools: s.attachedTools,
|
|
42
|
+
status: s.status,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function openBoardUartStream(params) {
|
|
46
|
+
const consumerId = `stream-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
47
|
+
const sessionId = buildSessionId(params.port, params.baud);
|
|
48
|
+
const session = await addSessionConsumer({
|
|
49
|
+
consumerId,
|
|
50
|
+
sessionId,
|
|
51
|
+
port: params.port,
|
|
52
|
+
baud: params.baud,
|
|
53
|
+
onData: params.onData,
|
|
54
|
+
onError: params.onError,
|
|
55
|
+
});
|
|
56
|
+
let closed = false;
|
|
57
|
+
return {
|
|
58
|
+
write: (data) => writeToSession(session.id, data),
|
|
59
|
+
close: async () => {
|
|
60
|
+
if (closed)
|
|
61
|
+
return;
|
|
62
|
+
closed = true;
|
|
63
|
+
removeSessionConsumer(session.id, consumerId);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { DefaultTimeouts, ErrorCodes, error } from '../core/errors.js';
|
|
5
|
+
function envDisabled() {
|
|
6
|
+
return process.env.DISABLE_BOARD_UART === '1' || process.env.EMBEDLINK_DISABLE_BOARD_UART === '1';
|
|
7
|
+
}
|
|
8
|
+
function isWindows() {
|
|
9
|
+
return os.platform() === 'win32';
|
|
10
|
+
}
|
|
11
|
+
function normalizePortPath(p) {
|
|
12
|
+
if (!p)
|
|
13
|
+
return p;
|
|
14
|
+
if (isWindows()) {
|
|
15
|
+
const m = /^(com)(\d+)$/i.exec(p);
|
|
16
|
+
if (m)
|
|
17
|
+
return `COM${m[2]}`;
|
|
18
|
+
}
|
|
19
|
+
return p;
|
|
20
|
+
}
|
|
21
|
+
export class BoardUartManager {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.mod = null;
|
|
24
|
+
this.modTried = false;
|
|
25
|
+
this.runtimeDisabled = false;
|
|
26
|
+
}
|
|
27
|
+
setRuntimeDisabled(disabled) {
|
|
28
|
+
this.runtimeDisabled = !!disabled;
|
|
29
|
+
}
|
|
30
|
+
isRuntimeDisabled() {
|
|
31
|
+
return this.runtimeDisabled;
|
|
32
|
+
}
|
|
33
|
+
isDisabled() {
|
|
34
|
+
return envDisabled() || this.runtimeDisabled;
|
|
35
|
+
}
|
|
36
|
+
async loadModule() {
|
|
37
|
+
if (this.modTried)
|
|
38
|
+
return this.mod;
|
|
39
|
+
this.modTried = true;
|
|
40
|
+
try {
|
|
41
|
+
const m = await import('serialport');
|
|
42
|
+
if (m && (m.SerialPort || (m.default && m.default.SerialPort))) {
|
|
43
|
+
this.mod = { SerialPort: m.SerialPort || m.default.SerialPort };
|
|
44
|
+
}
|
|
45
|
+
else if (m) {
|
|
46
|
+
this.mod = { SerialPort: m.SerialPort };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
this.mod = null;
|
|
51
|
+
}
|
|
52
|
+
return this.mod;
|
|
53
|
+
}
|
|
54
|
+
async isAvailable() {
|
|
55
|
+
if (this.isDisabled())
|
|
56
|
+
return false;
|
|
57
|
+
const m = await this.loadModule();
|
|
58
|
+
return !!m;
|
|
59
|
+
}
|
|
60
|
+
async list() {
|
|
61
|
+
if (!(await this.isAvailable()))
|
|
62
|
+
return [];
|
|
63
|
+
let ports = [];
|
|
64
|
+
try {
|
|
65
|
+
const m = (await this.loadModule());
|
|
66
|
+
// serialport 的 list() 在某些环境会卡死(例如没有权限/驱动异常)。
|
|
67
|
+
// 这里必须保证快速返回,不能把整个 Agent/Host 链路拖死。
|
|
68
|
+
const listPromise = Promise.resolve(m.SerialPort?.list?.());
|
|
69
|
+
const list = await new Promise((resolve) => {
|
|
70
|
+
const t = setTimeout(() => resolve([]), 800);
|
|
71
|
+
listPromise
|
|
72
|
+
.then((v) => {
|
|
73
|
+
clearTimeout(t);
|
|
74
|
+
resolve(Array.isArray(v) ? v : []);
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {
|
|
77
|
+
clearTimeout(t);
|
|
78
|
+
resolve([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
if (Array.isArray(list)) {
|
|
82
|
+
ports = list.map((p) => ({
|
|
83
|
+
path: p.path || p.comName || p.friendlyName || String(p),
|
|
84
|
+
friendlyName: p.friendlyName || p.manufacturer,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
// 在非 Windows 环境下,追加 /dev/pts 下的伪终端,方便配合 socat 做虚拟串口联调
|
|
92
|
+
if (!isWindows()) {
|
|
93
|
+
ports = this.appendPseudoTtys(ports);
|
|
94
|
+
}
|
|
95
|
+
return ports;
|
|
96
|
+
}
|
|
97
|
+
async status() {
|
|
98
|
+
const disabled = this.isDisabled();
|
|
99
|
+
const mod = disabled ? null : await this.loadModule();
|
|
100
|
+
const hasModule = !!mod;
|
|
101
|
+
const ports = hasModule && !disabled ? await this.list() : [];
|
|
102
|
+
return { disabled, hasModule, ports };
|
|
103
|
+
}
|
|
104
|
+
appendPseudoTtys(existing) {
|
|
105
|
+
const out = [...existing];
|
|
106
|
+
const seen = new Set(out.map((p) => p.path));
|
|
107
|
+
const base = '/dev/pts';
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(base))
|
|
110
|
+
return out;
|
|
111
|
+
const names = fs.readdirSync(base);
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
// /dev/pts/N,过滤非数字项(如 ptmx)
|
|
114
|
+
if (!/^\d+$/.test(name))
|
|
115
|
+
continue;
|
|
116
|
+
const full = path.join(base, name);
|
|
117
|
+
if (seen.has(full))
|
|
118
|
+
continue;
|
|
119
|
+
try {
|
|
120
|
+
const st = fs.statSync(full);
|
|
121
|
+
if (!st.isCharacterDevice())
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
out.push({
|
|
128
|
+
path: full,
|
|
129
|
+
friendlyName: 'virtual-tty (pts)',
|
|
130
|
+
});
|
|
131
|
+
seen.add(full);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// 失败时忽略,保持原有端口列表
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
async writeOnce(params) {
|
|
140
|
+
const { port, baud, data } = params;
|
|
141
|
+
const timeoutMs = params.timeoutMs ?? DefaultTimeouts.boardUart.write;
|
|
142
|
+
if (params.mock) {
|
|
143
|
+
throw error(ErrorCodes.EL_INVALID_PARAMS, 'mock is disabled');
|
|
144
|
+
}
|
|
145
|
+
if (this.isDisabled()) {
|
|
146
|
+
throw error(ErrorCodes.EL_BOARD_UART_DISABLED, 'board-uart is disabled by environment');
|
|
147
|
+
}
|
|
148
|
+
const mod = await this.loadModule();
|
|
149
|
+
if (!mod) {
|
|
150
|
+
throw error(ErrorCodes.EL_BOARD_UART_MODULE_MISSING, 'serialport module is not installed for board-uart');
|
|
151
|
+
}
|
|
152
|
+
const ports = await this.list();
|
|
153
|
+
if (!ports.length) {
|
|
154
|
+
throw error(ErrorCodes.EL_BOARD_UART_NO_DEVICE, 'no board-uart device detected');
|
|
155
|
+
}
|
|
156
|
+
const path = normalizePortPath(port);
|
|
157
|
+
return await new Promise((resolve, reject) => {
|
|
158
|
+
let settled = false;
|
|
159
|
+
const t = setTimeout(() => {
|
|
160
|
+
if (settled)
|
|
161
|
+
return;
|
|
162
|
+
settled = true;
|
|
163
|
+
reject(error(ErrorCodes.EL_BOARD_UART_WRITE_FAILED, 'board-uart write timeout'));
|
|
164
|
+
}, timeoutMs);
|
|
165
|
+
try {
|
|
166
|
+
const sp = new mod.SerialPort({ path, baudRate: baud, autoOpen: false });
|
|
167
|
+
sp.open((openErr) => {
|
|
168
|
+
if (settled)
|
|
169
|
+
return;
|
|
170
|
+
if (openErr) {
|
|
171
|
+
settled = true;
|
|
172
|
+
clearTimeout(t);
|
|
173
|
+
return reject(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, String(openErr)));
|
|
174
|
+
}
|
|
175
|
+
sp.write(data, (wErr) => {
|
|
176
|
+
if (settled)
|
|
177
|
+
return;
|
|
178
|
+
if (wErr) {
|
|
179
|
+
settled = true;
|
|
180
|
+
clearTimeout(t);
|
|
181
|
+
sp.close?.(() => { });
|
|
182
|
+
return reject(error(ErrorCodes.EL_BOARD_UART_WRITE_FAILED, String(wErr)));
|
|
183
|
+
}
|
|
184
|
+
sp.drain?.(() => {
|
|
185
|
+
sp.close?.(() => {
|
|
186
|
+
if (settled)
|
|
187
|
+
return;
|
|
188
|
+
settled = true;
|
|
189
|
+
clearTimeout(t);
|
|
190
|
+
resolve(data.length);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
sp.on?.('error', (e) => {
|
|
196
|
+
if (settled)
|
|
197
|
+
return;
|
|
198
|
+
settled = true;
|
|
199
|
+
clearTimeout(t);
|
|
200
|
+
reject(error(ErrorCodes.EL_BOARD_UART_WRITE_FAILED, String(e)));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
if (settled)
|
|
205
|
+
return;
|
|
206
|
+
settled = true;
|
|
207
|
+
clearTimeout(t);
|
|
208
|
+
reject(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, String(e)));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async openStream(params) {
|
|
213
|
+
const { port, baud, onData, onError } = params;
|
|
214
|
+
if (this.isDisabled()) {
|
|
215
|
+
throw error(ErrorCodes.EL_BOARD_UART_DISABLED, 'board-uart is disabled by environment');
|
|
216
|
+
}
|
|
217
|
+
const mod = await this.loadModule();
|
|
218
|
+
if (!mod) {
|
|
219
|
+
throw error(ErrorCodes.EL_BOARD_UART_MODULE_MISSING, 'serialport module is not installed for board-uart');
|
|
220
|
+
}
|
|
221
|
+
const ports = await this.list();
|
|
222
|
+
if (!ports.length) {
|
|
223
|
+
throw error(ErrorCodes.EL_BOARD_UART_NO_DEVICE, 'no board-uart device detected');
|
|
224
|
+
}
|
|
225
|
+
const path = normalizePortPath(port);
|
|
226
|
+
return await new Promise((resolve, reject) => {
|
|
227
|
+
const timeoutMs = DefaultTimeouts.boardUart.open;
|
|
228
|
+
let settled = false;
|
|
229
|
+
let opened = false;
|
|
230
|
+
let sp;
|
|
231
|
+
const finishReject = (e) => {
|
|
232
|
+
if (settled)
|
|
233
|
+
return;
|
|
234
|
+
settled = true;
|
|
235
|
+
clearTimeout(t);
|
|
236
|
+
try {
|
|
237
|
+
sp?.close?.(() => { });
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
reject(e);
|
|
241
|
+
};
|
|
242
|
+
const finishResolve = (payload) => {
|
|
243
|
+
if (settled)
|
|
244
|
+
return;
|
|
245
|
+
settled = true;
|
|
246
|
+
clearTimeout(t);
|
|
247
|
+
resolve(payload);
|
|
248
|
+
};
|
|
249
|
+
const t = setTimeout(() => {
|
|
250
|
+
finishReject(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, `board-uart open timeout after ${timeoutMs} ms`));
|
|
251
|
+
}, timeoutMs);
|
|
252
|
+
try {
|
|
253
|
+
sp = new mod.SerialPort({ path, baudRate: baud, autoOpen: false });
|
|
254
|
+
const handleError = (e) => {
|
|
255
|
+
try {
|
|
256
|
+
onError?.(e);
|
|
257
|
+
}
|
|
258
|
+
catch { }
|
|
259
|
+
if (!opened) {
|
|
260
|
+
finishReject(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, String(e)));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
sp.on?.('error', handleError);
|
|
264
|
+
sp.on?.('data', (chunk) => {
|
|
265
|
+
try {
|
|
266
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
267
|
+
onData(buf);
|
|
268
|
+
}
|
|
269
|
+
catch { }
|
|
270
|
+
});
|
|
271
|
+
sp.open((openErr) => {
|
|
272
|
+
if (openErr) {
|
|
273
|
+
return finishReject(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, String(openErr)));
|
|
274
|
+
}
|
|
275
|
+
opened = true;
|
|
276
|
+
finishResolve({
|
|
277
|
+
write: (data) => new Promise((resolveWrite, rejectWrite) => {
|
|
278
|
+
try {
|
|
279
|
+
sp.write(data, (wErr) => {
|
|
280
|
+
if (wErr) {
|
|
281
|
+
return rejectWrite(error(ErrorCodes.EL_BOARD_UART_WRITE_FAILED, String(wErr)));
|
|
282
|
+
}
|
|
283
|
+
sp.drain?.(() => resolveWrite(data.length));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
rejectWrite(error(ErrorCodes.EL_BOARD_UART_WRITE_FAILED, String(e)));
|
|
288
|
+
}
|
|
289
|
+
}),
|
|
290
|
+
close: () => new Promise((res) => {
|
|
291
|
+
try {
|
|
292
|
+
sp.close?.(() => res());
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
res();
|
|
296
|
+
}
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
finishReject(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, String(e)));
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
let sharedManager = null;
|
|
308
|
+
export function getSharedBoardUartManager() {
|
|
309
|
+
if (!sharedManager) {
|
|
310
|
+
sharedManager = new BoardUartManager();
|
|
311
|
+
}
|
|
312
|
+
return sharedManager;
|
|
313
|
+
}
|