@mcesystems/usb-device-listener 1.0.30 → 1.0.31

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/types.d.ts CHANGED
@@ -8,10 +8,17 @@
8
8
  */
9
9
  export interface DeviceInfo {
10
10
  /**
11
- * Windows device instance ID
12
- * Format: "USB\VID_xxxx&PID_xxxx\..."
11
+ * Device identifier
12
+ * For devices with USB serial numbers: the serial number (formatted with dash for iOS style)
13
+ * For devices without serial: platform-specific format "USB\VID_xxxx&PID_xxxx\..."
14
+ * iOS devices: formatted as "XXXXXXXX-XXXXXXXXXXXXXXXX" (e.g., "00008030-001234567890402E")
13
15
  */
14
16
  deviceId: string;
17
+ /**
18
+ * Raw USB serial number (without formatting)
19
+ * Empty string if the device doesn't expose a serial number
20
+ */
21
+ serialNumber: string;
15
22
  /**
16
23
  * USB Vendor ID (decimal)
17
24
  * Use .toString(16) to convert to hex string
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAE/B;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,YAAY,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAE7C;;;;;;;;OAQG;IACH,aAAa,IAAI,IAAI,CAAC;IAEtB;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAE/C;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAErD;;;;;;;;;;;;;;OAcG;IACH,WAAW,IAAI,UAAU,EAAE,CAAC;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAE/B;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,YAAY,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAE7C;;;;;;;;OAQG;IACH,aAAa,IAAI,IAAI,CAAC;IAEtB;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAE/C;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAErD;;;;;;;;;;;;;;OAcG;IACH,WAAW,IAAI,UAAU,EAAE,CAAC;CAC5B"}
package/native/addon.cc CHANGED
@@ -97,6 +97,7 @@ Napi::Value ListDevices(const Napi::CallbackInfo& info) {
97
97
  for (size_t i = 0; i < devices.size(); i++) {
98
98
  Napi::Object obj = Napi::Object::New(env);
99
99
  obj.Set("deviceId", Napi::String::New(env, devices[i].deviceId));
100
+ obj.Set("serialNumber", Napi::String::New(env, devices[i].serialNumber));
100
101
  obj.Set("vid", Napi::Number::New(env, devices[i].vid));
101
102
  obj.Set("pid", Napi::Number::New(env, devices[i].pid));
102
103
  obj.Set("locationInfo", Napi::String::New(env, devices[i].locationInfo));
@@ -14,7 +14,8 @@
14
14
  * Platform-agnostic: used by both Windows and macOS implementations
15
15
  */
16
16
  struct DeviceInfo {
17
- std::string deviceId; // Platform-specific device instance ID
17
+ std::string deviceId; // Device ID (serial number if available, otherwise platform-specific)
18
+ std::string serialNumber; // USB serial number (raw, without formatting)
18
19
  uint16_t vid{0}; // Vendor ID
19
20
  uint16_t pid{0}; // Product ID
20
21
  std::string locationInfo; // Physical port location (platform-specific format)
@@ -25,6 +25,66 @@ static const char* GetUSBDeviceClassName() {
25
25
  return kIOUSBDeviceClassName;
26
26
  }
27
27
 
28
+ /**
29
+ * Get the USB serial number from the IOKit service
30
+ * Returns empty string if serial number is not available
31
+ */
32
+ static std::string GetSerialNumber(io_service_t service) {
33
+ // Try kUSBSerialNumberString first (standard USB property)
34
+ CFStringRef serialRef = (CFStringRef)IORegistryEntryCreateCFProperty(
35
+ service, CFSTR(kUSBSerialNumberString), kCFAllocatorDefault, 0);
36
+
37
+ // Fallback to "USB Serial Number" property name
38
+ if (!serialRef) {
39
+ serialRef = (CFStringRef)IORegistryEntryCreateCFProperty(
40
+ service, CFSTR("USB Serial Number"), kCFAllocatorDefault, 0);
41
+ }
42
+
43
+ if (!serialRef) {
44
+ return "";
45
+ }
46
+
47
+ // Convert CFString to std::string
48
+ CFIndex length = CFStringGetLength(serialRef);
49
+ CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
50
+ std::string result(maxSize, '\0');
51
+
52
+ if (CFStringGetCString(serialRef, &result[0], maxSize, kCFStringEncodingUTF8)) {
53
+ result.resize(strlen(result.c_str()));
54
+ } else {
55
+ result.clear();
56
+ }
57
+
58
+ CFRelease(serialRef);
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Format serial number in iOS-style dash-separated format
64
+ * iOS UDIDs are typically 24-25 characters and formatted as: XXXXXXXX-XXXXXXXXXXXXXXXX
65
+ * Example: 00008030-001234567890402E
66
+ */
67
+ static std::string FormatSerialAsDeviceId(const std::string& serial) {
68
+ if (serial.empty()) {
69
+ return "";
70
+ }
71
+
72
+ // iOS serial numbers are typically 24-25 chars without dashes
73
+ // Format: first 8 chars, dash, remaining chars
74
+ // If it already contains dashes, return as-is
75
+ if (serial.find('-') != std::string::npos) {
76
+ return serial;
77
+ }
78
+
79
+ // iOS UDID format: 8 chars + dash + rest (typically 16 chars)
80
+ if (serial.length() >= 24) {
81
+ return serial.substr(0, 8) + "-" + serial.substr(8);
82
+ }
83
+
84
+ // For shorter serials (like Android), return as-is
85
+ return serial;
86
+ }
87
+
28
88
  USBListener::USBListener() = default;
29
89
 
30
90
  USBListener::~USBListener() {
@@ -229,6 +289,7 @@ void USBListener::HandleDeviceAdded(io_iterator_t iterator) {
229
289
  auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
230
290
  Napi::Object obj = Napi::Object::New(env);
231
291
  obj.Set("deviceId", Napi::String::New(env, data->deviceId));
292
+ obj.Set("serialNumber", Napi::String::New(env, data->serialNumber));
232
293
  obj.Set("vid", Napi::Number::New(env, data->vid));
233
294
  obj.Set("pid", Napi::Number::New(env, data->pid));
234
295
  obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
@@ -280,6 +341,7 @@ void USBListener::HandleDeviceRemoved(io_iterator_t iterator) {
280
341
  auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
281
342
  Napi::Object obj = Napi::Object::New(env);
282
343
  obj.Set("deviceId", Napi::String::New(env, data->deviceId));
344
+ obj.Set("serialNumber", Napi::String::New(env, data->serialNumber));
283
345
  obj.Set("vid", Napi::Number::New(env, data->vid));
284
346
  obj.Set("pid", Napi::Number::New(env, data->pid));
285
347
  obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
@@ -336,12 +398,21 @@ bool USBListener::GetDeviceInfoFromService(io_service_t service, DeviceInfo& inf
336
398
  // Get Location ID (used for physical port identification)
337
399
  info.locationInfo = GetLocationId(service);
338
400
 
339
- // Create a unique device ID combining VID, PID, and location
340
- std::ostringstream ss;
341
- ss << "USB\\VID_" << std::uppercase << std::hex << std::setw(4) << std::setfill('0') << info.vid
342
- << "&PID_" << std::uppercase << std::hex << std::setw(4) << std::setfill('0') << info.pid
343
- << "\\" << info.locationInfo;
344
- info.deviceId = ss.str();
401
+ // Get USB serial number
402
+ info.serialNumber = GetSerialNumber(service);
403
+
404
+ // Use serial number as device ID if available (formatted for iOS style)
405
+ // Otherwise fall back to VID/PID/location combination
406
+ if (!info.serialNumber.empty()) {
407
+ info.deviceId = FormatSerialAsDeviceId(info.serialNumber);
408
+ } else {
409
+ // Fallback: Create a unique device ID combining VID, PID, and location
410
+ std::ostringstream ss;
411
+ ss << "USB\\VID_" << std::uppercase << std::hex << std::setw(4) << std::setfill('0') << info.vid
412
+ << "&PID_" << std::uppercase << std::hex << std::setw(4) << std::setfill('0') << info.pid
413
+ << "\\" << info.locationInfo;
414
+ info.deviceId = ss.str();
415
+ }
345
416
 
346
417
  return true;
347
418
  }
@@ -187,6 +187,7 @@ void USBListener::HandleDeviceChange(WPARAM wParam, LPARAM lParam) {
187
187
  auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
188
188
  Napi::Object obj = Napi::Object::New(env);
189
189
  obj.Set("deviceId", Napi::String::New(env, data->deviceId));
190
+ obj.Set("serialNumber", Napi::String::New(env, data->serialNumber));
190
191
  obj.Set("vid", Napi::Number::New(env, data->vid));
191
192
  obj.Set("pid", Napi::Number::New(env, data->pid));
192
193
  obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
@@ -203,6 +204,7 @@ void USBListener::HandleDeviceChange(WPARAM wParam, LPARAM lParam) {
203
204
  auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
204
205
  Napi::Object obj = Napi::Object::New(env);
205
206
  obj.Set("deviceId", Napi::String::New(env, data->deviceId));
207
+ obj.Set("serialNumber", Napi::String::New(env, data->serialNumber));
206
208
  obj.Set("vid", Napi::Number::New(env, data->vid));
207
209
  obj.Set("pid", Napi::Number::New(env, data->pid));
208
210
  obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
@@ -267,9 +269,9 @@ bool USBListener::GetDeviceInfo(const std::string& devicePath, DeviceInfo& info)
267
269
  if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
268
270
  if (_wcsicmp(instanceId, deviceIdW.c_str()) == 0) {
269
271
  std::wstring idW = instanceId;
270
- info.deviceId = std::string(idW.begin(), idW.end());
272
+ std::string fullDeviceId = std::string(idW.begin(), idW.end());
271
273
 
272
- if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
274
+ if (!GetVidPid(fullDeviceId, info.vid, info.pid)) {
273
275
  SetupDiDestroyDeviceInfoList(deviceInfoSet);
274
276
  return false;
275
277
  }
@@ -280,6 +282,14 @@ bool USBListener::GetDeviceInfo(const std::string& devicePath, DeviceInfo& info)
280
282
  return false;
281
283
  }
282
284
 
285
+ // Get serial number and use as device ID if available
286
+ info.serialNumber = GetSerialNumber(deviceInfoSet, devInfoData);
287
+ if (!info.serialNumber.empty()) {
288
+ info.deviceId = FormatSerialAsDeviceId(info.serialNumber);
289
+ } else {
290
+ info.deviceId = fullDeviceId;
291
+ }
292
+
283
293
  SetupDiDestroyDeviceInfoList(deviceInfoSet);
284
294
  return true;
285
295
  }
@@ -324,6 +334,55 @@ bool USBListener::GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t
324
334
  }
325
335
  }
326
336
 
337
+ /**
338
+ * Get USB serial number from device instance ID
339
+ * The serial number is typically the last part after the last backslash
340
+ */
341
+ std::string USBListener::GetSerialNumber(HDEVINFO deviceInfoSet, SP_DEVINFO_DATA& devInfoData) {
342
+ WCHAR instanceId[MAX_PATH];
343
+ if (!SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
344
+ return "";
345
+ }
346
+
347
+ std::wstring idW = instanceId;
348
+ std::string deviceId(idW.begin(), idW.end());
349
+
350
+ // The serial number is typically after the last backslash in the device instance ID
351
+ // Format: USB\VID_XXXX&PID_XXXX\SERIAL_NUMBER
352
+ size_t lastBackslash = deviceId.rfind('\\');
353
+ if (lastBackslash != std::string::npos && lastBackslash + 1 < deviceId.length()) {
354
+ return deviceId.substr(lastBackslash + 1);
355
+ }
356
+
357
+ return "";
358
+ }
359
+
360
+ /**
361
+ * Format serial number in iOS-style dash-separated format
362
+ * iOS UDIDs are typically 24-25 characters and formatted as: XXXXXXXX-XXXXXXXXXXXXXXXX
363
+ * Example: 00008030-001234567890402E
364
+ */
365
+ static std::string FormatSerialAsDeviceId(const std::string& serial) {
366
+ if (serial.empty()) {
367
+ return "";
368
+ }
369
+
370
+ // iOS serial numbers are typically 24-25 chars without dashes
371
+ // Format: first 8 chars, dash, remaining chars
372
+ // If it already contains dashes, return as-is
373
+ if (serial.find('-') != std::string::npos) {
374
+ return serial;
375
+ }
376
+
377
+ // iOS UDID format: 8 chars + dash + rest (typically 16 chars)
378
+ if (serial.length() >= 24) {
379
+ return serial.substr(0, 8) + "-" + serial.substr(8);
380
+ }
381
+
382
+ // For shorter serials (like Android), return as-is
383
+ return serial;
384
+ }
385
+
327
386
  void USBListener::EnumerateConnectedDevices() {
328
387
  HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
329
388
  if (deviceInfoSet == INVALID_HANDLE_VALUE) {
@@ -338,9 +397,9 @@ void USBListener::EnumerateConnectedDevices() {
338
397
  if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
339
398
  DeviceInfo info;
340
399
  std::wstring idW = instanceId;
341
- info.deviceId = std::string(idW.begin(), idW.end());
400
+ std::string fullDeviceId = std::string(idW.begin(), idW.end());
342
401
 
343
- if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
402
+ if (!GetVidPid(fullDeviceId, info.vid, info.pid)) {
344
403
  continue;
345
404
  }
346
405
 
@@ -349,6 +408,14 @@ void USBListener::EnumerateConnectedDevices() {
349
408
  continue;
350
409
  }
351
410
 
411
+ // Get serial number and use as device ID if available
412
+ info.serialNumber = GetSerialNumber(deviceInfoSet, devInfoData);
413
+ if (!info.serialNumber.empty()) {
414
+ info.deviceId = FormatSerialAsDeviceId(info.serialNumber);
415
+ } else {
416
+ info.deviceId = fullDeviceId;
417
+ }
418
+
352
419
  // Cache device with exclusive lock
353
420
  {
354
421
  std::unique_lock lock(m_cacheMutex);
@@ -359,6 +426,7 @@ void USBListener::EnumerateConnectedDevices() {
359
426
  auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
360
427
  Napi::Object obj = Napi::Object::New(env);
361
428
  obj.Set("deviceId", Napi::String::New(env, data->deviceId));
429
+ obj.Set("serialNumber", Napi::String::New(env, data->serialNumber));
362
430
  obj.Set("vid", Napi::Number::New(env, data->vid));
363
431
  obj.Set("pid", Napi::Number::New(env, data->pid));
364
432
  obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
@@ -394,9 +462,9 @@ std::vector<DeviceInfo> USBListener::ListAllDevices() {
394
462
  if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
395
463
  DeviceInfo info;
396
464
  std::wstring idW = instanceId;
397
- info.deviceId = std::string(idW.begin(), idW.end());
465
+ std::string fullDeviceId = std::string(idW.begin(), idW.end());
398
466
 
399
- if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
467
+ if (!GetVidPid(fullDeviceId, info.vid, info.pid)) {
400
468
  continue;
401
469
  }
402
470
 
@@ -405,6 +473,14 @@ std::vector<DeviceInfo> USBListener::ListAllDevices() {
405
473
  continue;
406
474
  }
407
475
 
476
+ // Get serial number and use as device ID if available
477
+ info.serialNumber = GetSerialNumber(deviceInfoSet, devInfoData);
478
+ if (!info.serialNumber.empty()) {
479
+ info.deviceId = FormatSerialAsDeviceId(info.serialNumber);
480
+ } else {
481
+ info.deviceId = fullDeviceId;
482
+ }
483
+
408
484
  devices.push_back(info);
409
485
  }
410
486
  }
@@ -74,6 +74,7 @@ private:
74
74
  bool GetDeviceInfo(const std::string& devicePath, DeviceInfo& info); // Get device info from Windows API (for connections)
75
75
  bool GetDeviceInfoFromPath(const std::string& devicePath, DeviceInfo& info); // Parse device info from path (for disconnections)
76
76
  bool GetLocationInfo(DEVINST devInst, std::string& locationInfo); // Get physical port location
77
+ std::string GetSerialNumber(HDEVINFO deviceInfoSet, SP_DEVINFO_DATA& devInfoData); // Get USB serial number
77
78
  bool GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t& pid); // Parse VID/PID from device ID string
78
79
  void EnumerateConnectedDevices(); // Enumerate devices on startup
79
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcesystems/usb-device-listener",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Native cross-platform USB device listener for Windows and macOS",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",