Sending MTP commands through WPD (Part 3 - data from device)

Let's use the GetDevicePropValue command (MTP spec - section D.2.21) to illustrate this. GetDevicePropValue takes one parameter - the device property code that we want to retrieve the current value for. We'll retrieve the BatteryLevel device property (MTP spec - section C.2.2) which is of type UINT8.

From the WPD API, we will need this sequence of commands:

  1. WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_READ - to initiate the transfer
  2. WPD_COMMAND_MTP_EXT_READ_DATA - to actually transfer the data
  3. WPD_COMMAND_MTP_EXT_END_DATA_TRANSFER - to flag command completion and retrieve the MTP response code

Initiating the sequence

 #include <portabledevice.h>
#include <portabledeviceapi.h>
#include <wpdmtpextensions.h>

// We'll return the BatteryLevel in the BYREF parameter
HRESULT GetBatteryLevel(IPortableDevice* pDevice, BYTE& bBatteryLevel)
{
    HRESULT hr = S_OK;
    const WORD PTP_OPCODE_GETDEVICEPROPVALUE = 0x1015; 
    const WORD PTP_DEVICEPROPCODE_BATTERYLEVEL = 0x5001; 
    const WORD PTP_RESPONSECODE_OK = 0x2001;     // 0x2001 indicates command success

    // Build basic WPD parameters for the command
    CComPtr<IPortableDeviceValues> spParameters;
    if (hr == S_OK)
    {
        hr = CoCreateInstance(CLSID_PortableDeviceValues,
                              NULL,
                              CLSCTX_INPROC_SERVER,
                              IID_IPortableDeviceValues,
                              (VOID**)&spParameters);
    }

    // WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_READ is the command we need here
    if (hr == S_OK)
    {
        hr = spParameters->SetGuidValue(WPD_PROPERTY_COMMON_COMMAND_CATEGORY, 
                                           WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_READ.fmtid);
    }

    if (hr == S_OK)
    {
        hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_COMMON_COMMAND_ID, 
                                              WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_READ.pid);
    }

    // Specify the actual MTP op-code that we want to execute here
    if (hr == S_OK)
    {
        hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_MTP_EXT_OPERATION_CODE, 
                                                      (ULONG) PTP_OPCODE_GETDEVICEPROPVALUE);
    }

    // GetDevicePropValue requires the property code as an MTP parameter
    // MTP parameters need to be first put into a PropVariantCollection
    CComPtr<IPortableDevicePropVariantCollection> spMtpParams;
    if (hr == S_OK)
    {
        hr = CoCreateInstance(CLSID_PortableDevicePropVariantCollection,
                                  NULL,
                                  CLSCTX_INPROC_SERVER,
                                  IID_IPortableDevicePropVariantCollection,
                                  (VOID**)&spMtpParams);
    }

    PROPVARIANT pvParam = {0};
    pvParam.vt = VT_UI4;

    // Specify the BatteryLevel property as the MTP parameter
    if (hr == S_OK)
    {
        pvParam.ulVal = PTP_DEVICEPROPCODE_BATTERYLEVEL;
        hr = spMtpParams->Add(&pvParam);
    }

    // Add MTP parameters collection to our main parameter list
    if (hr == S_OK)
    {
        hr = spParameters->SetIPortableDevicePropVariantCollectionValue(
                                          WPD_PROPERTY_MTP_EXT_OPERATION_PARAMS, spMtpParams);
    }  

    // Send the command to initiate the transfer
    CComPtr<IPortableDeviceValues> spResults;
    if (hr == S_OK)
    {
        hr = pDevice->SendCommand(0, spParameters, &spResults);
    }

    // Check if the driver succeeded in sending the command by interrogating WPD_PROPERTY_COMMON_HRESULT
    HRESULT hrCmd = S_OK;
    if (hr == S_OK)
    {
         hr = spResults->GetErrorValue(WPD_PROPERTY_COMMON_HRESULT, &hrCmd);
    }

    if (hr == S_OK)
    {
        printf("Driver return code (initiating): 0x%08X\n", hrCmd);
        hr = hrCmd;
    }

    // If the transfer was initiated successfully, the driver will return us a context cookie
    LPWSTR pwszCookie = NULL;
    if (hr == S_OK)
    {
         hr = spResults->GetStringValue(WPD_PROPERTY_MTP_EXT_TRANSFER_CONTEXT, &pwszContext);
    }

    // The driver will also let us know how many bytes will be transferred. This is important to
    // retrieve since we have to read all the data that the device will send us (even if it
    // isn't the size we were expecting), else we run the risk of the device going out of sync
    // with the driver.
    ULONG cbReportedDataSize = 0;
    if (hr == S_OK)
    {
        hr = pResults->GetUnsignedIntegerValue(WPD_PROPERTY_MTP_EXT_TRANSFER_TOTAL_DATA_SIZE, 
                                               &cbReportedDataSize);
    }

    // Note: The driver provides an additional property - WPD_PROPERTY_MTP_EXT_OPTIMAL_TRANSFER_BUFFER_SIZE
    // which suggests the chunk size that we should retrieve the data in. If your application will be 
    // transferring a large amount of data (>256K), then you should use this property to break down the
    // transfer into small chunks so that your app is more responsive.
    // We'll skip this here since device properties are never that big (especially BatteryLevel)

