diff --git a/lib/bindings.test.ts b/lib/bindings.test.ts index feeb7652..dc281f21 100644 --- a/lib/bindings.test.ts +++ b/lib/bindings.test.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { assert, shouldReject } from '../test/assert' import { makeTestFeature } from '../test/makeTestFeature' -import { BindingInterface, OpenOptions, PortInfo, SetOptions } from '@serialport/bindings-interface' +import { BindingInterface, OpenOptions, PortInfo, SetOptions } from '@noelneu/bindings-interface' import { autoDetect } from './index' import { MockBinding } from '@serialport/binding-mock' import { BindingsError } from './errors' diff --git a/lib/linux-list.ts b/lib/linux-list.ts index 3d3c9800..2d5a1d21 100644 --- a/lib/linux-list.ts +++ b/lib/linux-list.ts @@ -1,6 +1,7 @@ import { spawn } from 'child_process' import { PortInfo } from '@serialport/bindings-interface' import { ReadlineParser } from '@serialport/parser-readline' +import fs from 'fs' // get only serial port names function checkPathOfDevice(path: string) { @@ -14,6 +15,7 @@ function propName(name: string) { ID_SERIAL_SHORT: 'serialNumber', ID_VENDOR_ID: 'vendorId', ID_MODEL_ID: 'productId', + ID_MODEL_ENC: 'friendlyName', DEVLINKS: 'pnpId', /** * Workaround for systemd defect @@ -38,7 +40,7 @@ function propVal(name: string, val: string) { const match = val.match(/\/by-id\/([^\s]+)/) return (match?.[1]) || undefined } - if (name === 'manufacturer') { + if (name === 'manufacturer' || name === 'friendlyName') { return decodeHexEscape(val) } if (/^0x/.test(val)) { @@ -47,10 +49,24 @@ function propVal(name: string, val: string) { return val } +function getActiveDevices(): Set { + try { + /* eslint-disable comma-dangle */ + const validPaths = fs.readdirSync('/dev/serial/by-path').map(file => + fs.realpathSync(`/dev/serial/by-path/${file}`) + ) + /* eslint-enable comma-dangle */ + return new Set(validPaths) + } catch { + return new Set() + } +} + export function linuxList(spawnCmd: typeof spawn = spawn) { const ports: PortInfo[] = [] const udevadm = spawnCmd('udevadm', ['info', '-e']) const lines = udevadm.stdout.pipe(new ReadlineParser()) + const validDevices = getActiveDevices() let skipPort = false let port: PortInfo = { @@ -61,6 +77,7 @@ export function linuxList(spawnCmd: typeof spawn = spawn) { locationId: undefined, vendorId: undefined, productId: undefined, + friendlyName: undefined, } lines.on('data', (line: string) => { @@ -76,6 +93,7 @@ export function linuxList(spawnCmd: typeof spawn = spawn) { locationId: undefined, vendorId: undefined, productId: undefined, + friendlyName: undefined, } skipPort = false return @@ -118,6 +136,9 @@ export function linuxList(spawnCmd: typeof spawn = spawn) { }) udevadm.on('error', reject) lines.on('error', reject) - lines.on('finish', () => resolve(ports)) + lines.on('finish', () => { + const activePorts = ports.filter(port => validDevices.has(port.path)) + resolve(activePorts) + }) }) } diff --git a/package-lock.json b/package-lock.json index 18678e36..0de04156 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@types/debug": "4.1.12", "@types/mocha": "10.0.10", "@types/node": "22.13.8", + "@types/sinon": "^17.0.4", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "cc": "3.0.1", @@ -2168,6 +2169,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", diff --git a/package.json b/package.json index 367f22e1..d2718797 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/debug": "4.1.12", "@types/mocha": "10.0.10", "@types/node": "22.13.8", + "@types/sinon": "^17.0.4", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "cc": "3.0.1", diff --git a/src/darwin_list.cpp b/src/darwin_list.cpp index c39f7bec..82687ed0 100644 --- a/src/darwin_list.cpp +++ b/src/darwin_list.cpp @@ -162,6 +162,7 @@ static stDeviceListItem* GetSerialDevices() { memset(serialDevice->vendorId, 0, sizeof(serialDevice->vendorId)); memset(serialDevice->productId, 0, sizeof(serialDevice->productId)); serialDevice->manufacturer[0] = '\0'; + serialDevice->friendlyName[0] = '\0'; serialDevice->serialNumber[0] = '\0'; deviceListItem->next = NULL; deviceListItem->length = &length; @@ -183,6 +184,29 @@ static stDeviceListItem* GetSerialDevices() { io_service_t device = GetUsbDevice(modemService); if (device) { + + CFStringRef friendlyNameAsCFString = (CFStringRef) IORegistryEntryCreateCFProperty(device, + CFSTR(kUSBProductString), + kCFAllocatorDefault, + 0); + + if (friendlyNameAsCFString) { + Boolean result; + char friendlyName[MAXPATHLEN]; + + // Convert from a CFString to a C (NUL-terminated) + result = CFStringGetCString(friendlyNameAsCFString, + friendlyName, + sizeof(friendlyName), + kCFStringEncodingUTF8); + + if (result) { + snprintf(serialDevice->friendlyName, sizeof(serialDevice->friendlyName), "%s", friendlyName); + } + + CFRelease(friendlyNameAsCFString); + } + CFStringRef manufacturerAsCFString = (CFStringRef) IORegistryEntryCreateCFProperty(device, CFSTR(kUSBVendorString), kCFAllocatorDefault, @@ -300,6 +324,9 @@ void ListBaton::Execute() { if (*device.manufacturer) { resultItem->manufacturer = device.manufacturer; } + if (*device.friendlyName) { + resultItem->friendlyName = device.friendlyName; + } if (*device.serialNumber) { resultItem->serialNumber = device.serialNumber; } diff --git a/src/darwin_list.h b/src/darwin_list.h index 8eef0b7d..ce6e9f7f 100644 --- a/src/darwin_list.h +++ b/src/darwin_list.h @@ -14,6 +14,7 @@ void setIfNotEmpty(Napi::Object item, std::string key, const char *value); struct ListResultItem { std::string path; std::string manufacturer; + std::string friendlyName; std::string serialNumber; std::string pnpId; std::string locationId; @@ -38,6 +39,7 @@ struct ListBaton : public Napi::AsyncWorker { setIfNotEmpty(item, "path", (*it)->path.c_str()); setIfNotEmpty(item, "manufacturer", (*it)->manufacturer.c_str()); + setIfNotEmpty(item, "friendlyName", (*it)->friendlyName.c_str()); setIfNotEmpty(item, "serialNumber", (*it)->serialNumber.c_str()); setIfNotEmpty(item, "pnpId", (*it)->pnpId.c_str()); setIfNotEmpty(item, "locationId", (*it)->locationId.c_str()); @@ -56,6 +58,7 @@ typedef struct SerialDevice { char vendorId[MAXPATHLEN]; char productId[MAXPATHLEN]; char manufacturer[MAXPATHLEN]; + char friendlyName[MAXPATHLEN]; char serialNumber[MAXPATHLEN]; } stSerialDevice; diff --git a/src/serialport_win.cpp b/src/serialport_win.cpp index 7e79216a..40cee5de 100644 --- a/src/serialport_win.cpp +++ b/src/serialport_win.cpp @@ -871,9 +871,9 @@ void ListBaton::Execute() { reinterpret_cast(szBuffer), sizeof(szBuffer), &dwSize)) { locationId = wcsdup(szBuffer); } - if (SetupDiGetDeviceRegistryPropertyW(hDevInfo, &deviceInfoData, - SPDRP_FRIENDLYNAME, &dwPropertyRegDataType, - reinterpret_cast(szBuffer), sizeof(szBuffer), &dwSize)) { + if (SetupDiGetDevicePropertyW(hDevInfo, &deviceInfoData, + &DEVPKEY_Device_BusReportedDeviceDesc, &dwPropertyRegDataType, + reinterpret_cast(szBuffer), sizeof(szBuffer), &dwSize, 0)) { friendlyName = wcsdup(szBuffer); } if (SetupDiGetDeviceRegistryPropertyW(hDevInfo, &deviceInfoData,