|
1 | 1 | import { app } from 'electron' |
| 2 | +import { execFile } from 'child_process' |
2 | 3 | import { SerialPortStream } from '@serialport/stream' |
3 | 4 | import type { AutoDetectTypes } from '@serialport/bindings-cpp' |
4 | 5 | import { createRequire } from 'module' |
5 | 6 | import path from 'path' |
| 7 | +import { promisify } from 'util' |
6 | 8 | import { OsrSerialPortInfo, OsrSerialState } from '../src/types' |
7 | 9 |
|
8 | 10 | const DEFAULT_BAUD_RATE = 115200 |
9 | 11 | const runtimeRequire = createRequire(import.meta.url) |
| 12 | +const execFileAsync = promisify(execFile) |
10 | 13 |
|
11 | 14 | type SerialPortBinding = AutoDetectTypes |
12 | 15 | type RuntimeSerialPort = SerialPortStream<SerialPortBinding> |
@@ -49,7 +52,7 @@ export class OsrSerialManager { |
49 | 52 | } |
50 | 53 |
|
51 | 54 | async listPorts(): Promise<OsrSerialPortInfo[]> { |
52 | | - const ports = await getSerialPortBinding().list() |
| 55 | + const ports = await listAvailablePorts() |
53 | 56 | return ports |
54 | 57 | .map((port) => ({ |
55 | 58 | path: port.path, |
@@ -243,6 +246,62 @@ function getSerialPortBinding(): SerialPortBinding { |
243 | 246 | return cachedBinding |
244 | 247 | } |
245 | 248 |
|
| 249 | +async function listAvailablePorts(): Promise<RawSerialPortInfo[]> { |
| 250 | + const portMap = new Map<string, RawSerialPortInfo>() |
| 251 | + |
| 252 | + try { |
| 253 | + const ports = await getSerialPortBinding().list() |
| 254 | + for (const port of ports) { |
| 255 | + portMap.set(port.path, { |
| 256 | + path: port.path, |
| 257 | + manufacturer: port.manufacturer, |
| 258 | + serialNumber: port.serialNumber, |
| 259 | + vendorId: port.vendorId, |
| 260 | + productId: port.productId, |
| 261 | + pnpId: port.pnpId, |
| 262 | + }) |
| 263 | + } |
| 264 | + } catch { |
| 265 | + // Keep going and try platform-specific fallbacks below. |
| 266 | + } |
| 267 | + |
| 268 | + if (process.platform === 'win32') { |
| 269 | + for (const port of await listWindowsRegistryPorts()) { |
| 270 | + const existing = portMap.get(port.path) |
| 271 | + portMap.set(port.path, { |
| 272 | + path: port.path, |
| 273 | + manufacturer: existing?.manufacturer ?? port.manufacturer, |
| 274 | + serialNumber: existing?.serialNumber ?? port.serialNumber, |
| 275 | + vendorId: existing?.vendorId ?? port.vendorId, |
| 276 | + productId: existing?.productId ?? port.productId, |
| 277 | + pnpId: existing?.pnpId ?? port.pnpId, |
| 278 | + }) |
| 279 | + } |
| 280 | + } |
| 281 | + |
| 282 | + return Array.from(portMap.values()) |
| 283 | +} |
| 284 | + |
| 285 | +async function listWindowsRegistryPorts(): Promise<RawSerialPortInfo[]> { |
| 286 | + try { |
| 287 | + const { stdout } = await execFileAsync('reg', ['query', 'HKLM\\HARDWARE\\DEVICEMAP\\SERIALCOMM']) |
| 288 | + return stdout |
| 289 | + .split(/\r?\n/) |
| 290 | + .map(parseWindowsRegistryPortLine) |
| 291 | + .filter((port): port is RawSerialPortInfo => port !== null) |
| 292 | + } catch { |
| 293 | + return [] |
| 294 | + } |
| 295 | +} |
| 296 | + |
| 297 | +function parseWindowsRegistryPortLine(line: string): RawSerialPortInfo | null { |
| 298 | + const match = line.match(/\bREG_SZ\b\s+(COM\d+)\s*$/i) |
| 299 | + if (!match) return null |
| 300 | + return { |
| 301 | + path: match[1].toUpperCase(), |
| 302 | + } |
| 303 | +} |
| 304 | + |
246 | 305 | function buildPortDisplayName(path: string, manufacturer: string | null, serialNumber: string | null): string { |
247 | 306 | const detail = manufacturer || serialNumber |
248 | 307 | return detail ? `${path} (${detail})` : path |
|
0 commit comments