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
Comments
Anonymous
December 06, 2011
Hi, I am trying to send open session command, but in that it says that operation request dataset must have transaction ID set to 0x00000000. I wanted to know how to do that. I am able to set operation code and parameters but not able to set the transaction ID and Session ID to 0x00000000. How to set these values in operation request dataset while sending command?Anonymous
December 16, 2014
Any final solution with full source code sample application in C# .NET ? IMHO, better samples for minimize learning curve are real applications with full source code and good patterns&practices