An unusual ingredient in the water softener salt reservoir:

I figured it found a way in and can find its own way out, so I just closed the lid and backed carefully away …
The Smell of Molten Projects in the Morning
Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
Mechanical widgetry
Homing the MPCNC’s Z axis at the bottom end of its travel made no sense, but the Z stage lacks a convenient spot to mount / trigger a switch at the top of its travel, so this sufficed for initial tests & fiddling:

The EMT rail carrying the switch moves downward, tripping the lever when it hits the MPCNC’s central assembly.
Somewhat to my surprise, a TRCT5000-based optical proximity sensor (harvested from the Kenmore 158 Crash Test Dummy’s corpse) and a strip of black electrical tape work perfectly:

The PCB wears a shiny new epoxy coat:

I soldered the wires (harvested from the previous endstop) directly to the PCB, because the pinout isn’t the same and fewer connectors should be better.
The mount uses black PETG, rather than translucent orange, in hope of IR opacity, and wraps around the EMT rail at (roughly) the 2 mm standoff producing the peak response:

In truth, I set the gap by eyeballometric guesstimation to make the entire mount arc sit equidistant from the EMT:

The mount includes the 2 mm spacing around the EMT OD and puts the sensor tip flush with the arc OD, so it should be pretty close:

A strip of 3M permanent tape, cut to clear the 608 bearings, affixes the mount to the MPCNC’s central assembly. The solid model now includes a midline reference notch, with a height rounded up to the next-highest multiple of 2.0 mm. It needs a loop to anchor the cable.
The blue twiddlepot sets the comparator threshold midway between the response over black tape (incorrectly on = too low) and bare EMT (incorrectly off = too high), in the hope of noise immunity. The range spanned nearly half of the pot rotation, so I think it’s all good.
The sensor doesn’t trip when the edge of the tape exactly meets its midline, which meant I had to trim a strip of tape to suit. As part of setting the twiddlepot, I shut off the Z axis motor and laid some test strips on the EMT:

