node-serialport icon indicating copy to clipboard operation
node-serialport copied to clipboard

USB detection

Open JonathanWoodward opened this issue 4 years ago • 5 comments

@reconbot @GazHank

💥 Proposal

What feature you'd like to see

I would like the ability to detect USB serial ports being added and removed from the computer. Like most USB to serial devices such as FTDI you can set personalised USB VID's and PID's which are used to identify peoples bespoke hardware.

Motivation

To enable people to event driven detect their devices upon being plugged in to the system, open a serial port and start processing data.

Pitch

The concept would you you can initialise the monitoring of a custom VID and PID and it would return the data relevant to opening a serial port. In some instances it might not be a COM1 or ttyUSB0 but a FDTI USB driver.

Device *device = cDevice_create(uv_loop, impl);
device->add(device, VID1, PID1, added_device, removed_device);
device->add(device, VID1, PID2, added_device, removed_device);
device->search(device);

void added_device(Device *device, DeviceNode *node, const char *dev_name)
{
// open serial port
}

void removed_device(Device *device, DeviceNode *node, const char *dev_name)
{
// removal of resources
}
`

Linux can achieve this functionality using libudev:
`
static void Device_findUdevDevices(Device *self, struct udev *udev)
{
    const char *path;
    struct udev_list_entry *devices, *dev_list_entry;
    struct udev_device *dev;
    struct udev_enumerate *enumerate = udev_enumerate_new(udev);
    udev_enumerate_add_match_subsystem(enumerate, "tty");
    udev_enumerate_scan_devices(enumerate);
    devices = udev_enumerate_get_list_entry(enumerate);
	udev_list_entry_foreach(dev_list_entry, devices) {
		path = udev_list_entry_get_name(dev_list_entry);
		dev = udev_device_new_from_syspath(udev, path);	
		if(dev) {
			Device_udevProcessDevice(self, dev, "add");
			udev_device_unref(dev);
		}	
	}
    udev_enumerate_unref(enumerate);
}

static void Device_watcher_cb(uv_poll_t* watcher, int status, int revents)
{
    Device *self = (Device *)watcher->data;
    struct udev_monitor *udev_monitor = (struct udev_monitor *)self->udev_monitor;
    struct udev_device* dev = udev_monitor_receive_device(udev_monitor);
    cDevice_udevProcessDevice(self, dev, udev_device_get_action(dev));
    udev_device_unref(dev);
}
`

Windows is a bit more of a pain:

You need to create a discovery thread 
`
thread = CreateThread(NULL, 0, Device_win_discoveryThread, self, 0, NULL);

static DWORD WINAPI cDevice_win_discoveryThread(void *thread_data)
{
        Device *self = (Device *)thread_data;

	// Initialize and register the window class
	WNDCLASSEX wndClass;
	wndClass.cbSize = sizeof(WNDCLASSEX);
	wndClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
	wndClass.hInstance = (HINSTANCE)GetModuleHandle(NULL);
	wndClass.lpfnWndProc = (WNDPROC)Device_win_trampoline;
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hIcon = LoadIcon(0, IDI_APPLICATION);
	wndClass.hbrBackground = NULL; 
	wndClass.hCursor = LoadCursor(0, IDC_ARROW);
	wndClass.lpszClassName = TEXT("cDeviceClass");
	wndClass.lpszMenuName = NULL;
	wndClass.hIconSm = wndClass.hIcon;

	RegisterClassEx(&wndClass);

	// Create window
	HWND hWnd = CreateWindowEx(WS_EX_CLIENTEDGE | WS_EX_APPWINDOW, 
		wndClass.lpszClassName, 
		TEXT("DeviceFinder"), 
		WS_OVERLAPPEDWINDOW, 
		0, 0, 
		0, 0, 
		NULL, NULL, 
		(HINSTANCE)GetModuleHandle(NULL),
               self
	);
	self->hwnd = (void *)hWnd;

	ShowWindow(hWnd, SW_HIDE);
	UpdateWindow(hWnd);

	// Start a timer
	self->timer_count = 0;
	SetTimer(hWnd, TIMER_ID, TIMER_INTERVAL, NULL);

	// Message pump loops until the window is destroyed
	MSG msg;
	int retVal;
	while (0 != (retVal = GetMessage(&msg, NULL, 0, 0)))
	{
		if (retVal == -1) break;
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	KillTimer(hWnd, TIMER_ID);
	self->hwnd = NULL;
	return 1;
}

On Darwin I have not yet worked on the solution. If the project goes ahead I am happy to contribute to it.

Regards J cDevice.zip

JonathanWoodward avatar Nov 01 '21 09:11 JonathanWoodward

This is a very common request for ports. Listing ports over and over is a poor solution when we can have usb auto detection. The complixity is around cross platform support and which subsystem handles the port. Old com port or tty interfaces don't have this concept so we'd have to look at the usb interfaces and correlate between them. We do have some code that does this in windows (which seems to use old registry entries), but not so much in linux or osx iirc.

reconbot avatar Nov 01 '21 16:11 reconbot

On the Darwin point I wonder if the new driverkit will make this any easier

GazHank avatar Nov 01 '21 23:11 GazHank

This package does it correctly emit events when I plug in my device but for some reason, I had to add a timeout otherwise serialport wouldn't be able to connect.

https://github.com/MadLittleMods/node-usb-detection

currentoor avatar Jan 19 '22 22:01 currentoor

Yes, this is what I did on my version.

static int cDevice_win_proc(cDevice* self, HWND hWnd, unsigned int message, unsigned int wParam, LPARAM lParam) { LRESULT lRet = 1; switch (message) { case WM_CREATE: cDevice_win_registerDeviceToHwnd(WceusbshGUID, hWnd, &self->hDeviceNotify); break; case WM_DEVICECHANGE: { self->timer_count = TIMER_INTERVAL * 2; break; } case WM_CLOSE: if (self->hDeviceNotify != NULL) { UnregisterDeviceNotification((HDEVNOTIFY)self->hDeviceNotify); } DestroyWindow(hWnd); break; case WM_DESTROY: PostQuitMessage(0); break; case WM_TIMER: if(wParam == TIMER_ID) { if(self->timer_count > 0) { if(self->timer_count > TIMER_INTERVAL) { self->timer_count -= TIMER_INTERVAL; } else { self->timer_count = 0; uv_async_send(&win_async_cDevice); //cDevice_win_scanAllDevices(self); } } } break; default: lRet = DefWindowProc(hWnd, message, wParam, lParam); break; } return lRet; }

Thanks for pointing out that lib, I don't think it was public when I started my project 2 years ago.

JonathanWoodward avatar Jan 20 '22 09:01 JonathanWoodward

The other thing I found is you have to detect the serial port from the Windows ports class as followed:

if (SetupDiClassGuidsFromNameW(L"Ports", &pGuids, 1, &portGuids)) { cDevice_win_compare(self, &pGuids, portGuids, 1); }

Then you can match "USB\VID_%04X&PID_%04X" on the usbid:

if (!SetupDiEnumDeviceInfo(hDevInfo, index, &devInfo)) { break; } CM_Get_Device_IDW(devInfo.DevInst, scratch, 256, 0); wcstombs_s(NULL, usbId, 256, scratch, 256);

JonathanWoodward avatar Jan 20 '22 09:01 JonathanWoodward