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.

Category: Software

General-purpose computers doing something specific

  • Hard Drive Platter Mood Light: Neopixel Firmware

    Having accumulated a pile of useless hard drives, it seemed reasonable to harvest the platters and turn them into techie mood lights (remember mood lights?). Some doodling showed that four of Adafruit’s high-density Neopixel strips could stand up inside the 25 mm central hole, completely eliminating the need to putz around with PWM drivers and RGB LEDs: one wire from an Arduino Pro Mini and you’re done:

    const byte PIN_NEO = 6;				// DO - data out to first Neopixel
    

    The firmware creates three sine waves with mutually prime periods, then updates the RGB channels with raised-sine values every 10 ms. The PdBase constant defines the common conversion from milliseconds to radians:

    const float PdBase = 0.05 * TWO_PI / 1000.0;	// scale time in ms to radians
    

    The leading 0.05 = 1/20 means the sine wave will repeat every 20 s = 20000 ms.

    Dividing that period by three small primes produces an RGB pattern that will repeat every 5x11x17 = 935 PdBase cycles = 18.7×103 s = 5.19 h:

    const float Period[] = {PdBase/5.0,PdBase/11.0,PdBase/17.0};		// mutually prime periods
    

    That’s languid enough for me, although I admit most of the colors look pretty much the same. Obviously, you can tune for best picture by dinking with a few constants.

    A Phase array sets the starting phase to 3π/2 = -90 degrees:

    float Phase[] = {3.0 * HALF_PI,3.0 * HALF_PI,3.0 * HALF_PI};		// sin(3π/2 ) = -1, so LEDs are off
    

    Jiggling those starting phases produces a randomized initial color that’s close to dark:

    	MillisNow = MillisThen = millis();
    	randomSeed(MillisNow + analogRead(6) + analogRead(7));
    	printf("Phases: ");
    	for (byte i=0; i<3; i++) {
    		Phase[i] += random(-1000,1000) * HALF_PI / 1000.0;
    		printf("%d ",(int)(Phase[i]*RAD_TO_DEG));
    	}
    	printf(" deg\r\n");
    

    With all that in hand, converting from time to color goes like this:

    uint32_t SineColor(unsigned long t) {
    byte rgb[3];
    
    	for (byte i=0; i<3; i++) {
    			rgb[i] = Intensity[i]/2.0 * (1 + sin(t * Period[i] + Phase[i]));
    	}
    	return strip.Color(rgb[0],rgb[1],rgb[2]);
    }
    

    The rest of the code scales neatly with the strip length defined in the magic instantiation:

    Adafruit_NeoPixel strip = Adafruit_NeoPixel(12, PIN_NEO, NEO_GRB + NEO_KHZ800);
    

    Although the colors change very slowly, shifting them all one chip toward the end of the 144 Neopixel strip at each update produces a noticeable difference that reassured me this whole mess was working:

    		for (int i=strip.numPixels()-1; i>0; i--) {
    			c = strip.getPixelColor(i-1);
    			strip.setPixelColor(i,c);
    		}
    
    		c = SineColor(MillisNow);
    		strip.setPixelColor(0,c);
    		strip.show();
    

    And with that in hand, It Just Worked…

    However, it’s worth noting that each Neopixel draws a bit over 60 mA at full white, which works out to a smidge under 9 A for a 144 LED strip. Because they’re PWM devices, the LEDs are either full-on or full-off, so the peak current can actually be 9 A, regardless of any reduced duty cycle to limit the intensity.

    The Adafruit driver includes an overall intensity control, but I added an Intensity array with separate values for each channel:

    float Intensity[] = {128.0,128.0,128.0};							// pseudo current limit - PWM is always full current
    

    That would allow throttling back the blue LEDs a bit to adjust the overall color temperature, but that’s definitely in the nature of fine tuning.

    The Adafruit Neopixel guide recommends a honkin’ big cap right at the strip, plus a 470 Ω decoupling resistor at the first chip’s data input. I think those attempt to tamp down the problems caused by underpowered supplies and crappy wiring; running it at half intensity produced a maximum average current just under the supply’s 3 A limit.

    The complete Arduino source code:

    // Neopixel mood lighting for hard drive platter sculpture
    // Ed Nisley - KE4ANU - November 2015
    
    #include <Adafruit_NeoPixel.h>
    
    //----------
    // Pin assignments
    
    const byte PIN_NEO = 6;				// DO - data out to first Neopixel
    
    const byte PIN_HEARTBEAT = 13;		// DO - Arduino LED
    
    //----------
    // Constants
    
    const int UPDATEMS = 10ul - 4ul;		// update LEDs only this many ms apart minus loop() overhead
    
    const float PdBase = 0.05 * TWO_PI / 1000.0;	// scale time in ms to radians
    
    const float Period[] = {PdBase/5.0,PdBase/11.0,PdBase/17.0};		// mutually prime periods
    float Phase[] = {3.0 * HALF_PI,3.0 * HALF_PI,3.0 * HALF_PI};		// sin(3π/2 ) = -1, so LEDs are off
    float Intensity[] = {128.0,128.0,128.0};							// pseudo current limit - PWM is always full current
    
    //----------
    // Globals
    
    unsigned long MillisNow;
    unsigned long MillisThen;
    
    Adafruit_NeoPixel strip = Adafruit_NeoPixel(12, PIN_NEO, NEO_GRB + NEO_KHZ800);
    
    uint32_t FullWhite = strip.Color(255,255,255);
    uint32_t FullOff = strip.Color(0,0,0);
    
    //--- figure color from time in ms
    
    uint32_t SineColor(unsigned long t) {
    byte rgb[3];
    
    	for (byte i=0; i<3; i++) {
    			rgb[i] = Intensity[i]/2.0 * (1 + sin(t * Period[i] + Phase[i]));
    	}
    	return strip.Color(rgb[0],rgb[1],rgb[2]);
    }
    
    //-- Helper routine for printf()
    
    int s_putc(char c, FILE *t) {
      Serial.write(c);
    }
    
    //------------------
    // Set the mood
    
    void setup() {
    	
    uint32_t c;
    
    	pinMode(PIN_HEARTBEAT,OUTPUT);
    	digitalWrite(PIN_HEARTBEAT,LOW);	// show we arrived
    
    	Serial.begin(57600);
    	fdevopen(&s_putc,0);				// set up serial output for printf()
    
    	printf("Mood Light with Neopixels\r\nEd Nisley - KE4ZNU - November 2015\r\n");
    	
    /// set up Neopixels
    	
    	strip.begin();
    	strip.show();
    	
    // lamp test: run a brilliant white dot along the length of the strip
    	
    	printf("Lamp test: walking white\r\n");
    	
    	strip.setPixelColor(0,FullWhite);
    	strip.show();
    	delay(500);
    	
    	for (int i=1; i<strip.numPixels(); i++) {
    		digitalWrite(PIN_HEARTBEAT,HIGH);
    		strip.setPixelColor(i-1,FullOff);
    		strip.setPixelColor(i,FullWhite);
    		strip.show();
    		digitalWrite(PIN_HEARTBEAT,LOW);
    		delay(500);
    	}
    	
    	MillisNow = MillisThen = millis();
    	randomSeed(MillisNow + analogRead(6) + analogRead(7));
    	printf("Phases: ");
    	for (byte i=0; i<3; i++) {
    		Phase[i] += random(-1000,1000) * HALF_PI / 1000.0;
    		printf("%d ",(int)(Phase[i]*RAD_TO_DEG));
    	}
    	printf(" deg\r\n");
    	
    	c = SineColor(MillisNow);
    	printf("Initial time: %08lx -> color: %08lx\r\n",MillisNow,c);
    	
    	for (int i=0; i<strip.numPixels()-1; i++) {
    		strip.setPixelColor(i,c);
    	}
    	
    	strip.show();
    	
    }
    
    //------------------
    // Run the mood
    
    void loop() {
    	
    byte r,g,b;
    uint32_t c;
    
    	MillisNow = millis();
    	if ((MillisNow - MillisThen) > UPDATEMS) {
    		digitalWrite(PIN_HEARTBEAT,HIGH);
    		
    		for (int i=strip.numPixels()-1; i>0; i--) {
    			c = strip.getPixelColor(i-1);
    			strip.setPixelColor(i,c);
    		}
    
    		c = SineColor(MillisNow);
    		strip.setPixelColor(0,c);
    		strip.show();
    
    		MillisThen = MillisNow;
    		digitalWrite(PIN_HEARTBEAT,LOW);
    	}
    }
    
    
  • Tecumseh 36638 Throttle Knob

    The upper-left tab broke off this “knob” shortly after we got the leaf shredder:

    Throttle knob - broken original
    Throttle knob – broken original

    But it worked well enough that, following my usual course of action, I could ignore the problem. Until a few days ago, that is, when the remaining tab on that end pulled out of the slot on the engine and the whole affair bent into uselessness.

    It’s a $10 item from eBay (with free shipping), $8 from Amazon ($4, not eligible for Prime, so plus $4 shipping), out of stock at my usual online small engine source, and not worth biking a few dozen miles here & there to see if anybody has one. I know better than to look for repair parts at Lowe’s / Home Depot. It’s Tecumseh Part 36638, which may come in handy some day.

    So, we begin…

    It’s one of those pesky injection-molded miracle plastic doodads that can’t be printed in one piece, so I designed the tabs as separate parts and glued them in place. The solid model shows the intended assembly, with a bit of clearance around the tabs for tolerance and glue slop:

    Tecumseh Throttle Knob - solid model - show view
    Tecumseh Throttle Knob – solid model – show view

    External clearances aren’t an issue, so I made the base plate longer, wider, and thicker, which gave the tabs something to grab onto. The half-round knob is bigger, more angular, and uglier than the OEM knob, because I had trouble holding onto the original while wearing work gloves.

    Printing a few extra tabs allows the inevitable finger fumble:

    Throttle knob - on platform
    Throttle knob – on platform

    The tabs stand on edge to properly orient the printed threads around the perimeter: a great force will try to rip that triangular feature right off the tab, so wrapping the thread as shown maximizes the strength. Laying them flat on their backs would put the force in shear, exactly parallel to thread-to-thread bonds; I wouldn’t bet on the strength of those layers.

    The brim provides enough platform footprint around the tabs to keep them upright, but obviously isn’t needed around the knob. Although you could wrap a modifier mesh around one or the other, trimming the brim off the knob with a precision scissors seemed more straightforward.

    Slobbering generous drops of of IPS #4 solvent adhesive into the slots and over the tabs softened the PETG enough that I could ram the tabs into place, using a big pliers to overcome their feeble resistance:

    Throttle knob - glued latches
    Throttle knob – glued latches

    With the plastic still dazed from the fumes, I force-fit the knob into the slot on the engine:

    Throttle knob - installed
    Throttle knob – installed

    The tabs eased back into position and seem to be holding the knob in place. Worst case: make a new knob, butter up the tabs with slow epoxy, ram knob into slot, then poke a screwdriver inside to realign the tabs against the slot edges.

    The solvent had a few cloudy days to evaporate before the next shredding session, whereupon the throttle once again worked exactly the way it should.

    The OpenSCAD source code:

    // Tecumseh 36638 Throttle Knob
    // Ed Nisley KE4ZNU November 2015
    
    Layout = "Build";					// Build Show Tab Base
    
    //- Extrusion parameters must match reality!
    
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    Protrusion = 0.1;			// make holes end cleanly
    
    inch = 25.4;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    //----------------------
    // Dimensions
    
    BaseSize = [40,14,3.0];							// overall base plate outside engine controller slot
    
    Knob = [18,BaseSize[1],17];
    
    TabSize = [7.5,1.6,6.0];						// ovarall length, minimum width, overall height
    TabSocket = [8.0,2.0,BaseSize[2] - 2*ThreadThick];				// recess in base plate for tab 
    
    TabOuterSpace = 30.0;							// end-to-end length over tabs - sets travel distance
    SlotWidth = 7.75;								// engine controller slot width
    SlotThick = 1.5;								// engine controller slot thickness
    
    TabShape = [
    	[0,0],
    	[BaseSize[2] + TabSize[2],0],
    	[BaseSize[2] + TabSize[2],ThreadWidth],
    	[BaseSize[2] + SlotThick,2*TabSize[1]],
    	[BaseSize[2] + SlotThick,TabSize[1]],
    	[0,TabSize[1]]
    ];
    
    CapBaseOpening = [11,7.5,15];			// opening in base plate, Z = clearance from controller plate
    
    //----------------------
    // 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);
    }
    
    //----------------------
    // Pieces
    
    module Tab() {
    	
    	linear_extrude(height=TabSize[0]) {
    		polygon(points=TabShape);
    	}
    }
    
    
    module Base() {
    	
    	CornerRad = BaseSize[1]/8;
    
    	difference() {
    		union() {
    			linear_extrude(height=BaseSize[2])
    				hull()
    					for (i=[-1,1], j=[-1,1]) 
    						translate([i*(BaseSize[0]/2- CornerRad),j*(BaseSize[1]/2 - CornerRad)])
    							circle(r=CornerRad,$fn=4*4);
    			translate([Knob[0]/2,0,BaseSize[2] - Protrusion])
    				rotate([0,-90,0])
    					linear_extrude(height=Knob[0])
    						hull() {
    							translate([Knob[2] - Knob[1]/2,0])
    								circle(d=Knob[1],$fn=8*4);
    							translate([0,-Knob[1]/2,0])
    								square([Protrusion,Knob[1]]);
    						}
    		}
    		
    		translate([-CapBaseOpening[0]/2,-CapBaseOpening[1]/2,-Protrusion])
    			cube(CapBaseOpening + [0,0,-CapBaseOpening[1]/2 + Protrusion],center=false);
    			
    		translate([0,0,CapBaseOpening[2] - CapBaseOpening[1]/2])
    			rotate([0,90,0]) rotate(180/8)
    				cylinder(d=CapBaseOpening[1]/cos(180/8),h=CapBaseOpening[0],center=true,$fn=8);
    				
    		for (i=[-1,1], j=[-1,1])
    			translate([i*(TabOuterSpace/2 - TabSocket[0]/2),j*(SlotWidth/2 - TabSocket[1]/2),TabSocket[2]/2 - Protrusion])
    				cube(TabSocket + [0,0,Protrusion],center=true);
    	}
    }
    
    
    //----------------------
    // Build it
    
    if (Layout == "Base")
    	Base();
    	
    if (Layout == "Tab")
    	Tab();
    	
    if (Layout == "Show") {
    	Base();
    	
    		for (i=[-1,1], j=[-1,1])
    			translate([i*(TabOuterSpace/2 - TabSocket[0]/2),j*(SlotWidth/2 - TabSocket[1]/2),0])
    				translate([j < 0 ? TabSize[0]/2 : -TabSize[0]/2,j < 0 ? TabSize[1]/2 : -TabSize[1]/2,BaseSize[2] - 2*ThreadThick])
    					rotate([0,90,j < 0 ? -180 : 0])
    					Tab();
    }
    
    if (Layout == "Build") {
    	Base();
    	
    	for (i=[0:5])					// build a few spares
    		translate([-7*TabSocket[1] + i*3*TabSocket[1],BaseSize[1],0])
    			rotate(90)
    				Tab();
    }
    

    The original doodle showing the OEM knob dimensions and some failed attempts at fancy features:

    Tecumseh Throttle Knob - doodles
    Tecumseh Throttle Knob – doodles
  • Disabling Windows 10 Upgrade Nagware

    If you’re running Windows, then you have more experience than I do, but it seems Microsoft, for reasons best known to it, really really really wants you to upgrade to Windows 10, has been forcing nagware onto every Windows box in existence, and actively working to defeat efforts to remove said nagware.

    Our Token Windows Box, an off-lease Dell Optiplex 780 that arrived bearing Windows 7 Professional, will never, ever get upgraded, because it’s running a bunch of ancient Windows programs that interface with specific bits of hardware, none of which (most likely) will work with Windows 10. In any event, I see no reason to go through the hassle of “upgrading” an old machine, (maybe) resolving all the inevitable compatibility problems, and (maybe) having no way to roll back the upgrade, all for a few programs run, at most, monthly.

    Continually declining Windows 10 upgrade prompts isn’t my idea of a Good User Experience, but I’m also tired of manually inspecting and killing updates that re-re-re-install the nagware.

    The GWX Control Panel (“GWX” = “Get Windows 10” in MS-speak) seems to be the least awful way of dealing with this mess. It’s not offered by Microsoft, for obvious reasons, but is offered free-of-charge.

    Just do it…

  • HP 7475A Plotter: One-Button Demo Madness

    Back in the day, you could install a Genuine HP 09872-60066 Digitizing Sight in your Genuine HP 7475A plotter, maneuver the sight to an interesting point on the paper, press the Enter button, send the point’s coordinates through the serial port to the computer, then do whatever you like with the numbers.

    Here in the future, I twiddled the demo code that draws Superformula patterns to send a digitization command and await the response at the end of each plot. I can then change the paper, press the Enter button, and get the next plot: exactly what I need for the upcoming Poughkeepsie Mini Maker Faire.

    The only gotcha turns out to be that, having hacked the Chiplotle interface to use hardware handshaking, there’s no way to tell when the outgoing buffer has drained. Until that happens, the plotter can’t respond to the digitizing command and, eventually, Chiplotle kvetches about not hearing anything.

    The least awful solution seems to be sleeping for 40 seconds (!) while the plotter trudges through the last line of the legend (!!), then continuing apace:

            print "Waiting for plotter... ignore timeout errors!"
            sleep(40)
            while NoneType is type(plt.status):
                sleep(5)
    
            print "Load more paper, then ..."
            print "  ... Press ENTER on the plotter to continue"
            plt.clear_digitizer()
            plt.digitize_point()
            
            plotstatus = plt.status
            while (NoneType is type(plotstatus)) or (0 == int(plotstatus) & 0x04):
                plotstatus = plt.status
                
            print "Digitized: " + str(plt.digitized_point)
    

    When the interface times out, Chiplotle doesn’t set the status code to anything in particular (which makes sense), so you can’t do anything useful with it. Therefore, the operand order in the last while statement matters: you can’t convert a value of type NoneType into anything else.

    The other change wraps the entire plotting loop with an always-and-forever loop: hit Ctrl-C to break out at the end of the day.

    You can’t change the new plot’s paper size, because the digitizing command preempts the Enter button that’s part of the Enter+Size combination. That makes perfect sense, even in retrospect.

    Testing that gave me the opportunity to run all the pens, refilled and OEM, through their paces:

    HP 7475A - Superformula demo
    HP 7475A – Superformula demo

    The Sakura pens in their adapters continue to work well:

    HP 7475A - Superformula - Sakura pens
    HP 7475A – Superformula – Sakura pens

    They’re such unique snowflakes…

    The complete Python source code:

    from chiplotle import *
    from math import *
    from datetime import *
    from time import *
    from types import *
    import random
    
    
    def superformula_polar(a, b, m, n1, n2, n3, phi):
        ''' Computes the position of the point on a
        superformula curve.
        Superformula has first been proposed by Johan Gielis
        and is a generalization of superellipse.
        see: http://en.wikipedia.org/wiki/Superformula
        Tweaked to return polar coordinates
        '''
    
        t1 = cos(m * phi / 4.0) / a
        t1 = abs(t1)
        t1 = pow(t1, n2)
    
        t2 = sin(m * phi / 4.0) / b
        t2 = abs(t2)
        t2 = pow(t2, n3)
    
        t3 = -1 / float(n1)
        r = pow(t1 + t2, t3)
        if abs(r) == 0:
            return (0, 0)
        else:
          #     return (r * cos(phi), r * sin(phi))
            return (r, phi)
    
    
    def supershape(width, height, m, n1, n2, n3,
                   point_count=10 * 1000, percentage=1.0, a=1.0, b=1.0, travel=None):
        '''Supershape, generated using the superformula first proposed 
        by Johan Gielis.
    
        - `points_count` is the total number of points to compute.
        - `travel` is the length of the outline drawn in radians. 
           3.1416 * 2 is a complete cycle.
        '''
        travel = travel or (10 * 2 * pi)
    
        # compute points...
        phis = [i * travel / point_count
                for i in range(1 + int(point_count * percentage))]
        points = [superformula_polar(a, b, m, n1, n2, n3, x) for x in phis]
    
        # scale and transpose...
        path = []
        for r, a in points:
            x = width * r * cos(a)
            y = height * r * sin(a)
            path.append(Coordinate(x, y))
    
        return Path(path)
    
    
    # RUN DEMO CODE
    
    if __name__ == '__main__':
    
        override = False
    
        plt = instantiate_plotters()[0]
    #   plt.write('IN;')
    
        if plt.margins.soft.width < 11000:               # A=10365 B=16640
            maxplotx = (plt.margins.soft.width / 2) - 100
            maxploty = (plt.margins.soft.height / 2) - 150
            legendx = maxplotx - 2600
            legendy = -(maxploty - 650)
            tscale = 0.45
            numpens = 4
            # prime/10 = number of spikes
            m_values = [n / 10.0 for n in [11, 13, 17, 19, 23]]
            # ring-ness 0.1 to 2.0, higher is larger
            n1_values = [
                n / 100.0 for n in range(55, 75, 2) + range(80, 120, 5) + range(120, 200, 10)]
        else:
            maxplotx = plt.margins.soft.width / 2
            maxploty = plt.margins.soft.height / 2
            legendx = maxplotx - 3000
            legendy = -(maxploty - 700)
            tscale = 0.45
            numpens = 6
            m_values = [n / 10.0 for n in [11, 13, 17, 19, 23, 29, 31,
                                           37, 41, 43, 47, 53, 59]]   # prime/10 = number of spikes
            # ring-ness 0.1 to 2.0, higher is larger
            n1_values = [
                n / 100.0 for n in range(15, 75, 2) + range(80, 120, 5) + range(120, 200, 10)]
    
        print "   Max: ({},{})".format(maxplotx, maxploty)
    
        # spiky-ness 0.1 to 2.0, lower is spiky-er
        n2_values = [
            n / 100.0 for n in range(10, 60, 2) + range(65, 100, 5) + range(110, 200, 10)]
    
        plt.write(chr(27) + '.H200:')   # set hardware handshake block size
        plt.set_origin_center()
        # scale based on B size characters
        plt.write(hpgl.SI(tscale * 0.285, tscale * 0.375))
        # slow speed for those abrupt spikes
        plt.write(hpgl.VS(10))
        
        while True:
            
            # standard loadout has pen 1 = fine black
            plt.write(hpgl.PA([(legendx, legendy)]))
            plt.select_pen(1)
            plt.write(hpgl.LB("Started " + str(datetime.today())))
    
            if override:
                m = 4.1
                n1_list = [1.15, 0.90, 0.25, 0.59, 0.51, 0.23]
                n2_list = [0.70, 0.58, 0.32, 0.28, 0.56, 0.26]
            else:
                m = random.choice(m_values)
                n1_list = random.sample(n1_values, numpens)
                n2_list = random.sample(n2_values, numpens)
    
            pen = 1
            for n1, n2 in zip(n1_list, n2_list):
                n3 = n2
                print "{0} - m: {1:.1f}, n1: {2:.2f}, n2=n3: {3:.2f}".format(pen, m, n1, n2)
                plt.select_pen(pen)
                plt.write(hpgl.PA([(legendx, legendy - 100 * pen)]))
                plt.write(
                    hpgl.LB("Pen {0}: m={1:.1f} n1={2:.2f} n2=n3={3:.2f}".format(pen, m, n1, n2)))
                e = supershape(maxplotx, maxploty, m, n1, n2, n3)
                plt.write(e)
                pen = pen + 1 if (pen % numpens) else 1
    
            plt.select_pen(1)
            plt.write(hpgl.PA([(legendx, legendy - 100 * (numpens + 1))]))
            plt.write(hpgl.LB("Ended   " + str(datetime.today())))
            plt.select_pen(0)
            plt.write(hpgl.PA([(-maxplotx,maxploty)]))
            
            print "Waiting for plotter... ignore timeout errors!"
            sleep(40)
            while NoneType is type(plt.status):
                sleep(5)
    
            print "Load more paper, then ..."
            print "  ... Press ENTER on the plotter to continue"
            plt.clear_digitizer()
            plt.digitize_point()
            
            plotstatus = plt.status
            while (NoneType is type(plotstatus)) or (0 == int(plotstatus) & 0x04):
                plotstatus = plt.status
                
            print "Digitized: " + str(plt.digitized_point)
    
  • LED Ring Desk Lamp

    A defunct desk lamp emerged from the clutter and cried out for bright, new LEDs. This adapter puts a small LED ring and nine white LEDs on the original lamp head:

    Ring Light Mount - in operation
    Ring Light Mount – in operation

    Peering into the business end, before mounting it on the lamp, shows some abrasive adjustment on the inside layer:

    Ring Light Mount - LEDs installed
    Ring Light Mount – LEDs installed

    That layer printed over a quick-and-easy support spider:

    Ring Light Mount - solid model - bottom
    Ring Light Mount – solid model – bottom

    The Slic3r preview looking down through the layer just over the support shows that the perimeter of those LED holes doesn’t have much support:

    Ring Light Mount - Slic3r preview - bridge layer
    Ring Light Mount – Slic3r preview – bridge layer

    The obvious threads drooped in the predictable way, so I just clipped them off, sanded the high spots into submission, and epoxied everything in place:

    Ring Light Mount - LED wiring
    Ring Light Mount – LED wiring

    That nice Hilbert Curve infill is completely wasted inside the OEM shade, but the smooth curve around the rim had to be on the top surface.

    Rather than beefing up the support, you should print the bottom ring (or the top rim) separately, then glue it back on, but I wanted to see how well simple support worked with PETG.

    It came out reasonably well:

    Ring Light Mount - support spider
    Ring Light Mount – support spider

    That’s far more hair than usual, even for PETG, because I made the spider’s legs exactly three thread widths wide. Slic3r reduced the single infill thread to, literally, a hair that didn’t stick to the platform; the model now has four-thread-wide legs.

    Slic3r’s automatic support would do a better job of holding up the underside, albeit with more plastic and printing time:

    Ring Light Mount - Slic3r preview - auto support
    Ring Light Mount – Slic3r preview – auto support

    The top view looks about like you’d expect:

    Ring Light Mount - solid model - top
    Ring Light Mount – solid model – top

    Those two solid models show the small hole for the LED ring wiring, which I drilled into the as-printed plastic. The original layout included just the LED ring, with the wire through a big central hole, but then I realized the wall wart had enough moxie for a few more LEDs. So it goes.

    Anyhow, the lamp provides just enough illumination below my big monitors to suffice. The gooseneck might not be quite long enough, but that’ll be another project…

    The OpenSCAD source code:

    // LED Ring Light Mount
    // Ed Nisley KE4ZNU October 2015
    
    DoSupport = true;
    
    //- Extrusion parameters must match reality!
    
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    Protrusion = 0.1;			// make holes end cleanly
    
    inch = 25.4;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    //----------------------
    // Dimensions
    
    NumSides = 8*4;						// number of sides on each "cylinder"
    
    LENGTH = 0;
    ID = 1;
    OD = 2;
    
    Shade = [6.0,45.2,47.5];			// threaded end of OEM lamp shade
    RingLED = [4.5,36.0,51.0];
    
    SpotLED = [2.0,0,5.0];				// discrete LEDs in center
    NumSpots = 8;						// discrete LEDs around the one in the middle
    
    Support = [(RingLED[LENGTH] - 1*ThreadThick),0,(RingLED[OD] - 4*ThreadWidth)];
    NumSupports = NumSides/2;
    
    ThreadBase = RingLED[LENGTH] + SpotLED[LENGTH];
    OAHeight = ThreadBase + Shade[LENGTH];
    
    //----------------------
    // 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);
    }
    
    //----------------------
    // Build it
    
    	difference() {
    		union() {																				// overall shape
    			translate([0,0,ThreadBase])
    				rotate_extrude(convexity = 2, $fn=NumSides)
    					translate([Shade[OD]/2,0])
    						circle(r=Shade[LENGTH],$fn=NumSides);
    			cylinder(d=(Shade[OD] + 2*Shade[LENGTH]),h=ThreadBase,$fn=NumSides);
    			translate([0,0,ThreadBase])
    				cylinder(d=Shade[OD],h=Shade[LENGTH],$fn=NumSides);
    		}
    		
    		translate([0,0,ThreadBase - Protrusion])
    			cylinder(d=(Shade[ID] + HoleWindage),h=(Shade[LENGTH] + 2*Protrusion),$fn=NumSides);	// opening for shade thread
    			
    		translate([0,0,-Protrusion])
    			cylinder(d=(RingLED[OD] + HoleWindage),h=(RingLED[LENGTH] + Protrusion),$fn=NumSides);	// opening for LED ring
    			
    		rotate(180/NumSides)																		// LED ring power wire
    			translate([RingLED[ID]/2,0,0])
    				rotate(180/6)
    					PolyCyl(2.5,OAHeight,6);
    			
    		rotate(180/8  - 180/NumSides)
    			PolyCyl(SpotLED[OD],OAHeight,8);														// central LED SpotLED
    			
    		for (i=[0:NumSpots-1])																		// surrounding spots
    			rotate(i*360/NumSpots - 180/NumSides)
    				translate([(RingLED[ID] - 2*SpotLED[OD])/2,0,0])
    						rotate(180/8)
    							PolyCyl(SpotLED[OD],OAHeight,8);
    	}
    	
    //-- Support structure
    
    	if (DoSupport)
    		color("Yellow")
    		rotate(180/NumSides)													// align bars to flat internal faces
    			for (i=[0:NumSupports/2 - 1]) {
    				rotate(i * 360 / NumSupports)
    					translate([0,0,Support[LENGTH]/2])
    						cube([Support[OD],4*ThreadWidth,Support[LENGTH]],center=true);
    			}
    
    
  • USB-to-SATA Drive Adapter Performance

    The discussion about scrubbing hard drives suggested I really should be using larger block sizes to wring better performance from the hardware.

    So I ran variations on this theme:

    time sudo dd if=/dev/urandom of=/dev/sdc bs=4K count=32K
    

    For the BS (“block size”) parameter, 1K = 1024 and 1KB = 1000. Similarly for 1M vs. 1MB.

    The results, viewed as a picture because WordPress seems unable to import a formatted spreadsheet from LibreOffice like it used to:

    USB-SATA Adapter - Barracuda 7200.10 drive
    USB-SATA Adapter – Barracuda 7200.10 drive

    Each operation transfers 128 MB (128 x 220 = 131 x 106) bytes. The variations probably come from other stuff going on, most notably the USB-to-serial adapter driving the plotter while I’m testing a tweak to the Superformula demo code.

    Reads ever so much faster than writes, so the USB adapter definitely isn’t getting in the way; I assume the drive accepts the commands & data as fast as its little heads can carry them away. The data, being relentlessly pseudo-random, won’t get compressed along the way.

    So, in round numbers, the block size just absolutely does not make any difference.

    Update: Based on an early comment from Edward Berner to a previous post, I was looking in the wrong place:

    dd if=/dev/urandom of=/dev/zero bs=4K count=32K
    32768+0 records in
    32768+0 records out
    134217728 bytes (134 MB) copied, 9.63064 s, 13.9 MB/s
    dd if=/dev/urandom of=test.bin bs=4K count=32K
    32768+0 records in
    32768+0 records out
    134217728 bytes (134 MB) copied, 10.018 s, 13.4 MB/s
    dd if=test.bin of=/dev/zero bs=4K count=32K
    32768+0 records in
    32768+0 records out
    134217728 bytes (134 MB) copied, 0.0385358 s, 3.5 GB/s
    dd if=test.bin of=test2.bin bs=4K count=32K
    32768+0 records in
    32768+0 records out
    134217728 bytes (134 MB) copied, 0.45044 s, 298 MB/s
    

    I installed an SSD on this box a while ago, so the 3.5 GB/s disk-to-discard speed represents the SSD’s read rate. The 298 MB/s disk-to-disk speed would be its write speed, probably with some clever buffering going on.

    So the real bandwidth limitation in wiping a disk comes from the pseudo-random generator behind /dev/urandom, not the disk or USB interface. It would probably be faster to fill a 1 GB (or more) file with noise at 14 MB/s, then copy it enough times to fill the drive at whatever speed the drive can handle it.

    Thanks, Edward, for figuring that out!

  • SATA vs. USB Hard Drive Write Speeds

    The stack of drives-to-be-scrubbed disgorged a pair of SATA drives, so I plugged one of them into an internal SATA port and unleashed dd on it:

    time sudo dd if=/dev/urandom of=/dev/sdb bs=4096 count=10000
    10000+0 records in
    10000+0 records out
    40960000 bytes (41 MB) copied, 4.19793 s, 9.8 MB/s
    
    real	0m4.208s
    user	0m0.004s
    sys	0m2.880s
    
    time sudo dd if=/dev/urandom of=/dev/sdb bs=1024 count=40000
    40000+0 records in
    40000+0 records out
    40960000 bytes (41 MB) copied, 7.38392 s, 5.5 MB/s
    
    real	0m7.394s
    user	0m0.004s
    sys	0m3.505s
    
    time sudo dd if=/dev/urandom of=/dev/sdb bs=16384 count=2500
    2500+0 records in
    2500+0 records out
    40960000 bytes (41 MB) copied, 4.2042 s, 9.7 MB/s
    
    real	0m4.214s
    user	0m0.000s
    sys	0m2.880s
    

    The timing for a few (tens of) thousand blocks comes out somewhat below the long-term average, which was the same 12 to 14 MB/s produced by the USB 2.0 adapter. It just doesn’t get any better than that, mostly due to rotational delays.

    In round numbers, that’s 40 to 50 GB/h, so it’s best to start scrubbing those terabyte drives early in the day…