Chris Mullins I occasionally write about things. Usually these things are about computers. Thu, 29 Sep 2022 20:14:21 +0000 en-US hourly 1 Automating Blinds with a Retrofitted External Motor Mon, 17 Feb 2020 00:45:05 +0000 It’d be a real stretch to say I needed to automate the blinds in my apartment, but it was fun, and I’m loving the result. Blinds open automatically at sunrise and close at sundown. It’s nice to wake up to the sun!

I designed most of this setup myself, and wanted to share how I approached it. There was a lot of prior art (a particular shoutout to The Hookup’s Guide), but nothing that worked the way I was wanting. Guides I ran across either mounted a motor inside of a 2″ track (mine are 1″), or were for pull-cord style blinds.

I wanted a motor mounted on the outside of the track to drive the existing worm gear. I rent and don’t want to be kicking myself too hard when I move out. These are super easy to (un-)install.

There are few parts I’ll cover in this post:

  1. The 3D-printed mount design w/ assembly instructions
  2. The driver circuit PCB
  3. Driver firmware (esphome w/ cover component) + Integration with HomeAssistant
  4. Wiring tips

Bill of Materials

Before going into any detail, here’s a bill of materials (links may contain affiliate codes). If you’re into tinkering, you’ll probably have most of these parts already.


  1. Wemos D1 Mini ESP8266 dev board
  2. A4988 unipolar stepper driver
  3. 28BYJ stepper motor (important: converted to bipolar, see below)
  4. 1000 µF electrolytic capacitor
  5. LM2596 DC/DC buck converter
  6. Barrel jack
  7. 2A 12V power supply
  8. 4 Pin screw terminal
  9. Driver circuit PCB (Available on OSH Park). Gerber files on Github.


  1. A 3D printer
  2. eSun White PETG
  3. M3 screws, etc. for assembly (this kit has all but the M3*30 screws):
    1. 2x M3*30
    2. 3x M3*20
    3. 5x M3 nuts
    4. 6x M3 washers

3D-Printed Mount

The STLs are available on Thingiverse and PrusaPrinters. PrusaPrinters has gcode for the mounting arm, which requires support in some strategic locations.

The mount’s design is simple — it slides into the track and holds the motor in place. Angles and lengths are adjustable so that the motor can be moved into the appropriate position. It’s held together with some M3 screws.

The only piece of this that is not one-size-fits-all is the motor adapter. It connects the shaft of the stepper motor and the worm gear driver. While the 28BYJ end probably always looks the same, I had three different worm driver shafts across my apartment.

Assembly should be pretty self-evident. Let me know if not and I can elaborate.

Convert 28BYJ stepper to bipolar

28BYJ motors are unipolar steppers. In order to use them with the A4988 driver (which supports a higher supply voltage), they must be converted to bipolar. Fortunately, this is pretty simple. Just crack open the blue casing on the side and cut the center PCB trace. I used a small file to do this, but an X-Acto knife or small screwdriver would work just as well.

You’ll probably need to break some of the plastic legs on the casing to get it open, but it closes back up well enough without them. I also snipped the lead wire going to this trace to avoid future confusion.

Driver Board

The schematic for this board is pretty simple and has three main components: an ESP8266, an A4988 stepper driver, and a voltage regulator (I used an LM2596-based board).

This worked just fine on a perfboard, but because I wanted several of these, I used this project as an excuse to familiarize myself with Kicad and get some PCBs made from OSH Park (project available here, all kicad+gerber files are on Github). I was very happy with the result:

Assembly Instructions

  • Make sure to adjust the output voltage of the LM2596 to 3.3v! You’ll fry the D1 Mini if you don’t do this.
  • Soldering components should be pretty straightforward as they’re all through-hole.
  • I would recommend using headers for the A4988 and Wemos D1 Mini in case either gets damaged.
  • Most D1 Mini kits come with two sets of 8-pin headers. Since the A4988 is a 2×8 pin board as well, this works out perfectly.

Driver Firmware

For this project, I wanted to try out ESPHome. I’ve written my own firmware for almost all of my EPS8266 projects. It was fun while it lasted, but the thought of gluing MQTT libraries to crappy C++ code didn’t sound exciting this time.

ESPHome’s Template Cover component does the trick quite nicely. I’ve put the YAML definition on Github. Flashing the D1 Mini with generated firmware is pretty simple. I used this command:

esphome office_blinds.yaml compile && \ -b 460800 write_flash 0x0000 office_blinds/.pioenvs/office_blinds/firmware.bin

(obviously you’ll need to have esphome and esptool installed).

You’ll also need a secrets.yaml that contains your wifi credentials and a setup password:

wifi_ssid: "your wifi network name"
wifi_password: "your wifi password"
fallback_ap_password: "a random password"

The meat of the definition is in the template .blinds.yaml, including pin mappings from the ESP8266 and the A4988. If you chose pins different from those shown in the schematic above, make sure to correct them.

Parameters that are specific to a particular installation are substituted. In addition to names, you may also want to adjust the target parameter in the definition. This controls how many turns the motor completes when transitioning from one step to another. You can also negate it if you need to invert the direction (although you can accomplish this by reversing wiring as well).

The stepper position and cover state should be persisted across reboots.

HomeAssistant Integration

ESPHome works really seamlessly with HomeAssistant. Just add an ESPHome integration (Configuration > Integrations > + > ESPHome) using the hostname or IP of the ESP8266 (if you’re having trouble finding this, it’ll appear in serial logs after flashing the firmware).

