Technetra

Archive for January, 2009

Introducing Harald - The Ruby Bluetooth Tester

Thursday, January 22nd, 2009

A Domain Specific Language in Ruby for Describing Monitoring and Control Operations on Bluetooth Adapters and Devices

Micro-frameworks for Ruby-based unit testing are everywhere: there’s Test::Unit, RSpec, Shoulda and more.

In other domains such as web application development, there are also a variety of small but highly useful frameworks that have become popular: Sinatra, Merb, Camping, Wave.

Now there is Harald: a nano-framework for testing and monitoring Bluetooth adapters and devices on Linux. However, unlike the hulking Viking stature of its forebear, Harald Bluetooth, this Harald is rather sleek and well-mannered. But following the spirit of its namesake, this Harald (”the Bluetooth Tester”) seeks to simplify and at the same time unify the somewhat random collection of Bluetooth testing tools on Linux into a small Domain Specific Language (DSL).

This article presents a technical summary of the initial version of Harald. This version can be used to investigate, enumerate and test Bluetooth devices under Linux. It also incorporates a simple activity or message monitoring facility to track events and messages related to the Bluetooth adapters and devices attached to your system. Please see Building a Remote Message Monitor for the iPhone for an in-depth example of using this facility.

What is Harald?

Harald is a Ruby DSL for describing monitoring and control operations on Bluetooth adapters and devices in Linux. It is a simple language definition that enables the Bluetooth tester to enumerate components and query their properties. It uses a slightly customized Ruby D-Bus package to communicate cleanly with the BlueZ Bluetooth implementation on Linux. A script written using the Harald DSL consists of a sequence of Harald requests.

Getting Started

To display a listing of the properties of your Bluetooth system’s default adapter just enter the following lines into Ruby’s interactive shell irb or into a Ruby script file:

Listing 1: Show Properties

require 'rubygems'
require 'harald'
 
show "properties" do |result|
  pp result
end

To display a listing of the devices (such as headsets, mice and keyboards) known to the current Bluetooth default adapter enter:

Listing 2: List Devices

# omitting requires
list "devices", :label=>"my devices: " do |result|
  pp result
end

And to change the alias property of a Bluetooth device enter:

Listing 3: Change Alias Property

test_alias = "alter_ego_for_my_device"
set "device_alias", test_alias, :device_address=>"00:1A:45:6A:8B:4B", :label=>"changing alias to \"#{test_alias}\"\n" do |new_value|
  new_value.should_be test_alias
end

Note the use of a ‘should_be‘ assertion clause in the preceding example. The assertion clause, or predicate, is used here to test the equivalence of the new alias value (’new_value‘) with the target value (’test_alias‘) required by the test case. If they are the same, Harald displays the following message:

OK, got expected value "alter_ego_for_my_device"

Grammar

A Harald program is a series of Harald requests, normally bracketed with test reporting instructions and incorporating test assertions or predicates. Each Harald request therefore can be viewed as a simple test case and a series of requests can be regarded as a test scenario.

Listing 4: Harald Program Structure

reset :tests
Harald request 1
Harald request 2
.
.
.
Harald request n
report :passed

The format of a Harald request may be either

<VERB> <PROPERTY> [,<QUALIFIERS>] [<BLOCK>](for get style requests)

or

<VERB> <PROPERTY>, <VALUE> [,<QUALIFIERS>] [<BLOCK>](for set style requests)

depending on whether the it is a get style or a set style request.

VERB and PROPERTY are always required (unless the request is monitor). VALUE is required for set and forbidden for other requests. The other fields are optional. When BLOCK is expanded, the resulting Harald syntax looks like this:

Listing 5: Format of Harald Request

<VERB> <PROPERTY> [,<VALUE>] [,<QUALIFIERS>] [do |result|
  result.<PREDICATE> <some_value>]
end

where,

VERB: The verb field indicates the type of action to be executed. Harald defines action verbs (show, list, set, and monitor) for investigating and manipulating the properties of Bluetooth adapters and devices. Harald also supports a simple test framework using additional verbs such as report and reset.
PROPERTY: Properties are normally attributes of a Bluetooth component which, in this version of the Harald DSL, can be either a Bluetooth adapter, like a USB dongle, or a device, like a headphone or keyboard. In some cases, however, the property field contains a request modifier instead of an attribute. The Harald requests that use this field as a modifier include report, reset, and list.
VALUE: The value field provides the value to be assigned to a property by the set request. This field is required for set. It is not allowed for other Harald requests.
QUALIFIERS:   This optional field contains a dictionary or hash of key/value pairs that provide parameters for the request. For example, the specific adapter to be operated on is identified by the ‘adapter_path‘ key. This might appear as ‘:adapter_path => "/org/bluez/hci0"‘ in a ‘show "address"‘ request (see Listing 6 and Example of Qualifier Field below). The qualifier hash field also contains entries for labels and ids to be associated with the request.
PREDICATE: Harald defines predicates such as should_be, should_not_be and should_match. These predicates may be used with the block parameter (shown as result in Listing 5) returned as the Harald request is completed.

Action Verbs

Each Harald action verb — show, list, set, and monitor — operates on a property. Additional verbs are defined for Bluetooth devices, for example headphones and keyboards. Bluetooth devices connect to adapters. The additional verbs for devices include create_device, remove_device, and discover_services.

show

The following simple example of a show request instructs Harald to fetch the address of the Bluetooth adapter located at path “/org/bluez/hci0″. The address is displayed by printing out the value of the code block’s result parameter.

Listing 6: Show Adapter Address

show "address", :adapter_path => "/org/bluez/hci0" do |result|
  puts "The address of my adapter is #{result}"
end

The output from Listing 6 is:

The address of my adapter is 00:1F:3A:F2:52:C7

For adapters, the properties allowed for show are:

Listing 7: Adapter Properties

    address
    discoverable
    discoverabletimeout
    discovering
    mode
    name
    pairable
    pairabletimeout
    periodicdiscovery
    powered
    requestmode

Each property may be prefixed with ‘adapter_‘ (the default) to indicate that show should be applied to the adapter class of components. A property may be specified as a symbol or quoted string.

For devices, the properties allowed include:

Listing 8: Device Properties

    adapter
    address
    alias
    class
    icon
    name
    paired
    trusted
    uuids

Any device property, such as address, may contain the prefix ‘device_‘ to force Harald to apply the property to the device class of components. In this case, the property address would be written with an explicit prefix as ‘device_address‘. Otherwise, Harald depends upon implicit rules, such as the appearance of a qualifier like ‘:device_address‘ or ‘:device_path‘ in the qualifier hash (see below) to determine the target of the test case. Again, the property address by itself, without a prefix and without a device related key in the qualifier hash, would implicitly be associated by Harald with the system’s default adapter and not a device.

A hash of qualifiers (as shown in the syntax diagram of Listing 5) follows the property field in the show request syntax (but follows the value field in a set request). For some properties the provision of a hash is optional. Generally, qualifiers determine which adapter or device is being targeted as well as supply additional items, such as a label to be printed when the associated test case is executed.

An example hash of qualifiers is:

Listing 9: Example of Qualifier Field

{ :device_address=>"00:1A:45:6A:8B:4B", :label=>"\nTESTING some property: " }

Valid qualifier keys include ‘:device_address‘, ‘:adapter_address‘, ‘:id‘ and ‘:label‘. The qualifier ‘:id‘ optionally identifies a test case (e.g., :id=>"change adapter alias") and can be used to select a test case to run. In addition, for some requests such as ‘list :adapter‘ or ‘show :device_address‘, the qualifier key ‘:adapter_path‘ or ‘:device_path‘ may be given.

The final part of the Harald request is a standard code block identified by the following Ruby syntax:

Listing 10: Request Results Block

do |result| ... end

where ‘result‘ is a Harald Result object that holds the value of the property retrieved (or modified, in the case of set) by the Harald request. The Result object can be printed out or it can be saved in another variable. More commonly, however, it is used in a predicate that constructs an assertion about the expected value of a test case. Three predicates are allowed:

