Christian Findlay
Christian Findlay

Reputation: 7712

Can I Use Windows Hid API Calls in UWP (HidD_GetPreparsedData)?

I am trying to access Hid (USB) devices connected to my computer on UWP. I have no problem enumerating the devices and talking to them through Windows API calls in .NET Core. In UWP, I can enumerate the devices, but when I call HidD_GetPreparsedData with the same device (https://msdn.microsoft.com/en-us/library/windows/hardware/ff539679(v=vs.85).aspx), it returns false.

I'm thinking that because UWP has its own HID library, I'm supposed to use that, but I'm hoping to reuse my existing code. Any ideas why this call might be failing?

I did think that this was a permissions problem, so I downloaded the UWP HID Sample from here: https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/CustomHidDeviceAccess. I then modified the package manifest to use the device's VID and PID

<Capabilities>
  <DeviceCapability Name="humaninterfacedevice">
    <Device Id="vidpid:xxxx xxxx">
      <Function Type="usage:0005 *" />
    </Device>
  </DeviceCapability>
</Capabilities>

The device appears in UWP using the standard HID library in UWP. I can enumerate ALL devices (not just the ones I have specified access to), and my device shows up in the list of devices in the sample app.

Yet, when I compile and run my app, the HidD_GetPreparsedData returns false. So, I'm left wondering if I'm ever going to get this API call working. I.e. should I abandon the enterprise and just use the standard UWP HID library?

Upvotes: 0

Views: 2221

Answers (1)

Christian Findlay
Christian Findlay

Reputation: 7712

The answer is no. The UWP platform does not allow Windows Hid, or USB API calls. The API call does not appear here. That's even though the API doesn't explicitly fail. This begs the question of why UWP allows you to access some API calls but doesn't explicitly throw an exception when you call them.

Anyway, it is not necessary because there is a full API for USB, and Hid access. You need to define your devices in the package manifest like this:

Hid:

<DeviceCapability Name="humaninterfacedevice">

  <Device Id="vidpid:534C 0001">
    <Function Type="usage:0005 *" />
    <Function Type="usage:FF00 0001" />
    <Function Type="usage:ff00 *" />
  </Device>

  <Device Id="vidpid:1209 53C0">
    <Function Type="usage:0005 *" />
    <Function Type="usage:FF00 0001" />
    <Function Type="usage:ff00 *" />
  </Device>

  <Device Id="vidpid:1209 53C1">
    <Function Type="usage:0005 *" />
    <Function Type="usage:FF00 0001" />
    <Function Type="usage:ff00 *" />
  </Device>

</DeviceCapability>

USB:

  <!--Trezor Firmware 1.7.x -->
  <Device Id="vidpid:1209 53C1">
    <Function Type="classId:ff * *" />
  </Device>

</DeviceCapability>

Here are two classes for communicating with USB, and Hid devices on UWP from Device.Net.

public class UWPHidDevice : UWPDeviceBase<HidDevice>
{
    #region Public Properties
    public bool DataHasExtraByte { get; set; } = true;
    #endregion

    #region Public Override Properties
    /// <summary>
    /// TODO: These vales are completely wrong and not being used anyway...
    /// </summary>
    public override ushort WriteBufferSize => 64;
    /// <summary>
    /// TODO: These vales are completely wrong and not being used anyway...
    /// </summary>
    public override ushort ReadBufferSize => 64;
    #endregion

    #region Event Handlers
    private void _HidDevice_InputReportReceived(HidDevice sender, HidInputReportReceivedEventArgs args)
    {
        HandleDataReceived(InputReportToBytes(args));
    }
    #endregion

    #region Constructors
    public UWPHidDevice()
    {
    }

    public UWPHidDevice(string deviceId) : base(deviceId)
    {
    }
    #endregion

    #region Private Methods
    private byte[] InputReportToBytes(HidInputReportReceivedEventArgs args)
    {
        byte[] bytes;
        using (var stream = args.Report.Data.AsStream())
        {
            bytes = new byte[args.Report.Data.Length];
            stream.Read(bytes, 0, (int)args.Report.Data.Length);
        }

        if (DataHasExtraByte)
        {
            bytes = RemoveFirstByte(bytes);
        }

        return bytes;
    }

    public override async Task InitializeAsync()
    {
        //TODO: Put a lock here to stop reentrancy of multiple calls

        //TODO: Dispose but this seems to cause initialization to never occur
        //Dispose();

        Logger.Log("Initializing Hid device", null, nameof(UWPHidDevice));

        await GetDevice(DeviceId);

        if (_ConnectedDevice != null)
        {
            _ConnectedDevice.InputReportReceived += _HidDevice_InputReportReceived;
            RaiseConnected();
        }
        else
        {
            throw new Exception($"The device {DeviceId} failed to initialize");
        }
    }

    protected override IAsyncOperation<HidDevice> FromIdAsync(string id)
    {
        return HidDevice.FromIdAsync(id, FileAccessMode.ReadWrite);
    }
    #endregion

    #region Public Methods

    public override async Task WriteAsync(byte[] data)
    {
        byte[] bytes;
        if (DataHasExtraByte)
        {
            bytes = new byte[data.Length + 1];
            Array.Copy(data, 0, bytes, 1, data.Length);
            bytes[0] = 0;
        }
        else
        {
            bytes = data;
        }

        var buffer = bytes.AsBuffer();
        var outReport = _ConnectedDevice.CreateOutputReport();
        outReport.Data = buffer;

        try
        {
            var operation = _ConnectedDevice.SendOutputReportAsync(outReport);
            await operation.AsTask();
            Tracer?.Trace(false, bytes);
        }
        catch (ArgumentException ex)
        {
            //TODO: Check the string is nasty. Validation on the size of the array being sent should be done earlier anyway
            if (ex.Message == "Value does not fall within the expected range.")
            {
                throw new Exception("It seems that the data being sent to the device does not match the accepted size. Have you checked DataHasExtraByte?", ex);
            }
            throw;
        }
    }
    #endregion
}

public class UWPUsbDevice : UWPDeviceBase<UsbDevice>
{
    #region Fields
    /// <summary>
    /// TODO: It should be possible to select a different configuration/interface
    /// </summary>
    private UsbInterface _DefaultConfigurationInterface;
    private UsbInterruptOutPipe _DefaultOutPipe;
    private UsbInterruptInPipe _DefaultInPipe;
    #endregion

    #region Public Override Properties
    public override ushort WriteBufferSize => (ushort)_DefaultOutPipe.EndpointDescriptor.MaxPacketSize;
    public override ushort ReadBufferSize => (ushort)_DefaultInPipe.EndpointDescriptor.MaxPacketSize;
    #endregion

    #region Constructors
    public UWPUsbDevice() : base()
    {
    }

    public UWPUsbDevice(string deviceId) : base(deviceId)
    {
    }
    #endregion

    #region Private Methods
    public override async Task InitializeAsync()
    {
        await GetDevice(DeviceId);

        if (_ConnectedDevice != null)
        {
            var usbInterface = _ConnectedDevice.Configuration.UsbInterfaces.FirstOrDefault();

            if (usbInterface == null)
            {
                _ConnectedDevice.Dispose();
                throw new Exception("There was no Usb Interface found for the device.");
            }

            var interruptPipe = usbInterface.InterruptInPipes.FirstOrDefault();

            if (interruptPipe == null)
            {
                throw new Exception("There was no interrupt pipe found on the interface");
            }

            interruptPipe.DataReceived += InterruptPipe_DataReceived;

            //TODO: Fill in the DeviceDefinition...

            // TODO: It should be possible to select a different configurations, interface, and pipes

            _DefaultConfigurationInterface = _ConnectedDevice.Configuration.UsbInterfaces.FirstOrDefault();

            //TODO: Clean up this messaging and move down to a base class across platforms
            if (_DefaultConfigurationInterface == null) throw new Exception("Could not get the default interface configuration for the USB device");

            _DefaultOutPipe = _DefaultConfigurationInterface.InterruptOutPipes.FirstOrDefault();

            if (_DefaultOutPipe == null) throw new Exception("Could not get the default out pipe for the default USB interface");

            _DefaultInPipe = _DefaultConfigurationInterface.InterruptInPipes.FirstOrDefault();

            if (_DefaultOutPipe == null) throw new Exception("Could not get the default in pipe for the default USB interface");


            RaiseConnected();
        }
        else
        {
            throw new Exception($"Could not connect to device with Device Id {DeviceId}. Check that the package manifest has been configured to allow this device.");
        }
    }

    protected override IAsyncOperation<UsbDevice> FromIdAsync(string id)
    {
        return UsbDevice.FromIdAsync(id);
    }

    #endregion

    #region Event Handlers
    private void InterruptPipe_DataReceived(UsbInterruptInPipe sender, UsbInterruptInEventArgs args)
    {
        HandleDataReceived(args.InterruptData.ToArray());
    }
    #endregion

    #region Public Methods
    public override async Task WriteAsync(byte[] bytes)
    {
        if (_DefaultOutPipe == null) throw new Exception("The device has not been initialized.");

        if (bytes.Length > WriteBufferSize) throw new Exception("The buffer size is too large");
        await _DefaultOutPipe.OutputStream.WriteAsync(bytes.AsBuffer());

        Tracer?.Trace(false, bytes);
    }
    #endregion
}

Upvotes: 1

Related Questions