@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
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DefaultTimeouts, ErrorCodes, error } from '../core/errors.js';
|
|
4
|
+
import { getSharedBoardUartManager } from './manager.js';
|
|
5
|
+
import { appendBoardUartHistoryChunk } from './history.js';
|
|
6
|
+
const manager = getSharedBoardUartManager();
|
|
7
|
+
const sessions = new Map();
|
|
8
|
+
let defaultSessionId = null;
|
|
9
|
+
let boardUartPortHint;
|
|
10
|
+
let boardUartBaudHint;
|
|
11
|
+
const MAX_BUFFER_BYTES = 1024 * 1024;
|
|
12
|
+
let hardwareSuspendedOwner = null;
|
|
13
|
+
export function setBoardUartHardwareSuspended(owner) {
|
|
14
|
+
const v = (owner || '').trim();
|
|
15
|
+
hardwareSuspendedOwner = v ? v : null;
|
|
16
|
+
}
|
|
17
|
+
function assertHardwareNotSuspended() {
|
|
18
|
+
if (!hardwareSuspendedOwner)
|
|
19
|
+
return;
|
|
20
|
+
throw error(ErrorCodes.EL_BOARD_UART_SUSPENDED, `board-uart hardware suspended by ${hardwareSuspendedOwner}`);
|
|
21
|
+
}
|
|
22
|
+
function getTimestamp() {
|
|
23
|
+
const d = new Date();
|
|
24
|
+
const time = d.toLocaleTimeString('en-US', {
|
|
25
|
+
hour12: false,
|
|
26
|
+
hour: '2-digit',
|
|
27
|
+
minute: '2-digit',
|
|
28
|
+
second: '2-digit',
|
|
29
|
+
});
|
|
30
|
+
const ms = d.getMilliseconds().toString().padStart(3, '0');
|
|
31
|
+
return `[${time}.${ms}] `;
|
|
32
|
+
}
|
|
33
|
+
export function setDefaultBoardUartHint(port, baud) {
|
|
34
|
+
const p = (port || '').trim();
|
|
35
|
+
// Avoid hanging on invalid default port hints.
|
|
36
|
+
// On non-Windows platforms we only accept /dev/* style paths.
|
|
37
|
+
if (p) {
|
|
38
|
+
if (process.platform !== 'win32' && !p.startsWith('/dev/')) {
|
|
39
|
+
boardUartPortHint = undefined;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
boardUartPortHint = p;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
boardUartPortHint = undefined;
|
|
47
|
+
}
|
|
48
|
+
boardUartBaudHint = baud;
|
|
49
|
+
}
|
|
50
|
+
async function pickDefaultPortAndBaud() {
|
|
51
|
+
const baud = boardUartBaudHint && boardUartBaudHint > 0 ? boardUartBaudHint : 115200;
|
|
52
|
+
if (boardUartPortHint) {
|
|
53
|
+
return { port: boardUartPortHint, baud };
|
|
54
|
+
}
|
|
55
|
+
const ports = await manager.list();
|
|
56
|
+
if (!ports.length) {
|
|
57
|
+
throw error(ErrorCodes.EL_BOARD_UART_NO_DEVICE, 'no board-uart device detected for default session');
|
|
58
|
+
}
|
|
59
|
+
return { port: ports[0].path, baud };
|
|
60
|
+
}
|
|
61
|
+
function makeSessionId(port, baud) {
|
|
62
|
+
return `sess-${port}-${baud}`;
|
|
63
|
+
}
|
|
64
|
+
export function buildSessionId(port, baud) {
|
|
65
|
+
return makeSessionId(port, baud);
|
|
66
|
+
}
|
|
67
|
+
function handleIncoming(state, chunk) {
|
|
68
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
69
|
+
appendBoardUartHistoryChunk(state.id, buf);
|
|
70
|
+
// Logging logic
|
|
71
|
+
if (state.recording && state.logStream) {
|
|
72
|
+
try {
|
|
73
|
+
const text = chunk.toString('utf8');
|
|
74
|
+
const lines = (state.logLineBuffer + text).split('\n');
|
|
75
|
+
state.logLineBuffer = lines.pop() || '';
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
// Remove \r for cleaner log file if desired, or keep it. keeping for now.
|
|
78
|
+
state.logStream.write(getTimestamp() + line + '\n');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// ignore log errors
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
state.buffer.push(buf);
|
|
86
|
+
state.bufferBytes += buf.length;
|
|
87
|
+
// 丢弃最旧的数据,防止无限增长
|
|
88
|
+
while (state.bufferBytes > MAX_BUFFER_BYTES && state.buffer.length > 0) {
|
|
89
|
+
const old = state.buffer.shift();
|
|
90
|
+
state.bufferBytes -= old.length;
|
|
91
|
+
}
|
|
92
|
+
// 通知等待的读取
|
|
93
|
+
const waiters = Array.from(state.waiters);
|
|
94
|
+
for (const fn of waiters) {
|
|
95
|
+
try {
|
|
96
|
+
fn();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignore waiter errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 广播给订阅者
|
|
103
|
+
for (const consumer of state.consumers.values()) {
|
|
104
|
+
try {
|
|
105
|
+
consumer.onData(buf);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// ignore consumer errors
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function handleSessionError(state, e) {
|
|
113
|
+
for (const consumer of state.consumers.values()) {
|
|
114
|
+
try {
|
|
115
|
+
consumer.onError?.(e);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// ignore consumer errors
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function ensureSessionHardwareOpen(state) {
|
|
123
|
+
if (state.hardwareOpen)
|
|
124
|
+
return;
|
|
125
|
+
assertHardwareNotSuspended();
|
|
126
|
+
const stream = await manager.openStream({
|
|
127
|
+
port: state.port,
|
|
128
|
+
baud: state.baud,
|
|
129
|
+
onData(chunk) {
|
|
130
|
+
handleIncoming(state, chunk);
|
|
131
|
+
},
|
|
132
|
+
onError(e) {
|
|
133
|
+
handleSessionError(state, e);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
state.write = stream.write;
|
|
137
|
+
state.close = stream.close;
|
|
138
|
+
state.hardwareOpen = true;
|
|
139
|
+
}
|
|
140
|
+
async function ensureSession(id, port, baud, opts) {
|
|
141
|
+
const existing = sessions.get(id);
|
|
142
|
+
if (existing)
|
|
143
|
+
return existing;
|
|
144
|
+
assertHardwareNotSuspended();
|
|
145
|
+
let state;
|
|
146
|
+
const stream = await manager.openStream({
|
|
147
|
+
port,
|
|
148
|
+
baud,
|
|
149
|
+
onData(chunk) {
|
|
150
|
+
handleIncoming(state, chunk);
|
|
151
|
+
},
|
|
152
|
+
onError(e) {
|
|
153
|
+
handleSessionError(state, e);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
state = {
|
|
157
|
+
id,
|
|
158
|
+
label: opts.label || port,
|
|
159
|
+
role: opts.role || 'primary',
|
|
160
|
+
isDefault: opts.isDefault,
|
|
161
|
+
canWrite: opts.canWrite ?? true,
|
|
162
|
+
port,
|
|
163
|
+
baud,
|
|
164
|
+
recording: false,
|
|
165
|
+
logLineBuffer: '',
|
|
166
|
+
hardwareOpen: true,
|
|
167
|
+
buffer: [],
|
|
168
|
+
bufferBytes: 0,
|
|
169
|
+
reading: false,
|
|
170
|
+
waiters: new Set(),
|
|
171
|
+
consumers: new Map(),
|
|
172
|
+
write: stream.write,
|
|
173
|
+
close: stream.close,
|
|
174
|
+
};
|
|
175
|
+
sessions.set(id, state);
|
|
176
|
+
if (opts.isDefault) {
|
|
177
|
+
defaultSessionId = id;
|
|
178
|
+
}
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
async function getDefaultSession() {
|
|
182
|
+
if (defaultSessionId) {
|
|
183
|
+
const s = sessions.get(defaultSessionId);
|
|
184
|
+
if (s)
|
|
185
|
+
return s;
|
|
186
|
+
}
|
|
187
|
+
const { port, baud } = await pickDefaultPortAndBaud();
|
|
188
|
+
const id = makeSessionId(port, baud);
|
|
189
|
+
return ensureSession(id, port, baud, {
|
|
190
|
+
isDefault: true,
|
|
191
|
+
label: 'default',
|
|
192
|
+
role: 'primary',
|
|
193
|
+
canWrite: true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function getSessionOrThrow(sessionId) {
|
|
197
|
+
const id = sessionId && sessionId.trim().length > 0 ? sessionId : defaultSessionId;
|
|
198
|
+
if (!id) {
|
|
199
|
+
throw error(ErrorCodes.EL_BOARD_UART_SESSION_NOT_FOUND, 'no default board-uart session available (no port configured)');
|
|
200
|
+
}
|
|
201
|
+
const s = sessions.get(id);
|
|
202
|
+
if (!s) {
|
|
203
|
+
// 如果是查找默认会话且会话不存在,返回临时不可用错误而不是永久性错误
|
|
204
|
+
if (!sessionId || sessionId === 'default') {
|
|
205
|
+
throw error(ErrorCodes.EL_BOARD_UART_SESSION_TEMPORARILY_UNAVAILABLE, `default session temporarily unavailable, please retry: ${id}`);
|
|
206
|
+
}
|
|
207
|
+
throw error(ErrorCodes.EL_BOARD_UART_SESSION_NOT_FOUND, `board-uart session not found: ${id}`);
|
|
208
|
+
}
|
|
209
|
+
return s;
|
|
210
|
+
}
|
|
211
|
+
function toSessionInfo(s) {
|
|
212
|
+
return {
|
|
213
|
+
id: s.id,
|
|
214
|
+
label: s.label,
|
|
215
|
+
role: s.role,
|
|
216
|
+
isDefault: s.isDefault,
|
|
217
|
+
canWrite: s.canWrite,
|
|
218
|
+
port: s.port,
|
|
219
|
+
baud: s.baud,
|
|
220
|
+
recording: s.recording,
|
|
221
|
+
logPath: s.logPath,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
export function listExistingBoardUartSessions() {
|
|
225
|
+
const all = [];
|
|
226
|
+
for (const s of sessions.values()) {
|
|
227
|
+
all.push(toSessionInfo(s));
|
|
228
|
+
}
|
|
229
|
+
return all;
|
|
230
|
+
}
|
|
231
|
+
export async function listBoardUartSessions() {
|
|
232
|
+
const all = listExistingBoardUartSessions();
|
|
233
|
+
if (!all.length) {
|
|
234
|
+
try {
|
|
235
|
+
const s = await getDefaultSession();
|
|
236
|
+
all.push(toSessionInfo(s));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// 没有串口设备时返回空列表
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return all;
|
|
243
|
+
}
|
|
244
|
+
export async function writeToSession(sessionId, data) {
|
|
245
|
+
let s;
|
|
246
|
+
try {
|
|
247
|
+
s = sessionId ? getSessionOrThrow(sessionId) : await getDefaultSession();
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
// 如果是默认会话临时不可用,尝试重新创建
|
|
251
|
+
if (e.code === 'EL_BOARD_UART_SESSION_TEMPORARILY_UNAVAILABLE') {
|
|
252
|
+
s = await getDefaultSession();
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
await ensureSessionHardwareOpen(s);
|
|
259
|
+
if (!s.canWrite) {
|
|
260
|
+
throw error(ErrorCodes.EL_BOARD_UART_READONLY_SESSION, `board-uart session is read-only: ${s.id}`);
|
|
261
|
+
}
|
|
262
|
+
return s.write(data);
|
|
263
|
+
}
|
|
264
|
+
export async function readFromSession(params) {
|
|
265
|
+
let s;
|
|
266
|
+
try {
|
|
267
|
+
s = params.sessionId ? getSessionOrThrow(params.sessionId) : await getDefaultSession();
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
// 如果是默认会话临时不可用,尝试重新创建
|
|
271
|
+
if (e.code === 'EL_BOARD_UART_SESSION_TEMPORARILY_UNAVAILABLE') {
|
|
272
|
+
s = await getDefaultSession();
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
throw e;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
await ensureSessionHardwareOpen(s);
|
|
279
|
+
if (s.reading) {
|
|
280
|
+
throw error(ErrorCodes.EL_BOARD_UART_READ_IN_PROGRESS, `board-uart session is already reading: ${s.id}`);
|
|
281
|
+
}
|
|
282
|
+
const maxBytes = params.maxBytes && params.maxBytes > 0 ? params.maxBytes : 8192;
|
|
283
|
+
const quietMs = params.quietMs && params.quietMs > 0 ? params.quietMs : 200;
|
|
284
|
+
const timeoutMs = params.timeoutMs && params.timeoutMs > 0 ? params.timeoutMs : DefaultTimeouts.boardUart.read;
|
|
285
|
+
s.reading = true;
|
|
286
|
+
try {
|
|
287
|
+
const chunks = [];
|
|
288
|
+
let bytes = 0;
|
|
289
|
+
let lastActivity = Date.now();
|
|
290
|
+
const drainFromBuffer = () => {
|
|
291
|
+
while (s.buffer.length && bytes < maxBytes) {
|
|
292
|
+
const chunk = s.buffer.shift();
|
|
293
|
+
s.bufferBytes -= chunk.length;
|
|
294
|
+
const remaining = maxBytes - bytes;
|
|
295
|
+
if (chunk.length <= remaining) {
|
|
296
|
+
chunks.push(chunk);
|
|
297
|
+
bytes += chunk.length;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const part = chunk.subarray(0, remaining);
|
|
301
|
+
const rest = chunk.subarray(remaining);
|
|
302
|
+
chunks.push(part);
|
|
303
|
+
bytes += part.length;
|
|
304
|
+
s.buffer.unshift(rest);
|
|
305
|
+
s.bufferBytes += rest.length;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
drainFromBuffer();
|
|
311
|
+
return await new Promise((resolve) => {
|
|
312
|
+
let finished = false;
|
|
313
|
+
const finish = (timedOut) => {
|
|
314
|
+
if (finished)
|
|
315
|
+
return;
|
|
316
|
+
finished = true;
|
|
317
|
+
clearTimeout(timeoutTimer);
|
|
318
|
+
clearInterval(quietTimer);
|
|
319
|
+
s.waiters.delete(onNewData);
|
|
320
|
+
drainFromBuffer();
|
|
321
|
+
const remains = s.bufferBytes; // 缓冲区剩余字节数
|
|
322
|
+
resolve({ data: Buffer.concat(chunks, bytes), bytes, timedOut, remains });
|
|
323
|
+
};
|
|
324
|
+
const onNewData = () => {
|
|
325
|
+
lastActivity = Date.now();
|
|
326
|
+
drainFromBuffer();
|
|
327
|
+
if (bytes >= maxBytes) {
|
|
328
|
+
finish(false);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
s.waiters.add(onNewData);
|
|
332
|
+
const quietTimer = setInterval(() => {
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
if (now - lastActivity >= quietMs) {
|
|
335
|
+
finish(false);
|
|
336
|
+
}
|
|
337
|
+
}, quietMs);
|
|
338
|
+
const timeoutTimer = setTimeout(() => {
|
|
339
|
+
finish(true);
|
|
340
|
+
}, timeoutMs);
|
|
341
|
+
// 如果初次 drain 已经拿到了足够的数据,尽快返回
|
|
342
|
+
if (bytes >= maxBytes) {
|
|
343
|
+
finish(false);
|
|
344
|
+
}
|
|
345
|
+
else if (bytes > 0) {
|
|
346
|
+
// 有数据但未满,等待 quietMs 结束
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
s.reading = false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
export async function openManualSession(params) {
|
|
355
|
+
if (!params.port || !params.baud) {
|
|
356
|
+
throw error(ErrorCodes.EL_INVALID_PARAMS, 'port/baud are required for manual session');
|
|
357
|
+
}
|
|
358
|
+
const id = makeSessionId(params.port, params.baud);
|
|
359
|
+
const s = await ensureSession(id, params.port, params.baud, {
|
|
360
|
+
isDefault: false,
|
|
361
|
+
label: params.port,
|
|
362
|
+
role: 'manual',
|
|
363
|
+
canWrite: true,
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
id: s.id,
|
|
367
|
+
label: s.label,
|
|
368
|
+
role: s.role,
|
|
369
|
+
isDefault: s.isDefault,
|
|
370
|
+
canWrite: s.canWrite,
|
|
371
|
+
port: s.port,
|
|
372
|
+
baud: s.baud,
|
|
373
|
+
recording: s.recording,
|
|
374
|
+
logPath: s.logPath,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
export async function closeSession(sessionId) {
|
|
378
|
+
const s = getSessionOrThrow(sessionId);
|
|
379
|
+
if (s.isDefault) {
|
|
380
|
+
throw error(ErrorCodes.EL_INVALID_PARAMS, `cannot close default board-uart session via closeSession: ${sessionId}`);
|
|
381
|
+
}
|
|
382
|
+
sessions.delete(sessionId);
|
|
383
|
+
if (defaultSessionId === sessionId) {
|
|
384
|
+
defaultSessionId = null;
|
|
385
|
+
}
|
|
386
|
+
if (s.recording) {
|
|
387
|
+
await stopRecording(sessionId).catch(() => { });
|
|
388
|
+
}
|
|
389
|
+
for (const consumer of s.consumers.values()) {
|
|
390
|
+
try {
|
|
391
|
+
consumer.onError?.(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, 'board-uart session closed'));
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// ignore consumer errors
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
s.consumers.clear();
|
|
398
|
+
try {
|
|
399
|
+
await s.close();
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// ignore close errors
|
|
403
|
+
}
|
|
404
|
+
s.hardwareOpen = false;
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
export async function startRecording(sessionId, logDir) {
|
|
408
|
+
const s = getSessionOrThrow(sessionId);
|
|
409
|
+
if (s.recording)
|
|
410
|
+
return s.logPath || '';
|
|
411
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
412
|
+
const filename = `serial-${s.port.replace(/[^a-zA-Z0-9]/g, '_')}-${timestamp}.log`;
|
|
413
|
+
const fullPath = path.join(logDir, filename);
|
|
414
|
+
await fs.promises.mkdir(logDir, { recursive: true });
|
|
415
|
+
const stream = fs.createWriteStream(fullPath, { flags: 'a' });
|
|
416
|
+
s.recording = true;
|
|
417
|
+
s.logPath = fullPath;
|
|
418
|
+
s.logStream = stream;
|
|
419
|
+
s.logLineBuffer = '';
|
|
420
|
+
return fullPath;
|
|
421
|
+
}
|
|
422
|
+
export async function stopRecording(sessionId) {
|
|
423
|
+
const s = getSessionOrThrow(sessionId);
|
|
424
|
+
if (!s.recording)
|
|
425
|
+
return;
|
|
426
|
+
if (s.logStream) {
|
|
427
|
+
// Flush buffer
|
|
428
|
+
if (s.logLineBuffer) {
|
|
429
|
+
try {
|
|
430
|
+
s.logStream.write(getTimestamp() + s.logLineBuffer + '\n');
|
|
431
|
+
}
|
|
432
|
+
catch { }
|
|
433
|
+
}
|
|
434
|
+
s.logStream.end();
|
|
435
|
+
s.logStream = undefined;
|
|
436
|
+
}
|
|
437
|
+
s.recording = false;
|
|
438
|
+
s.logPath = undefined;
|
|
439
|
+
}
|
|
440
|
+
export async function addSessionConsumer(params) {
|
|
441
|
+
if (!params.consumerId) {
|
|
442
|
+
throw error(ErrorCodes.EL_INVALID_PARAMS, 'consumerId is required');
|
|
443
|
+
}
|
|
444
|
+
let session;
|
|
445
|
+
if (params.sessionId) {
|
|
446
|
+
const s = sessions.get(params.sessionId);
|
|
447
|
+
if (s) {
|
|
448
|
+
session = s;
|
|
449
|
+
}
|
|
450
|
+
else if (params.port && params.baud) {
|
|
451
|
+
session = await ensureSession(params.sessionId, params.port, params.baud, {
|
|
452
|
+
isDefault: false,
|
|
453
|
+
label: params.port,
|
|
454
|
+
role: 'shared',
|
|
455
|
+
canWrite: true,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
// 对于不存在的session,如果是default,尝试重新创建
|
|
460
|
+
if (params.sessionId === 'default') {
|
|
461
|
+
session = await getDefaultSession();
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
session = getSessionOrThrow(params.sessionId);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (params.port && params.baud) {
|
|
469
|
+
const id = makeSessionId(params.port, params.baud);
|
|
470
|
+
session = await ensureSession(id, params.port, params.baud, {
|
|
471
|
+
isDefault: false,
|
|
472
|
+
label: params.port,
|
|
473
|
+
role: 'shared',
|
|
474
|
+
canWrite: true,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
session = await getDefaultSession();
|
|
479
|
+
}
|
|
480
|
+
session.consumers.set(params.consumerId, {
|
|
481
|
+
id: params.consumerId,
|
|
482
|
+
onData: params.onData,
|
|
483
|
+
onError: params.onError,
|
|
484
|
+
});
|
|
485
|
+
return {
|
|
486
|
+
id: session.id,
|
|
487
|
+
label: session.label,
|
|
488
|
+
role: session.role,
|
|
489
|
+
isDefault: session.isDefault,
|
|
490
|
+
canWrite: session.canWrite,
|
|
491
|
+
port: session.port,
|
|
492
|
+
baud: session.baud,
|
|
493
|
+
recording: session.recording,
|
|
494
|
+
logPath: session.logPath,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
export function removeSessionConsumer(sessionId, consumerId) {
|
|
498
|
+
const s = sessions.get(sessionId);
|
|
499
|
+
if (!s)
|
|
500
|
+
return;
|
|
501
|
+
s.consumers.delete(consumerId);
|
|
502
|
+
}
|
|
503
|
+
export async function forceCloseAllSessions() {
|
|
504
|
+
const all = Array.from(sessions.values());
|
|
505
|
+
sessions.clear();
|
|
506
|
+
defaultSessionId = null;
|
|
507
|
+
for (const s of all) {
|
|
508
|
+
if (s.recording)
|
|
509
|
+
await stopRecording(s.id).catch(() => { });
|
|
510
|
+
for (const consumer of s.consumers.values()) {
|
|
511
|
+
try {
|
|
512
|
+
consumer.onError?.(error(ErrorCodes.EL_BOARD_UART_OPEN_FAILED, 'board-uart session closed by forceClose'));
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// ignore consumer errors
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
s.consumers.clear();
|
|
519
|
+
try {
|
|
520
|
+
await s.close();
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// ignore close errors
|
|
524
|
+
}
|
|
525
|
+
s.hardwareOpen = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
export async function closeAllSessionHardwarePreserveState() {
|
|
529
|
+
const closed = [];
|
|
530
|
+
for (const s of sessions.values()) {
|
|
531
|
+
if (!s.hardwareOpen)
|
|
532
|
+
continue;
|
|
533
|
+
try {
|
|
534
|
+
await s.close();
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// ignore close errors
|
|
538
|
+
}
|
|
539
|
+
s.hardwareOpen = false;
|
|
540
|
+
closed.push(s.id);
|
|
541
|
+
}
|
|
542
|
+
return closed;
|
|
543
|
+
}
|
|
544
|
+
export async function reopenSessionHardwareByIds(sessionIds) {
|
|
545
|
+
for (const id of sessionIds) {
|
|
546
|
+
const s = sessions.get(id);
|
|
547
|
+
if (!s)
|
|
548
|
+
continue;
|
|
549
|
+
try {
|
|
550
|
+
await ensureSessionHardwareOpen(s);
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// ignore open errors; status/read/write will surface later
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
export function hasSession(sessionId) {
|
|
558
|
+
return sessions.has(sessionId);
|
|
559
|
+
}
|