Listing 11: Predicate Methods

RESULT_OBJECT.should_be <some_expected_value | RESULT_OBJECT>

RESULT_OBJECT.should_match <expected_text_pattern>

or

RESULT_OBJECT.should_not_be <some_forbidden_value | RESULT_OBJECT>

These predicates are instance methods of the Result class that evaluate the truth of an assertion. They also increment internal pass and fail counters that can be reported at any time by issuing ‘report :passed‘ or ‘report :failed‘ requests. The counters are initialized or reset with a ‘reset :tests‘ request.

An important and very useful feature of Harald’s Result objects is that they also cascade. That is, most methods understood by the underlying value object contained within a Result object will automatically produce (through the magic of Ruby’s ‘missing_method‘ metaprogramming) a new Result object which can then be tested by the built-in predicate methods. This is illustrated by an example in the monitor section later in the article.

list

The list request is used to retrieve objects from the Bluetooth implementation. For example the following request will enumerate the Bluetooth adapters installed on your system:

Listing 12: List Bluetooth Adapters

list :adapters, :label=>"my adapters are: \n" do |result|
  pp result
end

The output from this example is:

my adapters are:
["/org/bluez/hci0"]

The Harald list request accepts the following targets (Bluetooth objects identified in the property field):

Listing 13: List Targets

    adapter
    adapter_properties
    adapters
    bus
    devices
    manager

These targets are not true properties. Instead, they are treated simply as qualifiers for the list request. They are used as follows:

  • list :adapter and list :bus return a BlueZ Adapter object and D-Bus Bus object respectively.
  • list :adapter_properties returns a Hash of properties associated with a specified (or the default) Bluetooth adapter.
  • list :adapters returns an array of adapter devices currently recognized by the Bluetooth system.
  • list :devices returns an array of device paths currently recognized. Normally these device paths are created during pairing requests, but they also can be created by issuing an appropriate D-Bus message via the Harald create_device request.
  • list :manager returns a BlueZ Manager object (which can be examined with pp, for example).

The list request accepts ‘:label=>"some description"‘ or ‘:id=>"some test case id"‘ in the hash qualifier following the property field. Other qualifier keys are ignored.

set

The set request provides an interface for updating adapter and device properties. Some properties are read-only, like ‘address‘ and ‘discovering‘. However, most properties can be modified by set.

In the following example of set, the current value of the mode property of the default Bluetooth adapter is first displayed in a show request block. Then the mode is turned off and the new mode value is confirmed. Finally the original mode is reset and re-displayed.

Listing 14: Show, Set and Reset Mode of Default Adapter

show :mode do |original_mode|
  puts "my original mode is #{original_mode}"
  set :mode, "off" do |new_mode| new_mode.should_be "off" end
  set :mode, original_mode do |reset_mode| puts "my mode is again #{reset_mode}" end
end

The output generated by this example is:

my original mode is connectable
OK, got expected value "off"
my mode is again connectable

Set operates on most of the same properties of adapters and devices as the show request. Set takes the value given by its third argument and updates its second argument, the specified property, accordingly. The type of the value can be a string, fixnum, or boolean, as required by the native format of the particular BlueZ property that is being modified. The value will be converted to an appropriate type if there is a mismatch. Note, however, that the given representation of the value must be convertible to the native format. For example, the value for property DiscoverableTimeout can be specified either as “123″ or 123. If specified as the string “123″, it will be converted to fixnum for conveyance through D-Bus to BlueZ. As another example, the device property trusted can be specified as true or false (TrueClass or FalseClass respectively). A target value for the property can also be given as the strings “true”, “on” or “yes” each of which will be converted to true. Any other value will be treated as false.

After the set property modification request has been issued, a result is returned as the parameter of the procedure block attached to the set request. The result will reflect the new value of the property as reported by the underlying Bluetooth system. Note that this value may be different than what was given to the set request as a value parameter. For example, an adapter’s mode can be set to connectable by giving the value ‘on’ or ‘connectable’ as the third argument to the set request. But the result will be reported by the BlueZ implementation as the string ‘connectable’ in any case. A good way to see what properties and values your adapter actually provides, as identified by Bluetooth, is to issue a list :adapter_properties request.

monitor

Monitor is perhaps the simplest of all of the Harald requests, as the following example amply demonstrates:

Listing 15: Monitor Request

monitor

There are no parameters and no code block required. All that is needed is to enter the request verb monitor. Of course, the hash qualifiers ‘:id‘ and ‘:label‘ can be optionally provided to allow the request to be selected from the command line or to print out a descriptive message when the request is executed. This request produces an output stream that consists of a continuous flow of messages from the Bluetooth system. The kinds of messages that monitor has registered to receive from the BlueZ implementation are printed out as a banner at the beginning of the monitoring session. They include the signals PropertyChanged, DeviceFound, DeviceDisappeared, DeviceCreated and DeviceRemoved. As devices are paired or removed, or other properties are modified, time-stamped notifications are printed by monitor. A typical session might look like this:

2009-01-20 00:31:10 Setting signals [PropertyChanged, DeviceFound, DeviceDisappeared, DeviceCreated,
DeviceRemoved] for /org/bluez/hci0
2009-01-20 00:31:31 Signal: DeviceRemoved, interface: org.bluez.Adapter, adapter: /org/bluez/hci0,
device: /org/bluez/hci0/dev_00_1A_45_6A_8B_4B
2009-01-20 00:31:31 Signal: PropertyChanged, interface: org.bluez.Adapter, adapter: /org/bluez/hci0,
property 'Devices' changed to ''
2009-01-20 00:36:54 Signal: PropertyChanged, interface: org.bluez.Adapter, adapter: /org/bluez/hci0,
property 'Discovering' changed to 'true'
2009-01-20 00:36:56 Signal: DeviceFound, [ 00:14:51:D3:3C:5B ], properties:     RSSI = -48
    Name = remy's mouse
    Class = 0x002580
    Alias = remy’s mouse
    Icon = input-mouse
    Address = 00:14:51:D3:3C:5B
2009-01-20 00:37:00 Signal: PropertyChanged, interface: org.bluez.Adapter, adapter: /org/bluez/hci0,
property 'Discovering' changed to 'false'
2009-01-20 00:37:03 Signal: DeviceCreated, interface: org.bluez.Adapter, adapter: /org/bluez/hci0,
device: /org/bluez/hci0/dev_00_14_51_D3_3C_5B
2009-01-20 00:37:03 Signal: PropertyChanged, interface: org.bluez.Adapter, adapter: /org/bluez/hci0,
property  'Devices' changed to '/org/bluez/hci0/dev_00_14_51_D3_3C_5B'
2009-01-20 00:37:36 Signal: PropertyChanged, interface: org.bluez.Adapter, adapter: /org/bluez/hci0,
property 'Devices' changed to '/org/bluez/hci0/dev_00_14_51_D3_3C_5B'
.
.
.

A monitor request should appear as the final item in your Harald script because it is designed to produce a continuous stream of notifications and does not return control to the main script once started. If test cases have previously been executed, a ‘report :passed‘ or ‘report :failed‘ request should be placed in the script before calling monitor. The following snippet is an example of this strategy:

Listing 16: Mixing Monitor with Other Actions

reset :tests, :id=>"grp1"
list :adapters, :label=>"my two adapters are: \n", :id=>"grp1_adapters" do |result|
  pp result
  result.length.should_be 2
end
 
show "address", :adapter_path => "/org/bluez/hci1", :id=>"grp1_address" do |result|
  puts "The address of my second adapter is #{result}"
  result.should_not_be nil
end 
report :passed, :id=>"grp1"
 
monitor :id=>"grp2"

The output from this script is:

my two adapters are:
["/org/bluez/hci0", "/org/bluez/hci1"]
OK, got expected value "2"
The address of my second adapter is 00:0A:3A:6F:99:1D
OK, did not get unallowed value ""
Passed 2 of 2 tests
2009-01-20 11:32:30 Setting signals [PropertyChanged, DeviceFound, DeviceDisappeared,
DeviceCreated, DeviceRemoved] for /org/bluez/hci0
^C
2009-01-20 11:32:31 Caught interrupt signal, exiting

In this example, monitor will normally be executed after the two previous test cases have completed and a report of their success or failure is displayed. However, if this script is invoked from the command line with the pattern selection option -p grp2, then only the monitor request will be run. More complex selection patterns can be given, for example -p grp[12] to run both groups of test cases. This is equivalent to writing -p 'grp1|grp2' as well as to the shorter but less descriptive alternative of providing no option for pattern selection at all. Also notice that the should_be predicate of the first test case (’grp1_adapters‘) is preceded by a method call to length. A neat feature of the Result class is that any Ruby method (to which the underlying value of the result object responds) can be applied to the result object and then the return value can be tested with the built-in predicates in the normal way.

create_device

Create_device is used to send a CreateDevice request to the Bluetooth implementation. CreateDevice generates a DBus device path from the device’s MAC address.

In the following example, create_device causes Harald to send a BlueZ CreateDevice message to the system’s default Bluetooth adapter. The result contains a newly minted value for the path of the device.

Listing 17: Create Device

daddr = "00:1A:45:6A:8B:4B" 
create_device daddr, :label=>"creating device at #{daddr}\n" do |result| 
  pp result
end

The output from this request is the following.

creating device at 00:1A:45:6A:8B:4B
#<TestComponent::Result:0xb79ab6b0
 @result=["/org/bluez/hci0/dev_00_1A_45_6A_8B_4B"]>

The create_device request accepts a device address (not path) as its first argument. Alternatively, the device address may be assigned to a ‘:device_address‘ key in the qualifier hash. The result parameter of the code block is a Result object that contains an array with the device path as its single element.

The BlueZ implementation provides additional device creation methods such as CreatePairedDevice and CancelDeviceCreation. These methods may be supported in a later version of Harald.

remove_device

In the next example, a list :devices request is used to find the first Bluetooth device attached to the default adapter. If a device is located, a remove_device request is issued which causes Harald to send a BlueZ RemoveDevice message to the Bluetooth adapter module requesting removal of the device with the specified path. The device is then re-created with create_device and success is tested with a should_be predicate.

Listing 18: Remove and Recreate First Device on Default Adapter

list :devices do |result|
  dpath = result.first
  unless dpath.nil?
    daddr = dpath.sub(/.*dev_/,'').gsub(/_/,':') 
    remove_device dpath, :label=>"removing device at #{dpath}\n"
    create_device daddr, :label=>"creating device at #{daddr}\n" do |result| 
      result.should_be dpath
    end
  end
end

This script displays the following output:

removing device at /org/bluez/hci0/dev_00_1A_45_6A_8B_4B
creating device at 00:1A:45:6A:8B:4B
OK, got expected value "/org/bluez/hci0/dev_00_1A_45_6A_8B_4B"

The remove_device request accepts a device address or path as its first argument. Alternatively, a device address may be assigned to a ‘:device_address‘ key, or a device path may be assigned to a ‘:device_path‘ key, in the qualifier hash. Note that remove_device does not use a code block to post back from the system the results of the operation. In this version of Harald, if a code block is supplied, it is ignored.

discover_services

The discover_services request causes Harald to fetch a device’s service records that match a given UUID. In the following example, the device address is provided in the first parameter and the UUID is given in the second. The result is a hash with an integer key (a BlueZ record handle) and a corresponding XML document that identifies the services associated with the UUID.

Listing 18: Discover Services

discover_services "00:1A:45:6A:8B:4B", "00001108-0000-1000-8000-00805f9b34fb", :label=>"starting service discovery\n" do |result|
  pp result
end

An equivalent request can be written with hash qualifier keys replacing the positional parameters.

Listing 19: Discover Services Using Alternative Syntax

discover_services(:device_address=>"00:1A:45:6A:8B:4B",
                  :service_pattern=>"00001108-0000-1000-8000-00805f9b34fb",
                  :label=>"starting service discovery\n"
                 ) { |r| pp r }

The output of either flavor of discover_services is:

starting service discovery
#<TestComponent::Result:0xb78acac0
 @result=
  [{65537=>"<?xml version="1.0" encoding="UTF-8" ?>
<record>
    <attribute id="0x0000">
        <uint32 value="0x00010001" />
    </attribute>
    <attribute id="0x0001">
        <sequence>
            <uuid value="0x1108" />
            <uuid value="0x1203" />
        </sequence>
    </attribute>
    <attribute id="0x0004">
        <sequence>
            <sequence>
                <uuid value="0x0100" />
            </sequence>
            <sequence>
                <uuid value="0x0003" />
                <uint8 value="0x02" />
            </sequence>
        </sequence>
    </attribute>
    <attribute id="0x0006">
        <sequence>
            <uint16 value="0x656e" />
            <uint16 value="0x006a" />
            <uint16 value="0x0100" />
        </sequence>
    </attribute>
    <attribute id="0x0009">
        <sequence>
            <sequence>
                <uuid value="0x1108" />
                <uint16 value="0x0100" />
            </sequence>
        </sequence>
    </attribute>
    <attribute id="0x0100">
        <text value="Headset" />
    </attribute>
    <attribute id="0x0302">
        <boolean value="true" />
    </attribute>
</record>
"}]>

How Testing Works

Action verbs reset and report and predicates should_be, should_not_be and should_match are the building blocks for automating test scenarios for the Bluetooth system under Linux. These expressions of the test framework specify assertions as well as enable you to summarize the results of arbitrary sequences of test cases.

Test reporting instructions include ‘reset :test‘, which resets the system’s pass/fail counters to 0, and ‘report :passed‘ which prints a simple report about how many tests or assertions have passed. Test assertions or predicates may be attached to the result parameter of the procedure block associated with a request. Each invocation of ‘should_be‘, ‘should_not_be‘ or ‘should_match‘ is an assertion that counts as a single test.

Advanced Examples

The following examples illustrate more sophisticated ways to use Harald’s DSL to help automate testing of the Linux Bluetooth implementation.

Listing 20: Changing, testing and reverting a device alias property

reset :tests
daddr = "00:1A:45:6A:8B:4B"
prop = :device_alias
show prop, :device_address=>daddr, :label=>"\nTESTING #{prop.to_s.upcase}: "  do |old_value|
  puts "my original #{prop}: \"#{old_value}\""
  test_value = "alias-of-jabra-headset"
  set prop, test_value, :device_address=>daddr, :label=>"changing #{prop} to \"#{test_value}\"\n" do |new_value|
      new_value.should_be test_value
  end
  show :device_properties, :device_address=>daddr do |new_props|
    print "my device properties are now: "; pp new_props.result
  end
  set prop, old_value, :device_address=>daddr, :label=>"reverting #{prop} to \"#{old_value}\"\n" do |reset_value|
    reset_value.should_be old_value
  end
end
report :passed

Running this Harald script produces the following output:

TESTING DEVICE_ALIAS: my original device_alias: "Jabra BT125"
changing device_alias to "alias-of-jabra-headset"
OK, got expected value "alias-of-jabra-headset"
my device properties are now: {"Connected"=>false,
 "UUIDs"=>
  ["00001108-0000-1000-8000-00805f9b34fb",
   "0000111e-0000-1000-8000-00805f9b34fb"],
 "Name"=>"Jabra BT125",
 "Trusted"=>false,
 "Class"=>2098180,
 "Icon"=>"audio-card",
 "Alias"=>"alias-of-jabra-headset",
 "Adapter"=>"/org/bluez/hci0",
 "Paired"=>true,
 "Address"=>"00:1A:45:6A:8B:4B"}
reverting device_alias to "Jabra BT125"
OK, got expected value "Jabra BT125"
Passed 2 of 2 tests

In the next example, each legal value for every adapter property is tested and then scores for the number of tests passed and failed are reported at the end. This kind of test scenario is especially useful for regression testing. Note that some adapter property names have been deprecated in recent versions of BlueZ. These are identified as “unknown adapter properties” by Harald in the current version of BlueZ being tested. For example, the properties Pairable and PairableTimeout are now called Discoverable and DiscoverableTimeout. Test cases that use the new names for these properties work.

Listing 21: Automate production of test cases for standard adapter properties

reset :tests
STANDARD_ADAPTER_PROPERTIES.each do |prop|
  show prop, :label=>"\nTESTING #{prop.to_s.upcase}: "  do |old_value|
    puts "my original #{prop}: \"#{old_value}\""
    STANDARD_ADAPTER_PROPERTY_SIGNATURES[prop.to_s].each do |test_value|
      next if test_value.nil?
      set prop, test_value, :label=>"changing #{prop} to \"#{test_value}\"\n" do |new_value|
        new_value.should_be test_value
      end
    end
    set prop, old_value, :label=>"restoring #{prop} to \"#{old_value}\"\n" do |reset_value|
      reset_value.should_be old_value
    end
  end
end
report :passed
report :failed

This Harald script produces the following output:

TESTING PAIRABLE: Unknown adapter property: pairable

TESTING NAME: my original name: "harald-0"
changing name to "temp_name_0"
OK, got expected value "temp_name_0"
restoring name to "harald-0"

OK, got expected value "harald-0"

TESTING ADDRESS: my original address: "00:1F:3A:F2:52:C7"
restoring address to "00:1F:3A:F2:52:C7"

Cannot set address property of adapter /org/bluez/hci0
OK, got expected value "00:1F:3A:F2:52:C7"

TESTING PAIRABLETIMEOUT: Unknown adapter property: pairabletimeout

TESTING MODE: my original mode: "connectable"
changing mode to "connectable"
OK, got expected value "connectable"
changing mode to "off"
OK, got expected value "off"
restoring mode to "connectable"
OK, got expected value "connectable"

TESTING DISCOVERABLE: my original discoverable: "false"
changing discoverable to "on"
OK, got expected value "on"
changing discoverable to "off"
OK, got expected value "off"
restoring discoverable to "false"
OK, got expected value "false"

TESTING DISCOVERABLETIMEOUT: my original discoverabletimeout: "5"
changing discoverabletimeout to "123"
OK, got expected value "123"
restoring discoverabletimeout to "5"
OK, got expected value "5"

TESTING POWERED: my original powered: "true"
changing powered to "on"
OK, got expected value "on"
changing powered to "off"
OK, got expected value "off"
restoring powered to "true"
OK, got expected value "true"

TESTING DISCOVERING: my original discovering: "false"
restoring discovering to "false"
Cannot set discovering property of adapter /org/bluez/hci0
OK, got expected value "false"

TESTING REQUESTMODE: Unknown adapter property: requestmode

TESTING PERIODICDISCOVERY: Unknown adapter property: periodicdiscovery
Passed 15 of 15 tests
Failed 0 of 15 tests

In examining this listing, note that ‘RequestMode’ and ‘PeriodicDiscovery’, as well as ‘Pairable’ and ‘PairableTimeout’ mentioned previously, are deprecated properties in the latest BlueZ implementation. Also note that adapter properties ‘Address’ and ‘Discovering’ are read-only, a fact we are notified about in the listing above by the “Cannot set PROPERTY…” messages.

Installing Harald

A gem for the version of Harald described in this article can be downloaded at harald-0.1.0.gem. After downloading this package, run ‘gem install harald-0.1.0.gem‘ to deploy it to your local gem directory. To use Harald you will also need to install the Ruby dbus package available from trac.luon.ne. The latest Subversion package for Ruby dbus can be obtained via ‘svn co https://svn.luon.net/svn/ruby-dbus/trunk ruby-dbus‘.

Running Harald

Running Harald is simple. Just execute the Ruby file containing your Harald script with the standard ruby command:

ruby YOUR_HARALD_SCRIPT.rb

A request or test case selection pattern can be entered using the -p option following the Harald script file name.

ruby YOUR_HARALD_SCRIPT.rb -p selection_pattern

All requests whose ids match the given pattern will be executed. If no selection pattern is provided, all requests will be run. Note that if a selection pattern is provided, then requests that do not have ids will not be executed. That is, when a selection pattern is given, only those requests that have positive matching ids will be eligible to run. The request id is the optional ‘:id‘ element of the qualifier hash parameter attached to a request. Test scenarios or suites can be generated by providing a regular expression that matches shared id patterns.

Summary

Harald is already a useful DSL for automating a variety of component tests against the BlueZ implementation of Bluetooth on Linux. However, many features of BlueZ (like agents and services) are not yet addressed by Harald. Still, we hope that this initial, limited DSL can form the basis for building more comprehensive and powerful tools to test and monitor the Bluetooth world on Linux. We also hope that Harald will serve as an inspiration for developing other Ruby-based DSLs suitable for system and infrastructure interaction.

Building a Remote Message Monitor for the iPhone

Saturday, January 3rd, 2009

Implementing a Web Application to Track Bluetooth Device Status

In our previous articles in the iPhone developer series, we covered building a simple iPhone Web App and deploying an iPhone web application. This third article presents a more complex as well as more useful iPhone web application. With this application we enable the iPhone to monitor messages from a remote desktop or server system. I’ve selected monitoring Bluetooth devices as the working example in this article. This same web app could be, and has been, adapted for other dedicated monitoring purposes as well — from tracking syslog messages and Apache webserver log entries to viewing any kind of periodically updated data source. Figure 1 below illustrates the user interface in portrait orientation:

Figure 1: Setup and Monitor screens in portrait orientation

Figure 1: Setup and Monitor screens in portrait orientation

Technology Stack

The components required to build a remote log or message monitor include a server-side data collector, a communications interface, and a mobile client application. The overall system takes advantage of technologies suited for each kind of component. For the server-side data collector, we will implement a Linux-based solution using Ruby. Ruby offers a rich array of interfaces to basic data sources across many different operating environments. To build the simple data collector required for our example, Ruby has a reasonably complete interface to the D-Bus interprocess communications system which provides the underlying message transport service for Linux’s implementation of Bluetooth, Bluez. Ruby also offers a wide selection of frameworks and domain specific languages for web services including Rails, Sinatra, Merb, etc. We’ll choose Sinatra for its simplicity, especially in expressing REST and AJAX interactions. For the iPhone mobile client, we’ll select iUI for its edge-to-edge native application look and feel.

Application Design

The iPhone iUI-based application layout is extremely simple. The application has a main window (shown in the 2nd panel of Figure 1 above) that displays messages captured from the Bluetooth monitoring module running on the remote server. Messages are formatted by JavasScript for the particular iPhone orientation (landscape or portrait) currently in effect. The main window displays a Stop/Start button in the top left corner and a Setup button in the top right corner. Pressing the Setup button slides in a new panel (see 1st panel of Figure 1) to permit changes to the message update timers and text highlight patterns used by the application.

The message update timers control how frequently AJAX fetches log messages from the server. The Busy Timeout value (default 1 second) is used to rapidly collect messages when the server has data to provide. The Idle Timeout value (default 10 seconds) is used when no message has been received recently from the server. This permits a back-off of requests issued during periods of inactivity.

The Setup panel also allows the user to register a regular expression locally for highlighting matching areas of text in the messages received from the server. The highlight color (default is yellow) can be modified as well.

Finally there is a toggle switch (showing off a nice iUI feature) used to turn on or off log collection activity.

Figure 2 below presents an overview of the architecture of the remote message monitor system.

iPhone Remote Message Monitor System Overview

Figure 2: iPhone Remote Message Monitor System Overview (click to enlarge)

The following sections describe some of the highlights of the system. Code listings for each major component are included.

Mobile Client Side

JavaScript/iPhone highlights

  • Complex message highlighting and word wrapping in native JavaScript. See wrap and subspan functions in the following listing.
  • Pattern matching for highlighting is case insensitive and supports user entry of any Javascript regular expression.
  • Peaceful co-existence of custom JavaScript event handlers with iUI’s event handlers.
  • JavaScript is used to interact with CSS selectors and properties.

Listing 1: JavaScript Message Retrieval and Processing: monitor_bt.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
WINDOW_INNERWIDTH = 320;
/* Note: the value of AJAX_REQUEST is set in source HTML file:
 * "get_log" for normal operation
 * "test_log" for testing
 */
(function () {
/* Program Variables */
     var BUSY_TIMEOUT = 1000;
     var IDLE_TIMEOUT = 10000;
     // var MATCH_PATTERN = "9\\s*0123456";
     var MATCH_PATTERN = "";
     var MATCH_COLOR = "FFFF00";
     var ACTIVE_FLAG = true;
     var UPDATE_OBJECT_LIST = [];
     var SET_TIMEOUT_OBJ;
     var XDE = unescape('%de');
     var XEE = unescape('%ee');
     var PXDE = RegExp(XDE, "g");
     var PXEE = RegExp(XEE, "g");
/* Device Variables */
     var BODY_LANDSCAPE = 400;
     var BODY_PROFILE = 240;
     var CURRENTWIDTH = -1;
/* Setup Variables */
     var SETUP_BUSY_TIMEOUT="", SETUP_IDLE_TIMEOUT="";
     var SETUP_MATCH_PATTERN="text-to-match", SETUP_MATCH_COLOR="";
/* Helpers */
     var $ = function(id) { return document.getElementById(id); };
     window.ibtmon = {
       getInnerWidth: function() {
         return (navigator.userAgent.indexOf('iPhone')!=-1)
           ?
             window.innerWidth
           :
             WINDOW_INNERWIDTH;
       },
       loadHandlers: function() {
         addEventListener("click", function(event) {
           function turnOffButtons() {
             $('stopButton').style.display = "none";
             $('customBackButton').style.display = "none";
             $('setupButton').style.display = "none";
             $('setupChange').style.display = "none";
             $('setupCancel').style.display = "none";
           }
           if (event.target.id == "stopButton") {
             ibtmon.toggleStartStop(event.target)
           } else if (event.target.id == "setupButton") {
             turnOffButtons();
             clearTimeout(SET_TIMEOUT_OBJ);
             $('setupChange').style.display = "inline";
             $('setupCancel').style.display = "inline";
             ibtmon.updateVisibles();
             UPDATE_OBJECT_LIST = [];
           } else if (event.target.id == "setupChange") {
              var i, item;
              for (i in UPDATE_OBJECT_LIST) {
                item = UPDATE_OBJECT_LIST[i];
                item.action(item.arg)
              }
              turnOffButtons();
              ibtmon.prefer_setup_values();
              if (ACTIVE_FLAG) {
                setTimeout("ibtmon.getLog()", BUSY_TIMEOUT); 
              }
              $('stopButton').style.display = "inline";
              $('setupButton').style.display = "inline";
              history.back();
           } else if (event.target.id == "setupCancel") {
              turnOffButtons();
              if (ACTIVE_FLAG) {
                setTimeout("ibtmon.getLog()", BUSY_TIMEOUT);
              }
              $('stopButton').style.display = "inline";
              $('setupButton').style.display = "inline";
              history.back();
           }
           /* event.preventDefault(); */
         }, false);
         CURRENTWIDTH = ibtmon.getInnerWidth();
         var pgTitle = ((CURRENTWIDTH==320)?"BT":"Bluetooth")+" Monitor";
         $('properties').setAttribute("title", pgTitle);
         $('pageTitle').innerHTML = pgTitle;
         ibtmon.configure_initial_setup_values();
         ibtmon.updateVisibles();
         setInterval(ibtmon.checkOrientation, 300);
         ibtmon.fetchLogging();
       },
       configure_initial_setup_values: function() {
         if (BUSY_TIMEOUT != "") { SETUP_BUSY_TIMEOUT = BUSY_TIMEOUT; }
         if (IDLE_TIMEOUT != "") { SETUP_IDLE_TIMEOUT = IDLE_TIMEOUT; }
         if (MATCH_PATTERN != "") { SETUP_MATCH_PATTERN = MATCH_PATTERN; }
         if (MATCH_COLOR != "") { SETUP_MATCH_COLOR = MATCH_COLOR; }
       },
       prefer_setup_values: function() {
         if (SETUP_BUSY_TIMEOUT != "") { BUSY_TIMEOUT = SETUP_BUSY_TIMEOUT; }
         if (SETUP_IDLE_TIMEOUT != "") { IDLE_TIMEOUT = SETUP_IDLE_TIMEOUT; }
 
         if (SETUP_MATCH_PATTERN != "" && SETUP_MATCH_PATTERN != "text-to-match") {
           MATCH_PATTERN = SETUP_MATCH_PATTERN;
         }
         if (SETUP_MATCH_COLOR != "") { MATCH_COLOR = SETUP_MATCH_COLOR; }
       },
       updateVisibles: function() {
         $('busy_timeout_id').value = BUSY_TIMEOUT;
         $('idle_timeout_id').value = IDLE_TIMEOUT;
         if (MATCH_PATTERN == "" || MATCH_PATTERN == "text-to-match") { 
           $('match_pattern_id').value = "text-to-match";
         } else {
           $('match_pattern_id').value = MATCH_PATTERN;
         }
         $('match_color_id').value = MATCH_COLOR;
         $('toggle_active_flag_id').setAttribute("toggled", ACTIVE_FLAG);
       },
       checkOrientation: function() {
         if (ibtmon.getInnerWidth() != CURRENTWIDTH) {
           CURRENTWIDTH = ibtmon.getInnerWidth();
           var pgTitle = ((CURRENTWIDTH==320)?"BT":"Bluetooth")+" Monitor";
           $('properties').setAttribute("title", pgTitle);
           $('pageTitle').innerHTML = pgTitle;
           ibtmon.prefer_setup_values();
           ibtmon.updateVisibles();
         }
       },
       subspan: function(str, left) {
         var firstlp = str.indexOf(XDE);
         var firstrp = str.indexOf(XEE);
         var lastlp = str.lastIndexOf(XDE);
         var lastrp = str.lastIndexOf(XEE);
         var spanned_str = str;
         if (lastlp > lastrp) {
           spanned_str = spanned_str + XEE;
         }
         if (firstrp < firstlp) {
           spanned_str = XDE + spanned_str;
         }
         spanned_str = "<span style='left: "+left+"px; position: relative;'>"+
           spanned_str+"</span>";
         return spanned_str;
       },
       wrap: function(str) {
         function fltlen(s) {
           var l = s.length;
           var pct = 0, c;
           for (var i = 0; i< l; i++) {
             c = s.charAt(i);
             pct += (c == XDE || c == XEE) ? 1 : 0;
           }
           return l - pct;
         }
         function fltslice(str, start, stop) {
           var len = str.length;
           var fltstr = "";
           var c;
           var i=0, j=0;
           while (i < start && j < len) {
             c = s.charAt(j++);
             if (c != XDE && c != XEE) { i++; } 
           }
           while (i < stop && j < len) {
             c = s.charAt(j++);
             fltstr += c;
             if (c != XDE && c != XEE) { i++; } 
           }
           return fltstr;
         }
         var j, s, r;
         var b = "<br />";
         var body = document.getElementsByTagName('body')
         var m = (body[0].getAttribute("orient") == "landscape")? 40 : 25;
         if (MATCH_PATTERN.length > 0) {
           var re = RegExp(MATCH_PATTERN, "gi");
           str = str.replace( re, function(hit) {
             var expanded_str="", i;
             for(i=0; i<hit.length; i++) {
               expanded_str += XDE + hit.charAt(i) + XEE;
             }
             return expanded_str;
           } )
         }
         s = str;
         var left = 0, p;
         r = "";
         while (fltlen(s) > m) {
             result = (p = fltslice(s, 0, m + 1).match(/\S*(\s)?$/))[1] ?
               m : fltlen(p.input) - fltlen(p[0]);
             j = (result == 0) ? m : result;
             r += ibtmon.subspan(fltslice(s, 0, j) + 
               (fltlen(s = fltslice(s, j, s.length)) ? b : ""), left);
             left = 20;
         }
         r += ibtmon.subspan(s+b,left);
         if (MATCH_PATTERN.length > 0) {
           var t = r.replace(PXDE, "<span style='background-color: "+MATCH_COLOR+";'>");
           r = t.replace(PXEE, "</span>");
         }
         return r;
       },
       getLog: function() {
         var req = new XMLHttpRequest();
         req.onreadystatechange = function() {
           var fetch_interval;
           if (req.readyState == 4) {
             var logdiv = $('logs');
             var resp = req.responseText;
             if (resp.length > 0) {
               logdiv.innerHTML += "<span class='log_line'>"+ibtmon.wrap(resp)+"</span>"; 
               logdiv.scrollTop += logdiv.scrollHeight;
               fetch_interval = BUSY_TIMEOUT;
             } else {
               fetch_interval = IDLE_TIMEOUT;
             }
             if ($('stopButton').text == 'Stop') {
               SET_TIMEOUT_OBJ = setTimeout("ibtmon.getLog()", fetch_interval);
             }
             scrollTo(0, 1);
           }
         }; 
         req.open("GET", "/"+AJAX_REQUEST, true);
         req.send(null);
       }, 
       toggleStartStop: function(obj) {
         if (obj.text == 'Stop') {
           ibtmon.stopLogCollection(obj);
         } else { 
           ibtmon.startLogCollection(obj);
         }
       },
       startLogCollection: function(obj) {
         obj.innerHTML = 'Stop';
         ACTIVE_FLAG = true;
         setTimeout("ibtmon.getLog()", BUSY_TIMEOUT); 
         $('toggle_active_flag_id').setAttribute("toggled", ACTIVE_FLAG);
         $('logMonitorStatus').innerHTML = "Active"
         $('logMonitorStatus').style.color = "green"
       },
       stopLogCollection: function(obj) {
         clearTimeout(SET_TIMEOUT_OBJ);
         obj.innerHTML = 'Start';
         ACTIVE_FLAG = false;
         $('toggle_active_flag_id').setAttribute("toggled", ACTIVE_FLAG);
         $('logMonitorStatus').innerHTML = "Inactive"
         $('logMonitorStatus').style.color = "red"
       },
       fetchLogging: function() {
         setTimeout("scrollTo(0, 1)", 1000);
         setTimeout("ibtmon.getLog()", 200);
       }, 
       /* setup functions */
       scheduleSetupChange: function(act, obj) {
         var item = new Object();
         item.action = act;
         item.arg = obj;
         UPDATE_OBJECT_LIST.push(item);
       },
       setBusyTimeout: function(o) {
         SETUP_BUSY_TIMEOUT = o.value;
       },
       setIdleTimeout: function(o) {
         SETUP_IDLE_TIMEOUT = o.value;
       },
       setMatchPattern: function(o) {
         SETUP_MATCH_PATTERN = o.value;
       },
       setMatchColor: function(o) {
         SETUP_MATCH_COLOR = o.value;
       },
       toggleActiveFlag: function(o) {
         if (o.getAttribute("toggled") === "true") {
           ACTIVE_FLAG = true;
           $('stopButton').innerHTML = 'Stop';
           $('logMonitorStatus').innerHTML = "Active"
           $('logMonitorStatus').style.color = "green"
         } else {
           clearTimeout(SET_TIMEOUT_OBJ);
           ACTIVE_FLAG = false;
           $('stopButton').innerHTML = 'Start';
           $('logMonitorStatus').innerHTML = "Inactive"
           $('logMonitorStatus').style.color = "red"
         }
       }
     };
})();

Listing 2: Stylesheet for Monitor and Setup Screens: monitor_bt.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
.property_row_key { left: 6px; font-size: 12px; font-weight: bold; position: absolute; }
.property_row_value { left: 140px; font-size: 12px; position: absolute; }
ul.panel_util { padding: 0px; }
 
#logs { width:100%; overflow-y:auto; text-wrap: normal; }
body[orient="landscape"] #logs { height: 180px }
body[orient="profile"] #logs { height: 300px }
 
ul.panel_util > li {
  position: relative;
  margin: 0;
  padding: 8px 0 8px 10px;
  font-size: 20px;
  font-weight: normal;
  list-style: none;
} 
 
ul.panel_util > li:not(:first-child) {
  border-top: 1px solid #E0E0E0;
 
} 
 
ul.panel_util > li > a {
  display: block;
  margin: -8px 0 -8px -10px;
  padding: 8px 32px 8px 10px;
  text-decoration: none;
  text-align: left;
  color: inherit;
  background: url(/images/listArrow.png) no-repeat right center;
}
 
#customBackButton {
  display: none;
  left: 6px;
  right: auto;
  padding: 0;
  max-width: 55px;
  border-width: 0 8px 0 14px;
  -webkit-border-image: url(/images/backButton.png) 0 8 0 14;
} 
 
#stopButton {
  position: absolute;
  left: 6px;
  right: auto;
  margin: 0;
  border-width: 0 5px;
  padding: 0 3px;
  width: auto;
  -webkit-border-image: url(/images/toolButton.png) 0 5 0 5;
}
 
