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:
- WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_READ - to initiate the transfer
- WPD_COMMAND_MTP_EXT_READ_DATA - to actually transfer the data
- 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
- Anonymous
May 29, 2009
PingBack from http://paidsurveyshub.info/story.php?title=dimeby8-sending-mtp-commands-through-wpd-part-3-data-from-device