Showing posts with label troubleshooting. Show all posts
Showing posts with label troubleshooting. Show all posts

Thursday, May 14, 2020

Old Phone/Tablet as an Info Board: Update 1 - The sneaky Mississauga buses

Since I wrote my original series 2 years ago, my custom made info board has proved a (moderately) trusty friend and a time saver, especially on busy mornings. It does need a manual refresh every now and then (roughly once every 1-2 weeks), and the Playbook itself needs a reboot once every couple of months, but this is part of regular maintenance and hey, the thing is still running fine on a year-2011 Playbook and a 2013 DiskStation, which is, in and of itself, a proof that I did a decent job, and an occasion to celebrate - so cheers :)

However, nothing is forever, especially on the Internet. Over time, the board started losing components, which prompted more in-depth maintenance and code revision. So I am writing a series of update posts to list and describe that maintenance.

One of the components that bit the dust lately was the departure board for the buses at our nearby bus stop (see my earlier post on how I did it, I will be referencing it often.). Contrary to my fear that my brute-force string parsing of a JSON in PHP would give me debugging headaches, it actually proved remarkably robust. Something totally different happened: the query for the JSON was simply giving me Error 404. Apparently, the company just scrapped the service and seems to have migrated to a wholly different platform. Well, as I said, nothing on the Internet is forever, so I needed to re-implement the web scraping for the next bus departure times.

I will forgo the description of a 3-hour long troubleshooting spent in Chrome's web inspector feature (mostly because it happened too long ago for me to recover the details of it); suffice it to say that I ended up finding a new request in this format:
https://www.triplinx.ca/en/NextDeparture/NearByNextDeparture?stopId=(6-digit stop ID)
resulting in the following:


Sweet. A simple Inspect shows a clear document structure:


I wasn't inclined to modify my "bake the page directly in PHP" method, but I could now afford to parse the document somewhat more intelligently, like so:

function getElementsByClass(&$parentNode, $tagName, $className) {
    $nodes=array();
    $childNodeList = $parentNode->getElementsByTagName($tagName);
    for ($i = 0; $i < $childNodeList->length; $i++) {
        $temp = $childNodeList->item($i);
        if (stripos($temp->getAttribute('class'), $className) !== false) {
            $nodes[]=$temp;
        }
    }
    return $nodes;
}

$url = "https://www.triplinx.ca/en/NextDeparture/NearByNextDeparture?stopId=$stop";
$code = file_get_contents($url,false,$context); 

$routes=array(); $times=array();$rid=0;
$dom = new DOMDocument();
@$dom->loadHTML($code);
$list_node=$dom->getElementsByTagName("ul")->item(0);
$items=$list_node->getElementsByTagName("li");
for($i=0;$i<$items->length;$i++)
{
  $thisraw = $items->item($i)->nodeValue;
  // set route - we are in the outer LI
  $token1 = "Bus"; $pos1 = strpos($thisraw,$token1)+strlen($token1)+2;
  $token2 = "Show"; $pos2 = strpos($thisraw,$token2)-2;
  $rawroute = substr($thisraw,$pos1,$pos2-$pos1);
  if (strlen($rawroute) <= 5) 
  {$thisroute=$rawroute; }
  else // we are in the inner LI, so get time for the already set route
  {
 $thisattr=0; $thisattr += (strpos($thisraw,"real time")!==FALSE) ? 1 : 0; // real time attr
  $attr[] = $thisattr;    
  $diritem = getElementsByClass($items->item($i),"span","next-departure-label")[0];
  $direction = $diritem->nodeValue;
        $direction = str_replace("towards ","", $direction);
  $routes[] = $thisroute . substr($direction,1,1) . " ";
  $timitem = getElementsByClass($items->item($i),"span","next-departure-duration")[1];
        $thistime = $timitem->nodeValue;
        $thistime = str_replace("<", "",$thistime); // filter < 1 minute:: gone anyways 
        $thistime = str_replace("<", "",$thistime);
        $entity = htmlentities($thistime, null, 'utf-8');
        $thistime = str_replace(" ", " ", $entity); 
        $thistime = html_entity_decode($thistime);
 $times[] = $thistime;
  $rid++;
  }
}

where I borrowed the function getElementsByClass() from StackOverflow; the snippet aimed at getting rid of all &nbsp;'s was likewise borrowed from there.

