Case Study
Pen Plotter Control System
Built an automated pen plotter for whiteboard drawing: RP2040 firmware controlling stepper motors and actuator hardware, with a Python GUI for interactive path planning, Bezier curves, and SVG file import.
Interactive Demo
Try the live simulation
The real EKF and Pure Pursuit controllers ported to TypeScript. Dial sensor noise, flip filter modes, and watch tracking change in real time.
Open Demo →Problem
Two-day take-home technical challenge: bring up a custom pen-plotter from bare hardware to something you can draw arbitrary shapes on. The hardware is a polar plotter — an RP2040 driving a rotating arm via a high-reduction stepper, and a linear actuator extending a pen along that arm — so every Cartesian point the user wants has to be converted into a (θ, r) pair and tracked accurately against ADC feedback.
The challenge was explicitly end-to-end: firmware, kinematics, a user-facing tool, and a calibration procedure, all delivered within the 48-hour window.
Approach
Split the system cleanly across the USB boundary. The RP2040 owns the hardware — stepper driver, ADC, pen actuator, closed-loop position control — and speaks a small command/response serial protocol (HOME, ROTATE <steps>, LINEAR <adc_target>, GET_POS). Everything above that lives in Python: kinematics, path planning, a matplotlib-based GUI with a click-to-draw interface, and a calibration routine.
That split meant the firmware stayed deterministic and easy to debug in isolation, while the application layer could iterate quickly on features (Bezier curves, SVG import, live plot) without reflashing the board.
Technical Details
Firmware runs on the RP2040 with a TMC5160 stepper driver over SPI, an ADS1015 ADC for actuator feedback, and a PCA9685 PWM driver for the pen. Motor resolution is 200 steps × 256 microsteps × a 20:1 gearbox — just over one million microsteps per revolution, which is what the θ-axis precision rides on. The linear axis uses closed-loop ADC control (0–834 counts over ~300 mm travel) with ±10 count tolerance and a 20-second safety timeout.
On the Python side, the kinematics layer converts (x, y) to (θ, r) via θ = atan2(y, x), r = √(x² + y²), interpolates along each segment at a configurable step size, and streams commands to the board. The GUI validates points against the workspace envelope before sending, shows a live plot of the arm position as commands execute, and supports Bezier and SVG inputs for more than just straight lines.
Outcome
Delivered a working end-to-end system within the 2-day window. Calibrated tracking tolerance landed at ±10 ADC counts — roughly ±3.6 mm on the linear axis — with sub-millimetre repeatability on rectangular test patterns. The live actuator visualisation made it obvious when a command had completed vs. when the hardware was still moving, which tightened the feedback loop during iteration.
The codebase is also the cleanest candidate in the portfolio for an in-browser simulation: the firmware's command protocol is small enough to reimplement in JavaScript and drive a visual RP2040 mock, which is the Milestone 5 interactive demo.
Tech Stack