This adds the cover component and allows you to control the blinds:

To address a situation where I need to calibrate the position, I have these scripts:

  alias: Office Blinds - Set Open Position
    - service: esphome.office_blinds_set_open_position
  alias: Office Blinds - Set Closed Position
    - service: esphome.office_blinds_set_closed_position

Calling them will tell ESPHome to consider its current position as either open or closed. In combination with the up/down/stop action buttons, this will allow you to get your blinds into the desired state.

Wiring Tips

You can use any sufficiently large four-wire cable to connect the driver to the motor. Ethernet cable works really well. I used one twisted pair for each lead. Stranded ethernet + crimped header would be best, but I only had solid core ethernet, so I soldered some headers on and called it a day.

Here are some pictures that hopefully help as a wire guide:

Note that you can reverse the order in which they’re connected (1/2/3/4 -> 1/4/2/3) to invert the directions the stepper turns for open/close.


September 29, 2022

  • Significant updates to the ESPHome yaml definitions
    • Stepper position persisted across reboots
    • Adds pin definitions for MS1/MS2/MS3 on A4988 used to control step granularity. These need to be pulled LOW during operation.
    • By default D4 is used for MS1. D4 is also tied to the internal LED on the D1 Mini. I have this pin kept HIGH during idle so the LED stays off. It’s pulled LOW while the stepper is running, which has the nice side-effect of the LED lighting up while the motor is (or should be) running.


]]> 18
Rescue Your Amazon Dash Buttons Fri, 20 Dec 2019 22:45:10 +0000 Earlier this year, Amazon announced that they’ll discontinue Dash Buttons. I don’t know how successful Dash Buttons were for their intended use, but Home Automation hackers have loved (mis-)using them for everything from warming up their coffee pot to keeping track of bodily functions.

Unfortunately for us hackers, Amazon is an unforgiving god. Not only have they stopped selling Dash Buttons, but they’ve removed the part of their app used to set new ones up. Even worse, to any button unfortunate enough to connect to The Mothership, Amazon has promised to issue a firmware update that bricks the device. It is therefore critical that you set up your buttons in an environment where they will not be able to phone home.

Fortunately for us hackers, Amazon is not an infallible god. Versions of their dash button firmware built on May 2016 and earlier are vulnerable to a buffer overflow attack during the high-frequency audio setup (Hunz did some truly awesome reverse engineering work to find this).

We can exploit this vulnerability to complete the setup.


In the following section, I’ll go into more detail about how this works.

For now, let’s do this!

  1. Put Dash Button in setup mode by holding down the button until the LED flashes blue.
    1. Connect to the Amazon ConfigureMe WiFi network and visit
    2. You’ll see the button’s hardware (MAC) address. Block its Internet access in your router’s settings. If you don’t do this, the button will get an over-the-air update when it phones home and get bricked.
  2. While in setup mode, play this .wav file through some earbuds aimed at the Dash Button.
    • If the LED turns green, the exploit worked! Carry on to step (3).
    • If the LED turns off, you’re probably on a firmware version that fixed the vulnerability. Unfortunately, you’re out of luck if this is the case. (unless someone finds another vulnerability)
  3. Put the Button in setup mode again.
  4. Connect to the WiFi network it creates — Amazon ConfigureMe.
  5. Visit this URL*:<wifi_network_name>&amzn_pw=<wifi_network_pw>. (obviously substituting wifi_network_name and wifi_network_pw for the desired values)

That’s it! It should work now.

[ * ]: Note that the setup AP is unsecured and this request is sent over HTTP. You’re sending this request in plaintext over the air. Don’t do this unless you’re comfortable with that. The buttons support a configuration flow that involves exchanging crypto keys, but this is far more involved. If there’s enough interest, I can write a script.

How this works

The Dash Button setup process has two high-level steps:

  1. Getting WiFi credentials from you.
  2. Exchanging a pair of secrets with The Mothership. Under normal operation, these are used to authenticate with Amazon when placing an order (it uses them to generate an HMAC secret).
    1. The button transfers a device secret baked into the firmware to Bezos HQ.
    2. The button retrieves and stores a customer secret from Amazon.

(1) is easy for us to fake. In fact, all we need is the final step from the previous section — visiting the URL with the amzn_ssid and amzn_pw parameters.

It’s (2) that’s keeping everyone from setting up their buttons. Amazon has stopped responding to these exchange requests, meaning the buttons will never complete the setup process unless we get creative.

The part of the firmware that handles high-frequency audio packets is vulnerable to a buffer overflow attack. From Hunz’s slides, you can see that it’s doing a memcpy without a length check:

Hunz put together some excellent scripts that pack an exploit payload in ARM assembly into an audio file.

I needed to do some reverse engineering of my own to find the function responsible for writing the customer secret to flash. I’d never so much as opened a disassembler, so this was a pretty fun challenge. Since IDA costs an arm, a leg, and the souls of any present and future offspring, I used Ghidra (I have no complaints, although a professional might). I put the archive file on Github.

The raw function that writes a customer secret to flash is at address 0x40FAA4:

It takes in a pointer and a length (which appears to always be 20). This function is normally wrapped in a bunch of code that calls Amazon’s servers, does validation, etc., but since we’ve got arbitrary code execution, we can just call it directly.

My exploit payload follows:

 The following three lines are parsed by Hunzs' scripts.
 Registers R1, R2, and R3 will be pre-populated with the
 specified values.

