The ice behind this sundog foretold a snowstorm:

When I first saw it, the contrail bisected the sundog, but we had to walk to a safer spot before I could fumble with the Pixel.
See? I’m not always searching for treasures amid the roadside trash …
The Smell of Molten Projects in the Morning
Ed Nisley's Blog: shop notes, electronics, firmware, machinery, 3D printing, and curiosities
The ice behind this sundog foretold a snowstorm:
When I first saw it, the contrail bisected the sundog, but we had to walk to a safer spot before I could fumble with the Pixel.
See? I’m not always searching for treasures amid the roadside trash …
I spotted a piece of jewelry during a recent walk:
The other side shows off The Shiny Bit:
I seem to have swapped the “front” and “rear” labels; the flat side faces the LED / HID bulb.
It looked even better after extraction and casual cleaning:
It seems someone with a relatively new car had a fairly high energy accident just north of Red Oaks Mill. The remainder of the debris consisted of shattered engineering plastic. We’ll never know the rest of the story.
Both lens surfaces have a slight nubbly finish, perhaps to produce some side light around the main beam. The rectangular opening apparently shaped the low beam and doesn’t appear movable, so perhaps the car had separate headlights for the high beams.
I’m not quite sure what to do with a chipped condenser lens, so it’s sitting on the windowsill (in a sun-safe orientation) along with many other glittery bits of glass I’ve collected over the years.
A Pogo Pin reference may be useful:
For pins, the suffix -hn indicates pin head shape, the most useful of which may be:
For sockets, the suffix -ntl gives:
From what I can find on eBay, all pins have 6 mm travel with typically 75 / 100 / 180 g spring force.
A picture ripped from the reference to forestall link rot:
Memo to Self: US-based eBay sellers charge three times more than Chinese sellers, but deliver in one-third the time.
[Update: Simon sends a link to Everett Charles Technologies, a pogo-pin manufacturer providing “Probably much more information than anyone should ever want”. Of course, eBay / Amazon junk may not meet any particular specs, so scale your expectations accordingly.]
Watching the MPCNC plot Spirograph patterns led me to wonder about how much force the printed drag knife holder applies to the pen:
The HP 7475A plotter spec calls for 19 g = 0.67 oz of downward force on the pen, so, in an ideal world, one might want to use one’s collection of aging plotter pens in a similar manner.
Plotter pen, meet digital scale:
Stepping the pen downward in 0.1 mm increments produced a set of numbers and a tidy linear fit graph:
I hereby swear I’m not making things up: the spring constant really is a nice, round 100 g/mm!
I set plot_z = -1.0
in the GCMC program, with Z=0.5 touched off atop a defunct ID card on the paper surface to compensate for any tabletop warp / bow / misalignment, plus any errors from the tool length probe. An eyeballometric scan against a straightedge shows pretty nearly no misalignment, which means the holder mashes the pen against the paper with about 100 g of force, five times the HP spec.
A distinct case of pen abuse rears its ugly head.
It’s time to conjure a height probe for the tool holder.
The GCMC Spirograph Generator program chooses parameters using pseudo-random numbers based on a seed fed in from the Bash script, so I was surprised to see two plots overlap exactly:
The two overlapping traces are the 15 inward-pointing wedges around the central rosette.
The first one:
(PRNG seed: 38140045) (Paper size: [16.50in,14in]) (PlotSize: [15.50in,13.00in]) (Stator 3: 150) (Rotor 4: 40) (GCD: 10) (Offset: -0.94) (Dia ratio: -0.27) (Lobes: 15) (Turns: 4) (Plot scale: [5.11in,4.29in]) (Tool change: 1) T1 M6
The second one:
(PRNG seed: 74359295) (Paper size: [16.50in,14in]) (PlotSize: [15.50in,13.00in]) (Stator 3: 150) (Rotor 4: 40) (GCD: 10) (Offset: -0.93) (Dia ratio: -0.27) (Lobes: 15) (Turns: 4) (Plot scale: [5.12in,4.30in]) (Tool change: 3) T3 M6
The Offset isn’t quite the same, but the pen width covers up the difference.
With only four Stators and 17 Rotors, the probability of picking the same pair works out to 0.25 × 0.059 = 1.4%. You can sometimes get the same number of Lobes and Turns from several different Stator + Rotor combinations, but these were exact matchs with the same indices.
The Pen Offset within the Rotor comes from a fraction computed with ten bit resolution, so each Offset value represents slightly under 0.1% of the choices. If any four adjacent values look about the same, then it’s only eight bits of resolution and each represents 0.4%.
The Rotor and Stator set the Diameter ratio, but the sign comes from what’s basically a coin flip based on the sign of a fraction drawn from 256 possibilities; call it 50%.
Overall, you’re looking at a probability of 28 ppm = 0.0028%, so I (uh, probably) won’t see another overlay for a while …
I don’t know how to factor the PRNG sequence into those numbers, although it surely affects the probability. In this case, two different seeds produced nearly the same sequence of output values, within the resolution of my hack-job calculations.
Whatever. It’s good enough for my simple purposes!
An improved version of my GCMC Spirograph pattern generator, now with better annotation and tool changes:
The GCMC code sets the stator and rotor gear tooth counts, the rotor diameter, and the pen offset using a pseudo-random number generator. This requires randomizing the PRNG seed, which I do in the calling script with the nanosecond of the current second: rnd=$(date +%N)
.
The G-Code file name also comes from the timestamp:
ts=$(date +%Y%m%d-%H%M%S) fn='Spiro_'${ts}'.ngc' # blank line to make the underscore visible
Which means you must call the Bash script slowly to generate a pile o’ plots:
for i in {1..60} ; do sh /mnt/bulkdata/Project\ Files/Mostly\ Printed\ CNC/Patterns/spiro.sh ; sleep 1 ; done
Sift through the heap with drag-n-drop action using an online G-Code previewer. There seems no clean way to convert G-Code to a bitmap on the command line, although you can do it manually, of course.
The GCMC program spits out the G-code for one plot at a time, so the Bash script calls it four times to fill a sheet of paper with random patterns:
for p in $(seq 4) do rnd=$(date +%N) gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -q "$Spirograph" >> $fn done
The -q
parameter tells GCMC to not include the prolog and epilog files, because the calling script glues those onto the lump of G-Code for all four plots.
The -D Pen=$p
parameter tells the GCMC program which “tool” to select with a Tn M6
tool change command before starting the plot. Although plotter pens have a well-defined position in the holder and a pretty nearly constant length, you must have a tool length probe installed and configured:
Set the overall sheet size in inches or millimeters to get a plot centered in the middle of the page with half-inch margins all around:
Paper='PaperSize=[16.5in,14in]
With all that in hand, those good old black ceramic-tip pens give impeccable results:
The surviving ones, anyhow. I must apply my collection of Sakura Micron pens to this task.
The other three colors come from fiber pens with reasonably good tips:
They’re a lot like diatoms: all different and all alike.
The GCMC and Bash source code as a GitHub Gist:
# Spirograph G-Code Generator | |
# Ed Nisley KE4ZNU - December 2017 | |
Paper='PaperSize=[16.5in,14in]' | |
Flags="-P 2" | |
LibPath="-I /opt/gcmc/library" | |
Spirograph='/mnt/bulkdata/Project Files/Mostly Printed CNC/Patterns/Spirograph.gcmc' | |
Prolog="/home/ed/.config/gcmc/prolog.gcmc" | |
Epilog="/home/ed/.config/gcmc/epilog.gcmc" | |
ts=$(date +%Y%m%d-%H%M%S) | |
fn='Spiro_'${ts}'.ngc' | |
echo Output: $fn | |
rm -f $fn | |
echo "(File: "$fn")" > $fn | |
cat $Prolog >> $fn | |
for p in $(seq 4) | |
do | |
rnd=$(date +%N) | |
gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -q "$Spirograph" >> $fn | |
done | |
cat $Epilog >> $fn |
// Spirograph simulator for MPCNC used as plotter | |
// Ed Nisley KE4ZNU - 2017-12-23 | |
// Spirograph equations: | |
// https://en.wikipedia.org/wiki/Spirograph | |
// Loosely based on GCMC cycloids.gcmc demo: | |
// https://gitlab.com/gcmc/gcmc/tree/master/example/cycloids.gcmc | |
// Required command line parameters: | |
// -D Pen=n pen selection for tool change and legend position | |
// -D PaperSize=[x,y] overall sheet size: [17in,11in] | |
// -D PRNG_Seed=i non-zero random number seed | |
include("tracepath.inc.gcmc"); | |
include("engrave.inc.gcmc"); | |
//----- | |
// Greatest Common Divisor | |
// https://en.wikipedia.org/wiki/Greatest_common_divisor#Using_Euclid's_algorithm | |
// Inputs = integers without units | |
function gcd(a,b) { | |
local d=0; | |
if (!isnone(a) || isfloat(a) || !isnone(b) || isfloat(b)) { | |
warning("Values must be dimensionless integers"); | |
} | |
while (!((a | b) & 1)) { // remove and tally common factors of two | |
a >>= 1; | |
b >>= 1; | |
d++; | |
} | |
while (a != b) { | |
if (!(a & 1)) {a >>= 1;} // discard non-common factor of 2 | |
elif (!(b & 1)) {b >>= 1;} // ... likewise | |
elif (a > b) {a = (a - b) >> 1;} // gcd(a,b) also divides a-b | |
else {b = (b - a) >> 1;} // ... likewise | |
} | |
local GCD = a*(1 << d); // form gcd | |
// message("GCD: ",GCD); | |
return GCD; | |
} | |
//----- | |
// Max and min functions | |
function max(x,y) { | |
return (x > y) ? x : y; | |
} | |
function min(x,y) { | |
return (x < y) ? x : y; | |
} | |
//----- | |
// Pseudo-random number generator | |
// Based on xorshift: | |
// https://en.wikipedia.org/wiki/Xorshift | |
// http://www.jstatsoft.org/v08/i14/paper | |
// Requires initial state from calling script | |
// -D "PRNG_Seed=$(date +%N)" | |
function XORshift() { | |
local x = PRNG_State; | |
x ^= x << 13; | |
x ^= x >> 17; | |
x ^= x << 5; | |
PRNG_State = x; | |
return x; | |
} | |
//----- | |
// Spirograph tooth counts mooched from: | |
// http://nathanfriend.io/inspirograph/ | |
// Stators has both inside and outside counts, because we're not fussy | |
Stators = [96, 105, 144, 150]; | |
Rotors = [24, 30, 32, 36, 40, 45, 48, 50, 52, 56, 60, 63, 64, 72, 75, 80, 84]; | |
//----- | |
// Start drawing things | |
// Set these variables from command line | |
comment("PRNG seed: ",PRNG_Seed); | |
PRNG_State = PRNG_Seed; | |
// Define some useful constants | |
AngleStep = 2.0deg; | |
Margins = [0.5in,0.5in] * 2; | |
comment("Paper size: ",PaperSize); | |
PlotSize = PaperSize - Margins; | |
comment("PlotSize: ",PlotSize); | |
//----- | |
// Set up gearing | |
s = (XORshift() & 0xffff) % count(Stators); | |
StatorTeeth = Stators[s]; | |
comment("Stator ",s,": ",StatorTeeth); | |
r = (XORshift() & 0xffff) % count(Rotors); | |
RotorTeeth = Rotors[r]; | |
comment("Rotor ",r,": ",RotorTeeth); | |
GCD = gcd(StatorTeeth,RotorTeeth); // reduce teeth to ratio of least integers | |
comment("GCD: ",GCD); | |
StatorN = StatorTeeth / GCD; | |
RotorM = RotorTeeth / GCD; | |
L = to_float((XORshift() & 0x3ff) - 512) / 100.0; // normalized pen offset in rotor | |
comment("Offset: ", L); | |
sgn = sign((XORshift() & 0xff) - 128); | |
K = sgn*to_float(RotorM) / to_float(StatorN); // normalized rotor dia | |
comment("Dia ratio: ",K); | |
Lobes = StatorN; // having removed all common factors | |
Turns = RotorM; | |
comment("Lobes: ", Lobes); | |
comment("Turns: ", Turns); | |
//----- | |
// Crank out a list of points in normalized coordinates | |
Path = {}; | |
Xmax = 0.0; | |
Xmin = 0.0; | |
Ymax = 0.0; | |
Ymin = 0.0; | |
for (a=0.0deg ; a <= Turns*360deg ; a += AngleStep) { | |
x = (1 - K)*cos(a) + L*K*cos(a*(1 - K)/K); | |
if (x > Xmax) {Xmax = x;} | |
elif (x < Xmin) {Xmin = x;} | |
y = (1 - K)*sin(a) - L*K*sin(a*(1 - K)/K); | |
if (y > Ymax) {Ymax = y;} | |
elif (y < Ymin) {Ymin = y;} | |
Path += {[x,y]}; | |
} | |
//message("Max X: ", Xmax, " Y: ", Ymax); | |
//message("Min X: ", Xmin, " Y: ", Ymin); // min will always be negative | |
Xmax = max(Xmax,-Xmin); // odd lobes can cause min != max | |
Ymax = max(Ymax,-Ymin); // ... need really truly absolute maximum | |
//----- | |
// Scale points to actual plot size | |
PlotScale = [PlotSize.x / (2*Xmax), PlotSize.y / (2*Ymax)]; | |
comment("Plot scale: ", PlotScale); | |
Points = scale(Path,PlotScale); // fill page, origin at center | |
//----- | |
// Set up pen | |
if (Pen > 0) { | |
comment("Tool change: ",Pen); | |
toolchange(Pen); | |
} | |
//----- | |
// Plot the curve | |
feedrate(3000.0mm); | |
safe_z = 1.0mm; | |
plot_z = -1.0mm; | |
tracepath(Points, plot_z); | |
//----- | |
// Put legend in proper location | |
feedrate(500mm); | |
TextSize = [3.0mm,3.0mm]; | |
TextLeading = 1.5; // line spacing as multiple of nominal text height | |
MaxPen = 4; | |
line1 = typeset("Seed: " + PRNG_Seed + " Stator: " + StatorTeeth + " Rotor: " + RotorTeeth,FONT_HSANS_1); | |
line2 = typeset("Offset: " + L + " GCD: " + GCD + " Lobes: " + Lobes + " Turns: " + Turns,FONT_HSANS_1); | |
maxlength = TextSize.x * max(line1[-1].x,line2[-1].x); | |
textpath = line1 + (line2 - [-, TextLeading, -]); // undef - n -> undef to preserve coordinates | |
if (Pen == 1 || Pen > MaxPen ) { // catch and fix obviously bogus pen selections | |
textorg = [PlotSize.x/2 - maxlength,-(PlotSize.y/2 - TextLeading*TextSize.y)]; | |
} | |
elif (Pen == 2) { | |
textorg = [-PlotSize.x/2,-(PlotSize.y/2 - TextLeading*TextSize.y)]; | |
} | |
elif (Pen == 3) { | |
textorg = [PlotSize.x/2 - maxlength, PlotSize.y/2 - TextSize.y]; | |
} | |
elif (Pen == 4) { | |
textorg = [-PlotSize.x/2, PlotSize.y/2 - TextSize.y]; | |
} | |
else { | |
Pen = 0; // squelch truly bogus pens | |
textorg = [0mm,0mm]; // just to define it | |
} | |
if (Pen) { // Pen = 0 suppresses legend | |
placepath = scale(textpath,TextSize) + textorg; | |
comment("Legend begins"); | |
engrave(placepath,safe_z,plot_z); | |
} | |
if (Pen == 1) { // add attribution along right margin | |
attrpath = typeset("Ed Nisley - KE4ZNU - softsolder.com",FONT_HSANS_1); | |
attrpath = rotate_xy(attrpath,90deg); | |
attrorg = [PlotSize.x/2,5*TextLeading*TextSize.y - PlotSize.y/2]; | |
placepath = scale(attrpath,TextSize) + attrorg; | |
comment("Attribution begins"); | |
engrave(placepath,safe_z,plot_z); | |
} | |
goto([-,-,25.0mm]); |
Gotta play with my new toy:
That’s with a set of liquid ink and ceramic tip plotter pens. They’re unbelievably cranky, but produce wonderfully fine lines:
Text comes out exactly the way vector lettering should look:
There’s a slight shake visible at 500 mm/min = 8.3 mm/s, but it’s Good Enough.
All the pen-and-ink traffic around the center produced a ring of damp green fuzz:
The artsy part of the plot ran at 1800 mm/min = 30 mm/s, with little of the wobbulation at 6000 mm/min = 100 mm/s. None of that would matter with a router, of course.
It’s a nice, Christmasy design in kinda-red and sorta-green.
From the stack of plots accumulating near the MPCNC bench:
Plots 7 and 9 show the tape sutures required to produce a 26×18 inch sheet covering the MPCNC’s full work area. The squat plots fit on B-size sheets and the rest come from 17×14 inch artist’s sketchpad sheets.
I used Google PhotoScan to capture and rectangularize paper sheets from the floor or atop the bench, then battered the contrast and crushed the file size with a one-liner:
i=1 ; for f in 1* ; do printf -v dn "Spiro %02d.jpg" $(( i++ )) ; convert $f -level '10,80%' -density 300 -define jpeg:extent=300KB tweaked/"$dn" ; done
The plots look great in person (modulo some incremental software improvements), but the slideshow images look awful because:
They’re not (yet) art and there’s no point in a high-quality workflow.
Enjoy the day …