.row > textarea {
  box-sizing: border-box;
  -webkit-box-sizing: border-box;
  margin: 0;
  border: none;
  /*padding: 12px 10px 0 110px;*/
  height: 200px;
  font-size: 16px;
  width:100%;
  background-color:#FFFF00;
}
 
input.setupTextItem {
  padding: 12px 10px 0 170px;
}
 
.log_line {
  font-family: monospace;
}

Server Side

Ruby/Sinatra highlights

  • Use of Sinatra’s configure section to implement data source monitoring in a background thread.
  • Data collection and distribution strategy: a main HTML page is served to the iPhone client as a normal web page which incorporates AJAX callbacks for message updates. Periodic requests are issued from the client (monitor_bt.js) via AJAX and serviced by Sinatra with response snippets.
  • A useful message test service is also implemented. Please see the code listing below for detailed discussion.

Listing 3a: Sinatra-based Web Service: monitor_bt.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
require 'rubygems' 
require 'sinatra'
require 'dbus'
include DBus
require File.expand_path(File.dirname(__FILE__) + '/dbus-helper')
#require 'ruby-debug'
 
configure do
  # Sinatra's configure block is executed once when the application
  # is launched.
  $log_lines = Queue.new
  def log msg
    t = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    $log_lines.enq "#{t} #{msg}"
  end
  # Include and execute the customized Bluetooth monitor in a separate
  # thread. This monitor references the log method above to enqueue
  # messages onto the Ruby queue instance $log_lines.
  #
  require File.expand_path(File.dirname(__FILE__) + '/monitor_bt_for_web')
  MonitorBluetooth::MonitorBluez.new.run
  #
  # Actually, any record oriented message producer can be used
  # as a data source. For example, replacing the
  # "require ...monitor_bt_for_web" lines above with
  # the following code, will monitor syslog. 
  # require 'file/tail'
  # Thread.new {
  #   File::Tail::Logfile.open("/var/log/syslog") do |logf|
  #     logf.backward(0).tail { |line| log line }
  #   end
  # }