I spun the leadscrew with one hand, held the sensor with the other, twiddled the trimpot, trimmed the upper and lower ends of the tape, and generally had a fine time. The sensor responds equally well to a half-wide strip of tape (in the upper picture), with the distinct advantage of not encroaching on the 608 bearing tracks.
The GRBL setup now homes Y and Z toward the positive end of their travel, with X still toward the negative end while a set of extension cables remains in transit around the planet.
The OpenSCAD source code as a GitHub Gist:
| // TCRT5000 Z Axis Endstop Mount | |
| // Ed Nisley KE4ZNU – 2017-12-04 | |
| /* [Build Options] */ | |
| Layout = "Show"; // [Build, Show, Block] | |
| Section = true; // show internal details | |
| /* [Extrusion] */ | |
| ThreadThick = 0.25; // [0.20, 0.25] | |
| ThreadWidth = 0.40; // [0.40] | |
| function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); | |
| /* [Hidden] */ | |
| Protrusion = 0.01; // [0.01, 0.1] | |
| HoleWindage = 0.2; | |
| ID = 0; | |
| OD = 1; | |
| LENGTH = 2; | |
| /* [Sizes] */ | |
| RailOD = 23.5; // actual rail OD | |
| OptoPCB = [32.5,14.2,1.6]; // prox sensor PCB | |
| ComponentHeight = 5.0; // max component height above PCB | |
| OptoSensor = [5.8,10.2,10.5]; // sensor head below PCB | |
| OptoOffset = 3.0; // sensor head center from PCB edge | |
| OptoRange = 2.0; // sensor to rail distance | |
| TapeThick = 1.0; // foam mounting tape | |
| WallThick = 4.0; // basic wall thickness | |
| Block = [WallThick + OptoRange + RailOD/2 + (OptoPCB[0] – OptoOffset), | |
| RailOD/2 + OptoRange + OptoSensor[2] – TapeThick, | |
| IntegerMultiple(OptoPCB[1] + 2*WallThick,2.0)]; // basic block shape | |
| echo(str("Block: ",Block)); | |
| NumSides = 6*4; | |
| //- Adjust hole diameter to make the size come out right | |
| module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes | |
| Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2); | |
| FixDia = Dia / cos(180/Sides); | |
| cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides); | |
| } | |
| //- Shapes | |
| // Main block constructed with PCB along X, opto sensor at Y=0 | |
| module PCBBlock() { | |
| difference() { | |
| translate([-(WallThick + OptoRange + RailOD/2), | |
| (-Block[1] + RailOD/2 + OptoRange), | |
| -Block[2]/2]) | |
| cube(Block,center=false); | |
| for (i=[-(RailOD/2 + OptoRange + WallThick), | |
| (OptoPCB[0] – OptoOffset)]) | |
| translate([i,0,0]) | |
| rotate([90,0,0]) rotate(45) | |
| cube([2*ThreadWidth,2*ThreadWidth,2*Block[2]],center=true); | |
| translate([0,(RailOD/2 + OptoRange),0]) | |
| cylinder(d=(RailOD + 2*OptoRange), | |
| h=(Block[2] + 2*Protrusion), | |
| $fn=NumSides,center=true); | |
| rotate([90,0,0]) | |
| cube(OptoSensor + [0,0,Block[1]],center=true); | |
| } | |
| } | |
| //- Build things | |
| if (Layout == "Block") | |
| PCBBlock(); | |
| if (Layout == "Show") { | |
| translate([0,-(RailOD/2 + OptoRange),0]) | |
| PCBBlock(); | |
| color("Yellow",0.5) | |
| cylinder(d=RailOD,h=2*Block[2],$fn=NumSides,center=true); | |
| } | |
| if (Layout == "Build") | |
| translate([0,0,OptoSensor[2] – TapeThick]) | |
| rotate([90,0,0]) | |
| PCBBlock(); |
The original doodles, including a bunch of ideas left on the cutting room floor:

As part of entombing the endstop PCBs in epoxy, I tweaked the switch mounts to (optionally) eliminate the screw holes and (definitely) rationalize the spacings:

The sectioned view shows the cable tie slot neatly centered between the bottom of the switch terminal pit and the EMT rail, now with plenty of meat above the cable tie latch recess. The guide ramp on the other side has a more-better position & angle, too.
A trial fit before dabbing on the epoxy:

The 3M black foam tape works wonderfully well!
After the epoxy cures, it’s all good:

The OpenSCAD source code as a GitHub Gist:
| // MPCNC Endstop Mount for Makerbot PCB on EMT tubing | |
| // Ed Nisley KE4ZNU – 2017-12-04 | |
| /* [Build Options] */ | |
| Layout = "Show"; // [Build, Show, Block] | |
| Holes = false; // holes for switch screws | |
| Section = true; // show internal details | |
| /* [Extrusion] */ | |
| ThreadThick = 0.25; // [0.20, 0.25] | |
| ThreadWidth = 0.40; // [0.40] | |
| function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); | |
| /* [Hidden] */ | |
| Protrusion = 0.01; // [0.01, 0.1] | |
| HoleWindage = 0.2; | |
| ID = 0; | |
| OD = 1; | |
| LENGTH = 2; | |
| /* [Sizes] */ | |
| RailOD = 23.5; // actual rail OD | |
| SwitchHeight = 8.0; // switch PCB distance from rail OD | |
| Strap = [5.5,50,2.0]; // nylon strap securing block to rail | |
| StrapHead = [8.2,3.0,5.5]; // recess for strap ratchet head | |
| Screw = [2.0,3.6,7.0]; // thread dia, head OD, screw length | |
| HoleOffset = [2.5,19.0/2]; // PCB mounting holes from PCB edge, rail center | |
| SwitchClear = [6.0,15,3.0]; // clearance around switch pins | |
| SwitchOffset = [6.0,0]; // XY center of switch from holes | |
| StrapHeight = (SwitchHeight – SwitchClear[2])/2; // strap center from rail | |
| Block = [16.4,26.0,RailOD/2 + SwitchHeight]; // basic block shape | |
| //- Adjust hole diameter to make the size come out right | |
| module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes | |
| Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2); | |
| FixDia = Dia / cos(180/Sides); | |
| cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides); | |
| } | |
| //- Shapes | |
| // Main block constructed centered on XY with Z=0 at top of rail | |
| module PCBBlock() { | |
| difference() { | |
| translate([-Block[0]/2,-Block[1]/2,-RailOD/2]) | |
| cube(Block,center=false); | |
| translate([(SwitchOffset[0] + HoleOffset[0] – Block[0]/2), | |
| SwitchOffset[1], | |
| (SwitchHeight – SwitchClear[2]/2 + Protrusion/2)]) | |
| cube(SwitchClear + [0,0,Protrusion],center=true); | |
| if (Holes) | |
| for (j=[-1,1]) | |
| translate([HoleOffset[0] – Block[0]/2,j*HoleOffset[1],(Block[2]/2 – Screw[LENGTH])]) | |
| rotate(180/6) | |
| if (true) // true = loose fit | |
| PolyCyl(Screw[ID],Screw[LENGTH] + Protrusion,6); | |
| else | |
| cylinder(d=Screw[ID],h=Screw[LENGTH] + Protrusion,$fn=6); | |
| translate([0,0,StrapHeight]) | |
| cube(Strap,center=true); | |
| translate([0, // strap head recess | |
| (Block[1]/2 – StrapHead[1]/2 + Protrusion), | |
| StrapHeight – Strap[2]/2 + StrapHead[2]/2]) | |
| cube(StrapHead + [0,Protrusion,0],center=true); | |
| StrapAngle = atan((StrapHeight + RailOD/4)/Strap[2]); // a reasonable angle | |
| echo(str("Strap Angle: ",StrapAngle)); | |
| translate([0,-(Block[1]/2 – Strap[2]/(2*sin(StrapAngle))),StrapHeight]) | |
| rotate([StrapAngle,0,0]) | |
| translate([0,-Strap[1]/2,0]) | |
| cube(Strap,center=true); | |
| if (Section) | |
| translate([Block[0]/2,0,0]) | |
| cube(Block + [0,2*Protrusion,2*Block[2]],center=true); | |
| } | |
| } | |
| module Mount() { | |
| difference() { | |
| translate([0,0,RailOD/2]) | |
| PCBBlock(); | |
| rotate([0,90,0]) | |
| cylinder(d=RailOD,h=3*Block[0],center=true); | |
| } | |
| } | |
| //- Build things | |
| if (Layout == "Show") { | |
| Mount(); | |
| color("Yellow",0.5) | |
| rotate([0,90,0]) | |
| cylinder(d=RailOD,h=3*Block[0],center=true); | |
| } | |
| if (Layout == "Block") | |
| PCBBlock(); | |
| if (Layout == "Build") | |
| translate([0,0,Block[2]]) | |
| rotate([180,0,0]) | |
| Mount(); |
Using 3D printer style endstop switches has the advantage of putting low-pass filters (i.e. caps) at the switches, plus adding LED blinkiness, but it does leave the +5 V and Gnd conductors hanging out in the breeze. After mulling over various enclosures, it occured to me I could just entomb the things in epoxy and be done with it.
The first step was to get rid of the PCB mounting screws and use 3M permanent foam tape:

Get all the switches set up and level, mix up 2.8 g of XTC-3D (because I have way too much), and dab it on the switches until all the exposed conductors have at least a thin coat:

You should use a bit more care than I: the epoxy can creep around the corner of the switch and immobilize the actuator in its relaxed position. Some deft X-Acto knife work solved the problem, but only after firmly smashing the X axis against the nonfunctional switch.
Epoxy isn’t a particularly good encapsulant, because it cures hard and tends to crack components off the board during temperature extremes. These boards live in the basement, cost under a buck, and I have plenty of spares, so let’s see what happens.
At least it’s now somewhat more difficult to apply a dead short across the Arduino’s power supply, which comes directly from a Raspberry Pi’s USB port.
I measured stepper motor winding current with a pair of Tek Hall effect probes for future reference.

The pistol-shaped A6303 measures up to 100 A, so it’s grossly overqualified for the job. The much smaller A6302 goes to 20 A and is definitely the right hammer. The single-trace pix show 200 mA/div.
I’m using the default 12 V6 A MPCNC stepper power supply, with A4988 stepper driver boards on the Protoneer CNC Shield atop a knockoff Arduino UNO running GRBL firmware. The blue USB cable goes off to a Raspberry Pi running minicom for manual control.
All the pix use the same G-Code command: G1 X2.4 F180. Running at 180 mm/min = 3 mm/s eliminates pretty nearly all visible acceleration.
Each picture requires:
m9 to disable stepper powerCtrl-X = reset GRBL$x = unlockm8 = enable powerg1x2.4f180 motion for next imagex0 = return to originWith the A4988 stepper driver in 16:1 microstep mode:

Notice how some of the microsteps aren’t particularly crisp, notably around the zero crossings. I think the relatively low 12 V supply doesn’t give the A4988 enough control authority to boss the current around, resulting in difficulty holding the current setpoint, even at low speed:

More on that problem in a while.
In 8:1 microstep mode:

In 4:1 microstep mode:

In 2:1 microstep mode:

And, a rarity in modern times, both windings at 500 mA/div in full step mode:

The A4988 driver reduces the peak current to 1/√2 of the stepped sine wave peak to maintain the same average power dissipation and torque. For reasons I cannot explain, the full-step move takes far less time than the others; it must have something to do with how GRBL computes the average speed. It sounds like a robotic woodpecker hammering on the MPCNC’s frame, so I flipped back to 16:1 microstep mode after taking that picture.
Now I can refer to these from elsewhere …
GRBL responds to critical errors by disabling its outputs, which seems like a useful feature for a big-enough-to-hurt CNC machine like the MPCNC. Unlike the RAMPS 1.4 board, there’s no dedicated power-control pin, so I connected the Coolant output to the same DC-DC SSR I tried out with the RAMPS board:

With homing enabled, GRBL emerges from power-on resets and error conditions with the spindle and coolant turned off and the G-Code interpreter in a locked state requiring manual intervention, so turning the stepper power on fits right in:
$x – Unlock the controlsm8 – Coolant output on = enable stepper power$h – Home all axesThe steppers go clunk as the power supply turns on, providing an audible confirmation. The dim red LED on the SSR isn’t particularly conspicuous.
Turning the stepper power off:
m9 – Coolant output off = disable stepper powerI think the A4988 drivers maintain their microstep position with the stepper power supply off, because their logic power remains on. In any event, you probably wouldn’t want to restart after an emergency stop without clearing the fault and re-homing the axes.
The board has Cycle Start, Feed Hold, and Abort inputs just crying out for big colorful pushbutton switches.
Unlike the RAMPS board, the Prontoneer CNC Shield does not feed stepper power to the underlying Arduino UNO, leaving it safely powered by USB or the coax jack.
The MPCNC has the entire weight of the Z axis motor and stage resting on the leadscrew, so the instructions call for preloading the spring coupler by stretching it with the leadscrew butted against the motor shaft. The leadscrew end isn’t particularly flat, so I inserted a 1/4 inch ball bearing between the two before the stretch:

I’m reasonably sure the ball won’t make the slightest difference, but two slightly misaligned shafts can now pivot on a point, rather than grind against each other. There’s no evidence of misalignment; I feel better and that’s what counts.