exocortex

3018 placer: spring-loaded vacuum needle in the spindle clamp, one switch on the probe pin

The placer turns a stock 3018 desktop CNC into a ball pick-and-place machine. It presses 1,024 Ø3 mm brass balls into the snap cups of the SLA tiles. It replaces the spindle motor in the 3018's motor clamp (Ø42.0 × 34 bore, as measured for labrat's tool head). Labrat's plug and seat-cone idea is reused unchanged. Nothing else from labrat's syringe head is reused: the 3018's own Z axis does the insertion, so the placer needs no servo, gear train, rack or hub.

tool frame: z = 0 on the clamp's top face, spindle axis = z axis

  z  +5 .. +11   PNP-102 top bridge (rests on the flange = preload stop)      KW10 switch on the post above it
  z   0 .. +5    flange Ø48 + 45° seat cone (centres the plug on the clamp's bore edge)
  z -34 ..  0    plug Ø41.9 in the spindle clamp                                hose runs up the Ø7 axial channel
  z -56 .. -34   nose Ø34, two LM4UU pressed in at its bottom (shafts at x = ±11)
  z ~-76 .. -56  2 compression springs on the shafts (installed ~20.5 of 25 free)
  z ~-86 .. -76  PNP-101 nozzle block (clamps both shafts, holds the luer adapter on the axis)
  z ~-119        16 G blunt needle tip, on the spindle axis

The tip is 119.15 mm below the clamp's top face (report.json → placer.tip_z).

What it is

part what made / bought
PNP-100 body: plug Ø41.9 × 34, 45° seat cone, flange Ø48 × 5, nose Ø34 × 22, LM4UU seats, Ø7 hose channel, switch post with 3 mm slots FDM PETG, nose down, 6 walls, 40 % gyroid
PNP-101 nozzle block: clamps both shafts (M3 set screws), female-luer × barb adapter on the axis FDM PETG
PNP-102 top bridge: ties the shaft tops; resting on the flange sets the spring preload; lifts the switch lever FDM PETG
2 × LM4UU 4 × 8 × 12 linear ball bushings, pressed into the nose bought
2 × shaft Ø4 chromed/ground shaft, cut to 93 mm bought (300 mm, cut)
2 × spring compression, OD 6 × 0.5 wire × 25 free, ≈ 0.55 N/mm each (Spec.spring) bought
1 × KW10 hinge-lever microswitch, C + NO → 3018 board probe header (A5 + GND) bought
luer × barb female luer lock × 1/8 in barb (Chromtech P-859 class), barb up bought
needle 16 G blunt, luer lock, 1/2 in (OD 1.65, ID 1.19) bought
hose silicone 3 × 5 mm, about 1.5 m: needle adapter → axial channel → inline filter → pump bought
pump 12 V diaphragm vacuum pump (SparkFun ROB-10398, > 350 mmHg ≈ 47 kPa) on its own 12 V 2 A adapter, always on bought

Electronics: one switch on a header that is already there. Wire the switch exactly like a touch plate. Its normally-open contact goes to the probe pin, its common to GND. GRBL's internal pull-up holds the pin high, and the switch closing pulls it low ($6=0). The pump has no valve and no driver. It runs whenever its adapter is plugged in. No new board, firmware or relay is needed, and nothing connects to the spindle output.

Why the pump can stay on. The vacuum grip on a ball is the needle bore area times the vacuum: π × 0.595² mm² × 0.047 N/mm² = 0.05 N. That is about 44 times the ball's weight (1.2 mN), so pick-up and travel are safe. It is also far below the cup's pull-out force (target ≥ 1.5 N, coupon test 3). When the needle retracts, the cup keeps the ball and the needle lets go. Nothing has to switch the vacuum off.

Force-limited insertion

The two springs push the carriage down with a preload. The bridge rests on the flange, so the needle is rigid until the force on it exceeds that preload. Any further push lifts the carriage. After trip_travel (0.5 mm) the bridge closes the switch, and GRBL stops the probe move (G38.x).

quantity value from
spring rate 2 × 0.55 = 1.1 N/mm Spec.spring (measure, below)
preload 5.0 N (2.5 N per spring, 4.5 mm compression) Spec.preload_n; must exceed the ball snap force from coupon test 2
trip force 5.0 + 1.1 × 0.5 = 5.6 N the force on every ball when the press stops
hard stop 3 mm of carriage travel: 5.0 + 1.1 × 3 = 8.3 N if the switch fails, this is the most the 3018 can push through the needle
GRBL overshoot after the trip v²/2a = (2 mm/s)² / (2 × 100 mm/s²) = 0.02 mm at F120
post buckling Euler, fixed-free, stem Ø1.6, 5 mm, E 2 GPa: ≈ 63 N 7.6 × the hard-stop force
vacuum grip 0.05 N well below the cup retention

The snap force (ball through the cup lip) must stay below the preload. Otherwise the switch trips with the ball still on the lip, and the QA below reports "not seated". Coupon test 2 measures the snap force for each interference row. Pick the row that gives 2–3.5 N.

Feeding: array tray (chosen) vs bowl, tape, cartridge

option for 3 mm balls, low volume verdict
vibratory bowl feeder needs a tuned bowl, a linear track, an escapement and a controller. It works best on oriented parts, but a sphere needs no orienting. $150+ and a week of tuning no
component tape someone has to tape 1,024 balls (no vendor tapes loose balls) and add a peeler no
preloaded cartridge (tube magazine + escapement) needs an actuator for the escapement and jams on a ball that is 0.05 mm oversize later, at scale
array tray holes in a plate on a fixed grid, filled by pouring and shaking like a BGA reballing stencil. The pick position is pure arithmetic and no feeder logic is needed yes

PNP-210 ball tray: 3 mm black cast acrylic, 173.5 × 146.5 mm, laser cut from out/dxf/PNP-210.dxf. It has 36 × 30 = 1,080 holes, Ø2.4 on a 4.5 mm pitch, enough for one helmet plus 56 spares, and two Ø1.8 reference holes R1 and R2. A ball sits in its hole with its centre 0.9 mm above the top face and cannot fall through. Fill it once per helmet: pour about 1,200 balls on, tilt and shake until every hole holds one, then sweep the surplus with a card into the gutter on the +X edge of the PNP-211 base (193.5 × 152.5 × 8). A diode laser cuts black acrylic but not clear (clear passes 450 nm light). Never laser PVC, PC, ABS or PETG.

out/tray_state.json remembers the next hole, so tiles continue through one fill. A hole that turns out empty is skipped automatically (see QA).

Curved tiles on a 3-axis machine

Every post on a tile is parallel to the tile axis w, not to the local scalp normal. Posts tilt at most post_tilt_max_deg from the normal; see out/layout.json and the report.json check (limit 25°). Each tile carries three datum feet whose faces lie on one plane perpendicular to w. Feet A (round hole) and B (slot) sit on two Ø2 dowels of the universal fixture PNP-200, and foot C is the key. With the tile on the fixture, w is machine +Z, every insertion is a pure Z move, and one flat fixture holds all 16 tiles.

The same single pull direction lets the tile print without tilted features on the SLA printer and come out of a two-part injection mould later.

A 4th (A/B tilt) axis would let the posts follow the scalp normal (0° tilt). The tiles, the fixture and every toolpath would then need a second angle per contact. This is not worth it while the tilt stays under 25° and the contacts are decorative.

Bed layout and work coordinates

Bed coordinates in mm: X 0..300 left → right, Y 0..180 front → back (the 3018's travel).

 Y 180 ┌───────────────────────────────────────────────────────────────────┐
       │                                                                   │
   138 │ ┌──────────────┐                                                  │
       │ │ PNP-200      │  ┌────────────────────────────────────────────┐  │
       │ │ 96 × 96 × 8  │  │ PNP-211 base 193.5 × 152.5 (X 100..293.5)  │  │
    90 │ │      + G54   │  │  PNP-210 acrylic 173.5 × 146.5             │g │
       │ │   (48, 90)   │  │  36 × 30 holes @ 4.5                       │u │
    42 │ └──────────────┘  │ + R1 = G55 (106, 20.75)                    │t │
       │                   └────────────────────────────────────────────┘  │
 Y 0   └───────────────────────────────────────────────────────────────────┘
       X 0            96  100                                       293.5  300
item bed position (mm)
PNP-200 tile fixture, 96 × 96 × 8 X 0..96, Y 42..138
tile origin = zero hole = G54 XY zero (48, 90)
PNP-211 tray base, 193.5 × 152.5 × 8, sweep gutter on its +X edge X 100..293.5
PNP-210 acrylic, near corner (102, 16.75)
reference hole R1 = G55 XY zero (106, 20.75)
G55 origin expressed in G54 (58, −69.25) (estimate_s(tray_origin_g54=…))

Clamp both plates with the 3018's stock T-slot clamps against a printed fence at these positions. The holes span 157.5 × 130.5 mm and the tile about 45 × 45, all inside the travel.

WCS origin X / Y Z = 0
G54 tile fixture PNP-200 the tile frame; the Ø1.8 zero hole is the tile origin where the bare needle's probe trips on the fixture top (= the tiles' datum plane)
G55 ball tray PNP-210 reference hole R1 where the bare needle trips on the acrylic top

Both Z zeros are probe trips through the same spring, so the trip travel (0.5 mm) cancels: every later press stops at the same force it was zeroed with.

Zeroing (once per session, and after a needle change): 1. Pump off, no ball on the needle. Jog the needle tip into the fixture's Ø1.8 zero hole. The Ø1.65 needle centres in it to ±0.08 mm. Then G10 L20 P1 X0 Y0. 2. Lift, move 10 mm aside onto flat fixture top, run uv run python -m exo.stream --port <tty> zero --wcs G54 (G38.2 down at F60, then G10 L20 P1 Z0). 3. Repeat on the tray: needle into R1, G10 L20 P2 X0 Y0, then exo.stream zero --wcs G55 on the acrylic beside R1. 4. Check: $# lists G54/G55. Probe the fixture top 10 times. The spread of the trip Z must be ≤ 0.04 mm.

GRBL settings (GRBL 1.1)

setting value why
$6 0 probe = NO switch to GND (pin pulled low when triggered)
$110, $111 2000 mm/min travel (Spec.feed_xy). Stock 3018 configs often ship 1000. Raise it only as far as the machine runs without lost steps
$112 600 mm/min Z (Spec.feed_z)
$120, $121 200 mm/s² XY acceleration (start here; lower it if the machine skips steps)
$122 100 mm/s² Z acceleration (also keeps the probe overshoot at 0.02 mm)
$130, $131, $132 300, 180, 45 soft limits only if homing switches are fitted (most 3018s have none: zero by hand as above)

Z reach

Labrat measured the clamp's top face at 165 mm above the bed at Z 0, with 45 mm of Z travel. The needle tip sits about 119 mm below the clamp's top face. It is therefore about 46 mm above the bed at Z 0 and about 1 mm at Z −45. The deepest press needs the tip 12.0 mm above the bed and the highest travel move (fixture 8 + tallest ball top + carried ball 2.9 + safe_dz 6) 27.5 mm, against a reach of 0.8 .. 45.8 mm: 11 mm spare below and 18 mm above (report.json check "needle tip reach"). If your machine's clamp sits lower or a tile is taller, lift the tool 5 mm in the clamp: the plug is 34 long, so it still holds. Measure the clamp height before the first run.

Placement cycle and QA by trip height

For every contact, exo.gcode / exo.stream run this sequence (all coordinates come from out/layout.json):

G55 rapid over tray hole → approach → G38 down onto the ball (pick; vacuum holds it) → lift
G54 rapid over the post  → approach → G38 down: ball snaps into the cup, the spring trips the switch → lift

The trip height relative to the expected height says what happened (exo/gcode.py, geom()):

event trip, relative to expected rule action in exo.stream
ball picked from the tray 0 ± 0.25 needle on the ball top at tray +2.28 carry on
empty tray hole no trip within 1.2 mm G38.3 reports PRB:…:0 next hole (up to 3 tries), then stop "tray_empty"
placed 0 ± 0.25 needle tip 1.38 above the ball centre, ball seated log, next contact
not seated +0.3 … +1.4 (resting on the lip ≈ +0.6) snap force > preload press once more, 0.6 mm deeper; if it still fails, stop "unseated"
no ball (lost in transit) ≈ −2.6 (needle on the empty seat) pick again

The .nc files (out/gcode/T01.nc …) use G38.2 instead. GRBL alarms when a probe misses, which makes any sender (Candle, UGS) fail safe, but you only get QA and retries through exo.stream.

uv run python -m exo.gcode                                    # programs + out/gcode/summary.json
uv run python -m exo.stream --mock place T01                  # dry run against a simulated GRBL
uv run python -m exo.stream --port /dev/tty.usbserial-XXXX place T01 T02 T03
uv run python -m exo.stream --port /dev/tty.usbserial-XXXX place T04 --from 37   # resume after a stop

Every contact appends a row to out/place_log.csv (time, tile, k, tray hole, pick dz, seat dz, status). The per-tile yield and the dz histograms are the assembly-repeatability data for test 2 in the build plan.

Cycle time (out/gcode/summary.json, estimate_s): about 15 s per contact (two XY legs between the tray and the tile at 2000 mm/min, two probes of 1.8 mm at F120, Z lifts at 600). That is about 16 min per tile and 4.2 h for 1,024, unattended after the tray fill. Most of it is XY travel to the tray: a per-tile 8 × 8 tray beside the fixture would roughly halve it, at the cost of 16 refills.

Continuous multi-tile operation: print a second PNP-200 and load the next tile on it while the first one runs. Swapping plates needs no re-zero if both are clamped against the same printed fence. Probe the top once after the swap to confirm (≤ 0.04 mm). The tray state carries over between tiles.

Parametric toolpaths: the programs contain no hand-entered coordinates. Change pitch, grid, tile count, ball size, cup lip or stack in exo/spec.py, then rerun exo.layout, exo.build and exo.gcode. Positions, heights, travel planes and QA thresholds are all regenerated.

Building the tool

  1. Print PNP-100 (nose down, the flange's 2 mm ledge prints without support in PETG), PNP-101, PNP-102 and PNP-211: PETG, 0.2 mm, 6 walls.
  2. Press the two LM4UU into the nose seats with a vise until flush. The seat is 0.1 mm undersize.
  3. Cut the 4 mm shaft into 2 × 93 mm (cut-off wheel) and chamfer the ends so they don't damage the bushings' seals.
  4. Feed the silicone hose down through the bridge and the body's axial channel. Push it onto the barb of the luer adapter, then press the adapter, barb up, into PNP-101. A ring of epoxy seals it.
  5. Slide one spring onto each shaft. Push the shafts up through the LM4UU and into the bridge, which bottoms them 3.5 mm deep. Add a drop of CA in the bridge, then clamp the shafts in PNP-101 with 2 × M3 × 4 set screws (tap M3).
  6. Fit the KW10 to the switch post with 2 × M2 × 8 through the slots, lever over the bridge.
  7. Wire it: a 2-core lead from the switch C + NO to the 3018 board's PROBE header (A5 / GND). Send $6=0. Press the switch by hand and check that ? reports Pn:P.
  8. Fit the needle (16 G, 1/2 in) on the luer. Run the hose: adapter → inline filter → pump inlet. Plug the pump into its 12 V adapter.
  9. Loosen the spindle clamp, take the spindle out, insert PNP-100 until the seat cone sits on the clamp's top edge, and tighten.

Calibration

what how target
preload / trip force Put a kitchen scale on the bed and run G38.2 Z-5 F30 onto it. Read the scale when the move stops 510–570 g (5.0–5.6 N). Adjust with 0.5 mm shims between the bridge and the flange: each 0.5 mm lowers the preload by about 0.55 N
trip travel Slide the switch in its slots until it clicks 0.5 mm after the bridge leaves the flange (feeler gauge under the bridge) 0.4–0.6 mm
spring rate (Spec.spring[3]) With the scale under the needle, step down 0.5 mm at a time with the switch unplugged 1.1 N/mm for both springs. Enter the measured value in spec.py
repeatability 20 probes on the fixture top ≤ 0.04 mm spread
vacuum Pick a ball and shake the head at F2000 the ball stays on
first contacts Place on coupon EXO-301 and pull with a luggage scale pull-out ≥ 1.5 N, no cracked cups