// r1 = 0x400000 -- flash start
// r2 = 0x40faa5 -- write customer secret function ptr (+1 due to thumb)
// r3 = 0x40e721 -- set LED color function ptr +1

/* set up function call parameters */
PUSH {R1-R3}
MOV  R0, R1
MOV  R1, #0x14
/* call avocado_writeCustomerSecret */
POP  {R1-R3}

/* set LED to green */
LDR R0, =0x00FF00

/* let watchdog expire */
  B done

Since it doesn’t matter what the value of this secret is so long as we’re not intending to connect to Amazon, I pass an arbitrary address (0x400000 — the start of ROM) along with a length of 20 to the writeCustomerSecret function.

Then it sets the LED to green to indicate success. Firmware versions that have patched this vulnerability shut down when they receive the exploit.

Further fun

When starting on this effort, I soldered some magnet wire onto the UART test pads to get serial access. Hunz labels the pins in his talk:

Here’s a photo of my test rig:

The most interesting thing you can do with UART access is execute more code. The audio exploit payload is limited to a small number of instructions. Hunz supplied a payload that reads data from UART into SRAM and executes it (there may be a more convenient way to do this, but I’ve not dug in yet). Soldering onto these tiny pads is pretty challenging, so I modeled and 3D-printed a crude pogo pin fixture:

Pogo pin fixture. Connects to UART RX/TX pins. Gnd is connected separately.

I’d initially thought this was necessary in order to configure wifi credentials, but turns out you can do that using the setup AP, which is much easier.

You can’t do much of anything interesting in the serial console, but it at least appears that someone over at Amazon has a sense of humor:

****    TAOS Bootloader 0.2.11    ****

0x00000004 ms    0x000000FB us
Reset Trigger : FIRST POWER UP
(APP)(INFO)Chip ID 1503a0
(APP)(INFO)DriverVerInfo: 0x134a134a
(APP)(INFO)Firmware ver   : 19.4.10 Svnrev 13806
(APP)(INFO)Firmware Build Oct 26 2016 Time 17:55:34
(APP)(INFO)Firmware Min driver ver : 19.3.0
(APP)(INFO)Driver ver: 19.4.10 Svnrev 12577
(APP)(INFO)Driver SVN URL branches/WIFIIOT-1400_2
(APP)(INFO)Driver built at Mar  3 2017	15:19:38
DBG: Set MAC address 18:74:2E:4D:01:D3
> exit

    There is no exit from here. You are stuck in a forever loop...MUAHAHAHA!



  • Hunz’s reverse engineering work is incredible. If you’re into this sort of stuff at all (and if you’re reading this, you probably are), you’d very likely get a huge kick out of his 33c3 talk, which is where I learned about the audio vulnerability.

Future Work

The Dash Button hardware is pretty remarkable. It’s got a beefy microcontroller, (obviously) builtin wifi, a BLE chip, a microphone for the HFA audio configuration, and so on. I’ve not seen any official-looking estimates at the cost Amazon was paying for these things, but some have guessed in the ballpark of $20 — even at Amazon’s scale.

If we can find a way to flash custom firmware, they’d be perfect IoT buttons. As-is, they’re obviously a little janky. We’re monitoring networks for side-effects to trigger actions. This should theoretically be possible given that Amazon issues OTA firmware updates (the code that handles the OTA update process appears to be at 0x42391C).

I don’t know how much gas is left in my motivation-for-reading-assembly tank, but this is what I’d work on next with what’s left.

It’d be really neat if Amazon open-sourced an SDK. Yes, they’ve said they have a recycling program, but something tells me most buttons are gonna end up in the landfill.


  1. Amazon fixed the buffer overflow vulnerability in a later version of the firmware, but almost every button I have uses the May 2016 version out of the box (there’s been ~1/20 exceptions).
  2. Buttons get OTA updates if they connect to the Internet. So if you have a button that’s been phoning home, in all likelihood it’s been patched.
  3. My setup triggers on 802.11 probe requests, meaning I don’t need (or want) the button to connect to a network, only to try. As far as I can tell, though, they will.


If this work has brought you happiness or utility, it’s more than enough for me to hear those words.

If you’re feeling especially generous, and are open to a charitable donation, that’d make me very happy. Here are some whose mission I support (in no particular order):

]]> 72
Webpack with Arduino and PlatformIO Fri, 15 Nov 2019 05:05:21 +0000 Web development for embedded devices sucks. More often than not, it involves editing concatenated strings in a header file. Even some of the most popular web projects like Tasmota, WiFiManager, and esphome work like this.

If you’ve done any web development recently, it’s probably involved build tooling like Webpack. This makes it super easy to include external libraries, and to pack all of your assets (javascript, images, stylesheets) into an easy-to-distribute bundle. There’s also a wealth of incredible development tools like hot reloading, and support for alternative languages like Typescript.

Why is the experience so different when developing web applications for embedded devices?

To serve assets, you have these options:

  1. Embed the assets in the source code. This is a pain in the butt, but is a very straightforward approach and results in a complete firmware image.
  2. Side-load assets on, e.g., SPIFFS. This introduces an out-of-band step to get your project running.
  3. Serve assets from an external source. This introduces a point of failure, and requires Internet access.

(2) and (3) make for a much nicer development experience, but make for a crappy user experience. (1) is the reverse. It makes sense that developers optimize for their users.

