Most WPD API are wrappers around the SendCommand API. The documentation for the SendCommand API provides a pretty good example on how to send commands to a driver. So rather than reinvent the wheel, we'll take a look at how we can use the SendCommand API to send a custom MTP command to an MTP device.
We've seen that MTP commands may or may not have a data phase associated with them. Also if there's a data phase associated with a command, it can either be "to the device" or "from the device". This makes for three different ways of sending MTP commands. In this post, we'll cover the easiest one first - sending an MTP command without a data phase.
There are several MTP commands which do not involve a data phase - GetNumObjects, FormatStorage, DeleteObject, etc. We'll pick the non-destructive GetNumObjects command for our example. Section D.2.6 in the MTP spec covers the GetNumObject command (OpCode=0x1006). The command needs three parameters and returns the number of objects matching the parameter criteria in the first response parameter.
We'll need to include a new header file, wpdmtpextensions.h, for this to compile
#include <portabledevice.h> #include <portabledeviceapi.h> #include <wpdmtpextensions.h> HRESULT SendGetNumObjects(IPortableDevice* pDevice) { HRESULT hr = S_OK; const WORD PTP_OPCODE_GETNUMOBJECT = 0x1006; // GetNumObject opcode is 0x1006 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_WITHOUT_DATA_PHASE is the command we need here // Similar commands exist for reading and writing data phases if (hr == S_OK) { hr = spParameters->SetGuidValue(WPD_PROPERTY_COMMON_COMMAND_CATEGORY, WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITHOUT_DATA_PHASE.fmtid); } if (hr == S_OK) { hr = spParameters->SetUnsignedIntegerValue(WPD_PROPERTY_COMMON_COMMAND_ID, WPD_COMMAND_MTP_EXT_EXECUTE_COMMAND_WITHOUT_DATA_PHASE.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_GETNUMOBJECT); } // GetNumObject requires 3 params - storage ID, object format and parent object handle // 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 storage ID parameter. Most devices have 0x10001 have the storage ID. This // should be changed to use the device's real storage ID (which can be obtained by // removing the prefix for the WPD object ID for the storage) if (hr == S_OK) { pvParam.ulVal = 0x10001; hr = spMtpParams->Add(&pvParam); } // Specify object format code parameter. 0x0 can be specified to indicate this is unused if (hr == S_OK) { pvParam.ulVal = 0x0; hr = spMtpParams->Add(&pvParam); } // Specify parent object handle parameter. 0x0 can be specified to indicate this is unused if (hr == S_OK) { pvParam.ulVal = 0x0; 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 the MTP device 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: 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. Note that there is a distinction between the command // being successfully sent to the device and the command 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 the command was executed successfully, the MTP response parameters are returned in // the WPD_PROPERTY_MTP_EXT_RESPONSE_PARAMS property which is a PropVariantCollection CComPtr<IPortableDevicePropVariantCollection> spRespParams; if (hr == S_OK) { hr = spResults->GetIPortableDevicePropVariantCollectionValue(WPD_PROPERTY_MTP_EXT_RESPONSE_PARAMS, &spRespParams); } // The first response parameter contains the number of objects result PROPVARIANT pvResult = {0}; if (hr == S_OK) { hr = spRespParams->GetAt(0, &pvResult); } if (hr == S_OK) { printf("Reported number of objects: %d", pvResult.ulVal); PropVariantClear(&pvResult); // Not really required, but use it for completeness } return hr; }
I split the example into the three parts of preparing the parameters, actually sending the command and then parsing the response. As you noticed, sending the command is the easiest part, while preparing the parameters takes a bit of effort and so does extracting the response code and parameters.
The key things to remember here are:
Of course, it goes without saying that this code applies only to MTP devices. You can interrogate the WPD_DEVICE_PROTOCOL property for the DEVICE object to check if the device supports MTP.