Exploring Bluetooth with Tcl

Published

Taking a break from our series on Tcl 8.7 features, this post is about Bluetooth; in particular, how to discover and connect to services offered by Bluetooth devices.

The illustrative code here uses Tcl on Windows but the concepts and methods should translate easily enough to other languages and platforms as well.

Bluetooth has been around for close to three decades and has evolved into really three standards -- Bluetooth Classic, Bluetooth High Speed and Bluetooth Low Energy (LE). Bluetooth Classic is oriented towards relatively higher speeds up to 2.1Mbps while Bluetooth LE is geared for extrememly low power consumption and longer ranges at the cost of an order of magnitude lower data rates. Bluetooth High Energy is relatively uncommon.

This post focuses on Bluetooth Classic from the perspective of a client system. The code below uses the iocp_bt package from the iocp Tcl extension and assumes the commands have been placed on the namespace path. For convenience, the proc pdict prints a dictionary in an easier to read form.

% package require iocp_bt
0.1
% namespace path [list iocp::bt]
% proc pdict {d} {dict for {k v} $d {puts "$k: $v"}}

Enumerating radios

Supporting Bluetooth communication obviously requires the system to have one or more radios. Communicating generally does not require the client application to specify the radio as most system will only have one and even otherwise, most applications do not care about the radio in use. However, because Bluetooth device names are not unique, devices are uniquely identified by their radio's physical address. Thus it is sometimes necessary to know the radio's address so it can be compared when pairing devices with name conflicts.

The iocp::bt::radios command will list the radios present on the system.

% radios
ac:ed:5c:4b:fa:2c

By default, the command will only returns a list of the system's radio physical addresses. Notice Bluetooth physical addresses are 48-bit values represented similarly to IEEE MAC addresses.

More detail about the radio is available with iocp::bt::radio info.

% pdict [radio info]
Address: ac:ed:5c:4b:fa:2c
Name: IO
Class: 2752780
Subversion: 4096
Manufacturer: 2
MajorClassName: Computer
MinorClassName: Laptop
DeviceClasses: Networking Capturing Audio

Notice we didn't need to specify the radio in the radio info command because there is only one in the system.

Most of the fields above are purely informational. Class is a bit mask whose bits are defined in the Bluetooth specification to broadly classify devices. The MajorClassName and MinorClassName fields are the human-readable translations of these. The DeviceClasses is a list that further categorizes the device categories. The Manufacturer field is a numeric code assigned to each manufacturer and can be looked up online. Subversion is a version identifier that is manufacturer-specific.

The other two fields Name and Address are more meanigful because they can be used to identify the system. These are the values that show up in the dialog when a Bluetooth device is paired. There are two things to note about the name. First, unlike the address, it is not unique and multiple bluetooth devices may have the same name. Second, it is independent of the system name although it is often the same by default. It can be changed programmatically or through the Windows Bluetooth control applet.

Discovering devices

So that's about the local Bluetooth radio. What we are really interested in is the list of remote Bluetooth devices to which we can connect. To discover what devices are in the neighbourhood, a Bluetooth device broadcasts a discovery or device inquiry message. Devices that are close enough to receive a message and do not mind being discovered will respond appropriately. This process can take anywhere up to 15-20 seconds, a major irritant to the author.

From Tcl the iocp::bt::devices command returns a list of Bluetooth devices the system knows about. By default, the system returns the list of devices it already knows about so it will not include devices that have shown up since the last device inquiry message was broadcast. It is therefore advisable to specify the -inquire option to force a new device discovery cycle when calling the command.

% set devices [devices -inquire]
...Output elided...

The above will block for just over 10 seconds though that timeout can be changed through the -timeout option. The command has various filtering options as well. See the manual page for details.

The return value is a list of dictionaries each describing a discovered device. The ::iocp::bt::device print command prints this information for human consumption. To print a single device,

% pdict [lindex $devices 0]
Address: 38:e6:0a:c7:97:6b
Name: APN Phone
Class: 5898764
Connected: 0
Remembered: 1
Authenticated: 1
LastSeen: 2020 4 27 4 37 44 146 1
LastUsed: 2020 4 19 19 29 36 921 0
MajorClassName: Phone
MinorClassName: Smartphone
DeviceClasses: Networking Capturing {Object Transfer} Telephony

