简体   繁体   中英

How can two of the same model USB touch screens be differentiated on Windows 10?

I have two of the same model touch screen monitors connected to a Windows 10 Machine. The monitors are connected with HDMI for image and USB for touch input.

When I plug everything in and set it up using the build-in calibration "multidigimon.exe" I can set everything up so the touch screens work as expected.

However after a restart sometimes the touch inputs are registered on the wrong screen, so touching the right screen makes stuff happen on the left, and touching on the left screen makes stuff happen on the right screen.

I've already tried to see if I can find a way to have a script correct the problem, here is what I've figured out so far:

  1. multidigimon.exe writes registry keys in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wisp\Pen\Digimon. As the key it uses the Windows Object Manager path that corresponds to the USB touch device. As the value it uses the Windows Object Manager path that corresponds to the Display device. (I can see both of them with WinObj under "GLOBAL??"). Exporting the two entries into a.reg file look like this:
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wisp\Pen\Digimon]
    "20-\\\\?\\HID#VID_1FF7&PID_0F27&Col04#a&25dfa661&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}"="\\\\?\\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
    "20-\\\\?\\HID#VID_1FF7&PID_0F27&Col04#a&29d74c67&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}"="\\\\?\\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"

It consists of mostly the device instance path that can be seen in device manager under details for the device. In this case HID\VID_1FF7&PID_0F27&Col04\A&25DFA661&0&0003 and HID\VID_1FF7&PID_0F27&COL04\A&29D74C67&0&0003 the \ replaced with # and the class GUID also appended after another # . Info in part from this stackoverflow answer .

  1. Part of the device instance path is explained in this stackoverflow answer , but that only explains it for USB devices, what I'm dealing with is a HID device. So the VID_XXXX and PID_XXXX seem to mean the same thing, but ColXX is not explained, the part after the last \ is the instance specific id.

  2. After a restart it's random what actual touch HID device gets what instance specific id. So sometimes the right touch screen has the device instance path HID\VID_1FF7&PID_0F27&Col04\A&25DFA661&0&0003 and sometimes it has HID\VID_1FF7&PID_0F27&COL04\A&29D74C67&0&0003 , this seems pretty random*. The left touch screen gets the device instance path that the right one dosen't have.

*It probably depends on what screen starts up faster (they automatically turn on when the PC boots). As when I unplug the touch screen devices USB after boot and plug in one at a time, the first one always gets the same instance specific id.

Is there a way to tell the difference between the two devices? Maybe get information about what USB Port it is plugged into somehow?

Found a way to do it using PowerShell with embedded C# that uses the Win32 API.

Devices on Windows are on a device tree . Windows provides ways to navigate that tree in Cfgmgr32.h . Since in my case the devices were USB devices, I could use USB Device Tree Viewer to visualize and experiment before writing code.

Strangely the TouchScreens I connected don't identify as a single USB device, but instead as multiple USB Hubs behind one another with some devices connected (and some ports not connected), while the displays don't offer any USB Ports to plug something in. USB Device Tree Viwer also displays the Child devices of USB devices, even the HID devices I was looking for.

After testing for a while with unplugging and replugging the TouchScreens in different USB Ports and looking at what USB Device Tree Viwer showd my I could see that the Device IDs (in reality it is the Device instance ID) don't always stay exactly the same. The Device instance ID is is made up of the device ID and instance id , sometimes the instance id completly changed after replugging the USB devices.
I could also note the part of the Location IDs that stayed the same for the USB Ports even after replugging and restarting windows.

So the steps I needed to take in order to make the touch screens work across restarts are:

  1. Find the HID Devices staring with the specified Vendor and Product ID, Followed by COL04 that is connected to the right USB Port (checkd with the Location ID / Location Path).
  2. Remove the old probably wrong values under the HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon
  3. Add the newly found device paths in the right format as values in there. (Luckily the display devices keep the exact same path after restarts). I looked at the format after normally applying the touch screen to display mapping with the build in tools and just hacked together the right format with string interpolation.
  4. Get Windows to read in the new configuration. For that I found that wisptis.exe dosen't exist at all on my machine. After some testing I found that just terminating dwm.exe makes windows read in the new config. It also blacks the screen for a moment ad dwm.exe ist the desktop window manager, but doing it right after startup dosen't cause any problems. Even windows that are opened stay at the same position.

