CC Blog Projects Research & Design Hub

IoT Desk Controller

FIGURE 1 This is the corner of my desk with the added controls. The two-button keyboard aligns naturally with “up” and “down.” The blue board is the Adafruit RFID reader. The wires, taped to the bottom of the desk, lead to my control board in the desk’s cable tray.
Written by Chris Cantrell

Using a Raspberry Pi Pico

Tired of pushing a button to change his desk’s height, Chris Cantrell whipped up his own automated solution using a Raspberry Pi Pico, a sonar distance sensor, and an RFID ring. Now, he can change his desk’s height with a wave of his hand.

  • Raspberry Pi Pico
  • Sonar Distance sensor
  • RFID ring
  • Adafruit |
  • ExpressPCB |
  • Raspberry Pi |

With the pandemic behind us, the world is returning to the office. Some are going back hesitantly, having become used to the short commute and relaxed work attire. But I was excited to get back! I left an unfinished personal project at the office.

I have this wonderful, motorized desk at work. The entire six-foot surface rises and falls between sitting and standing positions with the push of a button—well, with the holding down of a button, which is embarrassingly outmoded in our modern world of automation. I used to cover my face so my friends at other companies wouldn’t see me changing my desk height manually. But no more! With a fun little project, I rid myself of this shame and humiliation.

In this article, I’ll show you how I used a Raspberry Pi Pico, a sonar distance sensor, and asynchronous CircuitPython code to automate my desk’s movement. Now my time is freed up for other essential tasks like stretching and moving my chair.

If you build a project, the features will come. I’ll show you how I added an RFID reader that lets me move the desk with the wave of a hand—a hand with a finger that is wearing an RFID ring.


My office desk has two buttons, one for up and one for down, built into the front-right corner of the desk. You can see the original buttons along with my new additions in Figure 1. I used a screwdriver to pop the cover from the original buttons, and you can see the insides of the button box in Figure 2. There are two small buttons on either side of a single-sided PCB. You can follow the wiring traces on the PCB in the photo. The center white wire connects to the bottom of each button. The gray wire on the left is for the up switch, and the black wire on the right is for the down switch. I used a paper clip to short the wire pads together. As expected, when I shorted white to gray, the desk moved up, and when I shorted black to white, it moved down.

This is the corner of my desk with the added controls. The two-button keyboard aligns naturally with “up” and “down.” The blue board is the Adafruit RFID reader. The wires, taped to the bottom of the desk, lead to my control board in the desk’s cable tray.
This is the corner of my desk with the added controls. The two-button keyboard aligns naturally with “up” and “down.” The blue board is the Adafruit RFID reader. The wires, taped to the bottom of the desk, lead to my control board in the desk’s cable tray.
I popped the cover from the desk's buttons using a small screwdriver. You can trace the circuit on the single-sided board within. The white wire is common to both switches. The gray wire is "up," and the black wire is "down."
I popped the cover from the desk’s buttons using a small screwdriver. You can trace the circuit on the single-sided board within. The white wire is common to both switches. The gray wire is “up,” and the black wire is “down.”

The three wires exit the bottom of the button box underneath the desk as a small cable that leads to the desk’s control box. The cable was dangling there, and it had plenty of slack, so I cut it and soldered 3-pin connectors to both ends of the cable. I used a second pair of connectors to secretly modify a friend’s desk when he was away. This was an excellent opportunity for some team-building office shenanigans. More on that later.

Next, I brought in my favorite microcontroller, the Raspberry Pi 2040 Pico (with Wi-Fi), and loaded it with a fresh install of CircuitPython. The desk wires are low-voltage signal wires to a processor, not current-carrying wires directly to the motors. I needed a couple of small relays (instead of a paperclip) to short the wires together.

First, I tried a pair of familiar, clicky, mechanical relays [1]. But I soon replaced them with solid-state relays [2]. You can find both parts on Circuit Cellar’s Article Materials and Resources webpage.

Figure 3 shows the schematic for my controller board. The relays are in the upper left corner of the figure. They momentarily connect the wires of the desk’s controller cable that is connected to the three screw terminals of J1. The three wires from the desk’s buttons come in on the three screw terminals of J2.

The schematic consists mainly of screw-terminal breakouts for the RPi Pico's GPIO pins. There are a few resistors for current-limiting and I2C pull-ups. The two solid-state relays are in the upper left corner.
The schematic consists mainly of screw-terminal breakouts for the RPi Pico’s GPIO pins. There are a few resistors for current-limiting and I2C pull-ups. The two solid-state relays are in the upper left corner.

