Wednesday, February 6, 2019

No more calculation babysitting!

Let's imagine you work with a computer on a daily basis (which, as it were, is a rather common scenario these days), and let's imagine your work includes "relatively lengthy" computational tasks -- lengthy enough that it is not productive to just sit there staring at the proverbial hourglass cursor (or meditating over the progress bar, or praying over the build output console window that it compiles without errors, or whatever it is). So...

So, after some time waiting (depending on your boredom threshold, about 30 seconds for me, I'd guess under 2 minutes for most people) you decide to multitask and switch to another task (work-related or otherwise). Before long, that other task immerses you and you realize that your lengthy computation was actually done minutes ago and you could have, and should have, resumed your workflow earlier. So in an attempt to avoid doing nothing and increase productivity, you just dumped it down the tubes.

Or consider another scenario. Your computation is now lengthy enough (say, 10-20 minutes) so that you decide to grab a coffee from the kitchen, or grab a quick bite/smoke/chat, or whatever it is. It would be cool if you could get an alert that your task has finished, so you can timely return and resume work without having to check on your workstation multiple times.

Or yet another scenario. You need to run an even lengthier calculation, perhaps a few hours on end, so you leave it running after hours. And you have several of them to run after each other, and you want to run as many as you can before the next working day. So again it would be cool if you can get an alert once your calculation finishes, rather than having to log in and check on the computation  progress repeatedly. More importantly, if your task is aborted early due to some mishap (which happened to me due to my own banana fingers more times than I'd confess), you really want to be alerted at once, rather than after what you thing the task should have taken, had it completed normally.

The first scenario can partially be mitigated by running Windows Task Manager, minimizing it, and noticing when the CPU usage drops, indicating that your task has finished. But you still need to remain vigilant and keep watching that tiny indicator, and with current multicore CPUs, the drop may be from 13% to some 2%, which is not very noticeable visually. Not to mention that the two remaining scenarios cannot be worked around in this way.

Wouldn't it be nice to have an automated monitor which can do this for you?

Yeah it sure would.

So let's see how we can do it in Windows Powershell. You can determine the CPU usage of a process using this function (loosely adapted from here):

function get-excel-CPU ($avgs=1)
{
 $result=0
 for ($i=1; $i -le $avgs; $i++)
 {
  $cpuinfo = Get-WmiObject Win32_PerfFormattedData_PerfProc_Process -filter "Name LIKE '%EXCEL%'"
  $result=$result + ($cpuinfo.PercentProcessorTime |Measure -max).Maximum
  start-sleep -m 150
 }
 $result = $result/$avgs
 $result
}

Note the following:

  • I use EXCEL as an example because I use it most often. It is trivial to modify it to work with any other program.
  • The function takes into account that there can be multiple instances of your program, and will report the CPU usage of the most CPU intensive process. Usually, this is what you want, because only one of your instances will be doing computations anyway, but you can easily fine-tune it to be more instance-specific.
  • The function measures CPU usage several ($avgs) times with a short waiting period in between, and then averages the measurement. This is done because some computations, mostly ones heavily on local or network I/O, will have wildly fluctuating CPU usage, so taking one measurement may trick the script into falsely deciding that the computation has finished. You may need to fine-tune the number of measurements and the wait time between them to reflect your specific computation pattern.

Now that we have a way to automatically determine the CPU usage, we can easily wrap it in a control loop:

$poll = 10
$homethresh = 600
$sensitivity = 5
$fullout = new-timespan -Seconds 86400
$mailout = new-timespan -Seconds $homethresh
$sw = [diagnostics.stopwatch]::StartNew()

$thiscpu = get-excel-cpu 5

if ($thiscpu -lt $sensitivity) 
{
 write-host $thiscpu, ": Excel not running, exiting."
}
else
{
 write-host "Initial CPU is ", $thiscpu
 $cnt=0
 while ($sw.elapsed -lt $fullout)
 {
  start-sleep -s $poll
  $thiscpu = get-excel-cpu 3
  $lock = is-locked
  write-host "Elapsed", $sw.elapsed, " -- CPU is ", $thiscpu, "   ",$lock
  if ($thiscpu -lt $sensitivity) {$cnt++} else {$cnt=0}
  if ($cnt -ge 2)
   {
    write-host "FINISHED!!!!"
     if ($lock -eq "UNLOCKED") {show-splash} else {phone-home}
    return
   }
 }  
 write-host "Timed out!"
}

Note the line if (...) {show-splash} else {phone-home} . There are two ways to alert you that the computation has finished. One is to show you a big splash screen, borrowed from here:

function show-splash 
{
 Add-Type -AssemblyName System.Windows.Forms
 $Form = New-Object system.Windows.Forms.Form
  $Form.Text = "Finished"
  $Form.AutoSize = $True
  $Form.AutoSizeMode = "GrowAndShrink"
  $Form.BackColor = "Lime"
  $Font = New-Object System.Drawing.Font("Arial",96,[System.Drawing.FontStyle]::Bold)
  $Form.Font = $Font
  $Label = New-Object System.Windows.Forms.Label
  $Label.Text = "Calculation finished!"
  $Label.AutoSize = $True
  $Form.Controls.Add($Label)
  $Form.Topmost=$True
  # -- this ensures your splash screen appears on top of other windows!
 $Form.ShowDialog()
}

The other is to simply send you an email that gets pushed to your smartphone or smart watch, borrowed from here (using Outlook rather than Send-MailMessage so that your IT department can safely inspect your outgoing email and won't mistake your script for a trojan):

function phone-home
{
 $Outlook = New-Object -ComObject Outlook.Application
 $Mail = $Outlook.CreateItem(0)
  $Mail.To = "youremailaddress@mailserver.com"
  $Mail.Subject = "Calculation finished"
  $Mail.Body ="Your calculation has finished. If you need to start another one, go for it."
 $Mail.Send()
}

Now, how to choose between the two? You will want the splash screen if you are sitting in front of your screen, and the email otherwise. So we need a way of discriminating between the two. Following this idea, we can use
function is-locked
{
try {
$currentuser = gwmi -Class win32_computersystem | select -ExpandProperty username
$process = get-process logonui -ea silentlycontinue
if($currentuser -and $process){"LOCKED"}else{"UNLOCKED"}
return}
#Always return LOCKED if logged in remotely
catch{"LOCKED";return} }

Finally, here is a BAT-file one-liner wrapper, called ps.bat to run your PowerShell script on systems where execution of random scripts has been disallowed by default (for good reason). We cannot override this default without admin privileges, but we can by pass it temporarily by calling

@powershell -ExecutionPolicy RemoteSigned .\%1.ps1

You can then call your PowerShell script, e.g., poll.ps1, and simply type ps poll in your command prompt to invoke it quickly.

Enjoy!

Friday, February 1, 2019

Electric cabinet lock and other small DIY

Every once in a while, we have people (such as cleaners, babysitters or contractors) who have access to our home while we are away or distracted. And I am perfectly aware that petty theft usually does not happen in these scenarios (one case is enough to ruin the perpetrator's career), we also know that from time to time, against all odds, it does happen. So we needed to come up with an idea of protecting my wife's jewelries from an opportunistic snatch.

In other words, I needed a lock on the jewelry drawer, preferably one that would be discreet, and would not involve drilling the front of the (moderately beautiful) dresser cabinet. 

In a previous edition of this scenario, I accomplished this using two magnetic child locks (like these ones, they come in a huge variety) - to open the drawer you'd need to simultaneously place magnetic keys at two unmarked, previously known spots, which makes for an excellent discrete opening mechanism. But this time the cabinet dimensions proved incompatible with the locks. To add to that, even a casual glimpse of the open drawer, sporting the big white child locks, would instantly reveal our trick. Finally, the mounting holes for magnetic locks look bad even on the inside of the cabinet. (Whoever tries to convince you that these locks would hold on an adhesive mount, has not tried it. Adhesive mounting tape can be strong, but is is invariably terrible for dynamic loads such as repeated banging from trying to open a cabinet with the lock engaged but forgotten about.)

So I was searching for a geometrically suitable lock and stumbled upon this part, which just happened to have just the right dimensions to fit between the back wall of the drawer and the back wall of the cabinet. In addition, an electromagnetic lock has the advantage of not requiring submillimeter-precision alignment between the lock and the armature/striker plate - something that would totally plague most mechanical designs (like this one). 

I already had an unused electric key switch, which could be discretely built into the back wall of the cabinet, totally out of sight yet within easy reach. The only missing piece now was how to power the lock. Using a DC power adapter seemed an easy choice, but it is very easily defeated by unplugging it from the wall. Using batteries (and placing them in the same space between the drawer and the cabinet) was more secure, but with the lock's power consumption of 100 mA, batteries would require replacement every 1-2 days. 

Therefore, an ideal trade-off would be a plugged in adapter with a battery back-up that would take over the lock if the adapter is unplugged. After giving it some thought I came up with this circuit:



The central component here is the relay K1 that connects the battery through the normally closed contacts and disconnects it when external DC power is present. The diode D1 is there to ensure that the battery is not getting charged by the adapter (non-rechargeable batteries don't like that). The diode D2 ensures that the battery is not getting discharged through the output circuitry of the adapter. The diodes D3 and D4 are flyback diodes, which prevent arcing and sparking at the key switch (or the power jack).

Finally, the resistor R1 is the current limiting resistor for the relay coil. It actually proved the most problematic component because of the need to mitigate heat dissipation in the enclosed space behind the cabinet. With some experimenting I found that the relay coil current for reliable operation was 40 mA, yielding 0.32 W of dissipated heat, so when I stupidly put a 0.125W resistor, it got pretty charred very soon. Even a 0.5W resistor was getting worryingly hot. Since I totally don't want my lock to start a house fire - that would be the exact opposite of what a "security lock" is supposed to do - I first hooked up four 800 Ohm, 0.5W  resistors in parallel (2W total). This got the resistors slightly warm to the touch but not hot. Here is how it looks like from the inside and outside:




As an upgrade, I later replaced the 2W resistor arrangement with a 5W component on a heat sink. Now operating at 6% capacity, the heat dissipation was small enough for the lock to run for days on end without getting warm. As an additional precaution, I have installed a thermal fuse designed to cut the AC power circuit should the temperature ever exceed 73 degrees C (this was the lowest value I could get off Amazon). So this is the new set-up:



The beauty of the design, aside from it being totally discrete, is its being both fail-secure and fail-safe. It is fail-secure in the short term, meaning that power outage will keep the lock running on battery power long enough for a malefactor to not want to stick around. On the other hand, the rightful owner can wait several days for the batteries to discharge, and have the cabinet open if the key gets misplaced or lost.


BONUS: Here's some more office DIY. At one of my workplaces, the desk phone used too much useful space on my desk, so I wanted it next to my desk instead. Not wanting to drill any holes in the shiny new company property, I came up with a mount out some stuff lying around in the office, namely:

  • an old cardboard small packet from a recent online order
  • some Scotch tape
  • some good supply of cable ties
  • and a jar of spare furniture bits and pieces, apparently mostly from IKEA. 
This is the "before" and "after" image. If interested, I can give you more detail.




Tuesday, November 20, 2018

Memory leak tester in Excel

In many scenarios involving complicated Excel calculations, especially those relying on extensive VBA code and/or custom-made add-ins, there is a danger that memory leaks could be created by programming errors such as careless object operations.

Most often, such memory leaks are discovered post factum, when, left unchecked, they cause slowdowns or crashes when an Excel session runs out of memory. In such a case, correcting the error becomes a major pain. How to spot a memory leak offender in a calculation intensive spreadsheet containing thousands of custom functions? 

To this end, I have written a very simple program that basically evaluates a given formula multiple times and measures how the memory consumption increased. I have used this googled snippet to determine the memory consumption:

Declare Function GetCurrentProcessId Lib "kernel32" () As Long

Function GetMemUsage()
  ' Returns the current Excel.Application memory usage in KB
  Set objSWbemServices = GetObject("winmgmts:")
  GetMemUsage = objSWbemServices.Get( _
    "Win32_Process.Handle='" & _
    GetCurrentProcessId & "'").WorkingSetSize / 1024

  Set objSWbemServices = Nothing ' We don't want to cause memory leaks here :)
    ' We don't want to cause memory leaks here :)  
End Function

and wrote a very simple wrapper: 

Sub Measure_Leak()
 Set here = ActiveSheet.Range("A2")
 template = here.Value
 i1from = here.Offset(0, 1).Value: i1to = here.Offset(0, 2).Value
 i2from = here.Offset(0, 3).Value: i2to = here.Offset(0, 4).Value
 Set there = here.Offset(0, 5)
 
 there.Offset(0, 1).Value = GetMemUsage()
 
 For i1 = i1from To i1to
 For i2 = i2from To i2to
  working = "=" & template
  On Error Resume Next
   working = Replace(working, "$1", i1)
   working = Replace(working, "$2", i2)
  On Error GoTo 0
  there.Formula = working
 Next i2, i1
 
 there.Offset(0, 2).Value = GetMemUsage()
 Set here = Nothing
 Set there = Nothing
 ActiveSheet.Calculate
End Sub

Now I put this on an even simpler spreadsheet which looks like this:



Basically, pressing the Measure button evaluates the template expression substituting $1 and $2 with values spanning two ranges, a total of (i2t-i2f+1)*(i1t-i1f+1) times. An increased memory footprint at the end of the execution means there is a memory leak.

In the screenshot we see that a built-in Excel function does not cause any memory leaks (hurra!)

To test it, let us define a really leaky VBA function using this example:

' Put this into class module Class1
Option Explicit
Private A As New Class2
Private Str As String
Private Sub Class_Initialize()
   Set A.B = Me ' Fool garbage collector
   Str = Space(1024 * 10) ' Allocate lots of memory 
End Sub

'Put this into class module Class2
Option Explicit
Public B As Class1

with two functions that look similar but one is known to be leaky:

Function vbaNoLeak()
 Dim MyObject As Class2
 Set MyObject = New Class2
 Set MyObject = Nothing
End Function

Function vbaLeak()
 Dim MyObject As Class1
 Set MyObject = New Class1
 Set MyObject = Nothing
End Function

...and...




Note that using this method to troubleshoot parts of your VBA macro won't always work because many functions are prohibited inside VBA functions (they abort immediately, basically ensuring that VBA functions have no side effects). But it is very easy to modify the code above to be callable as a procedure from within a VBA macro.


Tuesday, August 7, 2018

Laurel / Yanny Hands-On

Ever since the Laurel/Yanny auditory illusion went viral, I had a suspicion that we are dealing with a bifurcation-type illusion similar to the "figure or ground" type:


That guess seemed correct with the publishing of a NY Times article where you can "move a slider" to augment the clip in either direction, causing either "Laurel" or "Yanny" to be more pronounced.

I admit that this article is quite revealing and educating (it taught me how I can tweak my brain into hearing one or the other). But moving a slider still seems rather artificial. For all I know they could have been cheating, actually having two separate recordings and the slider mixing them in different proportions.

Can we prove this wasn't cheating? Hands on?

Yes we can.

Some snooping around reveals that here is a working version of the original recording:
https://ia802800.us.archive.org/28/items/YannyVsLaurelVideoWhichNameDoYouHear-Audio/Yanny%20vs%20Laurel%20video%20which%20name%20do%20you%20hear%20%E2%80%93%20audio.mp3

Let's go to Wolfram Cloud Computing and run this simple program:

url="(the url above)"
audio=AudioTrim[Audio[url],{0,4}]
CloudExport[AudioPitchShift[audio,1.1],"wav"] (*Laurel*)
CloudExport[AudioPitchShift[audio,0.85],"wav"] (*Yanny*)

We see that at the heart of it is AudioPitchShift which simply shifts the pitch of the recording by the desired amount. Looks like lower frequencies emphasize "Yanny" while higher frequencies bring out "Laurel", in agreement with the above mentioned Wikipedia article.

My conjecture, based on the observation that "Laurel" is lower-frequency than "Yanny", is that we tend to hear whatever is closer to the maximum frequency sensitivity of our ears, so whoever initially hears "Yanny" likely has an ear for higher-pitched tunes than the "Laurel" guy.



It could have been a bit easier if Wolfram Cloud could actually play audio from within the interface without resorting to CloudExport. Still, it opens nearly endless possibilities for experiments. Enjoy!

Monday, August 6, 2018

Old Phone/Tablet as an Info Board: Table of Contents

Hi folks, now that this long overdue series of posts is complete, here's a table of contents for the ease of the reading.



Enjoy!

TL/DR: This is a series of (moderately boring) posts about how to turn your old and outdated tablet or smartphone into an information board you can use at home. Uses range from purely practical to purely aesthetic.

Old Phone/Tablet as an Info Board Final Part: Flow Control and Asynchronous Dynamic Data Refresh

This is the final post about how to make an information board out of your old tablet or smartphone.

Let's assume you have all your elements that deliver your information as you want it, beautifully designed and tested, again using my board as an example:




OK, the information is up to date when the page has loaded. Now, there comes a question: how to keep all this information reasonably up to date, ready for you at any moment in case you need it? On-demand refresh of any kind is not an option - an info board you'd have to stand in front of, waiting, is a lousy info board.

It would seem easy at first - "just do a META REFRESH", - but there are several pitfalls to think through:

  1. It is obvious that you need to refresh all the elements with some frequency. But how to choose this frequency? Too seldom, your info is out of date half the time. Too frequently, and your board will spend more time refreshing than actually showing the info, and you risk being kicked out by your data sources for DDOS'ing them. 
  2. Further elaborating on the idea, it is clear that some elements need a more frequent refresh than some others: you are quite OK with a weather forecast obtained half an hour ago, but train departures from half an hour ago are totally useless to you. 
  3. Furthermore, the refresh frequency should be time dependent. You don't need frequent traffic updates in the middle of the night, but you do need them during commute hours.
  4. What to do if an element fails to refresh, or gets stuck? Should we perhaps retry sooner than its normal refresh cycle? Then again, what would be the reasonable timeout? Set it too short and you'll mistake normal wait times for "getting stuck".
What it all means that you will need to refresh asynchronously (i.e. some elements but not others), and dynamically (using time intervals that are element and time dependent), and you'll have to have a mechanism to check if the refresh was actually successful or needs to be retried. Also note that some elements (notably those that use jQuery on a hidden iframe) will need a two-step refresh: some method to reload the iframe, and another to process its contents once it has loaded and rendered in DOM (but no sooner).



So, let us begin by introducing a "refresh handler" like so:

var dummydate = new Date();
var rExample = {freq: 30, // refresh every 30 minutes by default
    prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}, // between 7:30 and 8:30 refresh every 5 minutes
            {freq: 15, start:{h:15, m:30}, end:{h:20, m:30}}], // between 15:30 and 20:30 refresh every 15 minutes
    last: dummydate, next: dummydate, fresh: true,  overdue: 0, //internal variables
    lag: 5, retry: 45, //internal constants
    handleRefresh: function(){...}, // refresh function 
    handleReady: function(){...}, // "is ready" function
    handlePost: function(){...} // post-refresh function for 2-stage refresh
    };  


We see that this handler is a self-contained object that handles all refresh code for "something on the board". Then, given an array of these handlers, we define an event poller to be called periodically:

const scale = 1; //seconds per minute: 1 for debugging; 60 for actual use
var REFRESH = [rWeather, rTrain1, rTrain2, rBus, rHourly, rMap, rTravel];

function pollEvent()
{
var now= new Date();
// add force refresh @ 2AM, ONCE

for (var i=0, len=REFRESH.length; i<len; i++)
{
 var handle = REFRESH[i];
 if(now > handle.next) {
 try{
  if (handle.handleReady()) {
    handle.overdue=0;
 var behind = parseInt((now.getTime() - handle.last.getTime())/1000/scale);
 $("#ref"+(i+1)).text(behind);$("#ref"+(i+1)).css("color","green");
  }
  else { console.log(i+" not ready")
        if (handle.overdue > 5) {console.log("***FORCE REFRESH***"); /*location.reload();*/};
  handle.overdue++;
  $("#ref"+(i+1)).text("x");$("#ref"+(i+1)).css("color","darkred");
  };
  
   console.log("refresh "+i); 
   handle.handleRefresh(); handle.fresh = true;
   postprocess(handle);
   handle.last = now;
   handle.next = new Date(now.getTime() + 1000*((handle.overdue==0)?handle.retry:scale*timespan(handle,now)));
   } 
catch(ignore) {handle.overdue++; $("#ref"+(i+1)).text("X");$("#ref"+(i+1)).css("color","red"); handle.next = new Date(now.getTime() + 1000*handle.retry);} 
 }
}
setTimeout(pollEvent,100*scale); // call again, every 6 seconds in production 
};


Here timespan() defines the refresh frequency for a given handler at a given time:


 function timespan(handle,stamp)
{
 var ts = handle.freq;
 for (var j=0; j<handle.prime.length; j++)
 {
  var thisprime = handle.prime[j];
  if (stamp.getHours()>= thisprime.start.h && stamp.getHours()<= thisprime.end.h 
  && ( stamp.getHours()>thisprime.start.h || 
     (stamp.getHours() == thisprime.start.h && stamp.getMinutes()>= thisprime.start.m))
  && ( stamp.getHours()<thisprime.end.h || 
     (stamp.getHours() == thisprime.end.h && stamp.getMinutes()<= thisprime.end.m))
   ) {ts = thisprime.freq;}
 }
 return ts;
}


Now define another function to ensure two-step refresh happens as fast as possible:


function postprocess(handle){ 
 try{
  if(handle.handleReady) {console.log("++"); handle.handlePost();}
  else {console.log("--");setTimeout(function(){postprocess(handle);}, 1000*((handle.fresh)?1:handle.lag)); handle.fresh=false; };
}
 catch(ignore) {}
}


It only remains to define some auxiliary routines


function frameload(frameid){$(frameid).attr("src",$(frameid).attr("src"));}
function nop(){return true;}
function clock(){
var now = new Date(); var h=now.getHours(); var m=now.getMinutes(); var s = now.getSeconds();
$("#nowclock").text(((h>=10)?h:("0"+h)) + ":" + ((m>=10)?m:("0"+m)) + ":" + ((s>=10)?s:("0"+s)));} 


and add an initial invocation upon document's DOM ready:


$( document ).ready(function() 
{
var now = new Date(); var h=now.getHours(); var m=now.getMinutes();
$("#loadclock").text(((h>=10)?h:("0"+h)) + ":" + ((m>=10)?m:("0"+m)));
setInterval(clock, 1000);
setTimeout(pollEvent,3000);
});



That's all. For reference here are the refresh handlers for all the elements:


 var rWeather = {freq: 30, 
 prime: [{freq: 15, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 15, start:{h:15, m:30}, end:{h:20, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 5, retry: 45,
 handleRefresh: function(){f(document, 'script', 'plmxbtn');},
 handleReady: function(){return $("#weather").find(".city").length>0;},
 handlePost:nop };
 
 var rTrain1 = {freq: 120, 
 prime: [{freq: 10, start:{h:5, m:30}, end:{h:9, m:0}}, 
   {freq: 2, start:{h:7, m:45}, end:{h:8, m:20}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 1, retry: 10,
 handleRefresh: function(){frameload("#go1");},
 handleReady: function(){return $("iframe#go1").contents().find(".currentDateMain").length>0;},
 handlePost:trainhide };
 
 var rTrain2 = {freq: 20, 
 prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 1, retry: 10,
 handleRefresh: function(){frameload("#go2");},
 handleReady: function(){return $("iframe#go2").contents().find(".currentDateMain").length>0;},
 handlePost:trainhide };
 
 var rBus = {freq: 20, 
 prime: [{freq: 5, start:{h:6, m:15}, end:{h:9, m:0}}, 
   {freq: 1, start:{h:7, m:45}, end:{h:8, m:15}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 1, retry: 10,
 handleRefresh: function(){frameload("#miway");},
 handleReady: function(){return $("iframe#miway").contents().find("#bustoken").length>0;}, 
 handlePost:nop };
 
 var rHourly = {freq: 60, 
 prime: [{freq: 15, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 15, start:{h:15, m:30}, end:{h:20, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 5, retry: 45,
 handleRefresh: function(){frameload("#forecast");},
 handleReady: function(){return $("iframe#forecast").contents().find("table.wxo-media")
   .find("[headers='header1']").length>0;},
 handlePost:forecast }; // *** add clear if exists
 
 var rMap = {freq: 30, 
 prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 5, start:{h:15, m:30}, end:{h:18, m:00}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 10, retry: 45,
 handleRefresh: refreshMap,
 handleReady: function(){return true;}, 
 handlePost:nop }; // *** add clear if exists?
 
 var rTravel = {freq: 20, 
 prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 5, start:{h:15, m:30}, end:{h:18, m:30}}, 
   {freq: 1, start:{h:7, m:50}, end:{h:8, m:10}}, 
   {freq: 1, start:{h:17, m:00}, end:{h:17, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 5, retry: 45,
 handleRefresh: navigate, 
 handleReady: function(){return ( parseInt($("#result1").text()) > 0 
   && parseInt($("#result2").text()) > 0);}, 
 handlePost:nop }; 




The code also includes forced refresh once daily at approximately 2:00 AM. This is done to prevent an occasional memory leak to screw our browser (we'll be running this 24/7 for months on end, remember?). I am leaving this part as an exercise for the reader - the flow chart is basically, "if the current time is between 2:00 and 3:00, and if the startup day is not the same as the current day, do location.reload()".

Still, I can see that the Playbook browser (or to be exact, an app called Backlight Override, which is just a browser wrapper that additionally prevents the backlight from ever going off) does crash - once every 3-4 weeks. Well, for me, this is a fairly acceptable "mean time between failures", even though anything more frequent than once a week would already border on annoying.


Thursday, August 2, 2018

Old Phone/Tablet as an Info Board Part 5: PHP capture-and-process

Finally, I describe one more method of putting information on the info board, suitable when there is a very limited amount of information you need from the target page, and when your output will have very little, if anything, to do with the target page layout. It is also the only possible method when the server outputs data in any format other than HTML.

The example is our local bus company, where I would like to get the bus departure info for a certain stop. In the browser, the result looks like this:



Now the biggest difficulty about using the previously described methods is the lack of a clearly defined URL associated with the results page - the results are dynamically generated by the parent page's javascript. After some sniffing around, it was possible to extract a working request link in the format
http://www4.mississauga.ca/PlanATrip/NextPassingTimes/RequestNextPassingTimes ?suggestionInputIdentifier=(stop_number) &suggestionInputType=Stop &stopInputIdentifier= &mustBeAccessible=false

which brings up the result in this JSON-like format:



Well, because the departure information I am after is somewhere in this JSON, I decided to parse it entirely in PHP using preg_match_all, extracting route number and arrival time for the next upcoming bus, as well as three more after it, like so:

<?php
date_default_timezone_set('EST');
$opts = array('http'=>array('header' => "User-Agent:MyAgent/1.0\r\n"));
$context = stream_context_create($opts);
$url = "http://www4.mississauga.ca/PlanATrip/NextPassingTimes/RequestNextPassingTimes?suggestionInputIdentifier=1127&suggestionInputType=Stop&stopInputIdentifier=&mustBeAccessible=false";
$timestamp = date("H:i");
$code = file_get_contents($url,false,$context); 
 
$code = strstr($code, 'NextPassingTimesList',false);
$code = strstr($code, 'NextPassingTimesLegend',true);
// now code contains only trip data
$rid = preg_match_all("/data-route-key=([^=]+)data-trip-key/",$code,$matches1);
$tid = preg_match_all("/NextPassingTimesTime ([^N]+)u003c\/div/",$code,$matches2);
for($i=0;$i<$rid;$i++)
{
$route = str_replace("\\","",$matches1[1][$i]); $route = str_replace("\"","",$route); $route = str_replace("r","",$route);$route = str_replace("n","",$route);
$routes[$i] = str_replace("~~East"," E",$route);
 
$time = $matches2[1][$i];
$id1=stripos($time,"u003e"); 
$time = substr($time,$id1+5,strlen($time)-($id1+5)-1);
$sid = stripos($time,"u003c");
if ($sid!==false) {$time = substr($time, 0, $sid-1);}
$times[$i] = $time;
}
$color = (strpos($times[0],"min")===false)?"rgb(255,128,128)":"red";
 
echo '<html><head></head><body>';
echo '<div style="color:rgb(255,128,128); font-family: Arial, Helvetica, sans-serif; font-size: 20px; position: relative; top: -5px;">';
echo '<span style="color:gray;">', $timestamp, '&nbsp;</span>';
echo '<span id="bustoken" style="background: ',$color,'; color:white;">', " ".$routes[0], '</span>',  '<span style="font-weight: bold; color:',$color,'">', "&nbsp;", $times[0], '</span>';
if ($rid > 1 && $tid > 1)
{
    echo "<br/>";
    echo '<span style="color:rgb(255,235,235);">', $timestamp, '&nbsp;</span>';
    for ($j=1; $j<(($rid>4)?4:$rid); $j++)
    {
        echo '<span style="background: rgb(255,128,128); color:white; font-size: 14px;">', " ".$routes[$j], '</span>',  '<span style="font-weight: bold; color: rgb(255,128,128);font-size: 14px;">', "&nbsp;", $times[$j], '</span> &nbsp;';
 
    };
}
echo '</div></body></html>';
?> 


Putting it into our standard PHP iframe like this:
<div id="bus" class="basic info" style="position: absolute; left: 150px; bottom: 270px; width: 415px; height: 50px; background:rgb(255,235,235);">
<iframe id="miway" scrolling="no" src="bus.php" style="height: 100px; width: 415px; border: none;"> </iframe>
</div>


The result looks like:


Note the following tricks:
  • Whenever the real-time information is available ("the bus is leaving in XX minutes"), I am accenting the color to highlight it.
  • I am also outputting the time of the query, so that the "XX minutes" format remains meaningful without having to query every single minute.
  • The line with date_default_timezone_set serves to ensure correct DST for $timestamp.
  • As a future development, I plan to supplement the "XX minutes" format with actual projected time, replacing it with "HH:MM (XX min)" and retaining accentuation.
  • As another exercise, I plan to change color accentuation depending on the remaining time - highlighting buses that are still reachable given the walking/running distance to the stop, and dimming the buses that aren't.

I admit that this method, as implemented, is quick-and-dirty and extremely brute-force, and is rather vulnerable to the underlying data format changes. A much more robust way would be to have the PHP parse the JSON "the proper way" and either process it using dedicated JSON functions, or simply echo the HTML result in its entirety, using jQuery to interact with it. However, my approach has worked surprisingly well for over 6 months already - and "if it ain't broken, won't fix it".