I put all of that together into the following script, I've used the windows task scheduler to run this script after the system is booted (I can't run it any earlier as the screens boot up slower than the PC, so that the USB devices are only "plugged in" after the PC is booted):

$code = @"
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;


public class Program
{
    // Required Constants from cfgmgr32.h
    public const int CM_LOCATE_DEVNODE_NORMAL = 0x00000000;
    public const int CR_SUCCESS = 0x00000000;
    public const int CM_DRP_LOCATION_PATHS = 0x00000024;
    
    // The function that finds all devices starting with a specified device id that are connected through the specified
    // connection (location path). Since not all devices directly have a location path it walks up the devicetree untill
    // a node that hase a location path is found and then checks that path if it matches.
    public static List<string> GetDeviceIdStartingWithAt(string match, string connection)
    {
        var devices = FindAllDevicesStartingWith(match);
        var ret = new List<string>();
        foreach (var device in devices)
        {
            IntPtr cur = device;
            while (!HasLocationPath(cur))
            {
                IntPtr next;
                int result = CM_Get_Parent(out next, cur, 0);
                if (result != CR_SUCCESS) {
                    break;
                }
                cur = next;
            }

            bool mark = false;
            foreach (var location in GetLocationPath(cur))
            {
                if (location.StartsWith(connection))
                {
                    mark = true;
                }
            }

            if (mark)
            {
                ret.Add(GetDeviceId(device));
            }
        }

        return ret;
    }
    static List<IntPtr> FindAllDevicesStartingWith(string match)
    {
        IntPtr rootDevice;
        CM_Locate_DevNodeA(out rootDevice, "", CM_LOCATE_DEVNODE_NORMAL);
        return FindMatchingChildren(match, rootDevice);
    }

    // Recursive function that gets all children for a device and filters using the DeviceId to only return children
    // whose deviceId starts as requested
    static List<IntPtr> FindMatchingChildren(string match, IntPtr device)
    {
        var children = GetAllChildren(device);
        var ret = new List<IntPtr>();
        foreach (var child in children)
        {
            if (GetDeviceId(child).StartsWith(match))
            {
                ret.Add(child);
            }

            ret.AddRange(FindMatchingChildren(match, child));
        }

        return ret;
    }

    // Function implementing the way to get all direct children of a specified node as described here:
    // https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/nf-cfgmgr32-cm_get_child
    static List<IntPtr> GetAllChildren(IntPtr device)
    {
        IntPtr firstChild;
        if (CM_Get_Child(out firstChild, device, 0) != CR_SUCCESS)
        {
            return new List<IntPtr>();
        }

        var ret = new List<IntPtr>();
        ret.Add(firstChild);
        IntPtr cur = firstChild;
        int result;
        do
        {
            IntPtr next;
            result = CM_Get_Sibling(out next, cur, 0);
            if (result == CR_SUCCESS)
            {
                ret.Add(next);
                cur = next;
            }
        } while (result == CR_SUCCESS);

        return ret;
    }
    
    // Just a quick helper function that checks if a device has a Location Path, because not all devices do. In my
    // testing devices that have a device ID starting with HID mostly don't have a location. 
    static bool HasLocationPath(IntPtr device)
    {
        Microsoft.Win32.RegistryValueKind kind;
        uint length = 0;
        CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, IntPtr.Zero, ref length, 0);

