Sending MTP commands through WPD (Part 2 - data to the device)
We'll pick the SetDevicePropValue MTP command (MTP spec - section D.2.22) to illustrate this example. This command requires the device property code as a parameter. We'll try setting the DateTime property (MTP spec - section C.2.18). The DateTime property is of type STRING and we've already covered in a previous post on how MTP strings are created. We'll use the PackString function from that post here.
When data is involved, sending MTP commands is broken up into three parts:
- Initiate the command (inform the device data is coming or is expected)
- Perform data transfer (write or read data)
- Flag command completion and retrieve response code
From the WPD API, this turns out to be a sequence of three commands:
- WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_WRITE or WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_READ
- WPD_COMMAND_MTP_EXT_WRITE_DATA or WPD_COMMAND_MTP_EXT_READ_DATA
- WPD_COMMAND_MTP_EXT_END_DATA_TRANSFER
This example will see the use of WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_WRITE, WPD_COMMAND_MTP_EXT_WRITE_DATA and WPD_COMMAND_MTP_EXT_END_DATA_TRANSFER.
Initiating the sequence
#include <portabledevice.h>
#include <portabledeviceapi.h>
#include <wpdmtpextensions.h>
HRESULT SetDateTime(IPortableDevice* pDevice, LPCWSTR pwszDateTime)
{
HRESULT hr = S_OK;
const WORD PTP_OPCODE_SETDEVICEPROPVALUE = 0x1016;
const WORD PTP_DEVICEPROPCODE_DATETIME = 0x5011;
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_WRITE 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_WRITE.fmtid);
}
if (hr == S_OK)
{
hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_COMMON_COMMAND_ID,
WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITH_DATA_TO_WRITE.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_SETDEVICEPROPVALUE);
}
// SetDevicePropValue 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 DateTime property as the MTP parameter
if (hr == S_OK)
{
pvParam.ulVal = PTP_DEVICEPROPCODE_DATETIME;
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);
}
// Figure out the data that we'll be sending - in this case it will be an MTP string
BYTE* pbBuffer = NULL;
DWORD cbBufferSize = 0;
if (hr == S_OK)
{
hr = PackString(pwszDateTime, &pbBuffer, &cbBufferSize);
}
// We need to inform the device how much data will arrive - this is a required parameter
if (hr == S_OK)
{
hr = spParameters->SetUnsignedLargeIntegerValue(WPD_PROPERTY_MTP_EXT_TRANSFER_TOTAL_DATA_SIZE,
&cbBufferSize);
}
// 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;
}
// The driver will return us a context cookie that we will need to use during our data transfer
LPWSTR pwszCookie = NULL;
if (hr == S_OK)
{
hr = spResults->GetStringValue(WPD_PROPERTY_MTP_EXT_TRANSFER_CONTEXT, &pwszContext);
}
Sending the data
// WPD_COMMAND_MTP_EXT_WRITE_DATA is the command where we actually send in the data
(void) spParameters->Clear();
if (hr == S_OK)
{
hr = spParameters->SetGuidValue(WPD_PROPERTY_COMMON_COMMAND_CATEGORY,
WPD_COMMAND_MTP_EXT_WRITE_DATA.fmtid);
}
if (hr == S_OK)
{
hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_COMMON_COMMAND_ID,
WPD_COMMAND_MTP_EXT_WRITE_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 need to specify the number of bytes arriving with this command. This allows us to
// send the data in chunks if required (multiple WRITE_DATA commands). In this case
// we will send the data in a single chunk
if (hr == S_OK)
{
hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_MTP_EXT_TRANSFER_NUM_BYTES_TO_WRITE,
cbBufferSize);
}
// Provide the data that needs to be transferred
if (hr == S_OK)
{
hr = spParameters->SetBufferValue(WPD_PROPERTY_MTP_EXT_TRANSFER_DATA, pbBuffer, cbBufferSize);
}
// Send the data to the device
spResults = NULL;
if (hr == S_OK)
{
hr = pDevice->SendCommand(0, spParameters, &spResults);
}
// Check if the data was sent successfully by interrogating COMMON_HRESULT
if (hr == S_OK)
{
hr = spResults->GetErrorValue(WPD_PROPERTY_COMMON_HRESULT, &hrCmd);
}
if (hr == S_OK)
{
printf("Driver return code (sending data): 0x%08X\n", hrCmd);
hr = hrCmd;
}
// The driver will inform us on the number of bytes that were actually transferred. Normally this
// should be the same as the number that we provided.
DWORD cbBytesWritten = 0;
if (hr == S_OK)
{
hr = spResults->GetUnsignedIntegerValue(WPD_PROPERTY_MTP_EXT_TRANSFER_NUM_BYTES_WRITTEN,
&cbBytesWritten);
}
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 and the data. Note that there is a distinction between the command
// and the data being successfully sent to the device and the command and data being handled successfully
// by the device
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 response parameters are present, it will be contained in the WPD_PROPERTY_MTP_EXT_RESPONSE_PARAMS
// property. SetDevicePropValue 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(pbBuffer);
CoTaskMemFree(pwszContext);
return hr;
}
Sending a command with data to the device turns out to be a little more involved than sending a command without data. Things to remember are that:
- Prepare your buffer and calculate your buffer size ahead of time
- Be sure to use the correct buffer sizes with the commands
- Be sure to reuse the context cookie
- Check the ...COMMON_HRESULT error code from the driver for each command