@mcesystems/usb-device-listener 1.0.92 → 1.0.94

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.
@@ -332,6 +332,13 @@ void USBListener::HandleDeviceAdded(io_iterator_t iterator) {
332
332
  } else {
333
333
  obj.Set("logicalPort", env.Null());
334
334
  }
335
+ Napi::Array pathArr = Napi::Array::New(env, data->portPath.size());
336
+ for (size_t j = 0; j < data->portPath.size(); j++) {
337
+ pathArr[j] = Napi::Number::New(env, data->portPath[j]);
338
+ }
339
+ obj.Set("portPath", pathArr);
340
+ obj.Set("parentHubVid", Napi::Number::New(env, data->parentHubVid));
341
+ obj.Set("parentHubPid", Napi::Number::New(env, data->parentHubPid));
335
342
  jsCallback.Call({obj});
336
343
  delete data;
337
344
  };
@@ -385,6 +392,13 @@ void USBListener::HandleDeviceRemoved(io_iterator_t iterator) {
385
392
  } else {
386
393
  obj.Set("logicalPort", env.Null());
387
394
  }
395
+ Napi::Array pathArr = Napi::Array::New(env, data->portPath.size());
396
+ for (size_t j = 0; j < data->portPath.size(); j++) {
397
+ pathArr[j] = Napi::Number::New(env, data->portPath[j]);
398
+ }
399
+ obj.Set("portPath", pathArr);
400
+ obj.Set("parentHubVid", Napi::Number::New(env, data->parentHubVid));
401
+ obj.Set("parentHubPid", Napi::Number::New(env, data->parentHubPid));
388
402
  jsCallback.Call({obj});
389
403
  delete data;
390
404
  };