        return length > 0;
    }
    
    // Wrapper to easily get the Location paths, the one starting with PCIROOT seems to stay the same across restarts
    // and also identifies what port something is connected to. The value returned is of type REG_MULTI_SZ
    // REG_MULTI_SZ is explained here: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
    static string[] GetLocationPath(IntPtr device)
    {
        Microsoft.Win32.RegistryValueKind kind;
        uint length = 0;
        CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, IntPtr.Zero, ref length, 0);

        if (length <= 0) 
            return Array.Empty<string>();
        
        IntPtr buffer = Marshal.AllocHGlobal((int)length);
        CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, buffer, ref length, 0);
        string ret = Marshal.PtrToStringUni(buffer, (int)length/2);
        Marshal.FreeHGlobal(buffer);
        return ret.Substring(0, ret.Length-2).Split('\0');
    }

    // Wrapper around CM_Get_Device_ID, dosen't check for errors
    static string GetDeviceId(IntPtr device)
    {
        uint length = 0;
        CM_Get_Device_ID_Size(ref length, device, 0);

        IntPtr buffer = Marshal.AllocHGlobal((int)length + 1);
        CM_Get_Device_ID(device, buffer, length + 1, 0);
        string ret = Marshal.PtrToStringAnsi(buffer);
        Marshal.FreeHGlobal(buffer);
        return ret;
    }
    
    
    // Win32 Functions required to get data. More information can be found here:
    // https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/
    [DllImport("cfgmgr32.dll", SetLastError = true)]
    static extern int CM_Locate_DevNodeA(out IntPtr pdnDevInst, string pDeviceID, int ulFlags);

    [DllImport("cfgmgr32.dll", SetLastError = true)]
    static extern int CM_Get_DevNode_Registry_Property(
        IntPtr deviceInstance,
        uint property,
        out Microsoft.Win32.RegistryValueKind pulRegDataType,
        IntPtr buffer,
        ref uint length,
        uint flags);

    [DllImport("cfgmgr32.dll", SetLastError = true)]
    static extern int CM_Get_Parent(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);

    [DllImport("cfgmgr32.dll", SetLastError = true)]
    static extern int CM_Get_Child(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);

    [DllImport("cfgmgr32.dll", SetLastError = true)]
    static extern int CM_Get_Sibling(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);

    [DllImport("cfgmgr32.dll", SetLastError = true)]
    static extern int CM_Get_Device_ID(IntPtr pdnDevInst, IntPtr buffer, uint bufferLen, uint flags);

    [DllImport("cfgmgr32.dll", SetLastError = true)]
    static extern int CM_Get_Device_ID_Size(ref uint pulLen, IntPtr dnDevInst, uint flags);
}
"@

Add-Type -TypeDefinition $code -Language CSharp

# Find the two touch screen devices
do {
    $left = [Program]::GetDeviceIdStartingWithAt("HID\VID_1FF7&PID_0F27&COL04", "PCIROOT(0)#PCI(0801)#PCI(0003)#USBROOT(0)#USB(1)#USB(3)")
    $right = [Program]::GetDeviceIdStartingWithAt("HID\VID_1FF7&PID_0F27&COL04", "PCIROOT(0)#PCI(0801)#PCI(0004)#USBROOT(0)#USB(1)#USB(4)")
    if ( ($left.Count -ge 1) -and ($right.Count -ge 1)) {
        break
    }
    Start-Sleep 1
} while ($true)

if (($left.Count -gt 1) -or ($right.Count -gt 1)) {
    # Too many matching devices found! Abort!
    Return
}

# Truncate and convert them to lower case for use in the Registry Key
$left = $left.Substring(28, 12).ToLower()
$right = $right.Substring(28, 12).ToLower()


# Delete the old values in the registry
$properties = Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon

foreach ($property in $properties.PSObject.Properties) {
    if ($property.Name.StartsWith("20-")) {
        Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name $property.Name
    }
}

# Adding the found devices in the registry. In my case the touch screens offer finger and pen input. The difference is Col04 and Col06 before the device instance id and after it 0003 or 0005
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col04#$left&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col06#$left&0005#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col04#$right&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col06#$right&0005#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"

# To apply the settings change the Desktop Window Manager is stopped. This dosen't look great as both screens flash black for a second before a new dwm instance is automatically started.
# There is probably a way to send some message to the dwm to read the configuration, but I haven't found it anywhere.
Stop-Process -Name dwm -Force

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM