Building a Remote Message Monitor for the iPhone
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
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.
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
wrapandsubspanfunctions 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 "> <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.


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) [...]