RxJS and the Observable Flic Button
With the observable pattern, monitoring Flic buttons for state changes is as easy as a button press or two or three.
This is part of a 2-part series. You can find the other part here:
Controlling IoT Devices with the Flic of a Button
In this part, I’m going to show you how to handle the problem of collecting button presses over time and executing custom actions based on the type of button press. To do this, we’ll be hooking into the Flic SDK (the open source service that handle’s low-level Bluetooth communication with Flic buttons) and putting in some customizations to the
buttonUpOrDown handler both with and without observables.
Tools of the trade:
Flic’s default configuration allows for only single-press, double-press, and press-and-hold. There’s no way to have a double-press-and-hold or a triple-press. Also the timeout is very long so much so that the buttons feel laggy. In fact, they feel so laggy that sometimes I’ll let off one thinking it was held down long enough only to have the single-press action fire.
So instead of listening to the
buttonSingleOrDoubleClickOrHold action that cleanly gives us the press type and let’s us choose an action based on that response, we have to instead create our own handler based on
buttonUpOrDown and determine the press type that way.
Step 1: Listening for Button Presses
The first thing we need to do is listen for when the Flic button is pressed and execute an action when that happens. It doesn’t matter right now what the action is just so long as it get executed after pressing the button.
To do this, we’ll need to setup the listener on
buttonUpOrDown and give it a handler. Since this handler will get both a
BUTTON_DOWN state, we need to only listen for
BUTTON_DOWN and execute our
singlePressAction when that happens.
Every time a button is added, it will call
listenForButtonPress. From there we can setup a listener on
buttonUpOrDown which we give a handler to tell us the current state of the button.
This example is pretty simple and does the bare minimum. It doesn’t even care about which button was pressed, only that a button was pressed and then executes an action as soon as it’s pressed down no matter how long you hold it. Passing around the Bluetooth address introduces some complexity we don’t need right now so it’ll show up in only the last few examples.
Step 2: Setting Up a Double-Press Handler
There’s a lot more that goes into checking for a double-press. You have to setup a timer after the first press and wait to see if another press comes within that timeframe. If it does, execute the double-press and reset the timer. If not, execute a single-press.
And just like that, our code’s already ridiculously complex. We haven’t even added press-and-hold and we’ve already got something that’s really messy. There is a much cleaner way to do this once you realize what we really care about, but that’s why we’re going through it step-by-step. Without these inner steps, it’ll be difficult to understand why the observable pattern is a real solution.
Step 3: Shortening the Press-and-Hold Time
Since double-press is already really complex, we can simplify our code by creating a press-and-hold listener separately. First we need to understand that a button press is actually a
BUTTON_DOWN then a
BUTTON_UP event. So far, we’ve only cared about the
BUTTON_DOWN part, but if we’re going to check if the user is holding the button down or not, we need to change our single-press to require both a
BUTTON_UP state. Our hold action should then execute only if a down and a
BUTTON_UP doesn’t happen within our time limit.
We’ve finally reaped some benefit from rewriting the button press handler by making the hold timeout is variable. We’ve set it to 300ms, 2–3 times faster than Flics default.
Step 4: Combining Our Press Action Handlers
Now that we’ve created a separate double-press and press-and-hold handler, we can combine them into one giant function. Sure it’s bulky, but it works.
We’ve finally rewritten Flic’s callback function and even set it up so the press timer is a lot shorter than the default. The buttons feel really responsive, but something still feels off.
Because Flic buttons are so expensive, you wanna get as much use out of each button as possible. Now that we’ve rewritten all this existing functionality, we can finally add double-press-and-hold and triple-press. Sadly, the code is completely unmaintainable. I can’t imagine fixing a bug in this mess or figuring out what it’s doing if I have to pick it up again in a week let alone a year.
Step 5: Enter Observables
Let’s start with a basic example just like before. We’re going to create an observable for this button that fires only when we get a
BUTTON_DOWN state change.
We’ve done things a bit differently this time around. Our listener will create an observable per button. That observable requires a creation function which passes an observer much like the
reject functions in a
new Promise declaration. That handles the first and most-important part; emitting our state changes.
Now our handler calls
observer.next when the button changes state instead of the usual callback function. This will trigger the chain of events attached to our observable until we hit a
.subscribe() method. When we get to the
subscribe, it’s just like a promise’s
.then(). The first arg you pass is the callback, the next is another callback for errors. Subscriptions can take a third argument which triggers when the observable completes, but in the case of listening to button presses, our observable never completes; it will keep listening and executing actions until the app is closed.
Step 6: Implementing the Observable Pattern
As you probably figured out, the real problem we’re trying to solve is how many
BUTTON_DOWN events occurred within a time limit. Knowing that, we can significantly simplify our code.
We know a button press is actually a
BUTTON_DOWN and a
BUTTON_UP while a hold action is a
BUTTON_DOWN with no
If we have more
BUTTON_UP counts, that means we had a hold action; otherwise, the numbers match, and we had a normal button press. The number of
BUTTON_DOWN counts in both instances will equal the number of presses.
There’s a bit of magic going on here with the buffer and debounce commands. When listening for button presses, we need to set a timer that executes our command once the timer is up. We also need to stop the previous action and reset the timer if another press comes in during that timeframe.
debounceTime function in RxJS listens for events on the
buttonUpDown$ observable. Any time one comes in, it sets a timer and waits to continue to the next step until that timer runs out. If you fire another event in that timeframe, it resets the timer and waits again. Once the timer runs out, it’ll emit an event to the
.buffer() function on the other
buttonUpDown$ observable which sends down an array of state changes that occurred since the debounce timer started.
At this point you’re probably wondering why there’s a check for
BUTTON_UP when it’s the first button state. This is an edge case that can occur when you’re trying to execute a hold command and then execute another command. By doing so, the debounce timer will start again, but this time with a
BUTTON_UP as the first command. Since that’ll mess up the calculations, the solution is to remove it if it’s the first action. Thinking about it logically, button presses will always start with a
BUTTON_UP was the only state change, and we remove it, that would leave us with an array of no button presses. This would also cause calculation problems so we filter the entire emission at this step preventing the subscriber from firing and protecting our code from any edge-case bugs.
BONUS Step 7: Going Back to the Old Method
Outfitted with this new knowledge of counting clicks based on a debounce timer, we can do the same code without observables, but with a slightly different implementation.
Using this new implementation, we’re doing the
setTimeout as before and clearing it on every press. This resets the timer and only until button states stop occurring will the timeout eventually fire. The
Promise was added merely for readability as it could be done with a function inside of
Because our state is mutated, we need to ensure we reset it after we’re done and ensure we’re sending a new object with those same props instead of the mutated object.
While observables aren’t required to solve complex problems like these, they sure do create a solution that’s much easier to reason about and maintain in the long run. Special use cases are far easier to implement when you’re calling an observable property and passing it a single function than mutating state, managing timers, and resetting vars.
In the end, we were able to accomplish our goals of reducing the hold timer and programming actions on any number of press types making our Flic buttons more responsive and versatile.
If you’re like to find out more about the Flic Controller software I used, you can find it and other home automation tools on my GitHub account: https://github.com/Sawtaytoes/Flic-Controller