Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
Category: Software
General-purpose computers doing something specific
This may not look like much, but it’s the first test of the p-MOSFET power switch that completely kills power to the Arduino Pro Mini board and the Hall Effect LED Blinky Light:
Power off – 30 mA load
The top trace is the base drive to the NPN transistor that holds the p-MOSFET on while the Arduino is running. When it’s time to shut off, the Arduino drops the base drive output, the MOSFET turns off, and the switched battery voltage in the bottom trace drops like a rock. The current is about 30 mA when the Arduino is running and immeasurably low when it’s off; the MOSFET spec says it’s less than 1 μA, which is fine with me.
The PCB has those components clustered in the upper left corner, with the Arduino Pro Mini perched on header pins to the right:
Hall LED PCB – power switch test
The test code is a crudely hacked version of the canonical Blink sketch that waits 5 s after it starts running, then pulls the plug:
// Modified from Arduino Blink example
// Drives external p-MOSFET power switch
// Ed Nisley - KE4ZNU - Sep 2013
int led = 13;
// HIGH to enable power supply
int PowerOn = 4;
// HIGH to light Status LED
int Status = 10;
unsigned long MillisThen;
void setup() {
pinMode(led, OUTPUT);
pinMode(PowerOn,OUTPUT);
digitalWrite(PowerOn,HIGH);
pinMode(Status,OUTPUT);
digitalWrite(Status,HIGH);
MillisThen = millis();
}
void loop() {
digitalWrite(led, HIGH);
delay(100);
digitalWrite(led, LOW);
delay(500);
if (((millis() - MillisThen) > 5000ul)) {
digitalWrite(Status,LOW);
delay(50);
digitalWrite(PowerOn,LOW);
digitalWrite(Status,HIGH);
}
}
It turns out that the Arduino runtime has a several-second delay after power comes up before the setup() routine starts running, so brief pulses from a vibration switch won’t last long enough to turn the thing on. That’s not a fatal flaw for now and, in fact, having to hold the power button in for a few seconds isn’t entirely a Bad Thing.
However, once the power turns on, a vibration switch could trigger an Arduino interrupt pin to reset a power-off timer. I’d be tempted to put the vibration switch in parallel with the button, with a pair of steering diodes that isolate the raw battery from the input pin.
This is, of course, a pure electronic implementation of a Useless Machine…
The brassboard PCB for the Hall effect blinky light is too bendy for the SMD parts to survive much debugging, particularly with all the wires hanging off the edges, so I whipped up a stiff mounting bracket that captures the whole thing, with a flange that fits in the work stand arms:
PCB Test Frame – solid model
I ran some self-tapping 4-40 hex-head screws into the holes while the plastic was still warm on the M2’s platform:
PCB stiffener with screws on M2 platform
Six screws seem excessive and I’ll probably wind up using just the middle two, but there’s no harm in having more holes and fittings than you really need.
The flange fits neatly into the board holder on the Electronics Workbench, above all the construction clutter:
PCB stiffener in board holder
The nice thing about having a 3D printer: when you need an object like this, a couple of hours later you have one!
The OpenSCAD source code, slightly improved based the results you see above:
// Test support frame for Hall Effect LED Blinky Light
// Ed Nisley KE4ZNU - Sept 2013
ClampFlange = true;
//- 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;
//- Screw sizes
inch = 25.4;
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;
//- PCB sizes
PCBSize = [46.5,84.0,1.0];
PCBShelf = 2.0;
Clearance = 4*[ThreadWidth,ThreadWidth,0];
WallThick = IntegerMultiple(4.0,ThreadWidth);
FrameHeight = 5.0;
ScrewOffset = 0.0 + Clear4_40/2;
OAHeight = FrameHeight + Clearance[2] + PCBSize[2];
FlangeExtension = 3.0;
FlangeThick = IntegerMultiple(1.5,ThreadThick);
Flange = PCBSize
+ 2*[ScrewOffset,ScrewOffset,0]
+ 2*[Washer4_40OD,Washer4_40OD,0]
+ [2*FlangeExtension,2*FlangeExtension,(FlangeThick - PCBSize[2])]
;
echo("Flange: ",Flange);
NumSides = 4*5;
//- 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);
}
//- Put peg grid on build surface
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);
}
//- Build it
ShowPegGrid();
difference() {
union() { // body block and screw bosses
translate([0,0,OAHeight/2])
color("LightBlue")
cube(PCBSize + Clearance + [2*WallThick,2*WallThick,FrameHeight],center=true);
for (x=[-1,1], y=[-1,0,1]) {
translate([x*(PCBSize[0]/2 + ScrewOffset),
y*(PCBSize[1]/2 + ScrewOffset),
0])
color("Orchid") cylinder(r=Washer4_40OD,h=OAHeight,$fn=NumSides);
}
if (ClampFlange)
translate([0,0,Flange[2]/2])
color("SeaGreen") cube(Flange,center=true);
}
for (x=[-1,1], y=[-1,0,1]) { // screw holes and washer recesses
translate([x*(PCBSize[0]/2 + ScrewOffset),
y*(PCBSize[1]/2 + ScrewOffset),
-Protrusion])
rotate((x-1)*90)
PolyCyl(Tap4_40,(OAHeight + 2*Protrusion));
translate([x*(PCBSize[0]/2 + ScrewOffset),
y*(PCBSize[1]/2 + ScrewOffset),
OAHeight - PCBSize[2]])
PolyCyl(1.2*Washer4_40OD,(PCBSize[2] + Protrusion),NumSides);
}
translate([0,0,OAHeight/2]) // through hole below PCB
cube(PCBSize - 2*[PCBShelf,PCBShelf,0] + [0,0,2*OAHeight],center=true);
translate([0,0,(OAHeight - (PCBSize[2] + Clearance[2])/2 + Protrusion/2)]) // PCB pocket on top
cube(PCBSize + Clearance + [0,0,Protrusion],center=true);
}
[Update: The talk went well and took a bit under three hours, although by mutual agreement we didn’t fire up the M2 at the end. I’ll work on a short talk about Design for Printability and we’ll run that with a separate printing session. A good time was had by all!]
Prompted by that suggestion, here’s the current collection of Devilspie2 scripts (in ~/.config/devilspie2/) that make my dual-monitor setup (left: 2560×1440 landscape, right: 1050×1680 portrait) usable with a single X session in Xubuntu 12.10. The window manager understands about the monitor layout, so maximizing a window will fill whatever monitor it’s currently occupying.
acroread.lua — maximized on portrait
if (get_window_name()=="Adobe Reader") then
unmaximize();
set_window_geometry(0,0,1000,100);
set_window_geometry(2561,0,1000,100);
maximize();
end
chromium.lua — right half of landscape
if (get_application_name()=="Chromium" and get_window_name() ~= "Print") then
set_window_geometry(1400,0,1150,1200);
maximize_vertically();
end
digikam.lua — right half of landscape, force large Search dialog, dammit
if (get_application_name() == "Digikam") then
debug_print("DigiKam conditional - top");
if (get_window_name() == "Advanced Search") then
debug_print("Digikam - Adv Search");
set_window_geometry(750,100,1000,1300);
else
debug_print("Main DigiKam window");
set_window_geometry(0,0,1400,1000);
maximize_vertically();
end
end
firefox.lua — left half of landscape, enlarge dialogs
if (get_application_name()=="Firefox") then
debug_print("FF conditional - top");
if (get_window_name() == "Print") then
set_window_position(700,350);
elseif (0 == string.find(get_window_name(),"Password")) then
set_window_position(0,0);
maximize_vertically();
end
end
gimp.lua — force Gutenprint dialog to the top, dammit
if (get_application_name() == "GNU Image Manipulation Program") then
debug_print("GIMP conditional - top");
if (string.find(get_window_name(),"Print")) then
debug_print("GIMP - GutenPrint")
set_window_position(700,350);
make_always_on_top();
else
debug_print("GIMP - Main window");
end
end
passwords.lua — put password dialogs in mid-screen
if (get_window_name()=="Password Required") then
debug_print("Password");
set_window_position(700,350);
end
pronterface.lua — force to middle-ish of Desktop 2
if (get_window_name()=="Printer Interface") then
set_window_workspace(2);
set_window_position(1200,750);
end
slic3r.lua — force to right side of Desktop 2
if (get_window_name()=="Slic3r") then
set_window_workspace(2);
set_window_geometry(1600,0,700,700);
end
terminal.lua — maximized on portrait
if (get_window_name()=="Terminal") then
set_window_position(2561,0);
maximize();
end
thunderbird.lua — left half of landscape, force big dialogs
if (get_application_name() == "Thunderbird") then
debug_print("TBird conditional - top");
if (1 == string.find(get_window_name(),"Print")) then
debug_print("TBird - print...");
set_window_position(700,350);
elseif (string.find(get_window_name(),"Sending") or
string.find(get_window_name(),"Confirm") or
string.find(get_window_name(),"Processing")) then
debug_print("TBird - generic dialog");
set_window_position(200,600);
elseif (string.find(get_window_name(),"Write:")) then
debug_print("TBird - writing");
set_window_geometry(1300,0,900,600);
maximize_vertically();
elseif (0 == string.find(get_window_name(),"Password")) then
debug_print("Main TBird window?");
debug_print(" name: ",get_window_name());
set_window_geometry(0,0,1300,1200);
maximize_vertically();
end
end
Although I’ve pretty much given up on torture tests, I saw a note about the troubles someone had with Triffid Hunter’s Bridge Torture Test object. I did a bit of tweaking to the OpenSCAD source to shorten the struts and add the pads (which could be done with Slic3r’s Brim settings), but it’s otherwise about the same. The clear span is about 50 mm:
Bridge Torture Test – solid model
Using my usual settings, with no special setup, the front looked OK:
Bridge torture test – overview
One strand came out rather droopy:
Bridge torture test – front
The bottom layer of the bridge isn’t as consolidated as it could be:
Bridge torture test – bottom
The overall speed dropped considerably as the Cool setting limited the layer time to 20 seconds; the Bridge settings didn’t apply.
I could probably tighten the bottom strands a bit, but it’s OK for a first pass.
Got a call from a friend who was having trouble getting BitDefender to accept its new license key, so I drove over; she’s at the top of a killer hill and I’d already biked my two dozen miles for the day. Solving that problem was straightforward, if you happen to know that they use “authorization” and “license” as synonyms and that you access the key entry dialog by clicking on a text field that doesn’t look at all clickable.
I should have declared victory and returned to the Basement Laboratory, but, no, I had to be a nice guy.
BitDefender kvetched that it had been 777 days since its last scan, so I set up some regularly scheduled scans and automagic updates for everything in sight; we agreed she’d just let the thing run overnight on Mondays to get all that done.
BitDefender also suggested a handful of critical Windows XP updates, plus the usual Adobe Flash and Reader updates, plus some nonsense about Windows Live Messenger that seemed to require downloading and installing a metric shitload of Microsoft Bloatware. Rather than leave all that for next Monday’s unattended update, I unleashed the critical ones, did the Flash and Reader updates, and stuffed the Messenger update back under the rug.
Then AOL recommended an urgent update to AOL Desktop 9.7. She has a couple of AOL email addresses, mostly for historic reasons, and I asked if she ever used the AOL Desktop. She wasn’t sure, so I lit up the installed AOL Desktop 9.6: “Oh, that’s how I get all my email!” OK, so we’ll update that, too.
After all the thrashing was done, the system rebooted and presented us with the single most unhelpful error message I’ve ever seen:
Windows Error – Ordinal Not Found
No, you chowderheads, that is not OK…
Searching on the obvious terms indicated this had something to do with Internet Explorer 8 (remember IE 8?) and produced a number of irrelevant suggestions. The least awful seemed to involve running the Microsoft System File Checker utility:
sfc /scannow
Which I did.
It ran for the better part of an hour, then suggested a reboot. During the shutdown, it replaced 29 files at an average of about 5 minutes per file.
After which, Windows restarted and displayed exactly the same error message. Actually, a series of them; various programs couldn’t locate a fairly wide selection of ordinals in several DLLs.
OK. I give up.
We located a tech who does this sort of thing for a living. I’ve offered to split the cost of getting the box up and running again, with the understanding that it may be easier to start with a fresh off-lease Dell box running Windows 7 than to exhume an aging Windows XP installation.
I stopped caring about Windows toward the end of the last millennium and now keep a Token Windows Box only for hardware like the HOBOWare dataloggers and software like TurboTax.
The cutter is still attached to the raft that, it seems, is required for passable results on the Afinia’s platform.
Having already figured out how to wrap a cutter around a shape, the most straightforward procedure starts by extracting the cutter’s shape. So, lay the cutter face down on the scanner and pull an image into GIMP:
Afinia Robot – scan
Blow out the contrast to eliminate the background clutter, then posterize to eliminate shadings:
Afinia Robot – scan enhanced
Select the black interior region, grow the selection by a pixel or two, then shrink it back to eliminate (most of) the edge granularity, plunk it into a new image, and fill with black:
Afinia Robot – scan filled
Now the magic happens…
Import the bitmap image into Inkscape. In principle, you can auto-trace the bitmap outline and clean it up manually, but a few iterations of that convinced me that it wasn’t worth the effort. Instead, I used Inkscape’s Bézier Curve tool to drop nodes (a.k.a. control points) at all the inflection points around the image, then warped the curves to match the outline:
Afinia Robot – Bezier spline fitting
If you’re doing that by hand, you could start with the original scanned image, but the auto-trace function works best with a high-contrast image and, after you give up on auto-tracing, you’ll find it’s easier to hand-trace a high-contrast image.
Anyhow, the end result of all that is a smooth path around the outline of the shape, without all the gritty details of the pixelated version. Save it as an Inkscape SVG file for later reference.
OpenSCAD can import a painfully limited subset of DXF files that, it seems, the most recent versions of Inkscape cannot produce (that formerly helpful tutorial being long out of date). Instead, I exported (using “Save as”) the path from Inkscape to an Encapsulated Postscript file (this is a PNG, as WordPress doesn’t show EPS files):
Afinia Robot – Bezier Curves.eps
It’s not clear what the EPS file contains; I think it’s just a list of points around the path that doesn’t include the smooth Bézier goodness. That may account for the grittiness of the next step, wherein the pstoedit utility converts the EPS file into a usable DXF file:
Unfortunately, either the EPS file doesn’t have enough points on each curve or pstoedit automatically sets the number of points and doesn’t provide an override: contrary to what you (well, I) might think, the -splineprecision option doesn’t apply to whatever is in the EPS file. In any event, the resulting DXF file has rather low-res curves, but they were good enough for my purposes and OpenSCAD inhaled the DXF and emitted a suitable STL file:
Afinia Robot – shape slab
To do that, you set the Layout variable to “Slab”, compile the model, and export the STL.
Being interested only in the process and its results, not actually cutting and baking cookies, I tweaked the OpenSCAD parameters to produce stumpy “cutters”:
Afinia Robot – solid model
You do that by setting the Layout variable to “Build”, compile the model, and export yet another STL. In the past, this seemed to be a less fragile route than directly importing and converting the DXF at each stage, but that may not be relevant these days. In any event, having an STL model of the cookie may be useful in other contexts, so it’s not entirely wasted effort.
Run the STL through Slic3r to get the G-Code as usual.
The resulting model printed in about 20 minutes apiece on the M2:
Robot Cutter – stumpy version
As it turns out, the fact that the M2 can produce ready-to-use cutters, minus the raft, is a strong selling point.
Given a workable model, the next step was to figure out the smallest possible two-thread-wide cutter blade, then run variations of the Extrusion Factor to see how that affected surface finish. More on that in a while.
The OpenSCAD source isn’t much changed from the original Tux Cutter; the DXF import required different scale factors:
// Robot cookie cutter using Minkowski sum
// Ed Nisley KE4ZNU - Sept 2011
// August 2013 adapted from the Tux Cutter
Layout = "Build"; // Build Slab
//- Extrusion parameters - must match reality!
ThreadThick = 0.25;
ThreadWidth = 0.40;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
MaxSize = 150; // larger than any possible dimension ...
Protrusion = 0.1;
//- Cookie cutter parameters
Size = 95;
TipHeight = IntegerMultiple(3.0,ThreadThick);
TipThick = 1.5*ThreadWidth; // 1.5* = thinnest 2-thread wall, 1.0* thread has gaps
WallHeight = IntegerMultiple(1.0,ThreadThick);
WallThick = 4.5*ThreadWidth;
LipHeight = IntegerMultiple(1.0,ThreadWidth);
LipThick = IntegerMultiple(5,ThreadWidth);
//- Wrapper for the shape of your choice
module Shape(Size) {
Robot(Size);
}
//- A solid slab of Tux goodness in simple STL format
// Choose magic values to:
// center it in XY
// reversed across Y axis (prints with handle on bottom)
// bottom on Z=0
// make it MaxSize from head to feet
module Tux(Scale) {
STLscale = 250;
scale(Scale/STLscale)
translate([105,-145,0])
scale([-1,1,24])
import(
file = "/mnt/bulkdata/Project Files/Thing-O-Matic/Tux Cookie Cutter/Tux Plate.stl",
convexity=5);
}
module Robot(Scale) {
STLscale = 100.0;
scale(Scale / STLscale)
scale([-1,1,10])
import("/mnt/bulkdata/Project Files/Thing-O-Matic/Pinkie/M2 Challenge/Afinia Robot.stl",
convexity=10);
}
//- Given a Shape(), return enlarged slab of given thickness
module EnlargeSlab(Scale, WallThick, SlabThick) {
intersection() {
translate([0,0,SlabThick/2])
cube([MaxSize,MaxSize,SlabThick],center=true);
minkowski(convexity=5) {
Shape(Scale);
cylinder(r=WallThick,h=MaxSize,$fn=16);
}
}
}
//- Put peg grid on build surface
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);
}
//- Build it
ShowPegGrid();
if (Layout == "Slab")
Shape(Size);
if (Layout == "Build")
difference() {
union() {
translate([0,0,(WallHeight + LipHeight - Protrusion)])
EnlargeSlab(Size,TipThick,TipHeight + Protrusion);
translate([0,0,(LipHeight - Protrusion)])
EnlargeSlab(Size,WallThick,(WallHeight + Protrusion));
EnlargeSlab(Size,LipThick,LipHeight);
}
Shape(Size); // punch out cookie hole
}