There’s good news: we can have our cake and eat it. It just requires some simple build tooling. We can use Webpack as we normally would, have it generate a messy header file that our compiler will happily pack into a firmware image for us.


I put together a skeleton example on Github, available here:

Hopefully this serves as a useful quickstart, but I’ll give a more detailed overview below.

Moving Pieces

There are three high-level parts:

  1. The main PlatformIO application (./platformio.ini, ./lib, ./src).
  2. The web app (./web)
  3. A Webpack plugin to cram the web assets into a header file and a PlatformIO build script to call it (./

./web is a self-contained Webpack application. We can iterate on it using the standard builtin development server. When we’re done, we can call the standard pio run --target upload to run the PlatformIO build. It’ll rebuild the web assets, pack them into a header file, and recompile our application.

The webserver is configured to dynamically serve the assets from the header file.


Now that the build process is set up, we can focus on writing a great web app. Here’s an example of something I built for one of my projects, epaper_templates (I’ll talk about the new version in a future post at some point).

epaper_templates bitmap editor

This is a simple bitmap editor with local image importing, undo/redo stack, resizing, etc. Writing this in raw Javascript would be a giant pain in the butt. Doing it with React and the full richness of the Javascript developer community was an absolute delight. It took me a couple of evenings.

Continuous Integration

It’s pretty straightforward to get a build tool like TravisCI to build and distribute pre-compiled firmware images, complete with web assets. My epaper_templates project does this for tagged releases. Its .travis.yml may serve as a useful reference.


This is, of course, not entirely free. Let’s discuss some of the costs:

  1. We’re embedding all of our assets in source. We’re obviously using progmem, but we still only have a couple of MBs of flash to play with. The above example is around 500 KB when it’s said and done.
  2. It’s is clearly more work than just putting stuff in header strings. If what you’re trying to do is simple, this is probably not the right approach for you.
  3. It adds dependencies to your build (e.g., node, npm).


When we have powerful tools, we can build really cool stuff without too much effort. Hopefully this is a useful reference for anyone wanting to use modern web development tooling in their embedded projects.

]]> 0
Ready-Made MiLight Hub Sat, 09 Nov 2019 06:06:26 +0000 h4nc over at the HomeAssistant community forums has put together a spectacular ready-made hardware set for my ESP8266 MiLight Hub software. This includes a PCB and a 3D-printed case.

These look super snazzy, and will remove a lot of the guesswork from the setup process (skip the jumper wire fiddling and head-scratching when you read the pinout wrong!)

Awesome job, h4nc!

h4nc is offering ready-made kits which include:

  • A NodeMCU pre-flashed with the latest version of ESP8266 MiLight Hub
  • An nRF24 module with antenna
  • 3D-printed case

To order one for yourself, please get in touch with him at h4nc.zigbee(a), or drop him a PM on the HomeAssistant community forum.

You can see (and leave) feedback on h4nc’s readymade kits on the aforementioned HomeAssistant forum post.

Open Source

h4nc graciously open-sourced his work. You can find the Gerber files on Github, and the 3D-printable STLs on Thingiverse.

If you do end up making one of these for yourself, please consider a donation to h4nc. Contact him at h4nc.zigbee(a)


]]> 3
Custom HomeAssistant Auth Provider Mon, 30 Sep 2019 09:10:43 +0000 Background

HomeAssistant has a builtin authentication system bundled with a couple of providers which allow you to control how users are authenticated. Although I have been impressed with how thorough and robust the implementation is, it’s not terribly easy to integrate with an external authentication provider. There is the command-line provider, but there you’re still limited to schemes that use username/passwords entered by the user in the UI.

I’ve been using client-side TLS certificates to authenticate with internal services for a couple of years, and have been really pleased with the result:

  • Done correctly, client certificates are very secure.
  • Putting aside a really expensive initial setup cost, client certificates provide a fantastic end-user experience.

My family likes using HomeAssistant, but I don’t want anyone to deal with another password. I’d rather just rely on the client certificate auth system I’ve got set up. I endeavored to accomplish this, and figured I’d share the process and the results.

The Problem

Despite the extensible provider architecture, HASS does not currently support custom providers. If you’re willing to don your hacker fedora, you can get this done, however. There are two basic problems that need solving:

  1. Create an auth provider that interacts with client certificates
  2. Find a way to plumb in a custom auth provider

In the following sections, I’ll detail the solution I found.

Custom Auth Provider

First, a bit of background on how I’ve got client certs set up. I wrote about an earlier version a while back, but it’s evolved since then. That said, it still works the same way using some nginx lua scripting:

  1. When requesting a protected resource, nginx validates a cookie (which is a signed JWT).
  2. If the cookie is valid, the user is let through. If not, they’re redirected to an auth endpoint.
  3. The auth endpoint validates a client certificate. On passing, it generates a JWT, writes it to a cookie, and redirects to (1).

Sidebar: This might seem convoluted, but I promise it makes some amount of sense. To avoid interrupting the flow of the writeup more than this sidebar already has, I’ll stuff my reasoning in an appendix.

Given this, it’d work nicely for the custom auth provider to operate by verifying the JWT in the cookie. The claims in the JWT contain a username we can use. Here’s a gist containing an auth provider that does this, and the relevant configuration changes. The rough flow here is:

  1. When the login flow is created, HASS puts the client’s cookies in the flow context.
  2. In a single go, we extract the relevant cookie from the flow context, parse the JWT it contains, verify the signature, and extract the appropriate claim from the payload to use as the username.
  3. Either create the username we found, or return the existing user with that username.

