The Kneespa DRx is a knee decompression machine. A Pi runs a PyQt5 touchscreen app, the app talks to an Arduino over serial at /dev/serial0, and the Arduino runs three actuators (axial, horizontal, and lateral) that press against somebody’s knee. There’s a pressure cap hard-coded at 80 lbs and a physical e-stop button, and I thought about both of them constantly.
I didn’t think about tests.
From the first commit in May 2025 until about February 2026, the Python side of this thing had no unit tests at all. I ran it on the hardware, trusted the hardware to tell me when I was wrong, and kept shipping features. Protocol 4 landed in July with 30-second oscillating cycles. I built a vanilla-JS demo of the knee for my portfolio. I cleaned node_modules/ out of the repo, which is its own story. Eight months of feature work on a medical device, no test suite.
Then I finally wrote one. Three commits over two weeks: a design doc, an 18-task TDD plan, then the suite itself, built on pytest, pytest-qt, a pty-backed FakeArduino, and PlatformIO mocks for the firmware side. 105 tests passing.
They started finding things right away.
What the tests found
The e-stop didn’t stop
run_pressure_sequence and set_to_pressure in main/helpers/protocols.py both have wait loops that sleep 50ms, check pressure, and repeat until they reach the target. Neither loop checked self.is_running. So if you hit e-stop during a ramp, the protocol thread kept pressurizing until the loop expired on its own schedule. The UI said STOPPED. The motor kept going.
The fix was one line at the top of each loop.
while self.current_pressure < target:
if not self.is_running:
return
# ...
That’s the whole patch. The thing that was supposed to protect somebody’s knee had never worked, and it came down to a missing guard clause. Not my proudest week.
The serial layer was throwing away its own ACKs
Every send() in main/helpers/arduino.py opened with self.serial_com.reset_input_buffer(). I wrote that line thinking it would clear stale bytes before the next command went out. What it actually did was discard whatever the Arduino had just sent, before anything got a chance to read it. The reader thread was racing the flush, and on a slow frame it lost. Status updates vanished. "OK" acknowledgments vanished. The keepalive T command kept timing out because its own reply was getting flushed by the next outgoing command.
I deleted the flush. Most of the “this feels laggy” complaints I’d been making to myself for a year came down to that one line.
Since I was already in the file, I added a threading.Event called _reader_ready so connect_to_arduino() waits for the reader thread to actually be listening before it calls verify_connection(). That cleared up a startup race that had been hiding inside a 10-second timeout. I also moved current_pressure and current_pos_c behind property getters and setters guarded by a _state_lock, because the reader thread and the protocol thread had been racing on shared ints. I never noticed that race. The tests did.
The 0-degree calibration mark was set to 0
main/config/kneespa.cfg is an INI file holding calibration marks for the lateral actuator. 0 degrees should be encoder position ~1390. Instead it said:
[CMarks]
0.0 = 0
The reset routine homes the lateral actuator to CMarks[0.0], so it was homing to 0, which sits below the bottom of the lateral range of 500 to 2400. Every reset drove the actuator past its mechanical limit until something stopped it. I’d been hearing that clunk for months and telling myself it was just how the home routine sounded.
The patch is one line in a config file:
-0.0 = 0
+0.0 = 1390
What I took from it
None of these were new bugs. The flush had been eating ACKs since day one, the e-stop had never worked, and the CMarks value was wrong in the very first checkin. I spent nine months quietly working around all three: longer timeouts to cover the flush problem, a shrug at the reset clunk, and an assumption that the e-stop worked because the UI said it did.
What I’d tell myself in May 2025:
- “Works on the hardware” isn’t the same as “works.” The hardware won’t tell you when your software is compensating for itself.
- Write the harness before you write the fourth protocol.
FakeArduinowith a pty took an afternoon. I could have had that afternoon nine months earlier. - Any wait loop on a machine that touches a person needs an
is_runningcheck. - Config files are code. A calibration file I never tested is code I never tested.
Next up is an adaptive pressure ramp that reacts to patient feedback. I’m writing the tests first this time.