Sunday, 9 December 2012

Sharing a file from WP8 using Bluetooth OBEX Object Push Profile


A WP8 app I’m working on needs Bluetooth image transfer capabilities built in, so I started looking at the Microsoft examples and samples as a starting point. For starters I used the following MSDN article:


The article suggests that you can connect to a device with an empty service name, since the selected device returns an empty string:

// Make sure ID_CAP_NETWORKING is enabled in your WMAppManifest.xml, or the next
// line will throw an Access Denied exception.
await socket.ConnectAsync(selectedDevice.HostName, selectedDevice.ServiceName);

This is not the case as a GUID format service address must be supplied which matches one of the listed device Bluetooth capabilities:
  • Advanced Audio Distribution Profile (A2DP 1.2)
  • Audio/Video Remote Control Profile (AVRCP 1.4
  • Hands Free Profile (HFP 1.5)
  • Phone Book Access Profile (PBAP 1.1)
  • Object Push Profile (OPP 1.1)
  • Out of Band (OOB) and Near Field Communications (NFC)
There are a couple of other examples but they aren't particularly helpful either.

I’ve never had any dealing with Bluetooth before, but done lots of TCP and serial comms and got a few pointers from Mike Hole to get me started. At first I looked at the various Bluetooth specs from here (I always like to see protocol specs when working on this kind of thing):


Some of these make good background reading, in particular the following:
General Object Exchange Profile (GOEP) 


Object Push Profile:


However these don’t provide the level of detail required as we need byte-level  description of the protocol. For this you need IrDA Object Exchange Protocol (OBEX) spec which is not available without being a member of IrDA but I found a copy after a bit of googling. There are two other profiles which looked applicable to transferring an image, FTP and BIP (Basic Imaging Profile) but the only applicable profile the phone supports is OPP, which can be used to transfer objects such as files.

Anyway, down to the code. These are a few methods which connect to a peer device, then send bytes from a file (for me this was an image) in packets and then disconnect.

Peer Search
This bit is the same as the examples:

private async void Search()
{
  this.IsLoading = true;
  this.IsShareExecutable = false;

  this.Message = Resources.StringTable.BTSearching;

  // Note: You can only browse and connect to paired devices!
  // Configure PeerFinder to search for all paired devices.
  PeerFinder.AlternateIdentities["Bluetooth:Paired"] = "";

  this.pairedDevices = await PeerFinder.FindAllPeersAsync();

  if (pairedDevices.Count == 0)
  {
    this.Message = Resources.StringTable.BTNoDevices;
  }
  else
  {
    await this.Share();
  }
}

Socket Connect
In my test, once a device was found, I went straight in to share a file from the first device:

StreamSocket _stream = null;

DataWriter _dataWriter;
DataReader _dataReader;

private async Task Share()
{
  this.CreatePackets();

  // Select a paired device. In this example, just pick the first one.
  PeerInformation selectedDevice = pairedDevices[0];
  int count = pairedDevices.Count;

  // Attempt a connection
  _stream = new StreamSocket();

  try
  {
    // Make sure ID_CAP_NETWORKING is enabled in your WMAppManifest.xml, or the next
    // line will throw an Access Denied exception.
    string oopUUID = "{00001105-0000-1000-8000-00805f9b34fb}";

    await _stream.ConnectAsync(selectedDevice.HostName, oopUUID);
    _dataWriter = new DataWriter(_stream.OutputStream);
    _dataReader = new DataReader(_stream.InputStream);

    // Send data
    int maxServerPacket = await this.ObexConnect();

    if (maxServerPacket > 0)
    {
      if (await ObexPushRequest())
      {
        // Success
      }
      else
      {
        // Failed
      }
    }

    this.ObexDisconnect();
  }
  catch (Exception ex)
  {
    this.IsLoading = false;

    this.Message = Resources.StringTable.BTFailed;
  }
}

This method opens a connection using the OOP UUID GUID, creates data packets, then performs an OBEX connect, followed by an object push and finally a disconnect.

Obex Connect

private async Task<int> ObexConnect()
{
  //send client request
  byte[] ConnectPacket = new byte[7];

  ConnectPacket[0] = 0x80;                       // Connect
  ConnectPacket[1] = (7 & 0xFF00) >> 8;       // Packetlength Hi Byte
  ConnectPacket[2] = (7 & 0xFF);                    // Packetlength Lo Byte
  ConnectPacket[3] = 0x10;                       // Obex v1
  ConnectPacket[4] = 0x00;                       // No flags
  ConnectPacket[5] = (2048 & 0xFF00) >> 8;    // 2048 byte client max packet size Hi Byte
  ConnectPacket[6] = (2048 & 0xFF);                    // 2048 byte max packet size Lo Byte

  _dataWriter.WriteBytes(ConnectPacket);
  await _dataWriter.StoreAsync();

  // Get response code
  await _dataReader.LoadAsync(1);
  byte[] buffer = new byte[1];
  _dataReader.ReadBytes(buffer);

  if (buffer[0] == 0xA0) // Sucess
  {
    // Get length
    await _dataReader.LoadAsync(2);
    buffer = new byte[2];
    _dataReader.ReadBytes(buffer);

    int length = buffer[0] << 8;
    length += buffer[1];

    // Get rest of packet
    await _dataReader.LoadAsync((uint)length - 3);
    buffer = new byte[length - 3];
    _dataReader.ReadBytes(buffer);

    int obexVersion = buffer[0];
    int flags = buffer[1];
    int maxServerPacket = buffer[2] << 8 + buffer[3];

    return maxServerPacket;
  }
  else
  {
    return -1;
  }
}

This establishes a connection and lets the server know the clients buffering capabilities. The client buffer can be something like 256 bytes to 64k - 1. The server then responds with it’s details, but on the laptop seemed to send back the client buffer size.

Create Packets
This method chops the file into 1k chunks and adds the necessary headers. The first packet has extra information about the file like it’s name and size and the last packet has a different op-code to indicate that the transmission has finished. The data to be transferred is in the _shareData variable.

private void CreatePackets()
{
  int bodyLength = 1024;
  this._packets = new List<byte[]>();           

  // Chop data into packets           
  int blocks = (int)Math.Ceiling((decimal)this._shareData.Length / bodyLength);

  System.Text.UnicodeEncoding encoding = new System.Text.UnicodeEncoding(true, false);
  byte[] encodedName = encoding.GetBytes(FILE_NAME + new char());

  for (int i = 0; i < blocks; i++)
  {
    int headerLength = i == 0 ? 14 + encodedName.Length : 6;

    // Chop data into body
    byte[] body = null;
    if (i < blocks - 1)
      body = new byte[bodyLength];
    else
      body = new byte[this._shareData.Length - (i * bodyLength)];

    System.Buffer.BlockCopy(this._shareData, i * bodyLength, body, 0, body.Length);

    // Create packet
    byte[] packet = new byte[headerLength + body.Length];
    this._packets.Add(packet);

    // Build packet
    int offset = 0;
    packet[offset++] = i != blocks - 1 ? (byte)0x02 : (byte)0x82; // 0x02 for first blocks, 0x82 for last
    packet[offset++] = (byte)((packet.Length & 0xFF00) >> 8);
    packet[offset++] = (byte)(packet.Length & 0xFF);

    // Payload details on first packet
    if (i == 0)
    {
      packet[offset++] = 0x01; // Name header
      packet[offset++] = (byte)(((encodedName.Length + 3) & 0xFF00) >> 8);
      packet[offset++] = (byte)((encodedName.Length + 3) & 0xFF);
      System.Buffer.BlockCopy(encodedName, 0, packet, offset, encodedName.Length);
      offset += encodedName.Length;
      packet[offset++] = 0xC3; // Length header
      packet[offset++] = (byte)((this._shareData.Length & 0xFF000000) >> 24);
      packet[offset++] = (byte)((this._shareData.Length & 0xFF0000) >> 16);
      packet[offset++] = (byte)((this._shareData.Length & 0xFF00) >> 8);
      packet[offset++] = (byte)(this._shareData.Length & 0xFF);
    }

    packet[offset++] = 0x48; // Object body chunk header
    packet[offset++] = (byte)(((body.Length + 3) & 0xFF00) >> 8);
    packet[offset++] = (byte)((body.Length + 3) & 0xFF);
    System.Buffer.BlockCopy(body, 0, packet, offset, body.Length);
  }
}

Push Request
Once the packets have been built sending them is very easy to do:

private async Task<bool> ObexPushRequest()
{
  foreach (var packet in this._packets)
  {
    _dataWriter.WriteBytes(packet);
    await _dataWriter.StoreAsync();

    // Get response code
    await _dataReader.LoadAsync(3);
    byte[] buffer = new byte[3];
    _dataReader.ReadBytes(buffer);

    // If not success and not continue it's an error
    if (buffer[0] != 0xA0 && buffer[0] != 0x90)
      return false;
    else if (buffer[0] == 0xA0) // Success
      return true;
    }

    return false;
}

Once the server has received a packet, it issues an 0x90 to say continue and once complete, 0xA0 for success.

Disconnect
Once finished a disconnect message can be sent, but it is not necessary and most devices and clients don’t implement it, so just disposing the IO objects will probably do:

private async void ObexDisconnect()
{
  byte[] bytes = new byte[3];
  bytes[0] = 0x81;
  bytes[1] = 0;
  bytes[2] = 3;

  _dataWriter.WriteBytes(bytes);
  await _dataWriter.StoreAsync();

  await _dataReader.LoadAsync(3);
  byte[] response = new byte[3];
  _dataReader.ReadBytes(response);

  _stream.Dispose();
  _stream = null;

  _dataReader.Dispose();
  _dataWriter.Dispose();
}

Testing
As long as a device is paired and waiting to accept a file, this should work nicely. In windows to wait for a file, right-click the Bluetooth icon in the system tray and select ‘Receive a File’. If everything works, you’ll see the file name appear and a progress bar as the file transfers.

Friday, 9 November 2012

Converting a Visual Studio Setup Project to Wix


Anyone who’s using Visual Studio 2012 and either opened up an old solution with a Setup Project (.vdproj) or created a new solution and looked for the setup project under project types will know it’s no longer there and no longer supported. This is not actually that surprising since TFS Build Server and MSBuild doesn’t support the legacy .vdproj files any way.

Wix is the recommended alternative; itis very powerful, but quite complicated to work with and there is currently no GUI.

The .vdproj can’t be converted directly, however one of the wix tools called dark can be used to decompile msi files into wxs xml format.

This is a step by step procedure for converting an MSI with an exe, a number of DLLs and files into a Wix project.

1. If you haven’t got Wix, install it and add a Wix project to your solution.

2. Decompile the msi with the following command line:

c:\Program Files (x86)\WiX Toolset v3.6\bin>dark c:\temp\setup.msi -o c:\temp\setup.wxs

Dark will output results like this, there will be a load of warnings like this:

Windows Installer Xml Decompiler version 3.6.3303.0
Copyright (C) Outercurve Foundation. All rights reserved.

setup.msi
c:\temp\setup.msi : warning DARK1060 : The _VsdLaunchCondition table is being decompiled as a custom table.
dark.exe : warning DARK1065 : The AdvtUISequence table is not supported by the Windows Installer XML toolset because it has been deprecated by the Windows Installer team.  Any information in this table will be left out of the decompiled output.
c:\temp\setup.msi : warning DARK1062 : The ModuleSignature table can only be represented in WindowsInstaller XML for merge modules.  The information in this table will be left out of the decompiled output.
c:\temp\setup.msi : warning DARK1062 : The ModuleComponents table can only be represented in Windows Installer XML for merge modules.  The information in this table will be left out of the decompiledoutput.
c:\temp\setup.msi : warning DARK1066 : The MsiPatchHeaders table is added to the install package bya transform from a patch package (.msp) and not authored directly into an install package (.msi). The information in this table will be left out of the decompiled output.
c:\temp\setup.msi : warning DARK1066 : The Patch table is added to the install package by a transform from a patch package (.msp) and not authored directly into an install package (.msi). The information in this table will be left out of the decompiled output.
c:\temp\setup.msi : warning DARK1066 : The PatchPackage table is added to the install package by a transform from a patch package (.msp) and not authored directly into an install package (.msi). The information in this table will be left out of the decompiled output.
dark.exe : warning DARK1058 : The AdvtExecuteSequence table contains an action 'MsiUnpublishAssemblies' which is not allowed in this table.  If this is a standard action then it is not valid for this table, if it is a custom action or dialog then this table does not accept actions of that type. This action will be left out of the decompiled output.

3. Export binaries using dark.exe:
c:\Program Files (x86)\WiX Toolset v3.6\bin>dark c:\temp\setup.msi -x c:\temp

4. Rename anything that sounds like an icon or bitmap in the outputted ‘Binary’ folder to have a .bmp suffix, I had:
  • DefBannerBitmap
  • NewFldrBtn
  • UpFldrBtn

These should be .ibd files:
  • MSVBDPCADLL
  • VSDNETCFG

The icon in the ‘Icon’ folder can be renamed .ico

Copy these folders into the WIX project, include them and set as resources.

5. Fix binary paths from

<Binary Id="MSVBDPCADLL" SourceFile="FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES" />
        <Binary Id="VSDNETCFG" SourceFile="FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES" />
        <Binary Id="DefBannerBitmap" SourceFile="FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES" />
        <Binary Id="UpFldrBtn" SourceFile="FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES" />
        <Binary Id="NewFldrBtn" SourceFile="FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES" />

To:

<Binary Id="MSVBDPCADLL" SourceFile="Binary\MSVBDPCADLL.ibd" />
    <Binary Id="VSDNETCFG" SourceFile="Binary\VSDNETCFG.ibd" />
    <Binary Id="DefBannerBitmap" SourceFile="Binary\DefBannerBitmap.bmp" />
    <Binary Id="UpFldrBtn" SourceFile="Binary\UpFldrBtn.bmp" />
    <Binary Id="NewFldrBtn" SourceFile="Binary\NewFldrBtn.bmp" />

And

<Icon Id="_7BBE63EC1B23A31B9D3734.exe" SourceFile="FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES" />

To:

<Icon Id="_7BBE63EC1B23A31B9D3734.exe" SourceFile="Icon\_7BBE63EC1B23A31B9D3734.ico" />

6. Fix project references by adding references to the required projects in the solution like you would in any normal project and then fix the binary components like this (DLLs are similar to EXEs):

<Component Id="C__EFA30D8EEF7020387C8D684F42EDADC6" Guid="{BA9BC921-7529-B2AF-D64D-ABF58AAF77AD}">
                <File Id="_EFA30D8EEF7020387C8D684F42EDADC6" Name="MyApplication.exe" KeyPath="yes" ShortName="MYAPPL~1.EXE" Assembly=".net" AssemblyManifest="_EFA30D8EEF7020387C8D684F42EDADC6" AssemblyApplication="_EFA30D8EEF7020387C8D684F42EDADC6" DiskId="1" Source="SourceDir\File\_EFA30D8EEF7020387C8D684F42EDADC6" />
                <Shortcut Id="_1EE6E64055BE47E2AD4588E6B6AC87EC" Directory="_380EF96A89404298A858064DE3EC207A" Name="MyApplication" ShortName="MYAPPLICATIONL~1" Icon="_7BBE63EC1B23A31B9D3734.exe" IconIndex="0" Show="normal" WorkingDirectory="TARGETDIR" Advertise="yes" />
            </Component>

To:

<Component Id="C__EFA30D8EEF7020387C8D684F42EDADC6" Guid="{BA9BC921-7529-B2AF-D64D-ABF58AAF77AD}">
        <File Id="_EFA30D8EEF7020387C8D684F42EDADC6" Name="MyApplication.exe" KeyPath="yes" ShortName="MYAPPL~1.EXE" Assembly=".net" AssemblyManifest="_EFA30D8EEF7020387C8D684F42EDADC6" AssemblyApplication="_EFA30D8EEF7020387C8D684F42EDADC6" DiskId="1" Source="$(var.MyApplication.TargetPath)" />
        <Shortcut Id="_1EE6E64055BE47E2AD4588E6B6AC87EC" Directory="_380EF96A89404298A858064DE3EC207A" Name="MyApplication" ShortName="MYAPPL~1" Icon="_7BBE63EC1B23A31B9D3734.ico" IconIndex="0" Show="normal" WorkingDirectory="TARGETDIR" Advertise="yes" />
      </Component>

7. Fix file components like this:

<Component Id="C__0BDFAFE635E591014F851368D952F145" Guid="{BE51E0B1-FFFA-CB90-0B48-D6A70E66312E}">
                <File Id="_0BDFAFE635E591014F851368D952F145" Name="Settings.csv" KeyPath="yes" ShortName="SETTINGS.CSV" DiskId="1" Source="SourceDir\File\_0BDFAFE635E591014F851368D952F145" />
            </Component>
To:

<Component Id="C__0BDFAFE635E591014F851368D952F145" Guid="{BE51E0B1-FFFA-CB90-0B48-D6A70E66312E}">
        <File Id="_0BDFAFE635E591014F851368D952F145" Name="Settings.csv" KeyPath="yes" ShortName="SETTINGS.CSV" DiskId="1" Source="$(var.MyApplication.TargetDir)" />
      </Component>

8. Now build and fix any errors. Installers can contain many different components and features so will vary in their complexity to get working.