Reading the data

     // If no data will be transferred we need to skip reading in the data
    BOOL bSkipDataPhase = FALSE;
    if (hr == S_OK && cbReportedDataSize == 0)
    { 
        hr = S_FALSE;
        bSkipDataPhase = TRUE;
    }

    // WPD_COMMAND_MTP_EXT_READ_DATA is the command where we actually read in the data
    (void) spParameters->Clear();
    if (hr == S_OK)
    {
        hr = spParameters->SetGuidValue(WPD_PROPERTY_COMMON_COMMAND_CATEGORY, 
                                           WPD_COMMAND_MTP_EXT_READ_DATA.fmtid);
    }

    if (hr == S_OK)
    {
        hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_COMMON_COMMAND_ID, 
                                                      WPD_COMMAND_MTP_EXT_READ_DATA.pid);
    }

    // We need to specify the same context that we received earlier
    if (hr == S_OK)
    {
        hr = spParameters->SetStringValue(WPD_PROPERTY_MTP_EXT_TRANSFER_CONTEXT, pwszContext);   
    }

    // We'll need to also allocate a buffer for the command to read data into - this should 
    // be the same size as the number of bytes we are expecting to read (per chunk if applicable)
    BYTE* pbBufferIn = NULL;
    if (hr == S_OK)
    {
        pbBufferIn = (BYTE*) CoTaskMemAlloc(cbReportedDataSize);
        if (pbBufferIn == NULL)
        {
            hr = E_OUTOFMEMORY;
        }
    }

    // Pass the allocated buffer as a parameter
    if (hr == S_OK)
    {
        hr = spParameters->SetBufferValue(WPD_PROPERTY_MTP_EXT_TRANSFER_DATA, 
                                                        pbBufferIn, cbReportedDataSize);
    }

    // Specify the number of bytes to transfer as a parameter
    if (hr == S_OK)
    {
        hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_MTP_EXT_TRANSFER_NUM_BYTES_TO_READ, 
                                                        cbReportedDataSize);
    }

    // Send the command to transfer the data
    spResults = NULL;
    if (hr == S_OK)
    {
        hr = pDevice->SendCommand(0, spParameters, &spResults);
    }

    // Check if the driver succeeded in tranferring the data
    HRESULT hrCmd = S_OK;
    if (hr == S_OK)
    {
         hr = spResults->GetErrorValue(WPD_PROPERTY_COMMON_HRESULT, &hrCmd);
    }

    if (hr == S_OK)
    {
        printf("Driver return code (reading data): 0x%08X\n", hrCmd);
        hr = hrCmd;
    }

    // IMPORTANT: The API doesn't really transfer the data into the buffer we provided earlier.
    // Instead it is available in the results collection.
    BYTE* pbBufferOut = NULL;
    ULONG cbBytesRead = 0;
    if (hr == S_OK)
    {
        hr = pResults->GetBufferValue(WPD_PROPERTY_MTP_EXT_TRANSFER_DATA, &pbBufferOut, &cbBytesRead);
    }

    // Reset hr to S_OK since we skipped the data phase
    if (hr == S_FALSE && bSkipDataPhase == TRUE)
    {
        hr = S_OK;
    }