end
 
get "/" do
  # For normal operation, put "http://your.server.location:4567/"
  # into the URL bar of the iPhone. This Sinatra route displays
  # the main HTML page configured for normal message display, in our
  # case, Bluetooth monitoring. See 'get "/get_log"' block below.
  @which_callback = "get_log"
  erb :index
end
 
get "/test" do
  # For testing, put "http://your.server.location:4567/test"
  # into the URL bar of the iPhone. This Sinatra route displays
  # the main HTML page configured for requesting test
  # messages (see the 'get "/test_log"' block below).
  # Note that Bluetooth or any other monitoring continues in
  # the background. Any accumulated monitoring messages can be
  # retrieved by requesting the normal non-test HTML page
  # (see 'get "/"' above).
  @which_callback = "test_log"
  erb :index
end
 
get "/get_log" do
  # Ajax request callback route configured by issuing
  # "http://your.server.location:4567/" from the web client.
  # The iPhone client should display Bluetooth D-BUS messages
  # collected by the Bluez Bluetooth monitor thread running
  # in the configure block above.
  #
  read_last_log
end
 
get "/test_log" do
  # Ajax request callback route configured by issuing
  # "http://your.server.location:4567/test" from the web client.
  # The iPhone client should repeatedly display the complete
  # message returned by this block (AAA...<words>...ZZZ), including
  # proper word wrap. A custom Javascript wordwrap utility (with
  # highlighting) is imported from monitor_bt.js. This utility
  # formats a whole number of words per display line based upon
  # device orientation. It will cut words in the middle when they
  # are longer than the current screen width. In portrait orientation
  # (320px width), you should see the following pattern displayed in
  # monospaced font:
  #
  # AAA 0123456789
  # abcdefghijklmnopqrstuvwxy
  # z 0123456789 0123456789
  # 0123456789 0123456789
  # 0123456789 0123456789
  # 0123456789
  # ABCDEFGHIJKLMNOPQRSTUVWXY
  # Z_ABCDEFGHIJKLMNOPQRSTUVW
  # XYZ 0123456789 0123456789
  # 0123456789 0123456789
  # 0123456789 012345678 ZZZ
  #
  # In landscape orientation (480px width), the iPhone should
  # display:
  #
  # AAA 0123456789
  # abcdefghijklmnopqrstuvwxyz 0123456789
  # 0123456789 0123456789 0123456789
  # 0123456789 0123456789 0123456789
  # ABCDEFGHIJKLMNOPQRSTUVWXYZ_ABCDEFGHIJKLM
  # NOPQRSTUVWXYZ 0123456789 0123456789
  # 0123456789 0123456789 0123456789
  # 012345678 ZZZ
  # 
  "AAA 0123456789 abcdefghijklmnopqrstuvwxyz 0123456789 "+
  "0123456789 0123456789 0123456789 0123456789 0123456789 "+
  "0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ_ABCDEFG"+
   "HIJKLMNOPQRSTUVWXYZ 0123456789 0123456789 "+
   "0123456789 0123456789 0123456789 012345678 ZZZ"