As a result I was now getting an array of bus routes and bus times in string arrays $routes[] and $times[] respectively. However, I still needed to sort my buses by departure time rather than by route as in the screenshot above; the trick is that some of the times are given as "5 min" while some others are like "2:35 pm".

I got around the problem by determining the "number of minutes till all departures" and storing it in $nummin[], like so

date_default_timezone_set('US/Eastern');
$rawtime = new DateTime();
$rid = (($rid>6)?6:$rid);
for ($j=0; $j<$rid; $j++)
{
  if (strpos($times[$j],"min")!==false)
  {
   $nummin[$j] = intval(str_replace("min","",$times[$j]));
  }
  else
  {
   $temptime = new DateTime($times[$j]);
   $fulltime = getdate($temptime->getTimestamp());
   $temptime->setDate($fulltime['year'],$fulltime['mon'],$fulltime['mday']);
   $nummin[$j] = intval(($temptime->getTimestamp() - $rawtime->getTimestamp())/60);
   if($nummin[$j]<0)$nummin[$j]+=(24*60); // add a day if needed
  } 
}

and then using it to bubble sort all three arrays (6 elements aren't worth doing anything more sophisticated):

for ($i=0; $i<$rid; $i++)
 for ($j=0; $j<$i; $j++)
  if($nummin[$i]<$nummin[$j])
  {
   $temproute = $routes[$i]; $routes[$i] = $routes[$j]; $routes[$j] = $temproute;
   $temptime = $times[$i]; $times[$i] = $times[$j]; $times[$j] = $temptime;
   $tempmin = $nummin[$i]; $nummin[$i] = $nummin[$j]; $nummin[$j] = $tempmin;
  }

For output, we would need the reverse operation, i.e. converting "in 5 minutes" to a valid departure time. The reason is that we can't afford to pull the timetable every single minute, so labels like "in 5 min" would very soon mean "in 5 minutes, as of 3 minutes ago" which isn't very convenient to use. To get around this confusion, I have employed the following trick:

for ($j=0; $j<$rid; $j++)
 {
  if (strpos($times[$j],"min")!==false)
  {
   $live = $times[$j];
   $mins = intval(substr($live,0,strpos($live," min")));
   $newtime = (clone $rawtime);
   $newtime->modify("+{$mins} minutes");
   $bustime = date("H:i",$newtime->getTimeStamp());
   $times[$j] = $bustime . " (" . $live . ")";
  } 
 }
if (strlen($times[0])<4) $times[0]= $timestamp." (now)";

Note the line with $newtime = clone $rawtime. It is very important that $newtime is cloned, otherwise $newtime->modify(...) will modify $rawtime and our code will work, but will produce a wrong timetable! Also note that the last line is a patch to ensure that "<1 min" is captured as "now" regardless of possible parsing errors upstream (spoiler: there are errors upstream).


As an afterthought, as I have both the number of minutes and time for all departures, I decided to output them both, breaking up the elements and giving them distinct IDs for future access. Here's how:
$css= ' style="font-weight: bold; color:rgb(255,128,128);"';
for ($j=0; $j<$rid; $j++)
{
 if (strpos($times[$j],"(")!==false)
 {$times[$j] = str_replace('(','</span><span id="busreal'.$j.'" '.$css.'>(',$times[$j]); }
 else { $times[$j] =    $times[$j].'</span> <span id="busreal'.$j.'" '.$css.'>('.$nummin[$j].' min)';}
 if ($j==0) $css=str_replace(';"','; font-size: 14px;"',$css);
}
. . .
echo '<span id="bustoken" style="background: ',$color,'; color:white;">', " ".$routes[0], '</span>',  '<span id="bustime0" style="font-weight: bold; color:',$color,'">', " ", $times[0], '</span>', '  <span id="realtime" style="color: ',$color,'; ">[...]</span>';
if ($rid > 1)
{echo "<br/>";
 echo '<span style="color:rgb(255,235,235);">', $timestamp, ' </span>';
 for ($j=1; $j<$rid; $j++)
  {echo '<span id="busnum'.$j.'" style="background: rgb(255,128,128); color:white; font-size: 14px;">', " ".$routes[$j], '</span>',  '<span 
   id="bustime',$j,'"  style="font-weight: bold; color: rgb(255,128,128);font-size: 14px;">', " ", $times[$j], '</span>  ';
  };
} 

You can surely notice that the "breaking up" thing was really an afterthought and is not neat code at all. I will probably yell at myself for doing it this ugly when I decide to refactor this in another 2 years. :)


