http://www.developer.com/

Back to article

Object Exchange (OBEX) Protocol Primer


December 27, 2005

What OBEX Stands For

According to its name, OBEX stands for OBject EXchange. It allows the devices to exchange standard objects such as files, business cards, and calendar infos. This is a vendor-independent protocol implemented on different OSes, such as Palm OS and Windows CE. The actual OBEX support on a particular mobile device depends on system configuration; in other words, vendors can decide what to put there, but at least the OBEX Push protocol is usually presented because it is supported by an Inbox application.

If you turn to the Windows Mobile area, you now can choose between IrDA and Bluetooth as underlying transports. OBEX does all the transport-specific work for you, so there is no need to worry about such things like BT devices being discovered and so forth. In addition, there are a number of COM interfaces to control OBEX as well as to perform device enumeration:

  • IObex
  • IObex2
  • IObexDevice
  • IHeaderCollection
  • IObexSink
  • IObexService
  • IObexServiceConnection
  • IInboxSink
  • IHeaderEnum
  • IPropertyBagEnum
  • IDeviceEnum
  • IObexCaps

I won't disscuss all of them, but I will cover IObexService, IObexServiceConnection, and IInboxSink; they are used in the article's sample project. Moreover, in many cases you will need Object Push rather than Object Pull, so you will cover only sending data process, leaving it up to the OS to implement receiving operations (or GET in terms of OBEX). You also can implement GET operation at low level—for example, with WinSock—according to OBEX protocol description.

Initialization

Your entry point to OBEX is the IObex and IObex2 interfaces. A common workflow, therefore, will be as follows:

  • Initialize OBEX
  • Find a connection point and command OBEX to send notifications on the remote device's arrival or departure
  • Enumerate devices
  • Handle OBEX events
  • Perform all required transfers
  • Shut down OBEX

The very first step is to make an initialization. You will assume that IObex2 is supported in your system:

int CObex::InitObex(HWND hWnd)
{
   HRESULT hr;
   CString sInfo;
   CComPtr<IObex> pObex;
   m_hWnd = hWnd;
   hr = pObex.CoCreateInstance(__uuidof(Obex),NULL,
                               CLSCTX_INPROC_SERVER);
   if(FAILED(hr))
   {
      sInfo.Format(_T("Failed to initialize Obex interface:
                   hr = 0x%X, err =  0x%Xn"),
                   hr, GetLastError());
      ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
      return 0;
   }
   hr = pObex->QueryInterface(__uuidof(IObex2),(void**)&m_pObex);
   if(FAILED(hr))
   {
      sInfo.Format(_T("Failed to initialize Obex interface:
                   hr = 0x%X, err =  0x%Xn"),
                   hr, GetLastError());
      ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
      return 0;
   }
   pObex.Release();
   if (!m_pObex)
      return 0;
   m_pObex->Initialize();
   IObexCaps *pObexCaps = NULL;
   hr = m_pObex->QueryInterface(IID_IObexCaps,
                                (LPVOID *)&pObexCaps);
   if(SUCCEEDED(hr))
   {
      pObexCaps->SetCaps(SEND_DEVICE_UPDATES);
      pObexCaps->Release();
   }
   CCriticalSection csLock;
   csLock.Lock(INFINITE);
   IObexSinkImpl *pSink = new IObexSinkImpl(hWnd);
   if (!pSink)
   {
      csLock.Unlock();
      return 0;
   }
   hr = m_pObex->QueryInterface(IID_IConnectionPointContainer,
                                (LPVOID *)&m_pContainer);
   if (!SUCCEEDED(hr) || (m_pContainer == 0))
   {
      csLock.Unlock();
      return 0;
   }
   hr = m_pContainer->FindConnectionPoint(IID_IObexSink,
                                          &m_pConPoint);
   if (!SUCCEEDED(hr) || (m_pConPoint == 0))
   {
      m_pContainer.Release();
      csLock.Unlock();
      return 0;
   }
   hr = m_pConPoint->Advise((IUnknown *)pSink, &m_dwCookie);
   csLock.Unlock();
   if (ERROR_SUCCESS != m_pObex->StartDeviceEnum())
      return 0;
   if(!SUCCEEDED(hr) || (pDeviceEnum == 0))
      return 0;
   return 1;
}

This code snippet illustrates the entire process: Create all required interfaces, look for connection point, and advise for sink object. Please note that, to receive callback notifications, you have to implement a simple COM-like class. In this sample, it is IObexSinkImpl. All it actually does is to implement the Notify method to pass events to the main application. After all objects are ready, you can start a device lookup. You can collect all devices in range either with a Notify callback or directly by a

CComPtr<IDeviceEnum> pDeviceEnum;
hr = m_pObex->EnumDevices(&pDeviceEnum, GUID_NULL);

call. This function returns the IDeviceEnum interface, which allows you to surf through device enumeration. The last parameter defines a device type to request. You want all devices in range, so sending CLSID_NULL will do. If you want a specific device class, you can do it by sending the appropriate parameter here. The following table lists possible values:

Value Description
CLSID_BthTransport A device that uses Bluetooth to communicate with other devices
CLSID_HeaderCollection The header collection of a device
CLSID_IpTransport A device that uses TCP/IP to communicate with other devices
CLSID_IrdaTransport A device that uses IRdA to communicate with other devices
CLSID_Obex An OBEX-class device
CLSID_PropertyBag The property bag of a device

Responding to OBEX Notifications

After the device discovering query has started, your application can get notified upon the connection and disconnection events. You may respond to such notifications; for example, by showing the device list and storing device info:

LRESULT CObexSampleDlg::OnObexEvent(WPARAM wParam, LPARAM lParam)
{
   int i;
   HRESULT hr;
   TCHAR szDevStr[128];
   POBEXDEVICEINFO pFoundDev;
   OBEXDEVICEINFO diDev;
   memset (&diDev, 0, sizeof (diDev));
   IPropertyBag *pDeviceBag = (IPropertyBag *)lParam;
   VARIANT var;
   VariantInit (&var);
   hr = pDeviceBag->Read(_T("Name"), &var, NULL);
   if (SUCCEEDED(hr))
   {
        diDev.dwFlags |= DEV_FLAG_NAME;
        lstrcpy (diDev.szName, var.bstrVal);
   }
   VariantClear(&var);
   hr = pDeviceBag->Read(_T("Address"), &var, NULL);
   if (SUCCEEDED(hr))
   {
        diDev.dwFlags |= DEV_FLAG_ADDRESS;
        if (var.vt == VT_BSTR)
            lstrcpy (diDev.szAddr, var.bstrVal);
        else if (var.vt == VT_I4)
            _stprintf (diDev.szAddr, _T("%08x"), var.ulVal);
        else
            diDev.dwFlags &= ~DEV_FLAG_ADDRESS;
   }
   VariantClear(&var);
   hr = pDeviceBag->Read(_T("Port"), &var, NULL);
   if (SUCCEEDED(hr))
   {
        diDev.dwFlags |= DEV_FLAG_PORT;
        if (var.vt == VT_BSTR)
            lstrcpy (diDev.szAddr, var.bstrVal);
        else if (var.vt == VT_I4)
            _stprintf (diDev.szAddr, _T("%08x"), var.ulVal);
        else
            diDev.dwFlags &= ~DEV_FLAG_PORT;
   }
   VariantClear(&var);
   hr = pDeviceBag->Read(_T("Transport"), &var, NULL);
   if (SUCCEEDED(hr))
   {
      if (var.vt == VT_BSTR)
      {
         TCHAR szTran[40];
         memset (szTran, 0, sizeof (szTran));
         wcsncpy (szTran, var.bstrVal, 38);
         wcslwr (szTran);
         LPOLESTR lpszIrDATransport = NULL;
         LPOLESTR lpszBthTransport  = NULL;
         if ( SUCCEEDED(StringFromCLSID(CLSID_IrdaTransport,
              &lpszIrDATransport)) &&
              SUCCEEDED(StringFromCLSID(CLSID_BthTransport,
              &lpszBthTransport)) )
         {
            if (wcsicmp (lpszIrDATransport, szTran) == 0)
               diDev.dwFlags |= DEV_TRANS_IRDA;
            else if (wcsicmp (lpszBthTransport, szTran) == 0)
               diDev.dwFlags |= DEV_TRANS_BTOOTH;
         }
         if ( lpszIrDATransport )
            CoTaskMemFree(lpszIrDATransport);
         if ( lpszBthTransport )
            CoTaskMemFree(lpszBthTransport);
      }
   }
   VariantClear(&var);
   hr = pDeviceBag->Read (TEXT("ServiceUUID"), &var, NULL);
   if (SUCCEEDED(hr))
   {
      TCHAR lpszIr[40];
      TCHAR lpszFtp[40];
      TCHAR lpszObjPush[40];
      if (var.vt == VT_BSTR)
      {
         CString sServiceUUID = CComBSTR(var.bstrVal);
         memset(lpszIr,0,sizeof(lpszIr));
         memset(lpszObjPush,0,sizeof(lpszObjPush));
         memset(lpszFtp,0,sizeof(lpszFtp));
         StringFromGUID2(IrMCSyncServiceClass_UUID,
                         lpszIr,sizeof(lpszIr)/sizeof(TCHAR));
         StringFromGUID2(OBEXObjectPushServiceClass_UUID,
                         lpszObjPush,
                         sizeof(lpszObjPush)/sizeof(TCHAR));
         StringFromGUID2(OBEXFileTransferServiceClass_UUID,
                         lpszFtp,sizeof(lpszFtp)/sizeof(TCHAR));
         if ( sServiceUUID.CompareNoCase(lpszIr) == 0 )
         {
            diDev.dwFlags |= (DEV_FLAG_UUID | DEV_SERVICE_IRMCSYNC);
         }
         else if ( sServiceUUID.CompareNoCase(lpszObjPush) == 0 )
         {
            diDev.dwFlags |= (DEV_FLAG_UUID | DEV_SERVICE_OBJPUSH);
         }
         else if ( sServiceUUID.CompareNoCase(lpszFtp) == 0 )
         {
            diDev.dwFlags |= (DEV_FLAG_UUID | DEV_SERVICE_FTP);
         }
      }
   }
   VariantClear(&var);
   diDev.pDevBag = pDeviceBag;
   lstrcpy (szDevStr, diDev.szName);
   lstrcat (szDevStr, _T("  "));
   if (diDev.dwFlags & DEV_SERVICE_OBJPUSH)
      lstrcat (szDevStr, _T("Object Push"));
   else if (diDev.dwFlags & DEV_SERVICE_FTP)
      lstrcat (szDevStr, _T("FTP"));
   else if (diDev.dwFlags & DEV_SERVICE_IRMCSYNC)
      lstrcat (szDevStr, _T("IrMC Sync"));
   else if (diDev.dwFlags & DEV_TRANS_IRDA)
      lstrcat (szDevStr, _T("IrDA"));
   pFoundDev = m_oObex.FindDevInfo (pDeviceBag);
   switch ((int)wParam)
   {
   case OE_DEVICE_ARRIVAL:
      if (pFoundDev->pDevBag)
         break;
      memcpy (pFoundDev, &diDev, sizeof (diDev));
      i = m_DevList.AddString(szDevStr);
      m_DevList.SetItemDataPtr(i,pFoundDev);
      break;
   case OE_DEVICE_UPDATE:
      i = LB_ERR;
      memcpy (pFoundDev, &diDev, sizeof (diDev));
      if (pFoundDev->pDevBag)
      {
         i = FindDevInList(pFoundDev);
         pFoundDev->pDevBag->Release();
      }
      if (LB_ERR != i)
         m_DevList.DeleteString(i);
      i = m_DevList.InsertString(i,szDevStr);
      m_DevList.SetItemDataPtr(i,pFoundDev);
      break;
   case OE_DISCONNECT:
   case OE_DEVICE_DEPARTURE:
      if (pFoundDev->pDevBag == 0)
         break;
      i = FindDevInList(pFoundDev);
      if (LB_ERR != i)
         m_DevList.DeleteString(i);
      pFoundDev->pDevBag->Release();
      pFoundDev->pDevBag = 0;
      break;
   case OE_QUERY_PASSWORD:
      break;
   }
   return 0;
}

Your callback implementation postsan IPropertyBag object in lParam. You can read various remote device properties such as Name, Address, and so forth via this interface, as shown in above sample.

All OBEX operations take some time, so it is not recommended that you perform anything that can stick the system in callback function. In addition, you have to handle messages in the message queue to avoid 'hanging up' effects in the GUI.

Sending Data

After the device discovery process has finished, it is really easy to send some data (a file in tour case) to the selected device. The application obtains an IObexDevice pointer through the IObex::BindToDevice function and then calls SendFile, below:

int CObex::SendFile(IObexDevice *pDevice, LPTSTR pszFileName,
                    DWORD dwFlags)
{
   LPTSTR pszName;
   DWORD dwBytesWritten;
   int nCnt, nFileSize, nTotal;
   HRESULT hr;
   BYTE pBuff[BUFFSIZE];
   CString sInfo;
   CFile File;
   sInfo.Format(_T("Sending file %s"), pszFileName);
   ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
   memset(pBuff,0,sizeof(pBuff));
   pszName = wcsrchr (pszFileName, '\\');
   if (pszName == 0)
      pszName = pszFileName;
   else
      pszName++;
   if ( !File.Open(pszFileName,CFile::modeRead) )
      return 0;
   nFileSize = File.GetLength();
   IHeaderCollection *pHC = 0;
   hr = CoCreateInstance(__uuidof(HeaderCollection), NULL,
      CLSCTX_INPROC_SERVER, __uuidof(IHeaderCollection),
      (void **)&pHC);
   if (FAILED(hr))
   {
       return -2;
   }
   if (dwFlags & DEV_SERVICE_FTP)
      pHC->AddTarget (sizeof (CLSID_FileExchange_NetOrder),
      (UCHAR *)&CLSID_FileExchange_NetOrder);
   hr = pDevice->Connect (NULL, 0, pHC);
   if (FAILED(hr))
   {
      sInfo.Format(_T("Failed to connect! hr = 0x%X, err =  0x%X\n"),
         hr, GetLastError());
      ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
      pHC->Release();
      return -3;
   }
   sInfo = _T("Connected");
   ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
   IHeaderCollection *pFileHC = 0;
   hr = CoCreateInstance(__uuidof(HeaderCollection), NULL,
      CLSCTX_INPROC_SERVER, __uuidof(IHeaderCollection),
      (void **)&pFileHC);
   if (FAILED(hr))
   {
      pHC->Release();
      pDevice->Disconnect (pHC);
      return -2;
   }
   hr = pFileHC->AddName(pszName);
   if (FAILED(hr))
   {
      pHC->Release();
      pFileHC->Release();
      return -3;
   }
   IStream *stOut = 0;
   hr = pDevice->Put(pFileHC, &stOut);
   if (FAILED(hr))
   {
      pDevice->Disconnect (pHC);
      pHC->Release();
      pFileHC->Release();
      return -4;
   }
   nTotal = nFileSize;
   while (nFileSize)
   {
      nCnt = __min (BUFFSIZE, nFileSize);
      dwBytesWritten = File.Read(pBuff, nCnt);
      if (!dwBytesWritten)
      {
         sInfo.Format(_T("ReadFile error %d"), GetLastError());
         ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
            break;
      }
      nCnt = (int)dwBytesWritten;
      sInfo.Format(_T("sending %d bytes"), nCnt);
      ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
      hr = stOut->Write (pBuff, nCnt, &dwBytesWritten);
      if(FAILED(hr))
      {
         sInfo.Format(_T("send error: hr = 0x%X, err = 0x%X"),
                      hr, GetLastError());
         ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
            break;
      }
      nFileSize -= (int)dwBytesWritten;
      sInfo.Format(_T("%d%% sent"), (nTotal-nFileSize)*100/nTotal);
      ::SendMessage(m_hWnd, WM_OBEX_INFO,0,(LPARAM)&sInfo);
   }
   stOut->Commit (STGC_DEFAULT);
   stOut->Release();
   pDevice->Disconnect (pHC);
   if(pHC)
      pHC->Release();
   if(pFileHC)
      pFileHC->Release();
   return 0;
}

Besides establishing an OBEX connection, an important point in the sample above is the IHeaderCollection stuff. Upon a successful connection, you have to fill in IHeaderCollection with actual data. For this purpose, you use an IStream object. IObexDevice::Put returns IStream objects to you, which are used to send your data to the remote device.

At the receiver's side—for example, for Palm OS devices—you might want to register your application with Exchange Manager to handle arrivals of specific data types. For some details, you can take a look at the Palm SDK documentation or at one of my previous articles.

Conclusion

OBEX makes your life easier when you face the need of 'object' transmission between different devices with different operating systems. Palm OS PDAs or Symbian devices will happily receive your files sent from Windows Mobile device because they all are OBEX compliant. Thus, with a little effort, you get a very nice-to-have feature to enjoy and to play with.

Download

Download the accompanying code's zip file here.

About the Author

Alex Gusev started to play with mainframes at the end of the 1980s, using Pascal and REXX, but soon switched to C/C++ and Java on different platforms. When mobile PDAs seriously rose their heads in the IT market, Alex did it too. Now, he works at an international retail software company as a team leader of the Mobile R department, making programmers' lives in the mobile jungles a little bit simpler.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date