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:
#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)
// 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; }
// 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:
[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.]