Several fields are analogous to those we saw earlier for radios. After all, remove devices are also radios. The additional fields should also be self-explanatory.

Resolving a specific device name

While the above returns a list of all known devices, often one knows the name of the device and only need to resolve it to its physical address to establish a connection. The iocp::bt::device address command returns this information. So to resolve the address of a device named ak,

% set ak_addr [device address ak]
ac:c1:ee:b3:e2:e8

Playing hide and seek

Bluetooth devices can choose to be non-discoverable by not responding to discovery broadcasts. They can also be configured to refuse all incoming connection attempts. From Tcl on Windows, these settings for the local radio can be obtained.

% radio configure [lindex [radios] 0]
-discoverable 1 -connectable 1

The two settings are independent. The behavior of the various combinations is shown below.

Discoverable Connectable Behavior
Yes Yes Other devices can discover and connect to it.
No Yes Cannot be discovered but other devices can connect if they already know the address.
Yes No Other devices can discover but not connect. Look but don't touch!
No No Cannot be discovered or connected to but outgoing connects still possible.

The ::iocp::bt::radio configure command can also be used to change these settings.

Discovering services

Once a device is discovered, an application needs to know what services it offers and how to access those services. Bluetooth defines the service discovery protocol which can be used to query a device for the services it offers. The device then returns a list of service discovery records which contain the requisite information.

From Tcl, this information is obtained with the ::iocp::bt::device services command.

set services [device services ak]
...Binary gobbledygook elided...

To understand the information contained in a servicec record, let us first decode a single record as a dictionary. Note the commands pertaining to service discovery records are contained in the iocp::bt::sdr namespace.

% set record [lindex $services end]
...More binary junk elided...
% pdict [sdr::decode $record]
0: uint 65549
1: sequence {{uuid 00001105-0000-1000-8000-00805f9b34fb}}
4: sequence {{sequence {{uuid 00000100-0000-1000-8000-00805f9b34fb}}} {sequence {{uuid 00000003-0000-1000-8000-00805f9b34fb} {uint 7}}} {sequence {{uuid 00000008-0000-1000-8000-00805f9b34fb}}}}
5: sequence {{uuid 00001002-0000-1000-8000-00805f9b34fb}}
9: sequence {{sequence {{uuid 00001105-0000-1000-8000-00805f9b34fb} {uint 258}}}}
256: text {OBEX Object Push }
512: uint 4101
771: sequence {{uint 1} {uint 2} {uint 3} {uint 4} {uint 5} {uint 6} {uint 255}}

A service discovery record (SDR) contains a set of service attributes that describe the service. An attribute is identified by an 16-bit integer that determines the type and semantics of its associated value. For example, in the above attribute 0 is an unsigned integer with value 65549, attribute 1 is a sequence which, in this example, contains a single element which is of type UUID.

Universal attributes are attributes in the range 0–511. Their definitions (type and semantics) are applicable to all Bluetooth service classes. In addition, a service class may define attributes that are specific to it from outside this range. The semantics of these will of course then differ between service classes.

The above decoded dump is still not very understandable so before moving on let us print the same record in human readable form.

% sdr::print $record
ServiceRecordHandle: 65549
ServiceClassIDList: sequence
    00001105-0000-1000-8000-00805f9b34fb OBEXObjectPush
ProtocolDescriptorList:
    ProtocolStack:
        00000100-0000-1000-8000-00805f9b34fb L2CAP ()
        00000003-0000-1000-8000-00805f9b34fb RFCOMM (7)
        00000008-0000-1000-8000-00805f9b34fb OBEX ()
BrowseGroupList: sequence
    00001002-0000-1000-8000-00805f9b34fb PublicBrowseRoot
BluetoothProfileDescriptorList: sequence
    00001105-0000-1000-8000-00805f9b34fb OBEXObjectPush v1.2
ServiceName: OBEX Object Push
0x200: 4101
0x303: sequence
    1
    2
    3
    4
    5
    6
    255

In the above, universal attributes are printed with their mnemonic equivalent (e.g. the attribute id 0 corresponds to ServiceRecordHandle) while attributes specific to the service class OBEXObjectPush are printed in their numeric form.