Retrieving the response

     // WPD_COMMAND_MTP_EXT_END_DATA_TRANSFER is the command to signal transfer completion
    (void) spParameters->Clear();
    if (hr == S_OK)
    {
        hr = spParameters->SetGuidValue(WPD_PROPERTY_COMMON_COMMAND_CATEGORY, 
                                           WPD_COMMAND_MTP_EXT_END_DATA_TRANSFER.fmtid);
    }

    if (hr == S_OK)
    {
        hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_COMMON_COMMAND_ID, 
                                                      WPD_COMMAND_MTP_EXT_END_DATA_TRANSFER.pid);
    }

    // We need to specify the same context that we received earlier
    if (hr == S_OK)
    {
        hr = spParameters->SetStringValue(WPD_PROPERTY_MTP_EXT_TRANSFER_CONTEXT, pwszContext);   
    }

    // Send the completion command
    spResults = NULL;
    if (hr == S_OK)
    {
        hr = pDevice->SendCommand(0, spParameters, &spResults);
    }

    // Check if the driver successfully ended the data transfer
    if (hr == S_OK)
    {
         hr = spResults->GetErrorValue(WPD_PROPERTY_COMMON_HRESULT, &hrCmd);
    }

    if (hr == S_OK)
    {
        printf("Driver return code (ending transfer): 0x%08X\n", hrCmd);
        hr = hrCmd;
    }

    // If the command was executed successfully, we check the MTP response code to see if the device
    // could handle the command. If the device could not handle the command, the data phase would 
    // have been skipped (detected by cbReportedDataSize==0) and the MTP response will indicate the
    // error. 
    DWORD dwResponseCode;
    if (hr == S_OK)
    {
        hr = spResults->GetUnsignedIntegerValue(WPD_PROPERTY_MTP_EXT_RESPONSE_CODE, &dwResponseCode);
    }

    if (hr == S_OK)
    {
        printf("MTP Response code: 0x%X\n", dwResponseCode);
        hr = (dwResponseCode == (DWORD) PTP_RESPONSECODE_OK) ? S_OK : E_FAIL;
    }

    // If the command was handled by the device, return the property value in the BYREF property
    if (hr == S_OK)
    {
        if (pbBufferOut != NULL)
        {
            bBatteryLevel = (BYTE)(*pbBufferOut);
        }
        else
        {
            // MTP response code was OK, but no data phase occurred
            hr = E_UNEXPECTED;
        }
    }

    // If response parameters are present, it will be contained in the WPD_PROPERTY_MTP_EXT_RESPONSE_PARAMS 
    // property. GetDevicePropValue does not return additional response parameters, so we skip this code 
    // If required, you may find that code in the post that covered sending MTP commands without data

    // Free up any allocated memory
    CoTaskMemFree(pbBufferIn);
    CoTaskMemFree(pbBufferOut);
    CoTaskMemFree(pwszContext);

    return hr;
}

Initiating the data transfer is pretty easy here. The driver lets us know how much data to expect. To transfer the data, we need to pre-allocate a buffer and provide that in our READ_DATA command parameters. Once the data is successfully read, the data is available in the results collection of the sent command. Once all data is transferred, we send the END_DATA_TRANSFER command and retrieve the response code.

Things to remember:

  • Allocate a buffer before sending the READ_DATA command - this is required
  • The transferred data is not in the allocated buffer but is, instead, in the results collection of the command
  • Watch out for the case when data will not be transferred

[The fact that the transferred data is not available in our allocated buffer but is in a different buffer is a side-effect of how the WPD API uses the WDF framework. I'll ping someone on the WDF team to comment on this but this issue may be addressed in a later iteration of the WPD API.]

Comments