Note that this won’t work out of the box because HASS does not forward cookies from the client that initiated the login flow. This tiny patch of HASS internals fixes that, though (we’ll cover this in the next session).

Auth Provider Plumbing

Now we’ve got to get HASS to recognize our changes. This isn’t difficult to solve, but there’s not a way to accomplish any of it without getting into the guts of HomeAssistant. So if you’re looking for a clean, officially supported solution, you’re unfortunately not gonna find it here.

All we’ve got to do is put the auth provider from the previous section the source directory that HASS expects (homeassistant/auth/providers), and apply the patch for mentioned in the previous section. We’ll also want a way to ensure the changes stick around after system updates.

If you’re using docker, this reasonably straightforward. Simply put the auth provider and the patched version of in the HASS configuration directory, and then link them to the appropriate places within the docker container. I use a shell script to manage updates, so I just tossed the link commands at the end:


run-container-cmd() {
  docker exec -u 0 -ti home-assistant /bin/bash -c "$@"

docker pull homeassistant/home-assistant
docker stop home-assistant
docker rm home-assistant

docker run -d \
  --name="home-assistant" \
  -v /srv/homeassistant:/config \
  -v /etc/localtime:/etc/localtime:ro \
  --net=host \
  --restart always \
  --device=/dev/ttyACM0:/zwaveusbstick:rwm \

if run-container-cmd "[[ ! -L /usr/src/homeassistant/homeassistant/components/auth/ ]]"; then
  echo "Auth directory does not exist. Creating links from config..."

  run-container-cmd "mv /usr/src/homeassistant/homeassistant/components/auth/ /usr/src/homeassistant/homeassistant/components/auth/"
  run-container-cmd "ln -s /config/custom_components/auth/ /usr/src/homeassistant/homeassistant/components/auth"
  run-container-cmd "ln -s /config/custom_components/auth/providers/*.py /usr/src/homeassistant/homeassistant/auth/providers"

  echo "Restarting..."
  docker restart home-assistant

And that’s about it! When HomeAssistant reboots, the login page should look like this:

Clicking on the “Next” button should result in the user being logged in, provided they’ve got the JWT cookie set (and they have to, given that nginx will not let this page render otherwise).


This works beautifully. No password required when logging into HomeAssistant, and no meaningful security concessions.

I am, however, pretty disappointed with how much hacking around it took to accomplish this. Hopefully custom stuff like this gets easier in future releases.

Appendix: What’s all this JWT Cookie Rigamarole?

The client certificate SSO flow mentioned previously probably seems a little Goldbergian. The reasons that I landed on and stuck with it are:

  • Some websites just don’t work with client certificates. Particularly — and this may have improved over the last few years without me noticing — web socket requests do not play well. Sometimes, client certs are not sent along with requests.
  • Scoped authorization. Client certificates alone provide authentication, but nothing other than binary authorization. This system provides a natural place for more finely grained authorization.
  • Provides an escape hatch for systems where using client certificates is a pain in the butt. Curling an endpoint? No problem. Grab an access token, and put it in an Authorization header.
  • Similarly to the previous point, provides a way to grant temporary access without needing to deal with CRLs. Since JWTs can include expiration times, you can make access however temporary you like.
]]> 1
DIY Pet Treat Dispenser Wed, 12 Jun 2019 02:57:13 +0000 When I first saw Internet-connected pet treat dispensers like PetCube, I laughed them off. “Who would ever want such a thing?” I have cats, but this just felt silly.

Then we went on vacation and left our cats at home (we hired a sitter, of course). There was one day where our sitter’s car broke down, and wasn’t able to come on the day he’d planned. We have an automatic food dispenser and a water filter, but we naturally started worrying about the furrier members of our family. Then I understood why these goofy treat dispenser products exist.

You’re a kitty!

I’ve seen PetCube used, and it seems like a good product. But cloud-based home video products aren’t something I’m terribly comfortable with. So I set out to build my own. (Also, if I’m being honest, I was looking for a tinker project anyway).


The list of features I wanted were:

  • 3D-printable. I wanted to make as much of the body out of 3D-printable parts as possible.
  • Local control. Cameras that phone home spook me. I planned on hooking it up through HomeAssistant to allow remote access.
  • Camera. Of course the whole point is to spy on cats.
  • Audio. Play short sound clips to let the kitties know they’ll get a treat if they come look cute in front of the camera.
  • Dispense on command. No stringent requirements on treat size, consistent dispense count, etc. Enough to make it worth Eleanor’s while, but not enough to make her too fat.

There are readily available components for all of these things. Tying them together on one perfboard was a pain in the butt, but doable. If I were competent at circuit design, I’d probably have done that instead.

Shopping List

These are the components I used to get the job done. (note that any links contain Amazon Affiliate referral codes.)

All told, this was probably around $50 in parts, plus a bunch of crap I already had lying around.

Printing and Assembling the Body

STLs are on Thingiverse.

I did not adjust parts to be in the proper printing orientation. It’s usually mostly obvious which way they should be oriented. The only slightly tricky ones are the upper half of the main body, which should be printed with the top facing the printbed (i.e., upside-down), and the hopper, which should also be printed upside-down. There are 10 parts in total

These parts are not very difficult to print for the most part, but there are some bridges that are a stretch. Make sure you’ve got your cooling settings dialed in. PLA is probably fine for everything, but I used PETG for the gears and shaft.

Assembly should be mostly straightforward, but I can post some pictures or a video if there’s confusion.

Controller Circuit

There’s nothing fancy going on here. It’s just connecting components together, but there are quite a lot of things to connect. There’s a sloppy Fritzing diagram and a pin mapping table on the Github project, so I won’t rehash it here.

I soldered everything together on perfboard. For what it was, it came out reasonably clean, but it was definitely a stretch. If I were doing this again, I might take the time to lay out an actual PCB and have one printed.


I have the ESP32 controller firmware I wrote on Github.

When first connecting, a setup AP named ESPXXXX (with random XXXX) will appear allowing you to enter your wifi details.

There’s currently no UI. After connecting to your wifi, the easiest way to configure it is via the REST API (use PUT /settings).

The REST API is documented in the Github README. If there’s enough interest, I can document the setup procedure in greater detail.

The Finished Product

Here are some additional pictures of the guts:


This was a really fun and challenging project. The end result is certainly not as polished as an off-the-shelf product, but I’ve been pretty surprised with how well it works.

My cats are unfortunately only occasionally interested in treats, but when they are, they come running. It serves the intended purpose as well as it can.

There are a ton of independent steps you’d need to follow to reproduce this. Honestly, I wouldn’t recommend it unless you’re looking to get your hands really dirty–no really, like encased in dirt.


]]> 4
Custom Prusa IKEA Lack Enclosure Parts Sun, 09 Sep 2018 05:05:50 +0000 Earlier this year, Prusa released their take on a 3D printer enclosure made from the famous IKEA Lack tables and printable parts.

There are a wealth of printable accessories for this enclosure.  I’ve found these ones really nice:

I’ve designed a few parts of my own that I’m pretty happy with.  I would not be surprised to learn there are equivalent or better alternatives to these.  I did try looking, but not too hard.  I was happy to have the design challenge.

Fan Mount

Thingiverse link.

Enclosures get hot enough to screw with PLA print quality.  I added a ventilation fan which is capable of keeping the temperature in safe ranges (~27 C).

This is a mount for a standard 120x120mm computer case fan.  I’m using this Corsair AF120 fan*.

The mount slides into a centered cutout approximately 129x129mm on one of the acrylic sheets (I’m using the rear one).  

I had intended for the cutout in my sheet to be closer to 122x122mm, but the company I bought the sheet from didn’t get the measurements exactly right.  It was nice to be able to easily resize the part in Fusion 360 and print it out to-size.

1″ Grommet

Thingiverse link.

I drilled a 1″ hole through the bottom table to feed these cables through:

  • Two Logitech C270 * USB cables
  • LCD ribbon cables
  • 24v cables from the PSU

To make the hole look nicer I “designed” a grommet to fit the crappy hole my 1″ drill made.

Birdseye Mount for Logitech C270

Thingiverse link.

The Logitech C270* is a super cheap (~$20) 720p USB webcam that works really well with Octoprint.

I have two of them in my setup.  First, the aforementioned x-axis mounted camera.  Great for making sure the print is looking good where it’s at.  Example view:

And the one placed in this mount, which gives a birds-eye view of the whole print bed.  Example view:

Modified Door Handles

Thingiverse link.

I redesigned the included door handles from scratch, mostly in order to improve my Fusion 360 design skills.

There are a few aesthetic differences, but the functional difference is that there are recesses appropriately sized for some 20x10x2mm N50 magnets* I had laying around.


I’ll share how I’m controlling the fan and lights in a future post.  Long story short, it’s an ESP8266 with some MOSFETs and ancillary circuitry.

[*] Contains affiliate link

]]> 3
Reusable Dash Button Case Sun, 02 Sep 2018 08:08:32 +0000 I use Dash Buttons* in quite a few places around my home — mostly as a substitute for a light switch where one is inconveniently located, or not present at all.

I prefer them to alternative options like the Flic Button* because they’re dramatically cheaper (a Dash is $5, compared to $35 for a Flic).  They’re also occasionally on sale for $0.99.

My only frustration with Dash buttons is that they’re meant to be disposable, despite being powered by a replaceable AAA battery.  The electronics are encased by two pieces of welded plastic.  It’s easy to break the weld, but difficult to reassemble in a pretty way.

Having recently started dabbling in 3D design and printing, I decided to create a reusable case.  The humble fruit of my efforts is here:

I’m happy with how this turned out — it’s easy to open the case and replace the battery without damaging anything.


Pretty straightforward.  I took apart the stock case using some channel locks to break the welds:

With a little bit of elbow grease, and a T5 screwdriver to remove the battery enclosure, it comes apart like so:

A pry tool can be used to remove the PCB if it doesn’t come off by itself.

Assembly is straightforward.  First, put the plastic button and the rubber seal in place.

Then the PCB is placed back on the pegs, battery enclosure placed on top, and T5 screws added back.  Do not over-tighten the screws!  The printed pegs are quite fragile and will break under too much pressure.

After adding the battery back, the lid can be pressed onto the body:

And that’s it!  Fully assembled Dash case.

Update: Sept 4, 2018

I’ve uploaded a slightly modified version.  The main change makes it harder to over-tighten screws making the button unpressable.

[ * ] Contains Amazon affiliate link

]]> 1
Security Hardening Octoprint/Octopi Mon, 23 Jul 2018 05:03:33 +0000 Octoprint is a great web frontend for 3D printers. Octopi is a raspbian-based image for a Raspberry Pi that comes with everything you need set up and configured.

Octoprint is an extremely convenient way to manage your 3D printer.  However, it’s capable of a lot of spooky things:

  1. If you have them, provides access to webcams showing prints
  2. Can set temperatures of both the tool and the heatbed
  3. Start whatever print you feel like
  4. Control steppers

In the best case, Octoprint gives whoever can access it the ability to see into your house and what’s going on with your printer.  In the worst case, someone with malicious intent could burn down your house, or at least wreck your printer.

The smartest approach here is probably to put Octoprint on a trusted network and refrain from poking holes in your router to allow access from the Internet.

But I’m not that smart.

In this post I’m going to outline a couple of things I did that make me feel better about exposing my Octoprint instance to the Internet.

Prior Art

First of all, Octoprint has builtin access controls.  And you should definitely use those.

I feel strongly that these are not sufficient, however:

  1. Unauthenticated users can do way too much.  Most importantly, they can view webcam feeds.  Yikes!
  2. There have been bugs with the builtin access controls.

Secondly, others have done things similar to what I’ve done.  However, there are a couple of things I’m going to do differently, and there are a few additional things I want to do.


  1. Every interaction with Octoprint should go through a reverse proxy.  It should not be possible to access any part of Octoprint except through the reverse proxy.
  2. The last requirement should apply even if you’re on my local network.  Something about unauthenticated Webcam feeds gives me the jeebies.  Even if they’re pointed at a corner.
  3. I’m not going to run a web-facing nginx instance on Octoprint.  I want to use my main server as an entry point.
  4. Use client certificates for auth (I covered this in a previous post).
  5. TLS via letsencrypt.

Close down the ports

By default, Octopi exposes the Octoprint web interface on port 80 (via haproxy), and the webcam feed via mjpeg_streamer on port 8080.

I didn’t want these ports accessible except through loopback.  This is easy enough to change.

To shut down access to the Octoprint instance, just disable haproxy:

$ sudo service haproxy stop
$ sudo update-rc.d haproxy disable

The Octoprint instance itself listens on port 5000 by default, and is bound to loopback.

To shut down access to mjpeg_streamer, we’ll have to fiddle with the script stored at


$ diff /root/bin/webcamd /root/bin/webcamd.bkup
< camera_http_options="-n -l"
> camera_http_options="-n"

This tells mjpeg_streamer’s http plugin to bind itself to loopback.  For it to take effect, make sure to restart the webcamd service (or just reboot the pi to be safe).

To test that this worked, try accessing http://octopi.local and http://octopi.local:8080.  You should get connection refused errors for both.

Open up the ports (on nginx server)

If you plan on running nginx on the pi, you can skip this step.  I have a different server running nginx.

In the last step, we shut down the ports to Octoprint.  Now we need to give the server running nginx a way to access them.

An easy way to accomplish this is with local SSH tunnels.  Setting this up is easy enough:

  1. Create a user on the octopi instance.  I called mine something to the effect of “ssh-proxy”
  2. Create a corresponding user on the server running nginx.  Generate an SSH key.
  3. Add the public key for ssh-proxy@nginx-server to ssh-proxy@octopi:~/.ssh/authorized_keys
  4. Set up autossh to establish a persistent SSH tunnel.  This will reestablish the tunnel when the pi reboots or connectivity is broken for any reason.  This is the command I used:
    sudo -u ssh-proxy bash -cl 'autossh -f -nNT -L 25000:localhost:5000 -L 28080:localhost:8080 -L 28081:localhost:8081 ssh-proxy@octopi'
  5. Execute the above command on boot.  I accomplished this by putting it in /etc/rc.local.

Now Octoprint should be available on the nginx server via port 25000.  Same deal for the webcam feed on 28080 (I have another webcam accessible via 28081).

Note that these should be bound to loopback because of the way the tunnel is set up.  No point in all of this noise if that’s not the case.

Make ’em accessible

Now we can go about this if it were a standard reverse proxy setup.  The backends are accessible by loopback on ports local to the nginx server.

You can set up authentication however you like.  It’s probably easy and safe to use TLS, HTTP auth, and something like fail2ban.

I like client certificates, and already had them set up for other stuff I run, so I’m using those.

This is my config:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;

upstream octopi_camera1 {

upstream octopi_camera2 {

upstream octopi_backend {

server {
  listen 80;
  listen 81;
  return 301 https://$host$request_uri;

server {
  listen 443 ssl; # managed by Certbot

  error_log  /var/log/nginx/ info;
  access_log /var/log/nginx/;

#.... bunch of SSL jazz auto-generated by certbot .....

  proxy_buffering off;
  proxy_redirect http:// https://;
  proxy_set_header        X-Real-IP $remote_addr;
  proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header        X-Forwarded-Proto $scheme;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;
  proxy_set_header Host $host;

  # I found this necessary in order to be able to upload large-ish gcode
  # files.
  client_max_body_size 1G;

  location /webcam/ {
    proxy_pass  http://octopi_camera1/;
    access_by_lua_file /etc/nginx/scripts/sso.lua;

  location /camera2/ {
    proxy_pass  http://octopi_camera2/;
    access_by_lua_file /etc/nginx/scripts/sso.lua;

  location / {
    proxy_pass  http://octopi_backend;
    access_by_lua_file /etc/nginx/scripts/sso.lua;

What’s this access_by_lua hocus pocus?

I covered this in a previous post.  The problem is that modern web applications don’t really play nicely with client certificates, and this seemed to include Octoprint.  There’s a bunch of wizardry with web sockets and service workers that don’t send the client cert when they’re supposed to.

The basic idea behind the solution is to instead authenticate by a couple of cookies with an HMAC.  When these cookies aren’t present, nginx redirects to a domain that requires the client certificate.  If the certificate is valid, it generates and drops the appropriate cookies, and the client is redirected to the original URL.

See the aforementioned post for more details.

Goes without saying, but…

The Raspberry Pi itself should be secured as well.  Change the default password for the pi user.

]]> 4
Single Sign On with client certificates Sun, 22 Jul 2018 21:08:02 +0000 In a previous post, I detailed a trick to get complicated webapps working with client certificates.

The problem this solves is that some combination of web sockets, service workers (and perhaps some demonic magic) don’t play nicely with client certificates.  Under some circumstances, the client certificate is just not sent.

The basic idea behind the solution is to instead authenticate by a couple of cookies with an HMAC.  When these cookies aren’t present, you’re required to specify a client certificate.  When a valid client certificate is presented, HMAC cookies are generated and dropped.  If the cookies are present, you’re allowed access, even if you don’t have a client certificate.

This has worked well for me, but I still occasionally ran into issues.  Basically every time I started a new session with something requiring client certs, I’d get some sort of bizarre access error.  I dug in a little, and it seemed like the request to fetch the service worker code was failing because the browser wasn’t sending client certificates.

This led me to double down on the HMAC cookies.

Coming clean

When I call this Single Sign On, please understand that I really only have the vaguest possible understanding of what that means.  If there are standards or something that are implied by this term, I’m not following them.

What I mean is that I have a centralized lua script that I can include in arbitrary nginx server configs, and it handles auth in the same way for all of them.

The nitty gritty

Rather than using HMAC cookies as a fallback auth mechanism and having “ssl_verifiy_client” set to “optional,” I do the following:

  1. If HMAC cookies are not present, nginx redirects to a different subdomain (it’s important that it’s on the same domain).  This server config requires the client certificate.
  2. If the certificate is valid, it generates and drops the appropriate cookies, and the client is redirected to the original URL.  The cookies are configured to be sent for all subdomains of a given domain.
  3. Now that the client has HMAC cookies, it’s allowed access.  If the cookies were present to begin with, the above is skipped.

The setup has a couple of pieces:

  1. An nginx
     for an “SSO” domain.  This is the piece responsible for dropping the HMAC cookies.
  2. A lua script which is included everywhere you want to auth using this mechanism.

This is the SSO server config:

server {
  listen 80;
  return 301 https://$host$request_uri;

server {
  listen 443 ssl; # managed by Certbot

  error_log  /var/log/nginx/;/error.log info;
  access_log /var/log/nginx/;/access.log;

#....bunch of stuff generated by certbot....#

  ssl_client_certificate /etc/ssl/ca/certs/ca.crt;
  ssl_crl                /etc/ssl/ca/private/ca.crl;
  ssl_verify_client      on;

  location / {
    access_by_lua_file "/etc/nginx/scripts/sso.lua";

And the SSO lua script:

-- Make this file only readable by the nginx process, and keep it away from web roots.
local HMAC_SECRET = “hunter2”

-- Set this to your domain.  Note that you’ll only be able to use this
-- for things that have this same TLD.
local DOMAIN = “”

local COOKIE_TTL = 864000
local crypto = require "crypto"

function ComputeHmac(msg, expires)
  return crypto.hmac.digest("sha256", string.format("%s%d", msg, expires), HMAC_SECRET)

function formatCookie(key, value)
  return string.format(
    "%s=%s; Secure; Path=/; Expires=%s; domain=.%s", 
    ngx.cookie_time(ngx.time() + COOKIE_TTL), 

if ngx.var.server_name == string.format(“sso.%s”, DOMAIN) then
  verify_status = ngx.var.ssl_client_verify

  if verify_status == "SUCCESS" then
    client = crypto.digest("sha256", ngx.var.ssl_client_cert)
    expires = ngx.time() + COOKIE_TTL

    ngx.header["Set-Cookie"] = {
      formatCookie("AccessToken", ComputeHmac(client, expires)),
      formatCookie("ClientId", client),
      formatCookie("AccessExpires", expires)

    return ngx.redirect(ngx.unescape_uri(ngx.var.arg_r))
  client = ngx.var.cookie_ClientId
  client_hmac = ngx.var.cookie_AccessToken
  access_expires = ngx.var.cookie_AccessExpires

  if client ~= nil and client_hmac ~= nil and access_expires ~= nil then
    hmac = ComputeHmac(client, access_expires)

    if hmac ~= "" and hmac == client_hmac and tonumber(access_expires) > ngx.time() then

  return ngx.redirect(string.format(“https://sso.%s/?r=%s”, DOMAIN, ngx.escape_uri("https://" .. ngx.var.http_host .. ngx.var.request_uri)))

An example of it being used:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;

upstream myservice {

server {
  listen 80;
  return 301 https://$host$request_uri;

server {
  listen 443 ssl; # managed by Certbot

#.....bunch of stuff managed by certbot.....#

  proxy_buffering off;
  proxy_redirect http:// https://;
  proxy_set_header        X-Real-IP $remote_addr;
  proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header        X-Forwarded-Proto $scheme;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;
  proxy_set_header Host $host;

  location / {
    proxy_pass  http://myservice;
    access_by_lua_file /etc/nginx/scripts/sso.lua;

]]> 2