Attribute 0, ServiceRecordHandle, is a 32 bit integer that uniquely identifies the service record within the responding device. It is present in every SDR but has no direct relevance for us.

The only other attribute that must be present in every SDR is attribute 1, ServiceClassIDList. This is a list of one or more UUID's, each representing a Bluetooth service class. All attributes contained in the SDR must belong to the set of attributes defined by one of the service classes in this list. The above SDR contains only one service class OBEXObjectPush. (Note this is the mnemonic for the class, the class identifier is the listed UUID.)

Attribute 4, ProtocolDescriptorList, tells us how to connect to the service corresponding to the SDR. The shown list is a protocol stack required to make the connection. RFCOMM is a reliable connection based protocol which is layered over the L2CAP packet based protocol. OBEX in turn is roughly the binary equivalent of HTTP, an application level protocol that runs over RFCOMM. There can be parameters associated with the protocol. In our example, the service is listening on RFCOMM port 7. More on this when we get to actually establishing a connection to the service.

The only other attribute of importance attribute 9, BluetoothProfileDescriptorList. A Bluetooth profile is a set of specifications that define services provided to satisfy a usage scenario. These specifications include the functions that must supported, underlying protocols, authentication modes, service record definitions and so on. Adhering to a profile allows any client for those services to connect to the server. In the above example, the SDR reflects that the corresponding service conforms to version 1.2 of the OBEXObjectPush profile.

Notice that the non-universal attributes 0x200 and 0x302 are shown in numeric form. They have no mnemonic equivalent.

Getting attribute values

Above we printed attribute values in human-readable form. What if you want to programmatically retrieve the value of an attribute? The iocp::bt::sdr::attribute get command is used for this purpose. Note it has to be passed the decoded binary SDR record.

% set sdr [sdr::decode $record]
% sdr::attribute get $sdr ServiceName
OBEX Object Push
% sdr::attribute get $sdr ProtocolDescriptorList
{{Protocol 00000100-0000-1000-8000-00805f9b34fb ProtocolName L2CAP ProtocolParams {}} {Protocol 00000003-0000-1000-8000-00805f9b34fb ProtocolName RFCOMM ProtocolParams {{uint 7}}} {Protocol 00000008-0000-1000-8000-00805f9b34fb ProtocolName OBEX ProtocolParams {}}}

The format of the returned value depends on the attribute. See the command documentation for details.

Connecting to a service

Now we finally get to connecting to a service. It so happens that if you know what service to connect to, you don't need all the ramblings above. You can just resolve the device name and service port number and connect to it.

% set dev [device address ak]
ac:c1:ee:b3:e2:e8
% set port [device port $dev OBEXObjectPush]
7
% set so [::iocp::bt::socket $dev $port]
bt000001E309F0DD30
% fconfigure $so 
-blocking 1 -buffering full -buffersize 4096 -encoding cp1252 -eofchar {{} {}} -translation {auto crlf} -peername {ac:c1:ee:b3:e2:e8 ac:c1:ee:b3:e2:e8 7} -sockname {ac:ed:5c:4b:fa:2c ac:ed:5c:4b:fa:2c 0} -error {} -connecting 0 -maxpendingreads 3 -maxpendingwrites 3 -maxpendingaccepts 0

The Bluetooth socket is now open and can be used just as any other Tcl channel. One caveat — for consistency with Tcl's socket command, the channel uses the system encoding and does line ending translation by default. Since most (not all) Bluetooth application protocols are binary, you generally need to configure the channel accordingly.

Once the connection is open, you can send files, images etc. to the device using the OBEX protocol. However, OBEX is the subject of some future post so for now we just close the channel.

% close $so

Checking for RFCOMM

The current iocp_bt package only supports communication over the RFCOMM protocol. The ProtocolDescriptorList attribute we saw earlier in the SDR record specifies which protocols can be used to access the service. The presence of RFCOMM in that attribute tells us we can use iocp_bt to connect to that service. The device port command will raise an error if this is not the case.

Coming up next ...

The next post in the Bluetooth series will look at the steps required to actually conduct OBEX operations over the Bluetooth connection. This will allow us to push files and other objects to the device.

References

  1. Bluetooth Core Specification

  2. Bluetooth Assigned Numbers and Service Discovery Protocol

  3. Iocp_bt Reference