Showing posts with label electronics. Show all posts
Showing posts with label electronics. Show all posts

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

Saturday, January 11, 2020

Tamper detection flip-flop circuit and my first soldering experience

Preface

In any modest-sized family home, locks tend to accumulate - door locks, shed locks, mailbox locks, cabinet locks, closet locks, lockbox locks... Locks breed keys, keys breed frustration when they get misplaced - and they always get misplaced when they are used a few times a year. And even though I did decide to learn lock picking (sic!) in the meantime, to have a last-resort method to open those pesky locks, and even bought a training set (double sic!!),  I never had time to practice it to the point of usability.

An obvious solution is a key cabinet. But, similar to the jewellery drawer lock I wrote about earlier, there is an inconvenience of having to lock the cabinet whenever you leave your house to cleaners or contractors and want to feel peace of mind. 

Of course you could lock it and keep its key on your key chain. But this was not fail safe (what if your main keys get misplaced, or worse, stolen?) Instead, inspired by the KGB spy stories, where agents would wrap a hair around their purse clasp to see if it's been opened (presumably by an opposing MI6 agent), I wanted to design an electronic detector that would alert me if the cabinet was opened in my absence.


Idea

The idea that immediately comes to mind is a simple contactor, i.e. en electromagnetic relay with its coil wired in series with its normally open contacts, like so:



The operation is pretty simple: the relay K1 is initially de-energized because its contacts are normally open; as soon as the (discreetly located) Start button is pushed, contacts are bypassed, the relay is energized and maintains the contacts closed. At that point the circuit is "armed" (signaled by the LED illuminating as shown above; R1 may be needed to limit the LED current). This goes on indefinitely until the circuit is broken (say by a reed switch attached to the door of the key cabinet, and I had a few of them lying around). The LED goes out, signalling that the cabinet was opened. The diode D1 is the flyback diode, especially important to protect the reed switch from arcing damage.

The drawbacks of this set-up are too obvious. The relay is an overkill to power a single LED (the relay coil consumes about 10 times more power). In addition, there is no way of telling if the LED went out because someone opened the cabinet or because the battery went out (and powering it from a wall adapter is a non-starter because it would give a false alarm for every tiniest power outage.) 

To solve the first drawback, I remembered I had an optocoupler lying around (the white 6-pin chip from this kit), which can work like a relay but for a fraction of power. To solve the second drawback I remembered that I had also ordered some MOSFETs for an earlier garage door opener mod project and ended up using bipolar transistors instead. So the resulting circuit was something like this (the link opens in a live circuit simulator) :



Again the operating principle is pretty simple. The optocopupler Q1 takes on the role of the relay K1; the resistor R1 in the previous diagram is split between R1 and R2 here, creating a voltage divider is such a way that when current flows through Q1, the gate of the MOSFET Q2 has just the right potential to close Q2. When Q1 is de-energized, Q2 becomes opened and illuminates the alarm LED through the resistor R3. 


Implementation and breadboard

When I needed to spend a few hours at a car dealership waiting for my car to be serviced, I took along a few parts, a multimeter and a breadboard and tried to implement my circuit (funny as it looked in the dealership lounge). After a few trials I ended up with this (again please feel free to play in the simulator):



The only modification, aside from tuning the values of the resistors to suit my actual optocoupler and MOSFET, came from the fact that my optocoupler had a separate base pin for the phototransistor, and leaving it floating meant that it could get energized at random upon power-up (defeating all the purpose of the circuit). So I had to introduce the D1-R4 circuit to pull the base pin down just so that Q1 always started with closed phototransistor (in the simulator, it looks detached because the optocoupler there is a generic part having no base lead).  

In the breadboard, the circuit looked like this (the red LED is actually D1 - I did not think I would need an ordinary diode, so I did not bring one to the shop and had to use a LED instead.):





Soldering and final installation

Some 6 months later (yes, because small kids);I found a bit of more time to attempt to actually solder it together on a protoboard PCB. I had never soldered anything serious before, so I made a few bad mistakes along the way:
  1. I learned that you cannot connect two adjacent pads with just a drop of solder - needed to use a tiny bit of wire, or strip a component lead leaving 1-2 mm slack and bending it to extend to an adjacent pad.
  2. I learned that it is not possible to melt more than one hole simultaneously unless you had a very specialized tip and super stable hands. So pre-filling the holes for the MOSFET (or worse, the optocoupler) was a disaster and I deeded to clean out the holes again using a desoldering pump (good I had a very basic one included with the soldering kit); a few times even this wasn't working and I had to resort to a Dremel with a very tiny drill bit. All in all, a third hand station I bought a few years before proved very useful here (finally).
  3. My decision to place components to both sides of the PCB (LEDs and the button on one side, the rest on the other) was a mistake. It would have been cleaner and more compact with everything on the same side. 
