Side project Hardware & software Ongoing

Motion tracking turret

Camera to contour to stepper angle to trigger

A pan/tilt turret that watches a room, finds whatever moved, aims at it, and fires a foam ball. The target is an entitled, panther-looking black cat named Mitski. The actual problem is getting from pixels to motor angles quickly enough to hit a moving cat, and reliably enough that the thing doesn't fire at shadows. I started it in 2023 on an ESP32, hit the limits of what that chip could do, and shelved it. I picked it back up in 2025 on a Raspberry Pi with an architecture that could actually hold the whole feature set. The current version covers image processing, motion control, a Flask control server, four CAD revisions of the mechanism, and a custom PCB. I wrote all of it myself, with no AI-generated code.

A SolidWorks assembly render of the current fourth revision, showing the barrel and hopper on the tilt axis, the gear train and stepper motors inside the frame, and the pan stage enclosed in the base.
The current design, V4 in SolidWorks. Direct drive, with the pan stage moved down into the base.
Four bare PCB revisions fanned out on a desk, a green V1 board behind three white boards labelled Mitski Killer V1.2A, V1.2B, and V1.2C, each with stepper driver footprints, screw terminals, and regulator sections silkscreened.
Board revisions, V1 through V1.2C. Each one a fabrication round to fix what the last one got wrong.
4 Mechanical revisions Belt-driven pan table through to direct drive, each one a rebuild rather than a tweak
3 Voltage rails on a custom PCB Designed in KiCad and iterated across four fabricated revisions to keep logic and motor power separate
0 Lines of AI-generated code Written by hand, unlike the Roadside work. Late on I asked ChatGPT the occasional question, but nothing here was generated
§ 01 The Problem

Two attempts, and why the first one stopped

The first build, in 2023, was an ESP32 system (Cam_Tracking_ESP) that I deliberately kept cheap, with as few external libraries as I could manage. That was the whole point at the time. I wanted to understand each layer instead of importing it. What stopped it was capability rather than effort. The ESP32 couldn't drive the kind of control interface I wanted, and it couldn't hold the control loop speeds that aiming and firing needed while processing frames at the same time.

Coming back to it in 2025 on a Raspberry Pi 4B meant giving up some of that and accepting prebuilt architecture, but it bought enough headroom for everything that followed. The Pi hosts the control GUI, runs the camera, processes frames, and drives the motors in one process.

01 Detect Frames get grayscaled and differenced against a stored base frame, then thresholded down to a binary mask.
02 Filter Contours under a configured area get thrown out. Whatever survives is merged into a single bounding box, and its center becomes the target.
03 Confirm A target only counts once it has been seen in ten frames in a row, and only if it isn't near the frame edge. Those two rules are what stopped it firing at noise and at things halfway out of view.
04 Convert Pixel offset becomes percentage-from-center, then motor degrees through a per-axis calibration constant. Both constants are configurable and get set from the calibration tab rather than hardcoded.
05 Aim and fire Pan and tilt steppers move to the angle, the spool motors come up to speed, and a servo drops a ball into them.
06 Re-baseline The base frame is cleared after every shot, since the turret has just moved and everything it can see has changed.
§ 02 Software

Mockable, so it can be tested without the hardware

Every hardware dependency sits behind an interface, picked at startup by a factory that reads config. Set mock on the camera or the GPIO layer and the system swaps in a stand-in that returns synthetic frames and swallows pin writes.

  • Why that matters. The whole application runs on a laptop with nothing plugged into it. That's also what lets CI work on a hardware project: GitHub Actions lints and runs the test suite on every push and pull request, on a runner that has no Pi attached.
  • Detection and motion are decoupled. An observer/event layer carries detections from the camera side over to the motor side, so image processing knows nothing about what acts on its output.
  • Motor control is layered. Raw stepper and servo drivers sit under an aiming controller and a firing controller, which both sit under a single motor-control facade. Pin-level detail stays at the bottom.
  • Anything tunable lives in JSON. Pin assignments, step mode, speeds, servo open and closed angles, minimum object size, spool time, and the degrees-per-percent aiming constants are all config, parsed into typed config classes at load.
  • Flask control server. A browser GUI streams the processed camera feed with its detection overlays, and handles settings and manual motor control. It can also show the raw difference mask or the drawn contours, which is what makes it possible to tune detection by eye.
  • Calibration tab. Aiming depends on how many motor degrees match one percent of frame offset, and that changes with lens, mounting, and gearing, so it can't be a constant in the source. The GUI walks the procedure instead. Click a point on the live frame and it stores that click as a ratio from center, along with the current stepper positions. Jog the turret until it is actually pointing at what you clicked, then save. It takes the motor deltas, divides by the click ratios, and pushes the resulting degrees-per-percent factors to the server for both axes.
The Mitski Killer web GUI on the Motor Control tab. A small processed grayscale frame at top left shows a detected bounding box drawn around the cat, the live color stream sits center, and DC motor, stepper, and servo control panels run along the bottom.
The control GUI mid-detection, with the bounding box drawn around Mitski in the processed frame at top left.
The Calibration tab, showing the clicked frame beside the live frame with a red crosshair marking the clicked point, a Save Calibration button, and readouts for X and Y click ratios, motor start positions, and degrees per percent move.
The calibration tab: click ratio and motor start position on the left, degrees-per-percent computed on save.
§ 03 Hardware

Mechanical and electrical, iterated the same way

None of the software matters if the thing underneath it can't hold an angle, which is where most of the revisions came from.

  • Mechanism. A pan/tilt table carrying a spinning-barrel launcher. Two hobby DC motors spool up a pair of wheels, and a servo chambers a 3/4-inch foam ball into them.
  • Four CAD revisions. The pan axis started belt-driven and ended up direct drive. The belt stage needed room for the motor, the pulleys, and the belt run itself, and that geometry didn't fit the package I was working within. SolidWorks part and assembly files are versioned in the repo through Git LFS.
  • Custom PCB. Designed and iterated in KiCad, from a green V1 through three more revisions, carrying three supply levels so logic and motor voltage stay separated on one board instead of a breadboard full of modules.
  • Aiming hardware. Two NEMA 17 steppers at 1/16 microstepping on both axes, driven through pigpio with software travel limits.
An early physical build of the turret: a red 3D-printed open frame holding a three-barrel hopper above a pair of spinning launch wheels, with the Raspberry Pi mounted inside the frame and the custom PCB and stepper drivers wired up on the desk in front of it.
An early revision built in red, kept here because it shows the wiring and the board in use. The open frame and exposed pan motor below it are what later revisions replaced.
§ 04 Status

Where it stands

It detects, aims, and fires. What I'm still working on is accuracy, hitting targets while they're moving, tidying up the wiring, improving the mechanical drive, and the finishing touches that take it from a working system to a finished one. Documentation is uneven in places, since this started as something I was building for myself rather than for anyone else to read.

View the repository on GitHub