Figuring which printer name corresponds to which device ID

11,993

Solution 1

Below is what I finally have been able to come up with.

Please confirm that SYSTEM\CurrentControlSet\Control\Print\Printers\{0}\PNPData is a supported path, and not just happens to be there in the current implementation, subject to future changes.

There's a little problem with structure alignment, for which I've posted a separate question.

public static class UsbPrinterResolver
{

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    private struct SP_DEVINFO_DATA
    {
        public uint cbSize;
        public Guid ClassGuid;
        public uint DevInst;
        public IntPtr Reserved;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    private struct SP_DEVICE_INTERFACE_DATA
    {
        public uint cbSize;
        public Guid InterfaceClassGuid;
        public uint Flags;
        public IntPtr Reserved;
    }


    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
    private struct SP_DEVICE_INTERFACE_DETAIL_DATA  // Only used for Marshal.SizeOf. NOT!
    {
        public uint cbSize;
        public char DevicePath;
    }


    [DllImport("cfgmgr32.dll", CharSet = CharSet.Auto, SetLastError = false, ExactSpelling = true)]
    private static extern uint CM_Get_Parent(out uint pdnDevInst, uint dnDevInst, uint ulFlags);

    [DllImport("cfgmgr32.dll", CharSet = CharSet.Auto, SetLastError = false)]
    private static extern uint CM_Get_Device_ID(uint dnDevInst, string Buffer, uint BufferLen, uint ulFlags);

    [DllImport("cfgmgr32.dll", CharSet = CharSet.Auto, SetLastError = false, ExactSpelling = true)]
    private static extern uint CM_Get_Device_ID_Size(out uint pulLen, uint dnDevInst, uint ulFlags);

    [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetupDiGetClassDevs([In(), MarshalAs(UnmanagedType.LPStruct)] System.Guid ClassGuid, string Enumerator, IntPtr hwndParent, uint Flags);

    [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int SetupDiEnumDeviceInfo(IntPtr DeviceInfoSet, uint MemberIndex, ref SP_DEVINFO_DATA DeviceInfoData);

    [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int SetupDiEnumDeviceInterfaces(IntPtr DeviceInfoSet, [In()] ref SP_DEVINFO_DATA DeviceInfoData, [In(), MarshalAs(UnmanagedType.LPStruct)] System.Guid InterfaceClassGuid, uint MemberIndex, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);

    [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int SetupDiGetDeviceInterfaceDetail(IntPtr DeviceInfoSet, [In()] ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData, IntPtr DeviceInterfaceDetailData, uint DeviceInterfaceDetailDataSize, out uint RequiredSize, IntPtr DeviceInfoData);

    [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
    private static extern int SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, int dwShareMode, IntPtr lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

    private const uint DIGCF_PRESENT = 0x00000002U;
    private const uint DIGCF_DEVICEINTERFACE = 0x00000010U;
    private const int ERROR_INSUFFICIENT_BUFFER = 122;
    private const uint CR_SUCCESS = 0;

    private const int FILE_SHARE_READ = 1;
    private const int FILE_SHARE_WRITE = 2;
    private const uint GENERIC_READ = 0x80000000;
    private const uint GENERIC_WRITE = 0x40000000;
    private const int OPEN_EXISTING = 3;

    private static readonly Guid GUID_PRINTER_INSTALL_CLASS = new Guid(0x4d36e979, 0xe325, 0x11ce, 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18);
    private static readonly Guid GUID_DEVINTERFACE_USBPRINT = new Guid(0x28d78fad, 0x5a12, 0x11D1, 0xae, 0x5b, 0x00, 0x00, 0xf8, 0x03, 0xa8, 0xc2);
    private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);


    private static string GetPrinterRegistryInstanceID(string PrinterName) {
        if (string.IsNullOrEmpty(PrinterName)) throw new ArgumentNullException("PrinterName");

        const string key_template = @"SYSTEM\CurrentControlSet\Control\Print\Printers\{0}\PNPData";

        using (var hk = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
                            string.Format(key_template, PrinterName),
                            Microsoft.Win32.RegistryKeyPermissionCheck.Default,
                            System.Security.AccessControl.RegistryRights.QueryValues
                        )
               )
        {

            if (hk == null) throw new ArgumentOutOfRangeException("PrinterName", "This printer does not have PnP data.");

            return (string)hk.GetValue("DeviceInstanceId");
        }
    }

    private static string GetPrinterParentDeviceId(string RegistryInstanceID) {
        if (string.IsNullOrEmpty(RegistryInstanceID)) throw new ArgumentNullException("RegistryInstanceID");

        IntPtr hdi = SetupDiGetClassDevs(GUID_PRINTER_INSTALL_CLASS, RegistryInstanceID, IntPtr.Zero, DIGCF_PRESENT);
        if (hdi.Equals(INVALID_HANDLE_VALUE)) throw new System.ComponentModel.Win32Exception();

        try
        {
            SP_DEVINFO_DATA printer_data = new SP_DEVINFO_DATA();
            printer_data.cbSize = (uint)Marshal.SizeOf(typeof(SP_DEVINFO_DATA));

            if (SetupDiEnumDeviceInfo(hdi, 0, ref printer_data) == 0) throw new System.ComponentModel.Win32Exception();   // Only one device in the set

            uint cmret = 0;

            uint parent_devinst = 0;
            cmret = CM_Get_Parent(out parent_devinst, printer_data.DevInst, 0);
            if (cmret != CR_SUCCESS) throw new Exception(string.Format("Failed to get parent of the device '{0}'. Error code: 0x{1:X8}", RegistryInstanceID, cmret));


            uint parent_device_id_size = 0;
            cmret = CM_Get_Device_ID_Size(out parent_device_id_size, parent_devinst, 0);
            if (cmret != CR_SUCCESS) throw new Exception(string.Format("Failed to get size of the device ID of the parent of the device '{0}'. Error code: 0x{1:X8}", RegistryInstanceID, cmret));

            parent_device_id_size++;  // To include the null character

            string parent_device_id = new string('\0', (int)parent_device_id_size);
            cmret = CM_Get_Device_ID(parent_devinst, parent_device_id, parent_device_id_size, 0);
            if (cmret != CR_SUCCESS) throw new Exception(string.Format("Failed to get device ID of the parent of the device '{0}'. Error code: 0x{1:X8}", RegistryInstanceID, cmret));

            return parent_device_id;
        }
        finally
        {
            SetupDiDestroyDeviceInfoList(hdi);
        }
    }

    private static string GetUSBInterfacePath(string SystemDeviceInstanceID) {
        if (string.IsNullOrEmpty(SystemDeviceInstanceID)) throw new ArgumentNullException("SystemDeviceInstanceID");

        IntPtr hdi = SetupDiGetClassDevs(GUID_DEVINTERFACE_USBPRINT, SystemDeviceInstanceID, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
        if (hdi.Equals(INVALID_HANDLE_VALUE)) throw new System.ComponentModel.Win32Exception();

        try
        {
            SP_DEVINFO_DATA device_data = new SP_DEVINFO_DATA();
            device_data.cbSize = (uint)Marshal.SizeOf(typeof(SP_DEVINFO_DATA));

            if (SetupDiEnumDeviceInfo(hdi, 0, ref device_data) == 0) throw new System.ComponentModel.Win32Exception();  // Only one device in the set

            SP_DEVICE_INTERFACE_DATA interface_data = new SP_DEVICE_INTERFACE_DATA();
            interface_data.cbSize = (uint)Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA));

            if (SetupDiEnumDeviceInterfaces(hdi, ref device_data, GUID_DEVINTERFACE_USBPRINT, 0, ref interface_data) == 0) throw new System.ComponentModel.Win32Exception();   // Only one interface in the set


            // Get required buffer size
            uint required_size = 0;
            SetupDiGetDeviceInterfaceDetail(hdi, ref interface_data, IntPtr.Zero, 0, out required_size, IntPtr.Zero);

            int last_error_code = Marshal.GetLastWin32Error();
            if (last_error_code != ERROR_INSUFFICIENT_BUFFER) throw new System.ComponentModel.Win32Exception(last_error_code);

            IntPtr interface_detail_data = Marshal.AllocCoTaskMem((int)required_size);

            try
            {

                // FIXME, don't know how to calculate the size.
                // See https://stackoverflow.com/questions/10728644/properly-declare-sp-device-interface-detail-data-for-pinvoke

                switch (IntPtr.Size)
                {
                    case 4:
                        Marshal.WriteInt32(interface_detail_data, 4 + Marshal.SystemDefaultCharSize);
                        break;
                    case 8:
                        Marshal.WriteInt32(interface_detail_data, 8);
                        break;

                    default:
                        throw new NotSupportedException("Architecture not supported.");
                }

                if (SetupDiGetDeviceInterfaceDetail(hdi, ref interface_data, interface_detail_data, required_size, out required_size, IntPtr.Zero) == 0) throw new System.ComponentModel.Win32Exception();

                // TODO: When upgrading to .NET 4, replace that with IntPtr.Add
                return Marshal.PtrToStringAuto(new IntPtr(interface_detail_data.ToInt64() + Marshal.OffsetOf(typeof(SP_DEVICE_INTERFACE_DETAIL_DATA), "DevicePath").ToInt64()));

            }
            finally
            {
                Marshal.FreeCoTaskMem(interface_detail_data);
            }
        }
        finally
        {
            SetupDiDestroyDeviceInfoList(hdi);
        }
    }


    public static string GetUSBPath(string PrinterName) {
        return GetUSBInterfacePath(GetPrinterParentDeviceId(GetPrinterRegistryInstanceID(PrinterName)));
    }

    public static Microsoft.Win32.SafeHandles.SafeFileHandle OpenUSBPrinter(string PrinterName) {
        return new Microsoft.Win32.SafeHandles.SafeFileHandle(CreateFile(GetUSBPath(PrinterName), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero), true);
    }

}

Usage:

using (var sh = UsbPrinterResolver.OpenUSBPrinter("Zebra Large"))
{
    using (var f = new System.IO.FileStream(sh, System.IO.FileAccess.ReadWrite))
    {
        // Read from and write to the stream f
    }
}

Solution 2

Try this (Python code):

import _winreg

HKLM = _winreg.HKEY_LOCAL_MACHINE

#------------------------------------------------------------------------------
def getDevicePath(printerName):
    key = _winreg.OpenKey(HKLM,
        r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printers\%s" \
        % printerName)

    value =_winreg.QueryValueEx(key, "Port")[0]
    assert value.startswith("USB"), \
           "Port does not start with 'USB': %s" % value

    printerPortNumber = int(value.replace(u"USB", u""))

    key = _winreg.OpenKey(HKLM,
            r"SYSTEM\CurrentControlSet\Control\DeviceClasses" \
            r"\{28d78fad-5a12-11d1-ae5b-0000f803a8c2}")

    idx = 0
    devicePath = None
    while True:
        try:
            subKeyName = _winreg.EnumKey(key, idx)
            subKey = _winreg.OpenKey(key, subKeyName)

            try:
                subSubKey = _winreg.OpenKey(subKey, r"#\Device Parameters")
                baseName = _winreg.QueryValueEx(subSubKey, "Base Name")[0]
                portNumber = _winreg.QueryValueEx(subSubKey, "Port Number")[0]
                if baseName == "USB" and portNumber == printerPortNumber:
                    devicePath = subKeyName.replace("##?#USB", r"\\?\usb")
                    break

            except WindowsError:
                continue

            finally:
                idx += 1

        except WindowsError:
            break

    return devicePath

Solution 3

Try this ... let me know if this helps ...

    static void Main(string[] args)
    {
        ManagementObjectSearcher s = new ManagementObjectSearcher(@"Select * From Win32_PnPEntity");
        foreach (ManagementObject device in s.Get())
        {
            // Try Name, Caption and/or Description (they seem to be same most of the time).
            string Name = (string)device.GetPropertyValue("Name");

            // >>>>>>>>>>>>>>>>>>>> Query String ...
            if (Name == "O2Micro Integrated MMC/SD controller")
            {
                /*
                 class Win32_PnPEntity : CIM_LogicalDevice
                {
                  uint16   Availability;
                  string   Caption;
                  string   ClassGuid;
                  string   CompatibleID[];
                  uint32   ConfigManagerErrorCode;
                  boolean  ConfigManagerUserConfig;
                  string   CreationClassName;
                  string   Description;
                  string   DeviceID;
                  boolean  ErrorCleared;
                  string   ErrorDescription;
                  string   HardwareID[];
                  datetime InstallDate;
                  uint32   LastErrorCode;
                  string   Manufacturer;
                  string   Name;
                  string   PNPDeviceID;
                  uint16   PowerManagementCapabilities[];
                  boolean  PowerManagementSupported;
                  string   Service;
                  string   Status;
                  uint16   StatusInfo;
                  string   SystemCreationClassName;
                  string   SystemName;
                };
                */

                try
                {
                    Console.WriteLine("Name         : {0}", Name);
                    Console.WriteLine("DeviceID     : {0}", device.GetPropertyValue("DeviceID"));
                    Console.WriteLine("PNPDeviceID  : {0}", device.GetPropertyValue("PNPDeviceID"));
                    Console.WriteLine("ClassGuid    : {0}", device.GetPropertyValue("ClassGuid"));
                    Console.WriteLine("HardwareID   :\n{0}", JoinStrings(device.GetPropertyValue("HardwareID") as string[]));
                    Console.WriteLine("CompatibleID :\n{0}", JoinStrings(device.GetPropertyValue("CompatibleID") as string[]));
                }
                catch (Exception e)
                {
                    Console.WriteLine("ERROR: {0}", e.Message);
                }
            }
        }
    }

    static string JoinStrings(string[] sarray)
    {
        StringBuilder b = new StringBuilder();
        if (sarray != null)
        {
            foreach (string s in sarray)
                b.Append("        '" + s + "'\n");
        }
        return b.ToString();
    }

Don't have a USB printer to test against, but this provides the information you are looking for (including for USB devices)...

Description  : O2Micro Integrated MMC/SD controller
DeviceID     : PCI\VEN_1217&DEV_8221&SUBSYS_04931028&REV_05\4&26B31A7F&0&00E5
PNPDeviceID  : PCI\VEN_1217&DEV_8221&SUBSYS_04931028&REV_05\4&26B31A7F&0&00E5
ClassGuid    : {4d36e97b-e325-11ce-bfc1-08002be10318}
HardwareID   :
        'PCI\VEN_1217&DEV_8221&SUBSYS_04931028&REV_05'
        'PCI\VEN_1217&DEV_8221&SUBSYS_04931028'
        'PCI\VEN_1217&DEV_8221&CC_080501'
        'PCI\VEN_1217&DEV_8221&CC_0805'

CompatibleID :         'PCI\VEN_1217&DEV_8221&REV_05'
        'PCI\VEN_1217&DEV_8221'
        'PCI\VEN_1217&CC_080501'
        'PCI\VEN_1217&CC_0805'
        'PCI\VEN_1217'
        'PCI\CC_080501'
        'PCI\CC_0805'

Also, for a URI, change the '\'s to '#'s in the URI you are intending of building.

so ..

usb\vid_0a5f&pid_0027\46a072900549\{28d78fad-5a12-11d1-ae5b-0000f803a8c2}

becomes

usb#vid_0a5f&pid_0027#46a072900549#{28d78fad-5a12-11d1-ae5b-0000f803a8c2}

====

As GSerg pointed out that Win32_Printer Class helps with the above code, but doesn't provide the device id.

But if I use Win32_Printer class and print out the "PortName" property, that, for the printers I have installed gives be a port/filename that I can use with CreateFile() and open the device.

e.g.:

Name         : Microsoft XPS Document Writer
Description  :
DeviceID     : Microsoft XPS Document Writer
PNPDeviceID  :
PortName  : XPSPort:


Name         : Fax
Description  :
DeviceID     : Fax
PNPDeviceID  :
PortName  : SHRFAX:

Here, writing to "XPSPORT:" or "SHRFAX:" sends data to the printer. What does this do for your USB printer?

Solution 4

Use WinObj from Microsoft to get the specific device name. http://technet.microsoft.com/en-us/sysinternals/bb896657.aspx . This will quickly get you the proper device name to use with CreateFile to write directly to your USB printer or simply writing directly to a USB printer adapter with old school parallel port output for custom circuitry!

To open the port associated with a specific printer, you may need to use ntcreatefile. Use the EnumPrinters function to return a printer_info_2 structure containing the port name for each printer. This port name can then be opened with ntcreatefile (an NT internal version of CreateFile) which is explained here: http://msdn.microsoft.com/en-us/library/bb432380(v=vs.85).aspx

Why does this work? There are three namespace levels in windows NT file/device names and the port name retrieved from EnumPrinters can only be opened with ntcreatefile because it is only in the NT namespace. There may be an equivalent win32 namespace link for certain devices and roundabout ways to match them with a printer name but this is difficult as others have shown in prior answers.

Check out the Global?? folder in WinObj tool to show the symbolic links between win32 namespace and NT namespace on your machine. The old school COM1, COM2, LPT1, etc. device names are simply windows NT namespace symbolic links as well. Google "win32 nt namespace" for a more detailed explanation. (Sorry, but as a new user, I can only post 2 hyperlinks.)

Share:
11,993
aelveborn
Author by

aelveborn

#SOreadytohelp

Updated on June 12, 2022

Comments

  • aelveborn
    aelveborn about 2 years

    My goal is to open a printer connected via USB using the CreateFile (and then issue some WriteFiles and ReadFiles).

    If the printer was an LPT one, I would simply do CreateFile("LPT1:", ...). But for USB printers, there is a special device path that must be passed to CreateFile in order to open that printer.

    This device path, as I was able to find, is retrieved via SetupDiGetClassDevs -> SetupDiEnumDeviceInterfaces -> SetupDiGetDeviceInterfaceDetail -> DevicePath member and looks like this:

    \\?\usb#vid_0a5f&pid_0027#46a072900549#{28d78fad-5a12-11d1-ae5b-0000f803a8c2}

    All that is fine, but what I have as the input is the human-readable printer name, as seen in Devices and Printers. The SetupDi* functions don't seem to use that, they only operate on device instance IDs. So the question is now how to get device instance ID from the printer name one would pass to OpenPrinter.

    It's not difficult to observe that the GUID part of the above is the GUID_DEVINTERFACE_USBPRINT, and \\?\usb is fixed, so the only bit I'm really interested in is vid_0a5f&pid_0027#46a072900549#. This path I can easily look up manually in the printer properties dialog:

    Go to Devices and Printers
    Right-click the printer
    Properties
    Switch to Hardware tab
    Select the printing device, such as ZDesigner LP2844-Z
    Properties
    Switch to Details tab
    Select 'Parent' from the dropdown.

    But I have no idea how to do that programmatically provided the only thing given is the printer name as seen in the Device and Printers panel.


    P.S. 1: I'm not interested in opening the printer with OpenPrinter and then using WritePrinter / ReadPrinter. That has been done, works fine, but now the goal is different.

    P.S. 2: I'll be OK with a simpler way to convert the readable printer name to something that can be passed to CreateFile.

    P.S. 3: This question, to which I have posted an answer, is very related to what I ultimately want to do.

    P.S. 4: The other way round is also fine: If it is possible to obtain the readable name from the SP_DEVINFO_DATA structure, that will also be the answer, although a less convenient one.

  • aelveborn
    aelveborn about 12 years
    This is promising; however, it does not help as of now. The Name it gives is not the name of the printer, it is the name of the driver. So again, I have no way of knowing whether that is the correct printer or not. And if I instead query Win32_Printer, then PNPDeviceID is blank, and DeviceID is useless (same as the printer name).
  • chkdsk
    chkdsk about 12 years
    does the PortName property help?
  • aelveborn
    aelveborn about 12 years
    No, PortName does not help. It is USB002, and CreateFile fails to open that. On contrary, if the port was "LPT1:", it would succeed. If you know how to make CreateFile open something like USB002, that would definitely be the answer.
  • chkdsk
    chkdsk about 12 years
    Have you tried .. \\.\USB002 ?? Also remembered that if the printer is shared then you can directly open the printer by name: "\\machine-name\printer-name".
  • aelveborn
    aelveborn about 12 years
    Yes, I've tried \\.\USB002 and \\?\USB002. The printer is not shared and will not be.
  • aelveborn
    aelveborn about 12 years
    It's not about C++, I'm happy with any language (and in fact, I'm using C# for this project), I just picked C++ as the easiest way to start playing with the API and structures without having a need to declare PInvoke attributes etc. The driver name is useless because it is same for the identical printers I have installed. If you look at my own answer to this question, I'm already doing something similar to what you linked to, but I'm not sure if reading the registry in this way is supported (as opposed to implementation-dependent).
  • Chibueze Opata
    Chibueze Opata about 12 years
    Hmm, if you are doing C#, then what is the need for the API calls?
  • aelveborn
    aelveborn about 12 years
    How else are you going to get that information? First translate redistry device ID to parent device ID, then the parent device ID to the usbprint interface path? .NET framework does not seem to provide a way.
  • Chibueze Opata
    Chibueze Opata about 12 years
    I'm actually saying you don't even need to use the CreateFile and OpenPrinter api calls to get printer information when there is WMI.
  • aelveborn
    aelveborn about 12 years
    I don't need printer information for its own sake. I only need printer information in order to open that printer with CreateFile. It is not possible to get away with .NET printing subsystem, because it opens printer in a different way, via a spooler. WMI does not provide the required information, as I have noted in a comment to PrashantGupta's answer.
  • Admin
    Admin almost 12 years
    I tried your sample and it gets to the point of: SetupDIEnumDeviceInfo(hdi, 0, ref printer_data) and this returns false. I called Marshal.GetLastWin32Error() and my error code is 259. I'm stumped. My printer is plugged into USB, and shows in registry.
  • walkingTarget
    walkingTarget over 11 years
    The above code should work for Plug and Play installed printers (majority of consumer printers). In my case, the printer wasn't installed via PnP and accordingly the PNPData key did not exist. It should also be noted that hard-linked registry key lookups like this aren't recommended as registry organization is subject to change. However, I haven't yet found an API function(s) to establish a relationship between a Printer Name in Devices and Printers and its actual device.
  • Xdg
    Xdg over 7 years
    Problem is that Printer is printer itself (with same name as driver) and PrintQueue (with name visible in Windows) is another device :(