Surprisingly, it was much easier for me to get a device working on the breadboard than in the soldered form. It proved quite hard to ensure consistently good soldering quality. I spent a few oddball hours during two days tracing and correcting bad solder points, going through some moments of despair and almost wanting to get back to the breadboard. 

But patience and perseverance won (and, oddly but fortunately, the components endured my multiple attempts). Here goes (note that D1 is now a proper diode :) :



The remaining step was to install it in some kind of case. I did not have anything fancy like a 3D printer so I used a pre-bought plastic box to put the board inside using good old-fashioned M3 screws. Unfortunately, the batteries did not fit on the inside (they would if I had not made the mistake #3 above). So outside they went, on the back of the box. The final result looks like this:




The Start button is hidden behind the fourth screw (the one beside the LEDs). It is shorter than the remaining three screws that hold the board in place. To arm, the screw needs to be removed, and the button can then be pressed through its hole.

In operation, it looks like this:



Bonus: The current consumption of the circuit is around 2 mA (two milliamps), in either state, and most of it is the LEDs. Which means that four AAA batteries would power it through months on end; if this proves not enough, I'll put four D batteries and will probably only need to replace them once every few years.

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.




Wednesday, July 5, 2017

Arduino LED driver and button press handler / debouncer: another "Hello, World"

Long story short, I've bought an Arduino for some small home automation (specifically, imitating remote control commands for a portable air conditioner in order to operate it on schedule).

Having run a few simple examples, I realized that whatever my device might be, I'd need some sort of a rudimentary GUI with some buttons to control the device and some LEDs to diagnose the device state. After some testing I realized that I need to solve several problems:
  1. The function digitalRead() only polls instantaneous button state ("is the button pressed now?"); some memory-based logic is needed for the detection of the event "the button was pressed and then released", which is a relevant GUI input. This logic should also filter out contact bounce , otherwise (as I quickly found out) a button press (and/or release) is very prone to being interpreted as several presses.
  2. I wanted to led LEDs blink (so that my device would signal its status without doubling up as a night light) concurrently with doing other tasks (such as the device's main function). 
  3. I wanted the routines to be general enough to scale to several LEDs and buttons concurrently and independently of one another.
Essentially I wanted to implement the two basic tutorials (Debounce and BlinkWithoutDelay) in a robust, encapsulated, scalable manner.

So, here's what I came up with, and decided to share it here in case it is useful for someone else.

Let's first define some global variables and structures:

// button names
#define B_UP 0
#define B_DN 1
#define B_SEL 2
#define B_MOD 3

//global constants
const byte nL=5;
const byte nB=4;
const byte pinL[nL]={11,10,9,6,5}; // should be PWM-enabled pins
const byte pinB[nB]={7,8,2,3};

const byte NOISE = 15; // ms to hit timed event for debouncing
const byte REP_DELAY = 400; // ms to initiate autorepeat
const byte REP_RATE = 100; // ms to regenerate autorepeat

//global variables, initialized
byte thisLED=0;
byte thisMODE=4;
For our array of LEDs we define some working data structures:
//data structures - LEDs
word quant[nL] = {125,125,125,125,125}; // time between ticks
word flash[nL] = {75,75,75,75,75}; // overrides if less than quant;
byte level[nL] = {120,140,100,80,40}; //brightness level 
byte mask[nL] = {B00000001,B00010001,B01010101,B00000011,B11001100};//bitwise mask

word next[nL]; // time of next LED handle event
word nextOff[nL]; // time of next turn-off event
byte cycle[nL] = {1,1,1,1,1}; //current bit in the mask, starting with 00000001, rotated left on every tick
The blinking pattern is defined as follows: every quant[i] milliseconds we define a "tick" for the LED number i; at each tick, we cycle the "current bit number" between 1 and 8 (using the internal variable cycle[i]), and turn the i-th LED either on or off, depending on the current bit in mask[i]. Additionally the LED is turned off on a "quench", when flash[i] milliseconds have elapsed since it was last turned on. Finally, level[i] simply defines the LED's brightness level. This can be illustrated like this:

This approach allows us to create a considerable variety of patterns ranging from simply "pulse N times per second" or "do N short blinks once in a while" to more complicated dash-dot patterns. It has its limitations (mostly related to the number of bits in the mask) but it is more than enough for the kinds of diagnostic output we aim for. Note that level and pattern are independently set for each LED.

By the same token we define some structures for the buttons:
//data structures - buttons
byte raw[nB] = {0,0,0,0}; //raw button state 
byte b_ready[nB] = {1,1,1,1}; //whether button is ready 
byte cooked[nB] = {0,0,0,0}; // filtered button state
byte autorepeat[nB] = {1,1,0,0}; //whether to autorepeat on button xx
byte event[nB] = {0,0,0,0}; //button press event, must be consumed on handle and re-generated on autorepeat

word last[nB] = {0,0,0,0}; // time last pressed

Here the only "parameter" is autorepeat[i], defining whether the button number i should auto-repeat. The rest are internal variables needed for debouncing and handling button press events.

Next we write the handler for the LEDs:

void pollLEDs()
{
  word now = (word)millis();
  for (byte i=0;i<nL;++i)
  {
    if ((int)(now-next[i])>=0) // next tick reached
    {
      nextOff[i] = next[i]+flash[i];
      next[i]+=quant[i]; // move next (and nextoff) forward
      boolean state = ((cycle[i] & mask[i])!=0); 
         // determine current state from mask
      analogWrite(pinL[i], (state)?level[i]:0); \
         // light up or quench according to state
      cycle[i]=cycle[i]<<1; if(cycle[i]==0) cycle[i]=1; 
         // cycle current bit in mask 
    }
    if ((int)(now-nextOff[i])>=0) 
    { // flash time exceeded before next tick
      nextOff[i]+=quant[i]; // just move forward
      analogWrite(pinL[i], 0); // quench
    }
  }
}

We see that it just implements the logic in the diagram above, using next[] and nextOff[] to store the timer values for the next tick and quench respectively. The only trick here is the use of bitwise arithmetic for cycle[] to enable quick comparison against mask[].

You can see that it is easy to make the time span between patterns longer than 8*quant[] without increasing the size of the mask by simply defining a separate period[nL] and then replace next[i]+=quant[i] with next[i]+=(cycle[i]!=1)?quant[i]:period[i]


Next we write the button handler:

void pollButtons()
{
 word now = (word)millis();
 for (byte i=0;i<nB;i++)
 {
   raw[i] = (digitalRead(pinB[i])==LOW)?1:0; // store to avoid repeated calls to digitalRead()
   // if button is ready and LOW recorded, record last pressed time and clear ready
   if (b_ready[i]==1 && cooked[i]==0 && raw[i]==1) {b_ready[i]=0;last[i]=now;}
   // if not ready and still LOW after tolerance, set filtered state, button press detected
   if (b_ready[i]==0 && cooked[i]==0 && raw[i]==1 && (word)(now-last[i]) > NOISE) cooked[i]=1;
   // if state is pressed and button released : clear filtered, restore b_ready, and generate event unless auto repeating
   if (b_ready[i]==0 && cooked[i]==1 && raw[i]==0 && (word)(now-last[i]) > NOISE) 
   {
     cooked[i]=0;
     b_ready[i]=1;
     if (autorepeat[i]<2) event[i]=1; else autorepeat[i]=1;
   }
   
   // handle auto repeat
   if (autorepeat[i]==1 && cooked[i]==1 && raw[i]==1 && (word)(now-last[i]) > REP_DELAY)
   { // initiate auto repeat
      autorepeat[i]=2;
      last[i] = now;
      event[i]=1;
   }
   if (autorepeat[i]==2 && cooked[i]==1 && raw[i]==1 && (word)(now-last[i]) > REP_RATE)
   { // if auto repeating, re-generate event periodically
      last[i] = now;
      event[i]=1;
   }
   
   //handle event once generated
   if (event[i]==1)
   {
     event[i]=0; //consume event
     control(i); //handle event
   }
 }
}

Note that it uses raw[i] to store the instantaneous state of the button number i, whereas ready[i] is a flag indicating whether that button is expected to receive input; it is set initially and cleared the instant the button press is detected (at which time the button timer is set via last[i]). The logic to determine whether the button has actually been pressed (so its filtered state, cooked[i], can be set) is "the button is still pressed some predetermined time after it was pressed initially"; I've used 15 ms as a ballpark and found it to work fine with my buttons. Once cooked[i] is set, the handler listens for the button release; once this happens, it generates an event (via setting event[i]) and return the button to its initial ready state.

For auto repeat support the amount of extension is minimal. We use the same timer to determine whether a certain time has elapsed since the button was pressed, and start generating events repeatedly at regular intervals until the button is released.

We can see from the code and description that this approach filters out the contact bounce both upon press and upon release. I am leaving it as an exercise for you to see why.


To test the implementation I made a "blink machine" quick breadboard circuit with five LEDs and four buttons, which would be used to control the way LEDs blink, like so:

 

The buttons functions are as follows:

  • [SELECT]: Cycle through LEDs to control.
  • [MODE]: Cycle through parameters to adjust:
    brightness, blink frequency, blink duration, blink pattern
  • [UP]: Increase the current parameter for the current LED.
  • [DOWN]: Decrease the current parameter for the current LED.
This corresponds to the following implementation of the event handler control()


void control(byte code)
{
 switch(code)
 {
   case B_SEL:
    thisLED++; if (thisLED>=nL) thisLED=0;
    notify();
   return;
   
   case B_MOD:
     thisMODE++; if (thisMODE>5) thisMODE=1;
     notify();
   return;

   case B_UP: case B_DN:
   {
    switch(thisMODE)
    {
  switch(thisMODE)
    {
      case 1: //level
       if (code==B_UP && level[thisLED]<255) level[thisLED]++ ;
       if (code==B_DN && level[thisLED]>0) level[thisLED]-- ;
       break;
      case 2: //period
       if (code==B_UP && quant[thisLED]<1000) quant[thisLED]+=20 ;
       if (code==B_DN && quant[thisLED]>40) quant[thisLED]-=20 ;
       break;
     case 3: //flash
       if (code==B_UP && flash[thisLED]<quant[thisLED]) flash[thisLED]+=10 ;
       if (code==B_DN && flash[thisLED]>20) flash[thisLED]-=10 ;
       break;
     case 4: //pattern up/down
       if (code==B_UP) mask[thisLED]++ ;
       if (code==B_DN) mask[thisLED]-- ;
       break;
     case 5: //pattern scramble/reset
       if (code==B_UP) mask[thisLED]=(byte)millis() ;
       if (code==B_DN) mask[thisLED]=1 ;
       break;
    }         
   }
   return;
 }
}

void notify() // show currently selected LED and mode
{
    analogWrite(pinL[thisLED],0);delay(100);
    for (byte i=1;i<=thisMODE;++i)
    {
      analogWrite(pinL[thisLED],255);delay(50);
      analogWrite(pinL[thisLED],0);delay(50);
    }
}

Note the fifth mode to quickly scramble and reset the bitwise mask. The auxiliary function notify() is used to visually indicate the currently selected LED and mode. Here, for simplicity and because the interface should be synchronous with user input, I do use delay() (pausing all other operations in the process). Exercise: During the operation of notify(), some of the LEDs may miss a tick. What will happen to their blinking pattern?

Finally to complete the sketch, here are its two main functions, setup() and loop(). Note that these are really short to isolate the button/LED workings from the rest of your code.

void setup() 
{
// initialize pins
  for (byte i=0;i<nL;++i) pinMode(pinL[i],OUTPUT);
  for (byte i=0;i<nB;++i) pinMode(pinB[i],INPUT_PULLUP);
  for (int j=0;j<nL;++j) {analogWrite(pinL[j],255);delay(75);analogWrite(pinL[j],0);delay(150);} // "splash screen"
  Serial.begin(9600); // optional, for debugging
// initialize LED timers
  word now = (word)millis();
  for (byte i=0;i<nL;++i)
  {
    next[i] = now+quant[i];
    nextOff[i] = next[i]+flash[i];
  }
}

void loop() 
{
    // just call the two handlers
    pollLEDs();
    pollButtons();
}

After some debugging the implementation tested fine, but the real use is that pollLEDs() and pollButtons() can be used within any sketch; of course, control() will need to be re-written. The pattern for the LEDs can be altered at runtime by changing the corresponding variables.
Some concluding thoughts:
  • I designed the LED pins to be PWM pins but non-PWM pins may be used just as well; just replace analogWrite with digitalWrite, disregarding level[].
  • LEDs can be any driving circuits that need timed controls. Similarly, buttons can be any form of digital input. If you use analog input, keep in mind that analogRead takes time and a much slower dead-time processing will be needed instead of debouncing.
  • This is really basic, but -- don't use pins 0 and 1 if you use serial communication for debugging. I've spent a few hours debugging until I figured out my debug messages were mimicking button presses.

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!