3rd December 2007 — release 1.5 (1.5.1203).
The library can be used at two levels. At the upper level it provides full support for all client-side OBEX operations, including Put, Get, SetPath, Delete, and Abort. At a lower lever it provides full general parsing and creating of OBEX PDUs (packets); this usage should be rare however and won’t be discussed further here.
The library also provides a parser for the OBEX Folder Listing objects.
The library currently includes no support for server-side operation, nor for reliable sessions. However the source code for a basic OBEX server is available on the website.
A short example may be most explanatory. The following PUTs a file to an IrDA peer.
Imports System
Imports System.IO 'e.g. FileStream, FileMode, etc.
Imports Brecham.Obex
Imports InTheHand.Net 'e.g. IrDAEndPoint
Imports InTheHand.Net.Sockets 'e.g. IrDAClient
' Available from http://32feet.net/.
Class VbPutSampleSample
Public Shared Sub Main(ByVal args() As String)
' Open file as selected by the user
If args.Length <> 1 Then
Console.WriteLine("No filename given.")
Exit Sub
End If
Dim filename As String = args(0)
Dim srcFile As New FileStream(filename, FileMode.Open, FileAccess.Read)
' Connect
Dim cli As New IrDAClient("OBEX")
Dim sess As New ObexClientSession(cli.GetStream, 4096)
sess.Connect
' And Send
Dim name As String = Path.GetFilename(filename)
Dim contentLength As Int64 = srcFile.Length
sess.PutFrom(srcFile, name, Nothing, contentLength)
cli.Close
End Sub
End Class
That is of course lacking any exception handling but is otherwise complete. The other operations are called in a similar fashion. The supported operations are as follows.
Stream supplied by the consumer.
This is as shown in the PUT sample above.
Stream
returned by the library.
An example of its usage is the PutGuiVb.vb sample.
Stream supplied by the consumer.
An example of its usage is the GetFolderListing C# sample.
Stream
returned by the library.
An example of its usage is the GetFolderListing C# sample, it is
also used internally by the GetFolderListing
method on ObexClientSession.
All OBEX protocol information is carried in headers, both the information
describing the objects being transferred and the data itself.
Common headers are
Name, Type, Length, and Body and EndOfBody carrying the data itself.
Most of the operations (Put, Get, SetPath etc) have a core method that
take a collection of headers, as an instance of the class
ObexHeaderCollection, but that is for very advanced uses only, instead
there are always also much more
user-friendly overloads that take various combinations of individual
header values (for Name, Type, Length etc).
For instance the Put operation has four overloads:
public ObexPutStream Put(ObexHeaderCollection headers);
public ObexPutStream Put(String name, String type);
public ObexPutStream Put(String name, String type, UInt32 length);
public ObexPutStream Put(String name, String type, Int64 length);
The most suitable overload can be chosen depending on what information is known
about the object. Also, in most cases a null value is accepted if a particular value
is not known. For instance in that case, both name and type
will accept null / Nothing. Some cases do require a header
value, for instance void SetPath(String folderName) obviously requires
a non-null value. A normal PUT and GET operation is thus
sess.PutFrom(srcStream, "name.txt", Nothing) and
sess.GetTo(dstStream, "name.txt", Nothing)
respectively.
The OBEX Length header is generally optional and can hold only a
32-bit unsigned integer so if a larger value is supplied is will not be
included in the request.
The Int64 overload exists mainly to accept the
value returned by Stream.Length, but also for non-CLS
Compatible environments (i.e. where UInt32 is not supported).
Finally, it is also possible also to access the response metadata returned
by the server in a GET operation. This is available through the
ResponseHeaders property on ObexGetStream.
Common headers include Length,
and Time. The Length header
being most useful, as it is necessary to allow progress reporting. See for
instance the FolderExplorer2 example which does byte-by-byte progress
reporting if that header is returned by the server, or bouncing bar pseudo
progress reporting if not.
If the server rejects or cannot handle an operation it will return an
error response code. On any unexpected response code the library throws an ObexResponseException, which contains the response code
received and any attached description, they are combined in the Message
it produces. For instance if the user on a Windows PC rejects a Put the
ObexResponseException will contain the following message,
where the library has added a textual translation of the numerical code:
Unexpected OBEX response code: 0xC3 (Forbidden).
There is one case where an ObexResponseException is initiated locally,
that’s in Connect where the peer server reports no support for the
requested service / application. A response code of 0xFE is used in
that case.
Any fundamental misbehaviour from the peer, for instance illegal length fields
in the response PDU, will cause a
ProtocolViolationException to be thrown. Any known
minor misbehaviours have been allowed for though — for instance the
Wireless Link application for IrDA OBEX in Windows sends a Success
code in cases where it should send a Continue.
The library does not catch any exception on accessing the Stream to
the peer server,
and thus any IOExceptions and SocketExceptions etc will be
forwarded to the consumer. The only other exception defined by the library is
ObexCreateTooLongException and should not occur when using the session
interfaces.
Finally, if Aborting a currently active command then that command can throw an
exception on being interrupted. If an ObexPutStream or
ObexGetStream operation is in progress then they are closed
and thus any operations on them will fail with ObjectDisposedException,
all the other operations (on ObexClientSession itself) fail
with InvalidOperationException.
Step-by-step forms of some of the operations are provided, for instance the
PutStream and GetStream forms of the Put and Get
operations, and they support the use of the fallback asynchronous method on Streams,
i.e.
BeginWrite / EndWrite and
BeginRead / EndRead respectively. However
currently there aren’t asynchronous forms of the initiating methods i.e.
Put, and Get, nor are there asynchronous versions
of other methods e.g. PutFrom / GetTo /
SetPath etc.
The calling application can of course call the methods asynchronously, whether
in a Background Worker component or by manually using a thread-pool thread, or even
through delegate’s BeginInvoke
feature. As noted above, one can ask the server to cancel any operation by calling
Abort, in this case from the main thread. However as noted below
if the peer might also ignore the abort and thus a timeout should be applied also.
With an operation running on a thread in the background, to get progress information one can convert a line like
sess.PutFrom(source, "file.txt", null); into
using(ObexPutStream putStream = sess.Put("file.txt", null)) {
while(true) {
int curCount = source.Read(buffer, 0, buffer.Length;
if (curCount == 0) {
break;
}
totalCount += curCount;
UpdateProgess(totalCount);
putStream.Write(buffer, 0, curCount);
}
}
See the PutGuiVb2 and FolderExplorer2 samples for complete examples of calling the library from a Background Worker component. Both samples update a progress bar control as the content is uploaded / downloaded respectively, and both also provide a “Cancel” button to allow the user to cancel the download. The example below includes a fragment of that program, it shows the work done by the background worker thread; that is copying the file content to the peer, and updating the progress status as it goes.
Sub DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) _
Handles backgroundWorker1.DoWork
Dim args As BgWorkerArgs = CType(e.Argument, BgWorkerArgs)
Dim buffer(1023) As Byte
Dim count As Int32
Dim updatePeriod As New TimeSpan(0,0,0,0, 250)
Dim lastProgress As DateTime = DateTime.UtcNow
Dim elapsed As TimeSpan
label1.Text = "Sending PUT content..."
Try
While(True)
If (backgroundWorker1.CancellationPending)
e.Cancel = True
args.Connection.ObexClientSession.Abort("User cancelled")
Exit While
End If
count = args.Source.Read(buffer, 0, buffer.Length)
If(count = 0)
Exit While
End If
args.putStream.Write(buffer, 0, count)
' Progress reporting; rate-limited.
elapsed = DateTime.UtcNow - lastProgress
If (elapsed > updatePeriod) Then
Dim percentage As Int32 = CType((100.0 * args.Source.Position) / args.Source.Length,Int32)
backgroundWorker1.ReportProgress(percentage)
lastProgress = DateTime.UtcNow
End If
End While
Finally
args.PutStream.Close
args.Source.Close
' If we wanted to do another Put to the same peer we wouldn't close
' the session and its underlying network stream here.
args.Connection.ObexClientSession.Dispose
End Try
End Sub
See the PutGUI sample for
an example of calling a library method asynchronously, using
delegate.BeginInvoke to run the operation and a callback method to
complete the operation. In summary the code is of the form shown below.
delegate void PutFromNtiCaller(Stream source, String name, String type, Int64 length);
private void button1_Click(object sender, EventArgs e)
{
…
state.m_putCaller = new PutFromNtiCaller(sess.PutFrom);
AsyncCallback cb = new AsyncCallback(PutCompleted);
state.SetStartTime();
IAsyncResult ar = state.m_putCaller.BeginInvoke(state.m_progressStream,
putName, null, state.m_fileStream.Length,cb, state);
…
}
void PutCompleted(IAsyncResult ar)
{
…
// Get the result of the Put operation. This doesn't need to
// be on the UI thread, but the rest does...
state.m_putCaller.EndInvoke(ar);
this.labelStatus.Text = "PutFrom took: " + state.Elapsed.ToString();
…
}
If one wants to monitor the
progress of the self-contained PutFrom or GetTo operations
one way would to create a new Stream type that follows the
‘Decorator Pattern’ and simply passes all
read / writes onto the actual stream whilst counting the number of bytes transferred.
See the PutGUI sample for an example of such code.
There is also no explicit support for timeouts in the library, but this will be considered
in the future based on feedback. At the moment the calling application will have to
cancel an operation if it is taking too long. Depending on the type of timeout
required one could set a timeout on the Stream or Socket
or by synchronizing with the asynchronous invocation of the
operation and calling Abort if it is taking too long.
Note however if the server on the peer fails to respond to the
Abort command then again no timeout will occur. To guard
against this one might consider setting a timeout before calling Abort.
One can set a communications timeout using NetworkStream.ReadTimeout,
Socket.ReceiveTimeout, or ultimately with
Socket.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.SendTimeout, timeout);
for instance.
For simplicity, of course simply closing the connection will always be easiest.
Of course things are somewhat different on the NETCF; there’s no delegate.BeginInvoke,
no BackgroundWorker component, and no fallback implementation of
BeginRead/-Write on Stream that provides for asynchronous
usage of the currently non-asynchronous ObexPut/-GetStreams for instance.
I’m very surprised that the BackgroundWorker component is not
included, I can understand that providing the other two may for instance provide
the ability for developers to unconsciously cause the creation of many threads,
but the BackgroundWorker explicitly uses at most one extra thread, and the extra
code in applications creating threads and handling the calling of ‘update’
operations on the UI thread must surely outweigh the increase in the framework assemblies
if the BackgroundWorker was provided…
In all cases therefore one has to manually create a new thread for the background
operation, whether by thrd = New Thread(methodCallback) + thrd.Start()
or by ThreadPool.QueueUserWorkItem(methodCallback). The PutGuiVb and
FolderExplorer samples create
a new thread explicitly, mimicing most of the BackgroundWorker
behaviour: handling
the thread creation, the ‘run worker completed’ callback, and the
‘report progress’ feature. The PutGuiCs sample
uses ThreadPool.QueueUserWorkItem encapsulated in code that mimics
the delegate.BeginInvoke behaviour.
The OBEX protocol defines a number of object types, these include
Folder Listing, Capability, and Object Profile.
The library provides a Folder Listing parser which returns the items
included in the folder-listing object. The simplest way to use it is
to use the GetFolderListing method on
ObexClientSession,
it returns an ObexFolderListing object which provides two
lists, one of the folders and another of the files in the current folder.
The example here shows
a simple example of displaying the folder listing on the console.
To change the current folder, use the SetPath methods.
Sub DisplayCurrentFoldersListing(sess As ObexClientSession)
Dim listing As ObexFolderListing = sess.GetFolderListing
If (listing.HasParentFolder) Then
Console.WriteLine("<DIR> ..")
End If
For Each folder As ObexFolderItem In listing.Folders
Console.WriteLine("<DIR> {0}", folder.Name)
Next
For Each file As ObexFileItem In listing.Files
Console.WriteLine(" {0}", file.Name)
Next
End Sub
The parser can also be accessed directly through the
ObexFolderListing object in the Brecham.Obex.Objects
namespace. This for instance allows one to access the individual items as
they arrive over the network instead of only all items being returned once
the end of the listing is received.
There are three issues to note with the content of the documents produced by third-party
devices.
During testing it was found that not all devices follow the specification. The Nokia
6670, for instance, produces documents where the folder item can contain
undefined attributes; the parser can be configured to fail or to discard the item
to when it encounters such a situation. Secondly the specification is not clear
what whitespace handling mode should be used if an item includes the Display Name
content,
I have seen no evidence that any device actually includes this information,
currently we include any whitespace.
Finally, again during testing a folder-listing entry from one OBEX server listed
a file whose size was 2,147,483,749 bytes as having instead a size of -2,147,483,547.
So beware of that.
There are also two issues with the encoding of the XML documents. Firstly
that the capabilities of the XmlTextReader in the Compact Framework are less
than in the full framework. The OBEX Folder Listing object is specified in
terms of a DTD and all the examples and real documents that I’ve seen include a
DOCTYPE element referencing the DTD. Unfortunately the XmlTextReader on NETCF
does not support DTDs. Now if it simply didn’t support DTDs and thus ignored
any such declaration (the DOCTYPE element) then all would be fine. But it
doesn’t, when is sees such an element it throws NotSupportedException. So
we have to pre-parse the document and strip any such element. We handle stripping
both the simple DOCTYPE element like
<!DOCTYPE folder-listing SYSTEM "obex-folder-listing.dtd">
and also the more complex form as returned
by the Nokia 6670.
<?xml version="1.0"?>
<!DOCTYPE folder-listing SYSTEM "obex-folder-listing.dtd"
[ <!ATTLIST folder mem-type CDATA #IMPLIED>
<!ATTLIST folder label CDATA #IMPLIED> ]>
<folder-listing version="1.0">
...
This functionality defaults to enabled only on the Compact Framework version; this can be overridden with the static StripDocType property on class ObexFolderListingParser.
Secondly, that various device types incorrectly include null bytes in the documents that they produce. The XmlTextReader classes in the Microsoft supplied class libraries will throw an error if such a byte is read. I’ve seen the fault firstly in the Belkin/Broadcom software on Windows, the documents it produces end with null bytes. This was known and worked-around in Beta 2, the parser being careful to stop reading immediately that it hits the document end element. However, feedback has identified that it may be that documents produced by other devices contain null bytes in the body of the document. Therefore code has been added that will strip all null bytes from a document as it is read. However I have seem no example of this behaviour. Thus, this feature defaults to disabled on both platforms; this can be overridden with the static StripNullBytes property on class ObexFolderListingParser.
Now ideally we wouldn’t have to do any of this
work, regardless of how good our code is, adding it has the natural effect of making
things a bit more fragile than if the XmlTextReader from the FCL handled all the
facilities that we needed (and third-party documents were completely valid).
This is most true for the null-byte stripping. Since this acts on a Stream
it does not know what text encoding the XmlTextReader will detect to use for the
document. In particular if the document is UTF-16 then stripping the nulls
will leave a corrupt document (unless it only contains ASCII characters and has
no byte-order-mark!). So consider carefully before enabling the
StripNullBytes feature.
Finally, the DTDs for both the Folder
Listing and Capability XML objects are included in the library as resources
and can thus be accessed for use in your own application’s XML parsers, either directly or with the supplied XmlResolver
(ObexXmlResolver).
As noted previously the library itself does not handle creating a connection to a peer device and the OBEX server there. Either simple code using IrDAClient / BluetoothClient directly can be used, or the release also includes a separate library to help with connection.
If creating the connection manually, then for IrDA a connection could be created with code as simple as the following:
IrDAClient cli = new IrDAClient("OBEX");
ObexSessionConnection sess = new ObexSessionConnection(cli.GetStream(), 8192);
sess.Connect()
And for Bluetooth as simple as this:
Dim addr As BluetoothAddress = [... pre-selected ...]
Dim cli As New BluetoothClient()
Dim ep As New BluetoothEndPoint(addr, _
InTheHand.Net.Bluetooth.BluetoothService.ObexObjectPush)
cli.Connect(ep)
Dim sess As New ObexSessionConnection(cli.GetStream(), 4096)
sess.Connect()
For further samples of connecting manually see the PutCmdline and VbPutSample.vb samples. Note that
the samples above show how to connect to the default OBEX Inbox / Bluetooth Object
Push Profile service, to connect to the OBEX Folder Browsing / Bluetooth File Transfer
Profile service different parameters are required, see the source of the library discussed below
for example code. In short to connect
to the Folder Browing service, on IrDA one connects to the same Service Name (OBEX)
but must do an OBEX connect to the target ObexConstant.Target.FolderBrowsing,
on Bluetooth one has to connect to the distinct FTP service and again to that OBEX
target, e.g.
BluetoothAddress addr = [... pre-selected ...]
BluetoothEndPoint ep = new BluetoothEndPoint(addr,
BluetoothService.ObexFileTransfer);
BluetoothClient cli = new BluetoothClient();
cli.Connect(ep);
ObexClientSession sess = new ObexClientSession(cli.GetStream(), 4096);
sess.Connect(ObexConstant.Target.FolderBrowsing);
In summary, for the Default/Inbox OBEX server, one uses:
OBEX (or for some servers, fallback to OBEX:IrXfer)BluetoothService.ObexObjectPushObexClientSession.Connect().
and for the Folder Browsing Service, one uses:
OBEXBluetoothService.ObexFileTransferObexClientSession.Connect(ObexConstant.Target.FolderBrowsing).
However, as noted above a separate library that handles this is included in the samples. It includes three classes, one to handle connections in a GUI application, one for console menu-driven applications like the GetFolderListings sample, and one that accepts a URI of a similar format to that used by 32feet.NET’s ObexWebRequest class.
They are all based on that same set of subclasses which implement connection to a peer device, to an OBEX server, and disconnecting and cleaning up. Disconnect and clean-up is implemented using the IDisposable interface, that is through calling a Dispose method. Once connected, the connected ObexClientSession instance is available through a property of the same name.
The library supports Bluetooth, IrDA, and TCP/IP connections. It also includes a Forms user-control, ProtocolComboBox, to all the selection of the protocol to be used.
So examples of their usage would be as the following.
Dim pf As System.Net.Sockets.ProtocolFamily = ProtocolComboBox1.SelectedProtocol
' Where ProtocolComboBox1 is an instance of Brecham.Obex.Net.Forms.ProtocolComboBox
Dim toFolderBrowsingService As Boolean = …a compile-time constant probably…
Dim conn As New GuiObexSessionConnection(pf, toFolderBrowsingService, label1)
If conn.Connect Then Exit Sub
' …use the connection… e.g.
conn.ObexSessionConnection.SetPath("images")
conn.ObexSessionConnection.Put(fileSource, "logo.gif", Nothing)
conn.Dispose
or
using(ConsoleMenuObexSessionConnection conn = new ConsoleMenuObexSessionConnection()){
if (!conn.Connect) { return; }
// …use the connection…
// Dispose is called by the 'using' block
}
See the various sample projects for more samples of usage.
As implied by the samples above the GuiObexSessionConnection constructor takes two arguments: which protocol to use (Bluetooth/IrDA/etc), and whether to connect to the OBEX Folder Browsing service or to the default Inbox service. It can also take a Windows Forms control on which status text will be displayed. The choice of Bluetooth device is made by the user from the Windows Bluetooth devices dialog, since there’s no equivalent dialog for IrDA it currently just chooses the first device, and finally for TCP/IP a simple dialog box is displayed to allow the entry of IP Address of Hostname.
The ConsoleMenuObexSessionConnection asks the user which protocol, device, and which of the two OBEX services to connect to on the console, and also displays any status messages there too.
Finally, as noted above the UriObexSessionConnection constructor takes a
URI in a similar format to that used by the ObexWebRequest class,
differing only in that it allows no path to be set, for instance
obex://12345678/ instead of obex://12345678/filename.vcf,
there are also two new constructors that take an BluetoothAddress or an IrDAAddress
directly.
As noted above the source is included to allow extension or modification if the supplied class do not suit your environment. There are three abstract class below the three concrete classes already described. The inheritance tree is as shown here. We should note that these classes are an appendix to the main library and have not been tested as thoroughly as the library’s code. However they are used in most of the sample programs and have been seen to work well there.
If creating a new concrete class for an OBEX session connection then
inherit from the ObexSessionConnection class, providing
overrides of the ChooseService, ChooseProtocol,
ChoosePeer, and ShowStatus methods. The
other sources will show examples of what each method should do.
Andy Hume