So, here is the final result:


And this is how it looks like in an embedded form:



What's with all the different colours, you ask, what does "leave in..." mean, and why is formatting so different? I bet you guessed it: the 16:31 bus is already too soon to catch, and the 16:33 can be caught just barely, if you leave now and make haste. In my next post, I am going to describe how I achieved this on the client side.

Sunday, May 10, 2020

Computer surgery, DVD player laparoscopy, and even more debugging without a debugger

Pre-prologue: Computer surgery

Long story short: the battery on my old Mac gave up the ghost in a relatively ugly way and I had to put a bit of effort for that ghost not to become a fire spirit. In one picture worth a thousand words, this is what happened:


Which meant that the only computer with a DVD drive was severely crippled, and I was getting fed up with hooking a computer to the TV whenever there was a "please Daddy may we watch..." So I decided to buy a simple standalone DVD/Blu-ray player.

Prologue: DVD players and world travellers

The trouble was, I have lived on 2 continents in 5 different countries (at one time, simultaneously), and while doing so, a hodge-podge mini collection of DVDs from at least 3 different regions materialized on my living room shelf. While I fully understand the region mechanism so that a new blockbuster may hit the box offices at different times in different parts of the world, this protection kind of makes zero sense for titles (or worse, indies) released around the world long ago. Of course there's VLC and dvdlibcss but hey, this whole thing was done to spare the hassle of having to connect a computer to the TV to play a disc.

In other words, I needed a region freeing or region switching mechanism for my player. I researched some forums before buying, and it seemed that at least *some* players of this kind could play discs from around the world; also there were already "multi-region" players of this kind freely available for sale on Amazon and third-party websites (presumably with modified firmware), so I thought that it might be worth to try my luck as well.

Expectedly but disappointingly, the unit was not multi-region out of the box. None of the methods "off the internet" worked either. None of the "magic" remote control codes. Some more in-depth sites suggested burning a special CD-R to enter the "advanced settings mode", but this did not work for me at all (and people were writing in the forum that this CD-R method was good for any player except North American, which was my case). The forums even mentioned that the only way was to physically get into the player by soldering some wires to its serial interface (yikes: I am not that big a fan of soldering  and my few circuits even tend to work better on a breadboard than in soldered form), so it seemed that I had to use the computer route for anything outside North America...

Entry route and initial troubleshooting

...or not? Further search got me to a page mentioning what is called "the Pandora exploit". Here's the essence of it:

$ cat /mnt/rootfs_normal/usr/local/bin/pandora/pandora.sh

#!/bin/sh
#

(...)

if [ -e /mnt/sda1/PandoraApp ]; then
    (...)  
    /mnt/sda1/PandoraApp -qws -display directfb
elif [ -e /mnt/sdb1/PandoraApp ]; then
    (...)
    /mnt/sdb1/PandoraApp -qws -display directfb
else
    (...)
    /usr/local/bin/pandora/PandoraApp -qws -display directfb 
fi
What this means is that when the player is asked to launch the Pandora app (I had no idea what this was, at the time), the player looks for PandoraApp executable in the following directories, in that order: /mnt/sda1/, /mnt/sdb1/, and finally /usr/local/bin/. Which is to say that if you put a shell script called PandoraApp in the root folder of a USB stick, plug it into your player, and launch Pandora, it will execute that script instead of the app.

This was worth trying out. Pandora isn't available in Canada so there was no menu item to call it, but as soon as I changed the player's country to US, that menu item happily appeared in the Premium menu of the player (which holds all the "streaming app" capabilities). Provided the player was connected to the Internet (i.e. had wi-fi set up and running), I was able to get to a nice splash screen where I was happily informed that, well, "Pandora isn't available in your country".

The exploit page mentioned setting up a reverse shell, but this seemed to me too complicated in a home network situation full of dynamic IPs and firewalls. Instead, what I put on the SD card was

ls > /mnt/sda1/check_a.txt; ls > /mnt/sdb1/check_b.txt;

This time, running Pandora resulted in the USB stick LED blinking once and then the player froze with a black screen, forcing me to do a hard reboot. The USB stick nicely contained the file check_a.txt, holding root directory listing, and confirming that the drive was indeed mounting under /mnt/sda1.

