The guiding idea is that the machine is a strangely shaped 3D printer. Motion runs on a printer mainboard under Klipper, which already solves homing, stepper drivers, end-stops, servos, PWM outputs and G-code macros; everything the printer world does not have (cameras, orchestration, recording, the web interface) runs in Python on a Raspberry Pi that is also Klipper’s host.
Controller. Motion and machine I/O run on Klipper MCU firmware; two board options exist. The bought one is a BIGTREETECH Octopus V1.1 with five TMC2209 drivers (X, Y, Z, wrist, carousel index) carrying everything: its servo header drives the cue-lever servo, its fan and heater MOSFET outputs drive the vacuum pump, the release valve and the LED bar (the LED output must be a real PWM pin so the strobe can run at 50.000 Hz), and its end-stop inputs take the axis end-stops, the wrist Hall sensor and the carousel home sensor. Vacuum sensing is not on the controller at all: it is an Adafruit MPRLS pressure breakout on the Pi’s I²C bus, read by the orchestrator, for the reasons under Sensors. The cheaper one, if the donor printer brings a Klipper-capable 32-bit board, uses that board’s four drivers for X, Y, Z and the wrist, and a Raspberry Pi Pico (RP2040, a few euros, fully supported by Klipper) as a second MCU for the rest: the carousel stepper through one TMC2209 stick on a small breakout, the cue-lever servo, the LED bar’s PWM, the pump and valve through MOSFETs, and the sensors. Klipper runs multi-MCU setups natively. The NXP MIMXRT1010-EVK on hand was considered for the second-MCU role and ruled out: Klipper’s i.MX RT port covers the RT1062/1064 (Teensy 4) but not the RT1011. Either way a 24 V 150 W supply feeds the board, a 24 → 5 V buck feeds the servo and nothing else, and a 24 → 12 V buck feeds the pump, the valve and the LED bar, which are 12 V parts on a 24 V machine.
Computer. Raspberry Pi 4 Model B rev 1.1, 2 GB, already on hand — two of them, the second a cold spare. It runs Klipper’s host process, Moonraker, the orchestrator, the vision code, the web interface and the audio capture. The Scarlett connects over USB; second-generation and later Scarletts are USB class compliant and need no driver on Linux.
Three things follow from the board rather than from choice. It has one CSI port instead of two, so the deck camera is the only ribbon camera and the wrist camera takes the USB branch described below, which this document already treated as the simpler option; the CSI-to-HDMI extender leaves the bill of materials entirely. It gives roughly two to two and a half times less CPU than a Pi 5, which the workload absorbs, because the only sustained vision load is the deck camera tracking the headshell inside a known region of interest: keep those frames downscaled to about 640 × 480 at 10 to 15 fps, and process the 12 MP label photographs one at a time rather than holding several full-resolution frames at once, which is also what 2 GB of RAM allows.
Everything that carries data hangs off a self-powered USB 3.0 hub — the Scarlett, the SSD, the wrist camera and the relay card — not off the Pi directly: a Pi 4 budgets only about 1.2 A across all four of its ports, and this machine runs unattended for a weekend, where a brown-out in the middle of a side is a lost recording. Every device gets a udev rule pinning its name, so /dev/video* and the ALSA card index do not shuffle across reboots and the orchestrator can address them by name. Two rev 1.1 quirks: the board refuses power from e-marked USB-C cables, so it wants the official supply or a plain cable, and its SD-card voltage regulator sits on the underside next to the slot where it is easy to knock off, so the card is set up once and left alone. Mounted in the open on the frame it still wants a heatsink under sustained vision load, and a fan if it runs above 70 °C.
Cameras. One Pi Camera Module 3 for the deck, on the Pi’s single CSI port with a 500 mm ribbon. The wrist camera rides on the gantry, so its cable runs through 1.5 m of cable chain; it is a USB webcam, which is simpler than carrying a ribbon signal that far and adequate for the job. Two are on hand, a Logitech C270 at 720p and a 1080p module; the C270 is the one fitted, because it is fixed focus and therefore cannot go hunting for focus in the middle of a cycle, which matters more here than the extra pixels. It is refocused by hand: the lens is turned to the working distance over a label and left there, and focus, exposure, gain and white balance are all locked before a batch so nothing drifts between records (v4l2-ctl -d /dev/video0 --list-ctrls-menus shows what the model actually exposes). At 720p it resolves the centre hole, the label edge and the groove bands, which is what the cycle needs from it; whether it also resolves a label well enough for the tagging stage is an open question, and the 1080p module is the first thing to try if it does not.
Deck interface. The Conrad Components 393905 USB relay card already on hand: four potential-free changeover contacts (24 V / 8 A), USB-powered, switched through the GPIO lines of its Silicon Labs CP210x bridge. On Linux the cp210x kernel driver exposes those lines as a gpiochip (gpioset), and the crelay tool supports this card directly, so the orchestrator on the Pi switches it with one call. Start/stop uses the DD 3120’s remote start/stop jack (6.3 mm), a contact-closure input for the fader-start feature of older mixers: relay channel 1 on a 6.3 mm plug, no soldering in the deck. Because start/stop is on the Pi side rather than in Klipper, the fault response is the orchestrator firing CUE_UP through Klipper and opening the relay through USB at the same moment, a few tens of milliseconds apart at most. Fader start normally means the platter runs while the contact is closed and stops when it opens, so DECK_START closes the contact and holds it, DECK_STOP opens it; confirm on the deck that it is level-driven rather than a toggle, and note that the jack has no effect on speed. The 33 and 45 buttons (there is a 78 too, unused) are momentary switches and get relay channels 2 and 3, soldered with thin wire across the switch legs on the underside of the button board and led out through the case; measure the voltage across each switch first and check whether the deck remembers its speed across power cycles. The quartz-lock button should be left engaged; the deck camera can confirm its LED. The deck’s pitch output, if it carries a speed or tacho signal, can feed a counter input on the controller as a second speed reference beside the strobe; what it actually outputs is an open question.
Sensors. Mechanical or optical end-stops on X, Y and Z at the home ends; a Hall sensor for the wrist’s home position; a slot-type optical sensor reading a home mark on the carousel’s tooth ring; an MPRLS pressure sensor on the cup line, on the Pi’s I²C bus; optionally a record-present reflective sensor at the pick position, though the wrist camera makes it redundant.
The vacuum sensor is a pressure sensor, not a switch, and it is on the Pi, not the controller. An industrial vacuum switch costs more than the rest of the sensing put together, and a switch only says sealed or not; the MPRLS gives a number, which tells a weak seal from none and goes into the sidecar file. The price is that the seal check cannot live inside Klipper: it is an orchestrator step, and a lost seal is caught one poll interval and one Moonraker round trip after it happens rather than inside the motion queue. That was weighed and accepted — see the decision log — because at every moment a record is carried it is a few centimetres above a surface made to receive it, and a tenth of a second is not what decides whether it lands well. The sensor sits beside the Pi on a tube stub from the valve, because a vacuum line does not care about length and I²C does.
Cabling. Three cable chains, not the two the concept drew: one along the X beam carrying everything that rides the gantry; one along the cross beam, because the Y carriage travels 625 mm and a loop will not do it; one along the Z column to the wrist. The vacuum hose runs the same way, on the outside of each bend. Chain lengths and every cable’s routed length, with wiggle room, are in 08-assembly-instructions.md.
The printer.cfg declares the four motion axes as a Cartesian kinematics with X, Y, Z, and the wrist as an additional manual stepper, the carousel as a second manual stepper, the cue-lever servo as a servo section, the pump, valve and LED as output_pin sections (the LED with pwm: True and a 20 ms cycle time for the strobe), while the relay card is driven from the orchestrator, not from Klipper: the speed channels pulsed for 150 ms, the start/stop channel held closed while the platter should run.
G-code macros implement the primitives the orchestrator composes: HOME_ALL, PUMP_ON, RELEASE (pump off, valve open for a moment), CUE_UP, CUE_DOWN, CAROUSEL_INDEX, STROBE_ON, LIGHT_ON, LIGHT_OFF, and EMERGENCY_LIFT, which is CUE_UP followed by a motion halt, bound so it can be fired without waiting for the queue; the orchestrator opens the start/stop relay at the same time.
Klipper’s own safety features do the low-level work: TMC stall detection on every axis, soft limits, and a hard limit on Z so the carriage cannot be driven below its lowest working height.
A Python service on the Pi that talks to Klipper through Moonraker’s API and owns the state machine of the cycle. Each step of the cycle in 02-operating-cycle.md is one state; each state issues a planned list of waypoints as G-code, waits for completion, checks its post-condition (vacuum seal, camera confirmation, sensor state), and either advances or raises a fault.
The record’s size is the orchestrator’s too: it reads it from the manifest line and lowers the pick and the return to that size’s centre height, the one number in the cycle that depends on what is in the slot; every other target is a datum. A “by hand” line makes it park the gantry and wait for the user at placement, at the flip and at removal, and run the side in between exactly as for a picked record.
The grip is the orchestrator’s, not Klipper’s. GRIP is a step: PUMP_ON through Klipper, then poll the MPRLS until the pressure falls below the seal threshold set during commissioning, with a timeout that raises a fault. Throughout every carry the orchestrator keeps polling, at 20 Hz or better, and a reading that rises back above the loss threshold sends an emergency stop through Moonraker and holds position. The abort is therefore one poll interval plus one round trip, under a tenth of a second; the pressure at grip and at release is logged with each side.
The motion planner is the same one the concept model uses: retreat, raise, ordered horizontal moves, lower, approach, with per-pose exceptions for the station. Poses are computed from a small set of calibrated datums: the pick slot’s centre, the spindle, the ring rest, the arm pivot and the arm’s rest, lead-in and run-out angles for the deck in use. Calibration is a guided procedure in the web interface that jogs the axes to each datum with the camera live.
Faults never attempt recovery. The orchestrator fires EMERGENCY_LIFT when a side is playing, stops everything else in place, records the state, and notifies the user.
OpenCV in Python on the Pi: picamera2 for the deck camera on the CSI port, V4L2 for the webcam on the wrist, with its focus, exposure, gain and white balance locked before every batch.
The wrist camera does four things on each face it sees: finds the centre hole (a dark circle of known size; its offset from the camera axis gives the cup-to-hole offset used when placing on the spindle), finds the label edge, finds the transition from label to lead-out band and from lead-in band to the outer edge (the groove bands differ from the smooth bands in reflectance), and saves a face-on label photograph named by slot and side.
The deck camera tracks the headshell as a dark rectangle in a known region, converts its image position to an arm angle through a calibration, and from the angle derives the stylus radius. It detects the run-out as the radius reaching the value measured on the face-on photo, and a skip as a radius jump larger than the groove pitch between frames. For speed, it tracks either the strobe-lit dots’ drift or a single marker on the rim between timestamped frames and reports mean rotation rate and wow; both values go into the recording’s sidecar file.
arecord or a small sounddevice loop captures from the Scarlett at 96 kHz / 24 bit into one WAV file per side, started before the stylus is lowered and stopped after the arm is parked. The WAVs are written to an external USB 3.0 SSD, never to the microSD card: a side is about 860 MB and a full 24-slot magazine about 40 GB, which would both fill the card and wear it out. An SSD rather than a bus-powered hard disk, which draws about 0.9 A on spin-up and puts a vibration source next to the turntable. The phono signal passes through the deck’s own phono stage (line output) or an external phono preamp before the Scarlett; the Scarlett has no phono input. A silence detector on the live stream provides the run-out confirmation and the skip detector’s audio half.
Each side produces a WAV, both label photos (side A’s from the pick, side B’s from the re-grip), and a JSON sidecar with slot, manifest line, measured speed and wow, cue and run-out radii, and timestamps. Track cutting, mastering and tagging operate on these later and are out of scope here.
A FastAPI service with a small single-page front end, served from the Pi on the local network. It provides the batch manifest editor (slot, size, speed, flags, or “by hand” for a shaped or off-centre disc that the user lays on the spindle when the machine asks; it refuses a 12” in the slot on the front side of a 7”), the live views of both cameras, a jog panel and the calibration procedure, a step-through mode that runs the cycle one state at a time for commissioning, the batch status with the current step and the queue, a pause and a resume, and the fault screen with the manual “retry from here”. Later the same interface hosts the Discogs lookup and tag review of the follow-up project.