Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
A place to store your vials of blended inkjet juice, plus a workstation for the plotter pen you’re refilling and that ink vial up front:
HP7475A Plotter Pen Refilling Station
The two pen holders accommodate ordinary fiber-tip pens and ceramic-tip pens. The slot along the front lets you keep track of the ink level, not that there’s much danger of running dry at 0.05 ml per refill from a vial holding 1 ml of blended ink. The big flange makes it harder for me to knock the damn thing over; avoiding an ink spill, even when you have a towel underneath, is a Good Thing.
The Slic3r tool path preview shows off the Hilbert Curve top & bottom infill:
Plotter Pen Refill Vial Holder – Slic3r preview
The OpenSCAD source code:
// HP7475A Plotter Pen Refill Station
// Ed Nisley KE4ZNU - August 2015
//- Extrusion parameters - must match reality!
ThreadThick = 0.25;
ThreadWidth = 0.40;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.1;
HoleWindage = 0.2;
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);
}
//------
// Dimensions
WallThick = 6*ThreadWidth;
BaseThick = IntegerMultiple(1.0,ThreadThick);
VialOD = 8.0;
VialOC = VialOD + WallThick;
VialArray = [4,4]; // number of vials in each direction
PenOD = [14.7,11.7]; // regular fiber pen body, ceramic *cap* dia
NumPens = len(PenOD); // really works for just two pens...
PenLength = 38;
FlangeOD = 18;
echo(str("Max pen OD: ",max(PenOD)));
echo(str("Number of pens: ",len(PenOD)));
Holder = [(VialOC*VialArray[0] + WallThick),(VialOC*VialArray[1] + 2*FlangeOD + WallThick),(3*VialOD + BaseThick)];
HolderRound = 5.0;
//- Build it
difference() {
union() {
hull() {
for (i=[-1,1], j=[-1,1]) {
translate([i*(Holder[0]/2 - HolderRound),j*(Holder[1]/2 - HolderRound),0])
cylinder(r=HolderRound,h=Holder[2],$fn=8*4);
}
}
hull() {
for (i=[-1,1], j=[-1,1]) {
translate([i*Holder[1]/2,j*(Holder[1]/2 - HolderRound),0])
cylinder(r=HolderRound,h=BaseThick,$fn=8*4);
}
}
for (i=[0:len(PenOD) - 1])
translate([(i*Holder[0]/2 - Holder[0]/4),-Holder[1]/4,BaseThick]) { // spacing is a total hack
rotate(180/12)
cylinder(d=FlangeOD,h=PenLength,$fn=3*4);
}
}
for (i=[0:VialArray[0] - 1] , j=[0:VialArray[1] - 1]) {
vx = i*VialOC - (VialOC*(VialArray[0] - 1)/2);
vy = j*VialOC - (VialOC*(VialArray[1] - 1)/2) + FlangeOD;
translate([vx,vy,BaseThick])
rotate(180/8)
PolyCyl(VialOD,Holder[2],8);
}
translate([0,(VialOD/2 - Holder[1]/2),BaseThick])
rotate(180/8)
PolyCyl(VialOD,Holder[2],8); // edges along open side => snug fit
for (i=[0:len(PenOD) - 1])
translate([(i*Holder[0]/2 - Holder[0]/4),-Holder[1]/4,BaseThick]) { // spacing is a total hack
rotate(180/12)
PolyCyl(PenOD[i],(PenLength + Protrusion),3*4);
}
}
Mary flattens seam allowances and prepares appliqué pieces with a Clover MCI-900 Mini Iron. The stand resembles the wire gadgets that came with soldering irons, back in the day:
Clover MCI-900 Mini Iron – Clover holder
That stand may be suitable on a workbench, but it’s perilously unstable on an ironing board. After fiddling around for a while and becoming increasingly frustrated with it, she asked for a secure holder that wouldn’t fall over and perhaps had a heat shield around the hot end.
I ran off a quick prototype to verify my measurements and provide a basis for further discussion:
Clover MCI-900 Mini Iron – Level holder
I proposed screwing that holder to a rectangle of leftover countertop extending under the hot end, with a U-shaped heat shield extending upward to keep fingers and fabric away from the blade. She decided the countertop might be entirely too heavy and the heat shield might be too confining, so she suggested just angling the iron upward and adding a flat platform to stabilize it.
Her wish being my command:
Clover MCI-900 Mini Iron – Angled holder
I’m still not convinced that having the hot end up in the air is a Good Thing, but she thinks it’s worth trying as-is. A pair of 10-32 screw holes under each end will let it mount to a base board, should that becomes necessary.
I’ll stick a foam sheet under the platform so it doesn’t slide around. The cord normally dangles downward off the side of the ironing board or work table, so the iron won’t get up and walk away, but it might pull the whole affair toward the edge.
I should fill the letters with JB Weld epoxy darkened with laser printer toner (who knew?) to make them stand out. They’re more conspicuous in person than in the picture, so maybe it doesn’t matter.
The slots holding the iron have a semicircular bottom and straight-wall sides, created by extruding hulled 2D shapes, arranging them along the iron’s central axis, and tilting the “iron” at the appropriate angle:
Clover Mini Iron Holder – solid model showing iron
That’s a 10° tilt, chosen because it looked right. The model recomputes itself around the key dimensions, so we can raise / lower the iron, change the angle, and so forth and so on, as needed.
Assuming that a hot end sticking out in mid-air isn’t too awful, this one looks like a keeper.
The OpenSCAD source code:
// Clover MCI-900 Mini Iron holder
// Ed Nisley KE4ZNU - August 2015
Layout = "Holder"; // Iron Holder
//- Extrusion parameters - must match reality!
ThreadThick = 0.25;
ThreadWidth = 0.40;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.1;
HoleWindage = 0.2;
inch = 25.4;
Tap10_32 = 0.159 * inch;
Clear10_32 = 0.190 * inch;
Head10_32 = 0.373 * inch;
Head10_32Thick = 0.110 * inch;
Nut10_32Dia = 0.433 * inch;
Nut10_32Thick = 0.130 * inch;
Washer10_32OD = 0.381 * inch;
Washer10_32ID = 0.204 * inch;
//------
// Dimensions
CornerRadius = 4.0;
CenterHeight = 25; // center at cord inlet on body
BodyLength = 110; // cord inlet to body curve at front flange
Incline = 10; // central angle slope
FrontOD = 29;
FrontBlock = [20,1.5*FrontOD + 2*CornerRadius,FrontOD/2 + CenterHeight + BodyLength*sin(Incline)];
CordOD = 10;
CordLen = 10;
RearOD = 22;
RearBlock = [15 + CordLen,1.5*RearOD + 2*CornerRadius,RearOD/2 + CenterHeight];
PlateWidth = 2*FrontBlock[1];
TextDepth = 3*ThreadThick;
ScrewOC = BodyLength - FrontBlock[0]/2;
ScrewDepth = CenterHeight - FrontOD/2 - 5;
echo(str("Screw OC: ",ScrewOC));
BuildSize = [200,250,200]; // largest possible thing
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);
}
// Trim bottom from child object
module TrimBottom(BlockSize=BuildSize,Slice=CornerRadius) {
intersection() {
translate([0,0,BlockSize[2]/2])
cube(BlockSize,center=true);
translate([0,0,-Slice])
children();
}
}
// Build a rounded block-like thing
module RoundBlock(Size=[20,25,30],Radius=CornerRadius,Center=false) {
HS = Size/2 - [Radius,Radius,Radius];
translate([0,0,Center ? 0 : (HS[2] + Radius)])
hull() {
for (i=[-1,1], j=[-1,1], k=[-1,1]) {
translate([i*HS[0],j*HS[1],k*HS[2]])
sphere(r=Radius,$fn=4*4);
}
}
}
// Create a channel to hold something
// This will eventually be subtracted from a block
// The offsets are specialized for this application...
module Channel(Dia,Length) {
rotate([0,90,0])
linear_extrude(height=Length)
rotate(90)
hull() {
for (i=[-1,1])
translate([i*Dia,2*Dia])
circle(d=Dia/8);
circle(d=Dia,$fn=8*4);
}
}
// Iron-shaped series of channels to be removed from blocks
module IronCutout() {
union() {
translate([-2*CordLen,0,0])
Channel(CordOD,2*CordLen + Protrusion);
Channel(RearOD,RearBlock[0] + Protrusion);
translate([BodyLength - FrontBlock[0]/2 - FrontBlock[0],0,0])
Channel(FrontOD,2*FrontBlock[0]);
}
}
//- Build it
if (Layout == "Iron")
IronCutout();
if (Layout == "Holder")
difference() {
union() {
translate([(BodyLength + CordLen)/2 - CordLen,0,0])
TrimBottom()
RoundBlock(Size=[(CordLen + BodyLength),PlateWidth,CornerRadius]);
translate([(RearBlock[0]/2 - CordLen),0,0])
TrimBottom()
RoundBlock(Size=RearBlock);
translate([BodyLength - FrontBlock[0]/2,0,0]) {
TrimBottom()
RoundBlock(Size=FrontBlock);
}
}
translate([0,0,CenterHeight])
rotate([0,-Incline,0])
IronCutout();
translate([0,0,-Protrusion])
PolyCyl(Tap10_32,ScrewDepth + Protrusion,6);
translate([ScrewOC,0,-Protrusion])
PolyCyl(Tap10_32,ScrewDepth + Protrusion,6);
translate([(RearBlock[0] - CordLen) + BodyLength/2 - FrontBlock[0],0,CornerRadius - TextDepth]) {
translate([0,10,0])
linear_extrude(height=TextDepth + Protrusion,convexity=1) // rendering glitches for convexity > 1
text("Mary",font="Ubuntu:style=Bold Italic",halign="center",valign="center");
translate([0,-10,0])
linear_extrude(height=TextDepth + Protrusion,convexity=1) // rendering glitches for convexity > 1
text("Nisley",font="Ubuntu:style=Bold Italic",halign="center",valign="center");
}
}
The M2 buzzed away for four hours on that puppy, with the first 2½ hours devoted to building the platform. That’s the downside of applying Hilbert Curve infill to two big flat surfaces, but the texture looks really good.
The elevation tension adjustment on both our bike helmet mirror mounts have become a bit sloppy. That’s no surprise, because I expected the tiny set screw in the tiny square hole near the top to eventually wear a depression in the ABS plastic arc upon which it bears:
So I got to do something I planned pretty much from the beginning of the project: cut a snippet of phosphor bronze spring stock to go between the Elevation mount and the arc, then bend the ends bent inward so they don’t slash an errant fingertip:
Helmet mirror mount – elevation slide
Slipped in place, the ends look like they stick out anyway, but they’re really just about flush:
Helmet mirror mount – El slide in place
Tightening the set screw pushes the strip against the arc, where it provides enough resistance to prevent slipping and enough smoothness for easy adjustment.
While I had the mounts up on the repair stand, I unscrewed the mirror shaft and snugged up the Azimuth pivot screw by a micro-smidgen to tighten that motion.
Four years ago, those ABS parts popped off the much-hacked Thing-O-Matic’s platform. The M2 produces somewhat better-looking results, but that yellow plastic has a certain charm…
A bit of tinkering with the OpenSCAD code that produced the DeoxIT bottle holder delivered a place for the cylindrical objects we use just before cycling:
Lip Balm Holder
The tubes are 1.5 diameters tall, minus a skosh, so the cylinders stand neatly inside and don’t want to fall over. I added about 1 mm clearance and you could taper the cylinder openings for E-Z insertion, although we can eke out a miserable existence with this thing as-is.
It works exactly as you’d expect:
Lip Balm Holder – in action
That big stick in the middle is actually skin sunscreen, not lip balm; let’s not get all pedantic. The intent is to keep those cylinders from rolling off the shelf and falling into awkward locations, which this will do.
The OpenSCAD source code is strictly from empirical:
Although I’d thought of a Mu-metal shield, copper foil tape should be easier and safer to shape into a simple shield. The general idea is to line the interior with copper tape, solder the joints together, cover with Kapton tape to reduce the likelihood of shorts, then stick it in place with some connector pin-and-socket combinations. Putting the tape on the outside would be much easier, but that would surround the circuitry with a layer of plastic that probably carries enough charge to throw things off.
Anyhow, the hexagonal circuit board model now sports a hexagonal cap to support the shield:
Victoreen 710-104 Ionization Chamber Fittings – Show with shield
The ad-hoc openings fit various switches, wires, & twiddlepots:
The general idea is to put the electrometer circuitry directly atop the Victoreen 710-104 ionization chamber, so as to minimize the distance from the center collector electrode to the electrometer input. After a few false starts, this looked promising:
Victoreen 710-104 Ionization Chamber Fittings – Show layout
The hexagonal circuit board fits the can so nicely that I’ll run with it, despite the over-the-top twee factor. Because it’s so hard to freehand a hex, I printed the green object as a tracing template, despite having the Slic3r preview show the parts just barely fitting on the M2 platform:
The skirt measures 0.25±0.05 around the entire perimeter, with a slight positive bias (platform too low) along the left side and a corresponding negative bias on the right. Both sides look just fine to me.
A pair of alignment pegs hold each board support in place while gluing:
Victoreen 710-104 Fittings – clamping
Next time around, I’ll glue the supports with the circuit board template laid in place to ensure the edges have the proper orientation, but they came out surprisingly close just by matching the outer perimeters. Of course, I probably bandsawed / belt sanded the carefully traced hex just slightly off-kilter.
The outer perimeter has 48 sides. Making it a multiple of three means each board support has the same pattern of sides and all will be interchangeable. Making it a multiple of four means each quadrant has the same pattern of sides and the ring looks pleasingly symmetrical. The factor-of-three is most important: you want interchangeable supports. Trust me on this.
The bottom ring keeps the solder dimple that seals the can base off the desk, but I also stuck a quartet of rubber feet on the can for better traction.
Here’s what it looks like with the two A23 12 V bias batteries in their holders, affixed to the can with foam tape:
Victoreen 710-104 Fittings – assembled
The OpenSCAD source code includes a few more tweaks:
// Victoreen 710-104 Ionization Chamber Fittings
// Ed Nisley KE4ZNU July 2015
Layout = "Show";
// Show - assembled parts
// Build - print them out!
// CanCap - PCB insulator for 6-32 mounting studs
// CanBase - surrounding foot for ionization chamber
// CanLid - generic surround for either end of chamber
// PCB - template for cutting PCB sheet
// PCBBase - holder for PCB atop CanCap
BuildTemplate = false; // true to build PCB template along with everything else
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
AlignPinOD = 1.75; // assembly alignment pins = filament dia
inch = 25.4;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//- Screw sizes
Tap4_40 = 0.089 * inch;
Clear4_40 = 0.110 * inch;
Head4_40 = 0.211 * inch;
Head4_40Thick = 0.065 * inch;
Nut4_40Dia = 0.228 * inch;
Nut4_40Thick = 0.086 * inch;
Washer4_40OD = 0.270 * inch;
Washer4_40ID = 0.123 * inch;
//----------------------
// Dimensions
OD = 0; // name the subscripts
LENGTH = 1;
Chamber = [91.0 + HoleWindage,38]; // Victoreen ionization chamber dimensions
Stud = [ // stud welded to ionization chamber lid
[6.5,IntegerMultiple(0.8,ThreadThick)], // flat head -- generous clearance
[4.0,9.5], // 6-32 screw -- ditto
];
NumStuds = 3;
StudSides = 6; // for hole around stud
BCD = 2.75 * inch; // mounting stud bolt circle diameter
PlateThick = 3.0; // layer atop and below chamber ends
RimHeight = 4.0; // extending up along chamber perimeter
WallHeight = RimHeight + PlateThick;
WallThick = 5.0; // thick enough to be sturdy & printable
CapSides = 8*6; // must be multiple of 4 & 3 to make symmetries work out right
PCBFlatsOD = 85.0 + 2*ThreadWidth; // hex dia across flats + clearance
PCBThick = 1.1;
PCB = [PCBFlatsOD / cos(30),PCBThick - ThreadThick]; // OD = tip-to-tip dia
echo(str("Actual PCB across flats: ",PCBFlatsOD - 2*ThreadWidth));
echo(str(" ... tip-to-tip dia: ",(PCBFlatsOD - 2*ThreadWidth)/cos(30)));
echo(str(" ... thickness: ",PCBThick));
HolderHeight = 11.0 + PCB[LENGTH]; // thick enough for PCB to clear studs
HolderShelf = 2.0; // shelf under PCB edge
echo(str("PCB holder height: ",HolderHeight));
echo(str(" ... across flats: ",PCBFlatsOD));
//----------------------
// Useful routines
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);
}
//- Locating pin hole with glue recess
// Default length is two pin diameters on each side of the split
module LocatingPin(Dia=AlignPinOD,Len=0.0) {
PinLen = (Len != 0.0) ? Len : (4*Dia);
translate([0,0,-ThreadThick])
PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-2*ThreadThick])
PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
translate([0,0,-Len/2])
PolyCyl(Dia,Len,4);
}
module ShowPegGrid(Space = 10.0,Size = 1.0) {
RangeX = floor(100 / Space);
RangeY = floor(125 / Space);
for (x=[-RangeX:RangeX])
for (y=[-RangeY:RangeY])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
//-----
module CanLid() {
difference() {
cylinder(d=Chamber[OD] + 2*WallThick,h=WallHeight,$fn=CapSides);
translate([0,0,PlateThick])
PolyCyl(Chamber[OD],Chamber[1],CapSides);
}
}
module CanCap() {
difference() {
CanLid();
translate([0,0,-Protrusion]) // central cutout
// cylinder(d=(BCD - 2*5.0),h=Chamber[LENGTH],$fn=CapSides);
rotate(180/6)
cylinder(d=BCD,h=Chamber[LENGTH],$fn=6);
for (i=[0:(NumStuds - 1)]) // stud clearance holes
rotate(i*360/NumStuds)
translate([BCD/2,0,0])
rotate(180/StudSides) {
translate([0,0,(PlateThick - (Stud[0][LENGTH] + 2*ThreadThick))])
PolyCyl(Stud[0][OD],2*Stud[0][LENGTH],StudSides);
translate([0,0,-Protrusion])
PolyCyl(Stud[1][OD],2*Stud[1][LENGTH],StudSides);
}
for (i=[0:(NumStuds - 1)], j=[-1,1]) // PCB holder alignment pins
rotate(i*360/NumStuds + j*15 + 60)
translate([Chamber[OD]/2,0,0])
rotate(180/4)
LocatingPin(Len=2*PlateThick - 2*ThreadThick);
}
}
module CanBase() {
difference() {
CanLid();
translate([0,0,-Protrusion])
PolyCyl(Chamber[OD] - 2*5.0,Chamber[1],CapSides);
}
}
module PCBTemplate() {
difference() {
cylinder(d=((PCBFlatsOD - 2*ThreadWidth)/cos(30)),h=max(PCB[LENGTH],3.0),$fn=6); // actual PCB size, overly thick
translate([0,0,-Protrusion])
cylinder(d=10,h=10*PCB[LENGTH],$fn=12);
}
}
module PCBBase() {
difference() {
cylinder(d=Chamber[OD] + 2*WallThick,h=HolderHeight,$fn=CapSides);
rotate(30) {
translate([0,0,-Protrusion]) // central hex
cylinder(d=(PCBFlatsOD - 2*HolderShelf)/cos(30),h=2*HolderHeight,$fn=6);
translate([0,0,HolderHeight - PCB[LENGTH]]) // hex PCB recess
cylinder(d=PCB[OD],h=HolderHeight,$fn=6);
for (i=[0:NumStuds - 1]) // PCB retaining screws
rotate(i*120 + 30)
translate([(PCBFlatsOD/2 + Clear4_40/2 + ThreadWidth),0,-Protrusion])
rotate(180/6)
PolyCyl(Tap4_40,2*HolderHeight,6);
for (i=[0:(NumStuds - 1)], j=[-1,1]) // PCB holder alignment pins
rotate(i*360/NumStuds + j*15 + 30)
translate([Chamber[OD]/2,0,0])
rotate(180/4)
LocatingPin(Len=PlateThick);
}
for (i=[0:NumStuds - 1]) // segment isolation
rotate(i*120 - 30)
translate([0,0,-Protrusion]) {
linear_extrude(height=2*HolderHeight)
polygon([[0,0],[Chamber[OD],0],[Chamber[OD]*cos(60),Chamber[OD]*sin(60)]]);
}
}
}
//----------------------
// Build it
ShowPegGrid();
if (Layout == "CanLid") {
CanLid();
}
if (Layout == "CanCap") {
CanCap();
}
if (Layout == "CanBase") {
CanBase();
}
if (Layout == "PCBBase") {
PCBBase();
}
if (Layout == "PCB") {
PCBTemplate();
}
if (Layout == "Show") {
CanBase();
color("Orange",0.5)
translate([0,0,PlateThick + Protrusion])
cylinder(d=Chamber[OD],h=Chamber[LENGTH],$fn=CapSides);
translate([0,0,(2*PlateThick + Chamber[LENGTH] + 2*Protrusion)])
rotate([180,0,0])
CanCap();
translate([0,0,(2*PlateThick + Chamber[LENGTH] + 5.0)])
PCBBase();
color("Green",0.5)
translate([0,0,(2*PlateThick + Chamber[LENGTH] + 7.0 + HolderHeight)])
rotate(30)
PCBTemplate();
}
if (Layout == "Build") {
if (BuildTemplate) {
translate([-0.50*Chamber[OD],-0.60*Chamber[OD],0])
CanCap();
translate([0.55*Chamber[OD],-0.60*Chamber[OD],0])
rotate(30)
PCBTemplate();
}
else {
translate([-0.25*Chamber[OD],-0.60*Chamber[OD],0])
CanCap();
}
translate([-0.25*Chamber[OD],0.60*Chamber[OD],0])
CanBase();
translate([0.25*Chamber[OD],0.60*Chamber[OD],0])
PCBBase();
}