Next time, what I put on the drive was:

ls -alR > /mnt/sda1/dirlist ;
/usr/local/bin/pandora/PandoraApp -qws -display directfb;

The last line ensured that the Pandora app was launched after what I instructed the player to do, allowing me to return to the main menu and avoiding the need for a hard reset. After a few unsuccessful tries, I got a nice full listing of the player's file system.

The reason for "a few tries" turned out to be that I was accidentally using a defective USB drive which, due to its old age or/and compatibility issues, had a corrupted file system that took the player forever to list. So, kind of "bad luck". However, I had a compensating "good luck" that the first stick I tried ended up working - the newer ones did not, even the first script wasn't being executed at all, presumably because newer drives were mounted under some different path. Had I started with a non-working stick, I might well have thought that the exploit didn't work anymore, most likely got patched in the newer firmware, and would have given up any further tries... the lesson of this was not to give up and "throw it until it sticks" a few more times.


Analysis and solution

Okay, I have the full listing of the player's system, now what? 

I spent some time carefully studying the listing, and after a few red herrings stumbled onto this:

./mnt/ubi_boot/var/local/acfg:
total 108
drwxr-xr-x 2 root root    232 Jun  1  2014 .
drwxr-xr-x 6 root root    424 Jan  1  1970 ..
-rw-r----- 1 root root 107677 Jun  1  2014 config_file.txt

So copying the file to the USB stick for investigation by doing

cp -f -v /mnt/ubi_boot/var/local/acfg/config_file.txt /mnt/sda1/ ;
/usr/local/bin/pandora/PandoraApp -qws -display directfb;

and opening the copied file in a hex viewer such as hiew immediately gave me this;

 Promising. Let us zoom in for a closer look:

Looking at the byte signature after each option, it seemed like the first non-zero byte marked something like the beginning of the data section (always 01); the second marked the length of the data (01 for one byte, 02 for two bytes etc.), and finally the last byte stored the relevant information (e.g. FF at 0x50C stands for 255 years Blu-ray age restriction).

At this point many would say "Got it!" and rush to change the bytes at 0x3E3 and maybe 0x408 from 01 to 00... well, not so fast. Remember the exploit only works when you have a bootable player! The file is not encrypted, that much we can see, but what if there is a checksum somewhere that needs to match with any edit of the file? A mismatch may, at best, trigger a factory reset, and at worst, brick the player. So I changed an option from the Settings screen, for example the above mentioned age restriction to, like, 128 years. Re-dump the file, and...

>fc /b config_file.txt config_file1.txt

Comparing files config_file1.txt and config_file.txt  0000050C: 80 FF

OK, we are safe at least on this front. 
For editing, you could either buy the full version of Hiew, or use Recordman which is from the same author. Here goes, for example, 


The modification of ..._BDAGE is needed for us to be able to visually confirm that the new file has taken effect; the modification of ..._REGIONFREE is just for good measure, I have no idea what it does. Store it as config_file_fix.txt, and do the following...


cp -f -v /mnt/ubi_boot/var/local/acfg/config_file.txt /mnt/sda1/config_file_pre.txt >/mnt/sda1/report.txt ;
cp -f -v /mnt/sda1/config_file_fix.txt /mnt/ubi_boot/var/local/acfg/config_file.txt >>/mnt/sda1/report.txt ;
cp -f -v /mnt/ubi_boot/var/local/acfg/config_file.txt /mnt/sda1/config_file_post.txt >>/mnt/sda1/report.txt ;
/usr/local/bin/pandora/PandoraApp -qws -display directfb;

... to find that it does not work, and the edits revert back upon player restart.

Well, after some more probing, reading, dumping, and more reading up on UBI, MTD and NAND flash, it occurred to me that write-behind cache might have been playing tricks on me; after all, the system totally doesn't expect a file modification at this point. So I tried adding /bin/sync after copying the file, hard-reset the player some time after the script executed, and it worked; looking at settings confirmed that the BD age restriction went down from 255 to 254 years.