@@ -433,6 +447,22 @@ bool USBListener::GetDeviceInfoFromService(io_service_t service, DeviceInfo& inf
433
447
  // Get Location ID (used for physical port identification)
434
448
  info.locationInfo = GetLocationId(service);
435
449
 
450
+ // Get raw location ID for port path decoding
451
+ CFNumberRef locationRef = (CFNumberRef)IORegistryEntryCreateCFProperty(
452
+ service, CFSTR(kUSBDevicePropertyLocationID), kCFAllocatorDefault, 0);
453
+ if (!locationRef) {
454
+ locationRef = (CFNumberRef)IORegistryEntryCreateCFProperty(
455
+ service, CFSTR("locationID"), kCFAllocatorDefault, 0);
456
+ }
457
+ uint32_t locationId = 0;
458
+ if (locationRef) {
459
+ CFNumberGetValue(locationRef, kCFNumberSInt32Type, &locationId);
460
+ CFRelease(locationRef);
461
+ }
462
+
463
+ // Fill port path and parent hub VID/PID (traverses IOKit hierarchy)
464
+ FillPortPathAndParentHub(service, locationId, info);
465
+
436
466
  // Get USB serial number
437
467
  info.serialNumber = GetSerialNumber(service);
438
468
 
@@ -480,6 +510,119 @@ std::string USBListener::GetLocationId(io_service_t service) {
480
510
  return ss.str();
481
511
  }
482
512
 
513
+ int USBListener::GetPortNumberFromService(io_service_t service) {
514
+ // Try "port" first (IOUSBHostDevice), then "PortNum" (older IOUSBDevice)
515
+ CFTypeRef portRef = IORegistryEntryCreateCFProperty(
516
+ service, CFSTR("port"), kCFAllocatorDefault, 0);
517
+ if (!portRef) {
518
+ portRef = IORegistryEntryCreateCFProperty(
519
+ service, CFSTR("PortNum"), kCFAllocatorDefault, 0);
520
+ }
521
+ if (!portRef || CFGetTypeID(portRef) != CFNumberGetTypeID()) {
522
+ if (portRef) CFRelease(portRef);
523
+ return 0;
524
+ }
525
+ int port = 0;
526
+ CFNumberGetValue((CFNumberRef)portRef, kCFNumberIntType, &port);
527
+ CFRelease(portRef);
528
+ return port;
529
+ }
530
+
531
+ bool USBListener::GetVidPidFromService(io_service_t service, uint16_t& vid, uint16_t& pid) {
532
+ CFTypeRef vidRef = IORegistryEntryCreateCFProperty(
533
+ service, CFSTR(kUSBVendorID), kCFAllocatorDefault, 0);
534
+ if (!vidRef) {
535
+ vidRef = IORegistryEntryCreateCFProperty(
536
+ service, CFSTR("idVendor"), kCFAllocatorDefault, 0);
537
+ }
538
+ CFTypeRef pidRef = IORegistryEntryCreateCFProperty(
539
+ service, CFSTR(kUSBProductID), kCFAllocatorDefault, 0);
540
+ if (!pidRef) {
541
+ pidRef = IORegistryEntryCreateCFProperty(
542
+ service, CFSTR("idProduct"), kCFAllocatorDefault, 0);
543
+ }
544
+ if (!vidRef || !pidRef || CFGetTypeID(vidRef) != CFNumberGetTypeID() ||
545
+ CFGetTypeID(pidRef) != CFNumberGetTypeID()) {
546
+ if (vidRef) CFRelease(vidRef);
547
+ if (pidRef) CFRelease(pidRef);
548
+ return false;
549
+ }
550
+ int v = 0, p = 0;
551
+ CFNumberGetValue((CFNumberRef)vidRef, kCFNumberIntType, &v);
552
+ CFNumberGetValue((CFNumberRef)pidRef, kCFNumberIntType, &p);
553
+ CFRelease(vidRef);
554
+ CFRelease(pidRef);
555
+ vid = static_cast<uint16_t>(v);
556
+ pid = static_cast<uint16_t>(p);
557
+ return true;
558
+ }
559
+
560
+ void USBListener::FillPortPathAndParentHub(io_service_t service, uint32_t locationId, DeviceInfo& info) {
561
+ info.portPath.clear();
562
+ info.parentHubVid = 0;
563
+ info.parentHubPid = 0;
564
+
565
+ std::vector<int> pathDeviceToRoot;
566
+ io_registry_entry_t current = service; // We don't retain - we don't own service
567
+ bool foundParentHub = false;
568
+
569
+ while (current) {
570
+ int portNum = GetPortNumberFromService(current);
571
+ if (portNum > 0) {
572
+ pathDeviceToRoot.push_back(portNum);
573
+ }
574
+
575
+ io_registry_entry_t parentEntry = 0;
576
+ kern_return_t kr = IORegistryEntryGetParentEntry(current, kIOServicePlane, &parentEntry);
577
+
578
+ if (current != service) {
579
+ IOObjectRelease(current);
580
+ }
581
+ current = 0;
582
+
583
+ if (kr != KERN_SUCCESS || parentEntry == 0) {
584
+ if (parentEntry) IOObjectRelease(parentEntry);
585
+ break;
586
+ }
587
+
588
+ // Check if parent is a USB device (hub or controller with VID/PID)
589
+ if (!foundParentHub) {
590
+ uint16_t pvid = 0, ppid = 0;
591
+ if (GetVidPidFromService(parentEntry, pvid, ppid)) {
592
+ info.parentHubVid = pvid;
593
+ info.parentHubPid = ppid;
594
+ foundParentHub = true;
595
+ }
596
+ }
597
+
598
+ current = parentEntry;
599
+ }
600
+
601
+ if (current && current != service) {
602
+ IOObjectRelease(current);
603
+ }
604
+
605
+ // Build port path: reverse to get root-to-device (match Windows format)
606
+ for (auto it = pathDeviceToRoot.rbegin(); it != pathDeviceToRoot.rend(); ++it) {
607
+ info.portPath.push_back(*it);
608
+ }
609
+
610
+ // If registry walk yielded no port path, decode from location ID
611
+ // macOS location ID encodes port path: nibbles (loc>>24)&0xFF, (loc>>20)&0x0F, etc.
612
+ if (info.portPath.empty() && locationId != 0) {
613
+ int p1 = static_cast<int>((locationId >> 24) & 0xFF);
614
+ int p2 = static_cast<int>((locationId >> 20) & 0x0F);
615
+ int p3 = static_cast<int>((locationId >> 16) & 0x0F);
616
+ int p4 = static_cast<int>((locationId >> 12) & 0x0F);
617
+ int p5 = static_cast<int>((locationId >> 8) & 0x0F);
618
+ if (p1 > 0) info.portPath.push_back(p1);
619
+ if (p2 > 0) info.portPath.push_back(p2);
620
+ if (p3 > 0) info.portPath.push_back(p3);
621
+ if (p4 > 0) info.portPath.push_back(p4);
622
+ if (p5 > 0) info.portPath.push_back(p5);
623
+ }
624
+ }
625
+
483
626
  void USBListener::EnumerateConnectedDevices() {
484
627
  // This is called at startup via HandleDeviceAdded when draining the iterator
485
628
  // No additional implementation needed
@@ -48,6 +48,9 @@ private:
48
48
  // Helper methods
49
49
  bool GetDeviceInfoFromService(io_service_t service, DeviceInfo& info);
50
50
  std::string GetLocationId(io_service_t service);
51
+ void FillPortPathAndParentHub(io_service_t service, uint32_t locationId, DeviceInfo& info);
52
+ static int GetPortNumberFromService(io_service_t service);
53
+ static bool GetVidPidFromService(io_service_t service, uint16_t& vid, uint16_t& pid);
51
54
  void EnumerateConnectedDevices();
52
55
  void RunLoopThread();
53
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcesystems/usb-device-listener",
3
- "version": "1.0.92",
3
+ "version": "1.0.94",
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",