The three jumpers, JP1, JP2, and JP3, let you select how the desk buttons are used. With the jumpers in the top position, the buttons are wired to the controller cable as they were originally. The relays are essentially buttons, and this jumper setting ORs the buttons and relays together. Either the relay or the button can short the wire to start the motor.

With the jumpers in the bottom position, the buttons are isolated from the motors. Instead, they are wired to two of the Pico’s GPIO pins. The shared wire is grounded. This allows the Pico to read the switches and decide when and if the desk should move. For my project, the jumpers are set in the top “OR” position.

I built the control circuit on a prototyping breadboard, as seen in Figure 4. You can see the Pico on the top side of the board surrounded by blue screw terminals. The two white chips at the top are the solid-state relays. The bottom of the board is a rat’s nest of 30 AWG wire-wrapping wire soldered point-to-point.

The controller is built on a breadboard with the RPi Pico in the middle, surrounded by the screw terminal blocks along the edges. The distance sensor is mounted on a wooden extension arm pointing to the floor. The back of the board is a colorful rat's nest of 30AWG point-to-point wiring.
The controller is built on a breadboard with the RPi Pico in the middle, surrounded by the screw terminal blocks along the edges. The distance sensor is mounted on a wooden extension arm pointing to the floor. The back of the board is a colorful rat’s nest of 30AWG point-to-point wiring.

With my control board wired between the desk buttons and motor controller, I plugged the Pico into my computer and typed some commands at the interactive REPL. I entered Python commands to import the GPIO libraries, configure the two relay pins as outputs, and set the “up” output pin high. The desk started rising! Then I scrambled to type the “set output low” command while the keyboard was moving.

I created a hardware object in the code to abstract the primitive actions I would need for control code: functions like motors(True, False) to start/stop the motors and getDistance() to read the desk height (coming up in the next section). By hiding your hardware behind code, you can prevent conditions like turning both motors on at once. That is the same as pressing the up and down buttons simultaneously, which is easy to do on my desk. I tried it. The motor controller ignores this button state and leaves the motors off. This might become a useful lockout feature. My software could turn on both relays simultaneously to lock the desk and prevent a user from moving it.


One of the first steps of every project I undertake is to put it on the web. Imagine my desk with its very own web server. I could go to its web page and see what its current height is. I could raise and lower it remotely when I’m working from home. That would amuse my coworkers in my absence. I could post a height change and get an email notification when the desk has reached its requested height. The possibilities are endless.

Adafruit makes a feature-rich web server for CircuitPython, but it is a synchronous library. I wanted to use Python’s asyncio capabilities for multitasking. I found Anthony Zhang’s “biplane” repo on GitHub [3]. It is a single-file webserver for CircuitPython that uses asyncio. Just download the one file and drop it in the lib directory of your Pico.

My web needs are simple. I added an endpoint for “GET /desk” that reads and writes the desk hardware. The writing is done through URL query parameters. And I added an endpoint for “GET /” that returns the contents of index.html on the filesystem. This webpage, in turn, presents a user interface and calls the “/desk” URL as needed.

Before you balk, I know that HTTP GET should be idempotent. It shouldn’t be used to change states (like the desk’s height); that’s what we have the POST verb for. But this is my simple project, and I like using the web browser to test the hardware. To control the motors, I point my web browser to my desk with a URL like “/desk?up=1&down=0” where “1” means the motor is on and “0” means the motor is off.

Now for the office shenanigans that I mentioned earlier. I waited for my friend Russ to go for coffee, and I hooked my control board into his cable that I had modified earlier. I carefully hid the board in the cable tray beneath his desk out of sight. When he returned and started working again, I pulled up my web browser and bumped his desk up an inch or two. He laughed and walked straight to my desk. I guess it was no mystery who would do such a thing.


I used an HC-SR04 ultrasonic distance sensor to measure the height of my desk [4], allowing the software to move the desk to preset heights. For my needs, there are only two presets: sitting and standing.

You can see the blue sensor board near the middle of Figure 4. It is mounted under a wooden extension arm, and it points down towards the floor. The controller board mounts in a cable tray beneath my desk, with the sensor arm sticking out of the tray.

The sensor has four wires: 5V, GND, Trigger, and Echo. You start a sensor reading by raising the Trigger ping high, which raises the Echo pin high and sends an ultrasonic burst from the transmitter. When the pulse reflects off a surface and returns to the receiver, the Echo pin goes low. Your code measures the burst’s time of flight (the time until Echo goes low) to determine the distance. CircuitPython has a library that twiddles the trigger, times the pulse, and performs the math for you. All you do is ask for a reading, and the library gives you back the distance in centimeters.

Listing 1 shows the DeskHeight task that moves the desk to a requested height. The asynchronous task wakes up twice a second and checks for a height request. It looks at the current desk height, determines whether the desk should go up or down, and turns on the appropriate motor. The inner while loop reads the distance ten times a second and turns the motors off when the desk reaches (or passes) the desired height.

LISTING 1This is the "go to requested height" function. The outer loop waits for a height request and starts the motors moving up or down. The inner loop polls the height sensor and stops the motors at the desired height. The inner loop sleeps for a tenth of a second between readings, allowing other tasks to use the CPU.

DRIFT_UP = 1.25class DeskHeight:    async def run_task(self):                while True:            self._abort = False            if self._mode != DeskHeight.MODE_GOTO:                await asyncio.sleep(0.5)                continue                        height = self._get_average_height()            if height < self._target_height:                       # Move up                         adj_target_height = self._target_height - DRIFT_UP                                self._hardware.set_motors(True, False)                while height < adj_target_height:                    if self._abort:                        break  # Back to the top of the WHILE                    await asyncio.sleep(0.1)                    height = self._hardware.get_height()                self._hardware.set_motors(False, False)                await asyncio.sleep(1) # Wait for any drift                            else:                   # Move down (same logic as move up)                pass                         self._mode = DeskHeight.MODE_IDLE

The await asyncio.sleep(0.1) puts the task to sleep for a tenth of a second, allowing other tasks to run while it sleeps. Another task may decide to abort the inner loop by setting the task’s abort flag.

While testing this routine, I discovered that my desk drifts slightly when the motors are stopped. I modified the inner loop to print the height when the motors are turned off. Then, I printed the distance again after sleeping for five seconds. I discovered the desk drifts about 1.25cm in either direction after the motors are stopped. I added that height adjustment before the inner loop.

I modified the web server code to look for a distance in the query parameters like “/desk?goto=18.2”. This allows the web browser to give an exact height for the desk. Soon after that a coworker/friend in our Fort Lauderdale office heard about my desk project. I’d come back from lunch to find my desk as high as it would go and coworkers frowning at me. Shenanigans, it turns out, work both ways.


Visiting a website to change my desk height quickly became tedious. To make it easier, I added two extra buttons to the desk: one for “sit” and one for “stand.” I found a stylish two-button USB mechanical keyboard on Amazon [5]. I didn’t need the USB interface, so I took the keyboard apart and desoldered the control chip. I cut traces and soldered in fly wires to route the button contacts directly to the USB cable—one common wire, one wire for up, and one wire for down—same as the desk’s original buttons.

I cut the USB connector from the host end of the keyboard and stripped the wires for my screw terminals. I only needed three of the four USB wires. Rather than waste the fourth wire, I added an LED to the keyboard.

The keyboard already had an RGB LED that shone through a clear layer of the case. I desoldered that LED. Then I found a tiny blue LED on Amazon that fits perfectly in the case [6]. I fly-wired the cathode to the common ground of the buttons and the anode to the fourth USB wire.

Next, I created the ExtraButton asynchronous task that monitors button presses. This task object keeps the two setpoint heights for “sit” and “stand.” When you press either of the buttons, the ExtraButton task sends the height request to the DeskHeight task.

When you press both buttons simultaneously, the keyboard enters the “mark setpoint” mode. The LED flashes, prompting you to press either of the buttons. The desk’s current height is assigned to that button, and the LED stops flashing.

Thus, the desk control is very intuitive. You use the built-in desk buttons to set the desired desk height. Then, you press both buttons, followed by a single button, to assign the current height to that button. Repeat the process for the other button, and Bob’s your uncle.

The blinking LED is managed by an asynchronous LED task that paces the on and off times. Other tasks request a flashing mode, and the LED task blinks the LED accordingly. I defined four flashing modes: fast blink, slow blink, solid on, and solid off.

Let’s review the asynchronous code tasks in the software so far. We have the DeskHeight task, which reads the sonar and controls the motors. We have the WebServer task, which handles web requests. We have the ExtraButtons task, which monitors the two-button keypad, and we have the LED task, which manages the blink times of the LED.

Listing 2 shows the task management code. Each task is implemented in a separate Python class. The constructor of each task object takes a dictionary of pointers to all the other tasks. Thus, one task can easily send requests to any other. The function is the Python asynchronous magic. This function starts the five tasks and waits (gathers) until they all finish. Since my tasks are endless loops, the run call never returns.

All five services are kept in a name-to-service dictionary. Each service is constructed with this dictionary, making it easy for one service to talk to any other. The "" line of code starts all five asynchronous tasks. The CPU weaves in and out of these tasks, giving each a tiny slice of time while the others are blocked.

# Hardware access functionshard = Hardware()# Dictionary of services for all to shareservices = {}# Create all the servicesservices[‘desk’]    = DeskHeight(hard, services)services[‘led’]     = LED(hard, services)services[‘rfid’]    = RFID(hard, services)services[‘buttons’] = ExtraButtons(hard, services)services[‘web’]     = WebServer(hard, services)# Start all the service async    services[‘desk’].run_task(),    services[‘led’].run_task(),    services[‘rfid’].run_task(),    services[‘buttons’].run_task(),    services[‘web’].run_task()))

My desk at work is dedicated to me; nobody else uses it. But what about an office-hoteling environment where employees share desks (at different times)? Or how about the desk at home that my wife and I share? We have very different standing heights. How can I smarten up my smart desk controller to handle multiple tenants, remembering the height setpoints for each?

The desk controller needs a way to identify different users. Users could log in to the desk’s website, which is tedious. Instead, I added an RFID reader to the desk controller. Each user wears an RFID ring with a unique ID. The user waves their ring over the reader to configure the height setpoints just for them.

Adafruit sells an RFID reader breakout board and RFID rings in various sizes [7][8]. I bought the board and a couple of rings for experimenting. The board connects to your microcontroller with SPI or I2C. I used I2C. There is also an interrupt line you can configure to tell you when a ring appears at the reader. Thus, your code doesn’t have to constantly poll the reader in an “are we there yet?” pattern.

Figure 1 shows the corner of my desk with all the new controls. The USB cable and RFID wires are taped along the bottom of the desk to my control board mounted out of sight in the desk’s cable tray.

Each ring has a unique ID, and the controller could keep a map of IDs to setpoints in its filesystem. But these rings are smarter than that. They have data storage built into the RFID chips. The Adafruit board can also write values that are persisted on the ring!

I used an eight-byte text string to store two setpoints on a ring. Text is great for debugging, and you can read and write text values on a ring with your mobile phone’s NFC tools.

Each setpoint is four bytes of text. The first byte is either “U” or “D” for “up” or “down” to identify which setpoint the value is. The next three bytes are the desired height in centimeters, with the last digit being a single decimal place. Thus, a value of 32.5cm is coded on the ring as “325”. And the eight-character string “U325D172” read from the ring says the “up” height is 32.5cm while the “down” height is 17.2cm.

While experimenting with the rings, I discovered that each ring has two RFID chips: one on top near the decorative crystal and one on the opposite (palm) side of the ring. I wrote the “up” height value to the top chip and the “down” value to the bottom chip. Then I waved my hand, palm up, over the RFID reader and commanded, “Up, desk!” in a deep, dramatic voice. I imagined myself looking like Yoda using the Force to lift the desk. Then I waved my hand, palm down in a pushing gesture, to return the desk to its lower height. This time I heeded the scowls from my co-workers and skipped the verbal command.

Ultimately, I modified the ExtraButtons task to write both setpoint values to the ring whenever either setpoint is marked. The RFID task manages all reads and writes to the ring. For write requests, the task gives you five seconds to present the ring before it stops waiting. In the normal “read” mode, the RFID task watches for a ring and reads both setpoints from the eight-byte record. The RFID task checks the current height and requests a move to whichever setpoint is farthest away. Thus, you can wave the same FRID chip to toggle the desk between the two positions. Or, if you really like the dual gestures, you can write the same values to both chips in the ring and reach out with the Force as the desk moves.


Advertise Here


What’s next for the IoT smart desk controller? I want to collect and graph metrics on time spent standing and sitting. That will be fun web development work. Maybe the desk will award me badges for accumulated hours of standing, like the coveted Suresh Joachim badge after 76 hours and 40 minutes [9].

I am also planning a hardware makeover for the controller board. I’ll use ExpressPCB to spin a real PCB to replace the fragile rat’s nest of wires. I’ll print plenty of extra boards for all my coworkers. They deserve compensation after putting up with the ups and downs of my rigorous testing.

But the bottom line is that I had a lot of fun with this project—all from hacking three little wires. This is one of the few projects I’ve completed over the years that I still use daily. Getting up throughout the day is a healthy habit, and knowing I built the controller makes me proud to push the up button and take a stand! 


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article

Chris Cantrell ( is a Staff Engineer for Vertiv. He
also teaches for Professional and Continuing Studies at the University of
Alabama in Huntsville. When he isn’t working, you’ll find him soldering on
some fun IoT project or digging around in the ROMs of an old arcade game.
Chris has written several articles for Circuit Cellar over the years.

Supporting Companies

Upcoming Events

Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

IoT Desk Controller

by Chris Cantrell time to read: 15 min