I have to give you a "proceed at your own risk" warning here, for two unrelated reasons. First off, I have no idea how robust the player software is against false moves such as accidentally erasing yor config file entirely or messing up in any other way; having actually done sudo chmod -r / on my first Mac (followed by sudo chmod +r / ... what does it mean "sudo not found"?!?) I can say that it is entirely possible to brick your player this way, and there will be no way back unless you really want to (and can) do the soldering thing. The other reason is that media industry does not pat us on the back for reverse engineering their code because 9 times out of 10 people try this with some form of piracy in mind. Well that wasn't my motive whatsoever (since I already had quite a few ways to play all of my entirely legal discs), and it should not be your motive either. So, don't try this at home unless you really know what you are doing, and if you do, proceed at your own risk and treat it like I treated it -- as a troubleshooting challenge (which I love) along with a comfort improving thing (which I value).

Thursday, November 2, 2017

Debugging without a debugger

Another retro post, from even earlier times than the last one.

This happened during my high school years when some of my friends already owned a computer but I had none. So we used to get together after school and binge play some video games -- starting with Lord of the Rings text adventures and Laser Squad hot-seats...

...and then, rummaging through then-abundant "bootleg software shops" (a post-USSR version of Game Stop, selling bunches of floppy disks with copied games or other software and with labels hand-printed on a 9-pin dot matrix printer) I discovered Eric the Unready (which you can actually play in an online emulator here).

I loved it at first sight. A wonderful piece of interactive fiction with hilarious jokes and puns, challenging puzzles and, myself an avid English learner at that time, an invaluable learning resource.

Unfortunately, right after playing through the first chapter, we ran into something annoying: copy protection feature. One of the "Prince of Persia"-type where you would be asked a few questions and would need the original printed game manual to answer these correctly and play on.



And needless to say we didn't have the manual -- and of course, neither did the shop we bought the game from.

Well, as a disclaimer... I do understand that software piracy is, in the big picture, bad bad BAD, but hey, we were 15 years old and had no idea of the big picture - nor any clue about copyrights and licensing. To us at that time, the fact that we had software on our computer meant that we could do as we pleased with it, especially given that we did buy it at a shop (sic!).

And even if someone were to lecture us on the proper course of action... At that time in that part of the world, an equivalent of $30 would be a decent monthly salary (yes, monthly, not daily, not hourly), and there was absolutely no way an ordinary person could possibly make a payment anywhere to a foreign country, or for that matter, to pay in any tender other than cash - no credit cards, no wire transfers, no bank accounts...

...so yes, I admit we were stealing apples from somebody else's garden, but quite unknowingly, almost unavoidably, and without causing anyone any real harm.



But all these sentiments aside, we were already hooked, and needed a way to play on.

Surely we had no Internet to look up the correct answers (there was no Google, no Chrome and even hardly any Internet Explorer yet!), we had no one to ask (the game was so out of mainstream, and the level of English command needed to play it was so untypical that we might well have been the only players in years). We also had nothing to tinker with the game with -- no hiew, no disassembler, no debugger proper.

We did have Game Wizard, a utility that you would normally use to save and reload in Tetris or make yourself infinite lives in Pacman. The way you do it would be to search the memory for "3" when you have 3 lives, then for "2" when you have 2 lives, and so on; with some luck you would find the address in memory that holds the lives variable for the game. You can then set it to 99 or freeze at 3 to get infinite lives.

And we gave it a try, for lack of anything better to do for the rest of the evening. We began by alternating memory searches in the state before vs. after the first question is answered -- hoping to reveal its correct answer by noticing different variable values depending on whether we chanced to answer correctly. Instead, we got:

XXXX:YYYY 01 02 01 02 01 02


As a wild guess I just set it to 4 instead...

...and got right through. (Apparently I inadvertently found the counter of the questions loop and moved right past it.)
All it took after that was to save the game (and the evening), granting us with endless hours of fun time.


Friday, June 30, 2017

The saga of an old Sears Craftsman garage door opener

This isn't exactly about curiouser code; rather, it's about curiouser electronics and troubleshooting.

Prologue

We bought a house with a built-in garage with an old (very very old, pre-1997) Sears Craftsman electric garage door opener, which the old owners claimed was defunct but never bothered to replace. Rather than just go and replace it myself, I tried my luck playing with the bundle of low-voltage wires dangling from it - and found that it did work if you connect the right wires.

Better than nothing.

Next, I downloaded the manual and tried searching Amazon for a possible replacement remote. And was lucky again: after a few unsuccessful tries, this little dongle successfully paired with the opener and worked.

Much better than nothing!

However, my curiosity was aroused at this point. The manual mentioned that there was a possibility to attach a pair of optional optical-beam safety sensors to prevent the door from closing if something was in the way, or to reverse the door closing if someone crossed the beam. Since the garage is very tight and I don't want the closing door to crash into my rear bumper (or, heaven forbid, to injure someone), this was definitely a feature I wanted.

Part 1. Research

The easiest and most obvious solution -- to order the safety sensors set as an official accessory from Sears -- was quickly put to an untimely end with a "No longer available" message on the official spare parts website.

The next one to try, less obvious but still easy, would be to order a set of 3rd-party sensors on Amazon (in the same way as I did with the remote) and hope that it works. Alas, no luck this time -- everything I could see on the aftermarket was for newer models, and would not be compatible with my opener.

The real snag was that my opener was "old but not too old" -- recent enough so that optical safety sensors were offered as an optional feature, but manufactured before these sensors were made mandatory. And while this was precisely what made it possible to get it running without the safety beam at all (I might have given up trying had I not been so lucky in the beginning), this was what rendered all newer safety sensors completely incompatible.

The reason? As explained nicely in this video, all newer models are designed in such a way that unless they get a "safe" signal from working sensors, the door won't close; just so that those pesky users won't bypass their faulty (or out-of-alignment or not-having-survived-mediocre-parking) sensors with a piece of jumper wire, the "safe" signal is a pulse train (with about 150 Hz frequency and some 10% duty cycle), which the receiver generates in presence of the sensor signal.

The manner my opener functioned was quite the opposite: it would work in all cases except when the sensor terminals are shorted (presumably by the sensors detecting the beam interruption), in which case the closing would not commence or, if already closing, would reverse.

Finally, I just decided to blindly try my luck with this set hoping I might still get the old kind, but alas, sadly but unsurprisingly, they were of the "pulse train" kind, nicely confirmed with an oscilloscope

Part 2. Simulation

So my job was to somehow translate between the two protocols. Some device (labeled "???" in the diagrams below) had to listen to the sensor output, decide if there was a "safe" pulse train, and then short the opener's sensor terminals if there were no pulses.

  

At this point, many would scream Arduino or even Raspberry Pi ("and your door will be able to post its status on Facebook"), but I quickly decided this would be too difficult, and a massive overkill to boot. I didn't want my door to post on Facebook. I wanted it to stop closing when the beam was crossed. Even this sequel video using an integrated-circuit (555 timer) square wave generator as a "pretty damn complicated piece of jumper wire to bypass the sensor" seemed a bit of an overkill to me (and as it were, resulted in at least one real kill...)

On the face of it, I thought a simple transistor switch might do the trick - if I could have the transistor normally open via some pull-down resistors, and rectify the pulse train in such a way that the signal would offset the opening potential and close the transistor (opening the switch) if pulses are present, at the same time retaining some power on the sensors so that they would resume normal operation if the beam was restored... not obvious. After a few evenings spent in a circuit simulator I found that a single transistor was a bit too unreliable; however a MOSFET or a Darlington transistor pair might do the trick, like so:

 

The one on the right is what I chose to implement as a final circuit. The two terminals on the right connect to the opener in parallel to the sensors: the upper right (positive) to the "white" screw and the white wire from the sensors; the lower right (ground) to the "black" screw and the striped wire from the sensors.

Part 3. Implementation

With a few of Amazon "shipped from China / Hong Kong and arriving several months later" orders, I finally assembled the circuit on a breadboard...


I scrapped the current limiting resistor in the output circuit because the short-circuit current was measured to be about 40 mA anyways, and put two 1M resistors in parallel to achieve 500k. The rest is as shown.
The real wonder? It worked out of the box.
And works still.

Epilogue

The opener is still in operation, sensors included, with the original breadboard circuit loosely hanging off the drywall. A few times it would refuse to close because there was something left in the doorway (I guess that may have saved those items from being crushed).
It isn't seamless, however: after I insulated the door with some styrofoam (making it quite a bit heavier), the gears in the opener did kick the bucket. However I was able to replace them relatively cheaply and quickly.

I still have plans to solder the circuit into something more permanent (and even bought all the necessary supplies for this), but these remain low priority (ain't broken? won't fix it, at least for now).
I still wonder whether I can somehow reverse engineer the operation of "Lock" and "Light" features from the wall control unit. If you happen to have this one lying around, please drop me a line!