end
 
helpers do 
  def read_last_log
    result = ""
    unless $log_lines.empty? # avoid blocking
      result = $log_lines.deq
    end
    result
  end
end
 
use_in_file_templates!
 
__END__
 
@@ index

Listing 3b: Sinatra-based Web Service: monitor_bt.rb In-file HTML Template

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<html>
  <head>
    <meta name="viewport" content="width=device-width; initial-scale=1.0;
       maximum-scale=1.0; user-scalable=0;"/>
    <style type="text/css" media="screen">@import "/stylesheets/iui.css";</style>
    <script type="application/x-javascript" src="/javascripts/iui.js"></script>
    <style type="text/css" media="screen">@import "/stylesheets/monitor_bt.css";</style>
    <script>AJAX_REQUEST = <%= "'#{@which_callback}'" %></script>
    <script src="/javascripts/monitor_bt.js"></script>
  </head> 
  <body onLoad="ibtmon.loadHandlers();">
 
    <div class="toolbar">
      <h1 id="pageTitle">Bluetooth Monitor</h1>
      <a id="stopButton" class="button" href="#dummy" style="display: inline;" 
         target="_self">Stop</a>
      <a id="customBackButton" class="button" href="/" target="_self" 
         onClick="history.go((navigator.userAgent.indexOf('iPhone') > -1) ? 
         -2 : -1);return false;">Back</a>
      <a id="setupButton" class="button" href="#setup" style="display: inline;" 
        target="_self">Setup</a> 
      <div>
        <a id="setupChange" class="button" href="#dummy" 
          style="display: none; right: 72px;">Change</a> 
        <a id="setupCancel" class="button" href="#dummy" 
          style="display: none;">Cancel</a> 
      </div>
    </div>
 
    <div id="properties" class="panel" title="Bluetooth Monitor" selected="true">
      <h2 class="setupItem">Log Monitor: 
      <span id="logMonitorStatus" style="color: green;">Active</span></h2> 
      <fieldset>
        <div class="row" style="text-align: left;">
          <div id="logs"></div>
        </div>
      </fieldset>
    </div> 
 
    <div id="setup" class="panel" title="Setup&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;">
      <h2 class="setupItem">Log Fetch Intervals</h2>
      <fieldset>
        <div class="row" style="text-align: left;">
          <label class="setupItem">Busy Timeout (ms.)</label>
          <input type='text' name='busy_timeout' id='busy_timeout_id' 
            class="setupTextItem" 
            onChange="ibtmon.scheduleSetupChange(ibtmon.setBusyTimeout,this)" 
            value="1000" tabindex="1" /><br />
          <label class="setupItem">Idle Timeout (ms.)</label>
          <input type='text' name='idle_timeout' id='idle_timeout_id' 
            class="setupTextItem" 
            onChange="ibtmon.scheduleSetupChange(ibtmon.setIdleTimeout,this)" 
            value="10000" /><br />
 
        </div>
      </fieldset> 
      <h2 class="setupItem">Highlight Pattern in Log</h2>
      <fieldset>
        <div class="row" style="text-align: left;">
          <label class="setupItem">Match Pattern</label>
          <input type='text' name='match_pattern' id='match_pattern_id' 
            class="setupTextItem" 
            onChange="ibtmon.scheduleSetupChange(ibtmon.setMatchPattern,this);" 
            onFocus="this.value='';" value="text-to-match" /><br />
          <label class="setupItem">Match Color</label>
          <input type='text' name='match_color' id='match_color_id' 
            class="setupTextItem" 
            onChange="ibtmon.scheduleSetupChange(ibtmon.setMatchColor,this);" 
 
            value="FF0000" /><br />
        </div>
      </fieldset> 
      <div class="row" style="text-align: left;">
       <label class="setupItem">Collect Logs</label>
       <div class="toggle" 
         onclick="ibtmon.scheduleSetupChange(ibtmon.toggleActiveFlag,this);" 
         id="toggle_active_flag_id"><span class="thumb"></span>
         <span class="toggleOn">ON</span><span class="toggleOff">OFF</span></div>
      </div>
    </div>       
 
  </body> 
</html>

Bluetooth highlights

  • We converted the Bluez 4.25 “monitor-bluetooth” test application from Python into Ruby.
  • Implementing and restructuring the code in Ruby allows easy integration into Sinatra’s web micro-framework.
  • In the process, we have added several more D-Bus Bluetooth signals to monitor and report.

