Technetra

Building a Remote Message Monitor for the iPhone

Robert Adkins,  January 3rd, 2009 at 11:29 pm

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.

Copyright © 2008 Technetra. This code is covered by the MIT License. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Introducing Harald - The Ruby Bluetooth Tester Article Index How to Deploy an iPhone Web Application

Comments

One Response to “Building a Remote Message Monitor for the iPhone”

  1. January 7th, 2009 at 3:03 am (Pingback)
    links for 2009-01-06 « Bloggitation Says:

    [...] Building a Remote Message Monitor for the iPhone (tags: ruby programming sinatra web2.0 iphone) [...]

Add a comment