Listing 4: Bluez Bluetooth-Monitor Module (Ruby version): monitor_bt_for_web.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
module MonitorBluetooth
 
  MGR_SIGNALS = [ "AdapterAdded", "AdapterRemoved", "DefaultAdapterChanged" ]
  DEV_SIGNALS = [ "PropertyChanged",
                  "DeviceFound",
                  "DeviceDisappeared",
                  "DeviceCreated",
                  "DeviceRemoved" ]
 
  class MonitorBluez
    #def log msg
    #  t = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    #  puts "#{t} #{msg}"
    #end
 
    def dbus_dev_sig_setup bus, path
      begin
        log "Setting signals [%s] for %s" % [ DEV_SIGNALS.join(', '), path ]
        for signal in DEV_SIGNALS
          begin
            obj = bus.get_object('org.bluez', path)
            adapter = SystemBus::interface(obj, 'org.bluez.Adapter')
            mr = MatchRule.new.from_signal(adapter, signal) 
            bus.add_match(mr) { |sig| 
              case sig.member
              when "PropertyChanged"
                property, value = sig.params
                log "Signal: %s, interface: %s, adapter: %s, property '%s' changed to '%s'" % 
                  [ sig.member, sig.interface, sig.path, property, 
                  (value.class == Array)?value.join("\n"):value ]
              when "DeviceFound"
                address, properties = sig.params
                prop_list = []
                properties.each { |key, value|
                  fmt = (key == "Class") ? "    %s = 0x%06x" : "    %s = %s"
                  prop_list <<  fmt % [key, value]
                }
                log "Signal: %s, [ %s ], properties: %s" % 
                  [ sig.member, address, prop_list.join("\n") ]
              when "DeviceCreated"
                address = *sig.params
                log "Signal: %s, interface: %s, adapter: %s,  device: %s" % 
                  [ sig.member, sig.interface, sig.path, address ]
              when "DeviceRemoved"
                address = *sig.params
                log "Signal: %s, interface: %s, adapter: %s,  device: %s" % 
                  [ sig.member, sig.interface, sig.path, address ]
              else
                log "Device signal: %s, details: %s" % [ sig.member, sig.inspect ]
              end
            } 
          rescue Exception => e
            log 'Failed to setup device: %s for signal: %s, error: %s' % [path, signal, e]
            exit 1
          end
        end
      rescue Exception => e
        log 'Failed to setup signal handler for device path: %s' % e
        exit 1
      end 
    end
 
    def dbus_mgr_sig_setup bus, manager
      begin
        for signal in MGR_SIGNALS
          mr = MatchRule.new.from_signal(manager, signal) 
          bus.add_match(mr) { |sig| 
            log "Interface: %s, adapter: %s, action: %s" % 
              [ sig.interface, sig.params, sig.member ]
            dbus_dev_sig_setup bus, *sig.params if sig.member == "AdapterAdded"
          } 
        end
      rescue Exception => e
        log 'Failed to setup signal handler for manager path: %s' % e
        exit 1
      end 
    end
 
    def run
      Thread.new {
        begin
          bus = DBus::SystemBus.instance
          manager = DBus::SystemBus::interface(bus.get_object("org.bluez", "/"), 
            "org.bluez.Manager") 
          path = *manager.DefaultAdapter
          adapter = DBus::SystemBus::interface(bus.get_object("org.bluez", path), 
            "org.bluez.Adapter")
        rescue Exception => e
 
          log 'Failed to setup Bluetooth monitor: %s' % e
          exit 1
        end 
 
        dbus_dev_sig_setup bus, path
        dbus_mgr_sig_setup bus, manager
 
        trap("SIGINT") { puts; log "Caught interrupt signal, exiting"; exit }
        trap("SIGTERM") { puts; log "Caught termination signal, exiting"; exit }
        main_loop = Main.new 
        main_loop << bus
        main_loop.run
      }
    end
 
  end
 
end

Conclusion

Overall, this web application demonstrates how to integrate a mobile client with a live data source. On the client side, we built a dynamic client that performs a single activity and allows the control of several of its operational parameters. It also demonstrates how to use AJAX to communicate with a web service that periodically provides message updates. On the server-side, we developed an interface to the messaging service which we exposed through a simple REST DSL written in the Ruby-based Sinatra micro-framework. The server’s major limitation, however, is that it does not currently handle multiple users because there is only one internal queue from which to serve messages. A straightforward enhancement would be to utilize multiple session-based message queues. Each user would then have their own view of the ongoing message traffic.

The complete source code for this application, in the form of a Sinatra project, is available at remote_msg_monitor.tar.gz.


Update: Building a Remote Message Monitor for the iPhone with Session Support

Jan 6, 2009 — The following version (see Listings 5a & 5b below) of our Sinatra-based message monitor has been updated to handle session-based queuing. This permits multiple users to share a single message collection feed. Furthermore, it employs a cleanup strategy that discards abandoned queues identified by having too many outstanding messages. If the number of messages accumulates beyond a specified maximum, then the overfull queue is assumed to be inactive and is deleted. If the client returns later, the absence of a corresponding queue re-initializes their session with a new queue. Note that Sinatra’s current implementation of session support requires cookies. Fortunately, only the id of the queue, a very small value, and not the queue itself needs to be kept in the cookie on the client. Storage and management of the queue itself is the sole responsibility of the server side program.

Listing 5a: Sinatra-based Web Service with Session Support: monitor_bt_with_session_support.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# Session-based Bluetooth Monitor
#
# monitor_bt_with_session_support.rb
#
# This version of the message monitor handles session-based
# queuing, allowing multiple users to share a single message
# collection feed. It also discards abandoned queues based on
# too many outstanding messages. If the number of messages
# accumulates beyond MAX_QUEUE_ENTRIES, then the overfull
# queue is assumed to be inactive and is deleted. Later, if
# the client returns, the absence of a corresponding queue
# re-initializes the session with a new queue.
#
require 'rubygems' 
require 'sinatra'
require 'dbus'
include DBus
require File.expand_path(File.dirname(__FILE__) + '/dbus-helper')
#require 'ruby-debug'
 
configure do
  # Sinatra's configure block is executed once when the application
  # is launched.
  enable :sessions
  $message_listeners = {}
  $message_listeners_mutex = Mutex.new
  MAX_QUEUE_ENTRIES = 50
  def log msg
    t = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    $message_listeners_mutex.synchronize {
      $message_listeners.each { |k,v| v.enq "#{t} #{msg}" }
      # assume client is no longer listening
      # if session queue exceeds MAX_QUEUE_ENTRIES
      $message_listeners.delete_if {|k,v| v.length > MAX_QUEUE_ENTRIES }
    }
  end
  # Include and execute the customized Bluetooth monitor in a separate
  # thread. This monitor references the log method above to enqueue
  # messages onto a Ruby queue instance for each session.
  #
  require File.expand_path(File.dirname(__FILE__) + '/monitor_bt_for_web')
  MonitorBluetooth::MonitorBluez.new.run
  #
  # Actually, any record oriented message producer can be used
  # as a data source. For example, replacing the
  # "require ...monitor_bt_for_web" lines above with
  # the following code, will monitor syslog. 
  # require 'file/tail'
  # Thread.new {
  #   File::Tail::Logfile.open("/var/log/syslog") do |logf|
  #     logf.backward(0).tail { |line| log line }
  #   end
  # }
end
 
get "/" do
  check_session
  @which_callback = "get_log"
  erb :index
end
 
get "/get_log" do
  # Ajax request callback route configured by issuing
  # "http://your.server.location:4567/" from the web client.
  # The iPhone client should display Bluetooth D-BUS messages
  # collected by the Bluez Bluetooth monitor thread running
  # in the configure block above.
  #
  read_last_log
end
 
helpers do 
  def read_last_log
    result = ""
    check_session
    $message_listeners_mutex.synchronize {
      log_lines = $message_listeners[session[:listener]]
      if log_lines.nil?
        session[:listener] = nil
        result = "expired session, restarting on next request"
      elsif log_lines.empty? # avoid blocking
        result = ""
      else
        result = log_lines.deq
      end
    }
    result
  end
  def check_session
    if session[:listener].nil?
      $message_listeners_mutex.synchronize {
        $message_listeners[(q = Queue.new).object_id] = q
        session[:listener] = q.object_id
      }
    end
  end
end
 
use_in_file_templates!
 
__END__

Listing 5b: Sinatra-based Web Service with Session Support: monitor_bt_with_session_support.rb In-file HTML Template — Same as Listing 3b.

© 2000-2009 Technetra. All rights reserved. Contact | Terms of Use

WordPress