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.

Author: Ed

  • Contact Bounce: Why Capacitors Don’t Fix It

    As part of our discussion around those Hall effect switches, I cautioned our Larval Engineer that she can’t use capacitors to “smooth out” mechanical switch bounce, even though all of her cronies and (most likely) her profs will advocate doing exactly that. The subject also came up at the local hackerspace when she showed off her project, so I should explain why capacitors don’t solve the problem.

    Here’s some switch contact bounce:

    Switch bounce - black panel-mount
    Switch bounce – black panel-mount

    Another push on the button, just to show how unpredictable the bounces can be:

    Switch bounce - black panel-mount - 2
    Switch bounce – black panel-mount – 2

    Note the horizontal scale: 10 ms/div. The smaller glitches appear only by courtesy of the scope’s glitch-catching mode; they’re down around a few microseconds.

    Now, let’s add the canonical 100 nF “debounce” capacitor in parallel with the contacts and record another set of bounces:

    Switch bounce - black panel-mount - 100 nF cap
    Switch bounce – black panel-mount – 100 nF cap

    Notice that the switch contacts bounce in a completely unpredictable manner.

    The pullup resistor is a rather stiff 1 kΩ, so the RC time constant is τ = 1 kΩ × 100 nF = 100 μs, but that applies only to the rising edges of the waveform as the switch opens. You can, indeed, see a slight rounding of those corners: the voltage requires about 5τ = 500 μs to reach 99% of the final voltage.

    The capacitor also forms an LC tank circuit with the usual parasitic wiring inductance, producing spikes that exceed the supply voltage: that’s the first half-cycle of the tank oscillation as the switch opens. The Q is fairly low due to the relatively high resistance, so the oscillations die out quickly. If this were feeding a microcontroller’s input pin, its input protection diodes would clamp the spikes to one diode drop above the supply voltage and below 0 V, but that’s an entirely different study.

    It should be obvious that adding the cap hasn’t done diddly squat to debounce the switch transition.

    Increasing the pullup resistor to the usual 10 kΩ will increase the time constant to τ = 1 ms, round off the leading edges a bit more, and further reduce the Q. It won’t debounce the longest transitions, which are on the order of 20 ms for this particular switch. You can’t increase the pullup too much, because you want enough current through it to ensure a valid logic level despite external noise (which is also an entirely different study); 100 kΩ may be as much as you can stand.

    But that’s just for glitches due to the switch opening. The closed switch puts a dead short across the capacitor, so the cap provides no filtering as the switch closes: the microcontroller will see every single low-going bounce. The photos show only bounces during the open→closed switch transition, but the closed→open transition can be equally ugly: yes, switches bounce closed as they open.

    That means the microcontroller will see glitches as the switch opens.

    So let’s increase the capacitor enough that the voltage can’t rise beyond the logic threshold until the switch stops bouncing. Ignoring LC tank effects, the voltage rises as 1 – e-t/τ, so we need that value to be less than 0.25 (for a bit of margin) of the supply voltage after the longest possible bounce as the switch opens. Let’s assume the switch has a single closed (low) glitch after a long time being open (high), at which time the voltage must still be under the logic threshold to prevent a false input. The datasheets only give the maximum bounce duration, if they give any bounce time at all, so let’s assume the longest bounce will be 60 ms.

    That says τ = -60 ms / ln(0.75) = 210 ms. Given a 10 kΩ pullup, that’s C = 210 ms / 10 Ω = 21 μF.

    No problem, right? Let’s just put a 22 μF electrolytic cap across every switch and be done with it!

    Well, except for the fact that most pushbutton switches can’t tolerate that much energy through their contacts. Assuming a 100 mΩ resistance and ignoring stray inductance, the initial current will be 5 V / 100 mΩ = 50 A with a time constant of τ = 22 μF × 100 mΩ = 2 μs. At the usual 5 V logic supply, the cap stores 22 μF × (5 V)2 = 550 μJ of energy, so we’re now burning the switch contacts with a 250 W pulse. Some switches have a maximum energy rating to deter exactly this design blunder, but you should not assume the lack of such a rating means the switch can handle anything you throw at it.

    No problem, let’s just put a resistor in series with the switch to reduce the initial current.

    I think you can see where this is going, though, so I’ll leave all that as an exercise for the student.

    Moral of the story: you must do debouncing in software by filtering the raw switch input. The trick will be to get that code right, which isn’t nearly as simple as you might think. In fact, the first half-dozen techniques you come up with won’t work, so use a dependable library and test the results… which is an entirely different study, too.

    If it’s any consolation, I didn’t know this stuff when I was a Larval Engineer, either. In fact, I didn’t learn much of it until after I made all the usual mistakes…

  • LED Forward Voltages

    Thinking about those batteries in the context of a really big LED tail light for a bike leads to wondering about the variation in LED forward voltages; it’s possible to drive LEDs in parallel if they’re well-matched for forward voltage. A quick-and-dirty test is in order to get some first-pass numbers… and I have bags of nominally identical red and amber LEDs.

    Applying a fixed voltage that produces 20 mA through 14 randomly chosen LEDs of each color, then measuring the voltage across each diode:

    LED Red V Amber V
    1 1.895 1.939
    2 1.893 1.921
    3 1.903 1.918
    4 1.895 1.921
    5 1.891 1.918
    6 1.935 1.906
    7 1.891 1.926
    8 1.904 1.930
    9 1.901 1.923
    10 1.894 1.927
    11 1.901 1.914
    12 1.894 1.939
    13 1.901 1.933
    14 1.903 1.925
    Minimum 1.891 1.906
    Average 1.900 1.924
    Maximum 1.935 1.939

    Pushing 20 mA through the five lowest voltage red LEDs requires 9.54 V. Applying that voltage to the five highest red LEDs produces 18.2 mA.

    Putting those two strings-of-five in parallel with 9.52 V produces 40 mA total: 16.6 mA in the low string and 19.9 mA in the high string, all measured with a fancy Tek Hall effect probe. No, those aren’t reversed and, yes, I did check twice: it makes no sense at all.

    Temperature matters a lot in such measurements and I wasn’t controlling for that, plus I didn’t have a constant-current supply. Better numbers await better instrumentation, but I think binning a couple bags of 100 LEDs based on forward current should be straightforward.

  • Wouxun KG-UV3D Battery Capacity

    After I get the next GPS+voice interface running on the (yet-to-be-bought) Wouxun KG-UV3D radio, a pair of reasonably new 1A17KG-3 7.4 V 1.7 A·h lithium battery packs will be floating around with nothing to do; the GPS interface connects an external battery to the radio, so there’s no need for the OEM battery.

    Before doing anything else, it’d be useful to know the actual capacity. The pack has flush terminals, so I snipped off two lengths of shield braid, jammed a wire into each one, and taped them in place:

    Battery pack - braid contacts
    Battery pack – braid contacts

    That obviously wasn’t going to last, so I added some closed-cell foam:

    Battery pack - foam compression
    Battery pack – foam compression

    And then, ever so gently, crunched a clamp around the whole mess:

    Battery Pack - clamped contacts
    Battery Pack – clamped contacts

    Crude, but workable, although the ragged start to the test showed I was too gentle. Another click of the clamp and everything settled down just fine:

    Wouxun Pack
    Wouxun Pack

    In round numbers, the pack delivers 1.6 A·h down to 7.0 V and then falls off very rapidly to the 6.0 V that ended the test.

    A string of three red / amber LEDs adds up to 3×1.9 = 5.7 V. A dumb DC blinky light running from 7.4 V has 77% efficiency, which isn’t all that bad, and 70% at the start. A current-regulating switcher might give 85% to 90% at the cost of considerable circuit complexity and wouldn’t be feasible for four independent blinky channels.

    The starting voltage, fresh from the charger, is just shy of 8.5 V, which is why I figured I could get away with 9 V from the external pack through the GPS interface. So far, so good.

    Obviously, if those packs are to be useful, I must conjure up a better battery holder. Having already designed a battery-shaped case for the GPS interface, it should be easy enough to build a radio-shaped mount for the pack.

  • Sherline Leadscrew Nut Removal

    The Y axis on my Sherline CNC mill has developed about 8 mils of backlash, a bit more than seems reasonable. Some poking around shows that the anti-backlash nut is loose while in the middle of the leadscrew and snug while at either end, which suggests the leadscrew thread is also worn. That’s no surprise, as I didn’t figure out that having a bellows over the leadscrew was a Good Thing until, let us say, considerably later than I should.

    If I must replace the leadscrew, I may as well take the whole XY assembly apart, clean everything, and replace the consumables. So I ordered a sack o’ parts from Sherline; they’re all cheap and readily available. The overall index has the exploded diagrams and the parts list for my mill boiled down to:

    • 54161 Y axis leadscrew (9 inch)
    • 50140 Y axis anti-backlash nut
    • 50200 Y axis nut
    • 50171 X axis leadscrew
    • 50130 X axis anti-backlash nut
    • 40890 X axis nut
    • 50150 anti-backlash lock

    The only gotcha: nowhere (that I can find, anyway) is it written how to get the leadscrew nuts out of the stage. It turns out that the holes through the stage aren’t uniform: the X narrower on the right and the Y on the front, so you must drive the X axis nut out to the left and the Y axis nut out to the rear. The counterbore is visible just behind the anti-backlash nut if you know what to look for, so you’re driving the axis nut away from the backlash nut.

    On the X axis:

    X axis leadscrew hole counterbore
    X axis leadscrew hole counterbore

    On the Y axis:

    Y axis leadscrew hole counterbore
    Y axis leadscrew hole counterbore

    Trust me on this: you cannot drive a 5/16 inch nut through a 19/64 inch counterbore. If you have a 19/64 inch transfer punch, that’s a dandy way to get the nuts out.

    The easiest way to loosen the socket head cap screw holding the flex coupling to the leadscrew is to grab the coupling in a lathe chuck (with the leadscrew protruding into the headstock) and then apply the hex key:

    Loosening leadscrew bolt
    Loosening leadscrew bolt

    They used red (high-strength) Loctite on all the leadscrew bolts, as well as on the tapered joint between the leadscrew and the flex coupling, and on the bearing preload nut… so I will, too.

  • Wouxun KG-UV3D GPS+Voice Interface: Improved Case

    This case has a few refinements beyond that one, but it’s recognizably a descendant. The main changes:

    • The HT cable port on the side has a nice polygonal roof to reduce overhang
    • The serial connector sits in a recess to allow a thicker top plate
    • Smaller opening for the LEDs; I’ll get a window in this one, fer shure, yeah
    • 4-40 screws hold the base plate on; setscrews may work and look better

    Looks like I’ll be using blue filament for this version, having just discovered the last of the weird colors in the bottom of the 5 gallon bucket serving as a storage bin.

    A view from the top:

    Solid Model - Oblique Exploded Top
    Solid Model – Oblique Exploded Top

    And from the base:

    Solid Model - Oblique Exploded Base
    Solid Model – Oblique Exploded Base

    The OpenSCAD source code:

    // Wouxun KB-UV3D Battery Pack Case
    // Ed Nisley KE4ZNU July 2012
    
    include </home/ed/Thing-O-Matic/lib/MCAD/units.scad>
    include </home/ed/Thing-O-Matic/Useful Sizes.scad>
    
    // Layout options
    
    Layout = "Show";
    					// Overall layout: Fit Show
    					// Printing plates: Build1 .. Buildn (see bottom!)
    					// Parts: TT3 Audio DSub Shell Base Top
    					// Shapes: RadioBase Contact
    					// Speaker-mic mount: PlugPlate
    
    ShowGap = 10;		// spacing between parts in Show layout
    
    //- Extrusion parameters must match reality!
    //  Print with +1 shells and 3 solid layers
    
    ThreadThick = 0.25;
    ThreadWidth = 2.0 * ThreadThick;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    //----------------------
    // Dimensions
    
    CaseOverallHeight = 31.5;				// from battery surface, must clear PCBs!
    CaseOverallWidth = 56;
    CaseOverallLength = 80.25;				// inside of base to end of compartment
    
    BatteryClearance = 1.5;					// contact seal height = air gap to compartment
    
    // Interface to radio battery contacts
    //	Length = shell length
    //		calculated after everything else, so as to fill the compartment
    
    ContactDia = 6.0;				// use rounded contact for simplicity
    ContactRecess = IntegerMultiple(0.75,ThreadThick);	// recess for contact plate
    ContactGapX = 10.5;				// X space between contacts
    Contact1Y = 52.5;				// offset from base to edge of contact
    Contact2Y = 56.5;
    ContactStudDia = Clear4_40;
    ContactStudHead = IntegerMultiple(Head4_40,ThreadWidth);
    ContactStudHeadThick = Head4_40Thick;
    
    PlateWidthMin = 53.0;
    PlateWidthMax = 54.5;
    PlateThick = IntegerMultiple(ContactRecess + ContactStudHeadThick,ThreadThick);
    PlateAngle = atan(PlateThick/(PlateWidthMax/2 - PlateWidthMin/2));
    
    echo("Battery plate thick: ",PlateThick);
    
    // Offsets from battery surface to PCB centerlines
    //	TT3 must be above HT back shell for DB9 clearance
    //	These must cooperate with the numbers in the case shell module
    
    TT3Offset = 17.5 + PlateThick;
    AudioOffset = 4.0 + PlateThick;
    
    // Plate interface to base alignment holes and notches
    
    BaseWidthInner = PlateWidthMin;
    BaseWidthOuter = CaseOverallWidth;
    BaseLength = CaseOverallHeight;					// perpendicular to battery surface
    BaseThick = IntegerMultiple(1.0,ThreadThick);	// minimum sheet thickness below teeth
    BaseWidthTaper = 5.0;							// ramp across entire width
    
    BaseOpeningMax = 43.0;
    BaseOpeningMin = 33.0;
    BaseOpeningY = 5.3;
    BaseOpeningDepth = IntegerMultiple(2.25,ThreadThick);
    
    BaseTotalThick = BaseThick + BaseOpeningDepth;
    echo("Base min thick: ",BaseThick," total: " ,BaseTotalThick);
    
    BaseTabWidth = 6.0;
    BaseTabThick = 2.0;
    BaseTabGap = 7.0;
    BaseTabOC = BaseTabWidth + BaseTabGap;
    BaseToothSection = 3*BaseTabWidth + 2*BaseTabGap;
    
    BaseToothBase = 5.8;
    BaseToothTip = 2.8;
    BaseToothThick = 2.0;
    BaseToothAngle = atan(BaseOpeningDepth/0.6);
    BaseToothOC = BaseTabOC;
    
    WedgeAngle = atan(BaseWidthTaper/((BaseWidthOuter - BaseWidthInner)/2));
    
    BaseEndLip = ThreadThick;			// should be 0.25 mm or so
    BaseEndWidth = (PlateWidthMin - 3*BaseToothBase - 2*BaseToothTip)/2;
    BaseEndAngle = atan((BaseOpeningDepth - BaseEndLip)/BaseOpeningY);
    
    SwitchBody = [8.6,3.7,3.3];			// mode switch
    
    // Plate interface to HT battery latch, cables, and connectors
    
    TopThick = IntegerMultiple(5.5,ThreadThick);	// plate thickness for stiffness behind latch bar
    echo("Top plate thick: ",TopThick);
    
    DB9Recess = TopThick - 4.0;			// recess to max TT3 PCB clearance behind DB9 plate
    
    TabEngageLength = 1.6;				// tab engaging surface length
    TabWidth = 3.0;						//  ... width
    TabEngageHeight = 4.5;				//  ... above battery compartment floor
    TabHeight = 7.5;					// tab ramp top above battery compartment floor
    TabOC = 40.0;
    
    LatchBarWidth = 3.4;				// sliding latch mechanism (brass L stock)
    LatchBarDepth = 3.4;
    LatchBarThick = 0.35;
    
    echo(" ... minimum: ",TopThick - LatchBarDepth);
    
    SplitOffset = TT3Offset - 3.5;
    
    TopBevel = 1.0;						// bevel at top of battery compartment
    TopBevelAngle = 45;
    
    PinOffsetWidth = 2.5;				// choose to center in sides of case shell
    PinOffsetHeight = 13.5;				// above baseplate bottom
    PinDepth = 7.0;						// into case shell
    PinDia = 1.2;
    
    ShellLength = CaseOverallLength - BaseThick - TopThick;
    
    echo("Shell length: ",ShellLength);
    
    // Speaker-mic plug plate
    
    PlugBaseThick = 2.5;				// recess depth
    PlugFillThick = 3.0;				// outer plate thickness
    
    //----------------------
    // 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);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
    Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-------------------
    // Component parts
    
    //-----
    // TinyTrak3+ PCB and component envelope
    //	Some dimensions should feed into the case shell, but don't
    
    module TinyTrak3(Length = 1.0) {
    
    PCBThick = 1.6;
    PCBWide = 36.5;
    TopHigh = 9.5;
    TopWide = PCBWide - 1.5;
    BotHigh = 2.5;
    BotWide = 35.0;
    
    PCBx = PCBWide/2;
    PCBy = (PCBThick + HoleWindage)/2;
    URx = TopWide/2;
    URy = PCBy + TopHigh;
    LRx = BotWide/2;
    LRy = PCBy + BotHigh;
    
    linear_extrude(height=Length,center=false,convexity=2) {
    	polygon(points=[[URx,URy],[URx,PCBy],[PCBx,PCBy],[PCBx,-PCBy],[LRx,-PCBy],[LRx,-LRy],
    					[-LRx,-LRy],[-LRx,-PCBy],[-PCBx,-PCBy],[-PCBx,PCBy],[-URx,PCBy],[-URx,URy]
    				   ]);
    }
    }
    
    //-----
    // Interface PCB and component envelope
    //	Some dimensions should feed into the case shell, but don't
    
    module AudioInterface(Length = 1.0) {
    
    PCBThick = 2.0;
    PCBWide = 49.5;
    TopHigh = 9.0 + Protrusion;
    TopWide = 46.0;
    BotHigh = 3.0;
    BotWide = 44.0;
    
    PCBx = PCBWide/2;
    PCBy = (PCBThick + HoleWindage)/2;
    URx = TopWide/2;
    URy = PCBy + TopHigh;
    LRx = BotWide/2;
    LRy = PCBy + BotHigh;
    
    linear_extrude(height=Length,center=false,convexity=2) {
    	polygon(points=[[URx,URy],[URx,PCBy],[PCBx,PCBy],[PCBx,-PCBy],[LRx,-PCBy],[LRx,-LRy],
    					[-LRx,-LRy],[-LRx,-PCBy],[-PCBx,-PCBy],[-PCBx,PCBy],[-URx,PCBy],[-URx,URy]
    				   ]);
    }
    }
    
    //-----
    // DB-9 (DE-9) panel opening
    // http://www.interfacebus.com/Connector_D-Sub_Mechanical_Dimensions.html
    //  DB-9 shell mounts on outside surface of case
    // This is for the solder terminal side
    
    module DSubMin9(Length = 1.0) {
    
    Holex = 0.984/2 * inch;
    HoleDia = Tap4_40;
    
    URx = 0.769/2 * inch;
    URy = 0.432/2 * inch;
    
    	linear_extrude(height=Length,center=false,convexity=3) {
    	  polygon(points=[[URx,URy],[URx,-URy],[-URx,-URy],[-URx,URy]]);
    	  for (x = [-1,1]) {
    		translate([x*Holex,0,0])
    		  rotate(45) circle(r=(HoleDia + HoleWindage)/2,$fn=4);
    	  }
    	}
    
    }
    
    //-----
    // Central case shape
    //	This *should* depend directly on the circuit board sizes, but doesn't
    //	The "Offset" parameters attempt to bottle up all the board sizes
    //	Support in LED window must be hand-fit to work correctly... and isn't needed!
    
    module CaseShell(Length=(ShellLength),Holes="true") {
    
    // Polygon coordinates are in XY plane
    
    URx = 40.0/2;
    URy = CaseOverallHeight;
    
    MRx = CaseOverallWidth/2;
    MRy = 15.0;
    
    LRx = CaseOverallWidth/2;
    LRy = (LRx - PlateWidthMin/2)*tan(PlateAngle);
    
    BRx = PlateWidthMax/2;
    BRy = PlateThick - 0*Protrusion;
    
    PRx = PlateWidthMin/2;				// combined battery plate
    PRy = 0;
    
    ScrewOffset = 20.0;					// from top end of case
    
    LEDWindow = [26.0,5.0,6];			// with case aligned vertically
    LEDOffset = [15,URy,(Length + TopThick - 25.0)];
    
    TrimPot1 = [-14,TT3Offset,(Length + TopThick - 30)];
    TrimPot2 = [-14,TT3Offset,(Length + TopThick - 37.5)];
    
    HTCableDia = 3.5;
    HTCableOffset = AudioOffset + HTCableDia/2 + 1.0;
    
    rotate([90,0,180])
    	union() {
    	  difference() {
    
    		  linear_extrude(height=Length,center=false,convexity=5)
    			polygon(points=[[URx,URy],[MRx,MRy],[LRx,LRy],[BRx,BRy],[PRx,PRy],
    							[-PRx,PRy],[-BRx,BRy],[-LRx,LRy],[-MRx,MRy],[-URx,URy]]);
    
    		if (Holes) {
    		  translate([0,AudioOffset,-Protrusion])
    			AudioInterface(Length + 2*Protrusion);
    
    		  translate([0,TT3Offset,-Protrusion])
    			TinyTrak3(Length + 2*Protrusion);
    
    		  for (y = [TT3Offset,AudioOffset])
    			translate([-CaseOverallWidth,y,(Length - ScrewOffset)])
    			  rotate([0,90,0])
    				rotate(0)					// Z rotation puts point upward for printing
    				PolyCyl(Tap4_40,CaseOverallWidth);
    
    		  translate(LEDOffset)
    			rotate([90,90,0])
    			  translate([-LEDWindow[0]/2,-LEDWindow[1]/2,-Protrusion])
    			  cube(LEDWindow,center=false);
    
    		  for (p = [TrimPot1,TrimPot2])
    			translate(p)
    			  rotate([-90,90,0])				// Y rotation puts point upward for printing
    				PolyCyl(3.0,URy);
    
    		  for (x=[-1,1]) {
    			translate([x*(CaseOverallWidth/2 - PinOffsetWidth),
    					  PinOffsetHeight,
    					  (Length - PinDepth)])
    			  rotate(45)						// align hole sides with case sides
    				  PolyCyl(PinDia,2*TopThick);
    			translate([x*(CaseOverallWidth/2 - PinOffsetWidth),
    					  PinOffsetHeight,
    					  -PlateThick])
    			  rotate(45)						// align hole sides with case sides
    				  PolyCyl(PinDia,(PlateThick + PinDepth));
    		  }
    
    		  for (x=[-1,1])						// setscrews to secure base plate
    			translate([x*(CaseOverallWidth/2 - 3*Tap4_40),
    					  TT3Offset,-Protrusion])
    			  rotate(360/(5*4))
    				PolyCyl(Tap4_40,2*TopThick);
    
    		  translate([-(ContactGapX/2 + ContactDia/2),0,(Contact1Y + ContactDia/2)])
    			rotate([90,0,0])
    			  Contact();
    		  translate([+(ContactGapX/2 + ContactDia/2),0,(Contact2Y + ContactDia/2)])
    			rotate([90,0,0])
    			  Contact();
    
    		  translate([CaseOverallWidth/2,HTCableOffset,(Length - HTCableDia/4)])
    			rotate([0,90,0])
    			  cube([(HTCableDia/2 + Protrusion),HTCableDia,CaseOverallWidth],center=true);
    		  translate([0,HTCableOffset,(Length - HTCableDia/2)])
    			rotate([0,90,0])
    			  cylinder(r=(1/cos(30))*HTCableDia/2,h=CaseOverallWidth,$fn=6);
    		}
    	  }
    
    if (false)
    	  if (Holes)
    		translate(LEDOffset)						// support plug in LED window
    		  rotate([90,90,0])
    			translate([-0.95*LEDWindow[0]/2,-0.80*LEDWindow[1]/2,ThreadWidth/2])
    			  cube([0.95*LEDWindow[0],0.80*LEDWindow[1],2*ThreadWidth],center=false);
    
    	}
    
    }
    
    //-----
    // Battery contact recess
    //  This gets subtracted from the bottom plate in two places
    // 	Align points to print upward
    
    module Contact() {
    
    if (true)
    union() {													// vertical printing with case
    	translate([0,0,-(ContactRecess + Protrusion)/2])
    	  PolyCyl(ContactDia,(ContactRecess + Protrusion),8);
    	translate([0,0,-(PlateThick + Protrusion)])
    	  rotate(60/2)
    	  PolyCyl(ContactStudDia,PlateThick,6);
    	translate([0,0,-(ContactRecess + ContactStudHeadThick/3)])
    	  PolyCyl(ContactStudHead,ContactStudHeadThick,8);				// allow for solder blob
    }
    else
    union() {												// horizontal printing alone
    	translate([0,0,-(ContactRecess - Protrusion)/2])
    	  PolyCyl(ContactDia,(ContactRecess + Protrusion),8);
    	translate([0,0,-(PlateThick + Protrusion)])
    	  PolyCyl(ContactStudDia,(PlateThick + 2*Protrusion));
    	translate([0,0,-(ContactRecess + ContactStudHeadThick/3)])
    	  PolyCyl(ContactStudHead,ContactStudHeadThick,8);				// allow for solder blob
    }
    
    }
    
    //-----
    // Radio bottom locating feature
    //  This polygon gets subtracted from the battery pack base
    
    module RadioBase() {
    
    linear_extrude(height=(BaseOpeningDepth + Protrusion),center=false,convexity=5)
    polygon(points=[
    			[-BaseOpeningMax/2,-Protrusion],
    
    			[-BaseOpeningMin/2,BaseOpeningY],
    			[-(BaseToothOC/2 + BaseToothBase/2),BaseOpeningY],
    
    			[-(BaseToothOC/2 + BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[-(BaseToothOC/2 - BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[-(BaseToothOC/2 - BaseToothBase/2),BaseOpeningY],
    
    			[ (BaseToothOC/2 - BaseToothBase/2),BaseOpeningY],
    			[ (BaseToothOC/2 - BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[ (BaseToothOC/2 + BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[ (BaseToothOC/2 + BaseToothBase/2),BaseOpeningY],
    			[ BaseOpeningMin/2,BaseOpeningY],
    
    			[ BaseOpeningMax/2,-Protrusion],
    
    			[ (BaseTabOC + BaseTabWidth/2),-Protrusion],
    			[ (BaseTabOC + BaseTabWidth/2),BaseTabThick],
    			[ (BaseTabOC - BaseTabWidth/2),BaseTabThick],
    			[ (BaseTabOC - BaseTabWidth/2),-Protrusion],
    
    			[ BaseTabWidth/2,-Protrusion],
    			[ BaseTabWidth/2,BaseTabThick],
    			[-BaseTabWidth/2,BaseTabThick],
    			[-BaseTabWidth/2,-Protrusion],
    
    			[-(BaseTabOC + BaseTabWidth/2),-Protrusion],
    			[-(BaseTabOC + BaseTabWidth/2),BaseTabThick],
    			[-(BaseTabOC - BaseTabWidth/2),BaseTabThick],
    			[-(BaseTabOC - BaseTabWidth/2),-Protrusion],
    		  ],
    		  convexity=5
    );
    }
    
    //-----
    // Battery pack base
    
    module Base() {
    
    difference() {
    
    	rotate([-90,180,0])						// main case shape
    	  CaseShell(BaseTotalThick,false);
    
    	translate([0,0,BaseThick])				// radio base interface
    	  RadioBase();
    
    	translate([0,0,BaseThick])				// tooth bevel
    	  rotate([(-90 + BaseToothAngle),0,0])
    		translate([0,-0.5,0])
    		  cube([(BaseToothSection + 2*Protrusion),1.0,10],center=true);
    
    	translate([-BaseWidthOuter,				// surface slope
    			  -(BaseThick + BaseEndLip)/tan(BaseEndAngle),0])
    	  rotate([BaseEndAngle,0,0])
    		difference() {
    		  cube([2*BaseWidthOuter,3*BaseOpeningY,BaseOpeningDepth],center=false);
    		  translate([(BaseWidthOuter - (BaseToothSection + 2*Protrusion)/2),0,0])
    			cube([(BaseToothSection + 2*Protrusion),1.2*BaseOpeningY,BaseOpeningDepth],center=false);
    		}
    
    	for (x=[-1,1])							// alignment pin holes
    	  translate([x*(CaseOverallWidth/2 - PinOffsetWidth),PinOffsetHeight,-Protrusion])
    		rotate(45)							// align hole side with plate side
    		  PolyCyl(PinDia,2*TopThick);
    
    	for (x=[-1,1])							// mounting setscews
    	  translate([x*(CaseOverallWidth/2 - 3*Tap4_40),
    				TT3Offset,-Protrusion])
    		rotate(-360/(-5*4))
    		  PolyCyl(Tap4_40,2*TopThick);
    
    	translate([(-SwitchBody[0]/2),TT3Offset,-SwitchBody[2]/2])	// mode switch
    	  scale([1,1,2])
    		cube(SwitchBody);
    
    }
    }
    
    //-----
    // Top plate with latch
    //	Split around TinyTrak3 serial connector
    //	 ... which must be at the same height as in the shell!
    //	The cable hole sizes & locations are entirely ad-hoc
    
    module TopPlate() {
    
    Cable1Dia = 5.0;
    Cable2Dia = 5.0;
    CableHoleLength = TopThick + 2*Protrusion;
    CableHoleZ = -Protrusion;
    
    DB9Plate = [32.0,13.5,1.25];					// plate surrounding connector body
    
    difference() {
    
    	rotate([-90,180,180])
    	  CaseShell(TopThick,false);
    
    	translate([0,-TT3Offset,-Protrusion])
    	  DSubMin9(TopThick + 2*Protrusion);
    
    	translate([0,-TT3Offset,(TopThick - DB9Plate[2]/2)])
    	  cube([DB9Plate[0],DB9Plate[1],(DB9Plate[2] + Protrusion)],center=true);
    
    	translate([-CaseOverallWidth,-SplitOffset,-2*Protrusion])		// split the plate
    	  cube([2*CaseOverallWidth,4*Protrusion,(TopThick + 2*Protrusion)]);
    
    	translate([0,0,(TopThick - TopBevel)])
    	  rotate([-TopBevelAngle,0,0])
    		translate([-CaseOverallWidth,-TopThick,0])
    		  cube([2*CaseOverallWidth,2*TopThick,2*TopThick],center=false);
    
    	for (x=[-1,1])
    	  translate([(x*TabOC/2),
    				(-TabHeight/2 + Protrusion),
    				(TopThick - TabEngageLength/2 + Protrusion/2)])
    		rotate([90,0,0])
    		  cube([TabWidth,
    				(TabEngageLength + Protrusion),
    				(TabHeight + Protrusion)],center=true);
    
    	translate([-CaseOverallWidth,
    			  -(TabEngageHeight + LatchBarWidth - BatteryClearance),
    			  (TopThick - LatchBarDepth)])
    	  cube([2*CaseOverallWidth,(LatchBarWidth + LatchBarThick),(LatchBarDepth + Protrusion)]);
    
    	for (x=[-1,1])
    	  translate([(x*CaseOverallWidth/4),
    				-(TabEngageHeight + LatchBarWidth + Clear2_56/2 - BatteryClearance + Protrusion),
    				0]) {
    		translate([0,0,-Protrusion])
    		  rotate(45)						// align sides with slot
    			PolyCyl(Tap2_56,(TopThick + 2*Protrusion));
    		translate([0,0,(TopThick - LatchBarDepth)])
    		  rotate(60)						// align sides with slot
    			PolyCyl((Head2_56 + Protrusion),TopThick,6);		// extra extra clearance
    	  }
    
    	for (x=[-1,1])
    	  translate([x*(CaseOverallWidth/2 - PinOffsetWidth),-PinOffsetHeight,-Protrusion])
    		rotate(45)						// align hole side with plate side
    		  PolyCyl(PinDia,2*TopThick);
    
    	for (x=[-1,1])						// coincidentally line up with latch tabs
    	  translate([(x*TabOC/2),-(SplitOffset - 3.0),-Protrusion])
    		scale([1,1.7,1])
    		  PolyCyl(Cable1Dia,CableHoleLength,6);
    }
    
    }
    
    //-----
    // Speaker-Mic plug mounting plate
    
    module PlugPlate() {
    
    JackOC = 11.20;						// 14.25 OD - (3.58 + 2.58)/2
    
    JackScrewDia = 4.6;
    JackScrewOffsetX = 1.00;
    JackScrewOffsetY = 5.25;			//  mounting screw to edge of lower recess
    
    PlugBaseWidth = 9.25;				// lower section of plate
    PlugBaseLength = 22.0;
    PlugBaseRadius = 1.75;
    
    Plug3Offset = 5.25;					// edge of base recess to 3.5 mm jack
    
    Plug2BezelDia = 7.1;				// 2.5 mm plug
    Plug2BezelThick = 1.04;
    Plug2ScrewDia = 6.0;
    Plug3ScrewLength = 3.0;
    
    Plug3BezelDia = 8.13;				// 3.5 mm plug
    Plug3BezelThick = 1.6;
    Plug3ScrewDia = 7.95;
    Plug3ScrewLength = 4.0;
    
    PlugFillOffsetX = JackScrewOffsetX - 0.5;		// base recess CL to fill CL
    PlugFillOffsetY = -10.5;				//  ... to edge of fill plate
    PlugFillWidth = 11.0;
    PlugFillLength = 34.00;
    PlugFillRadius1 = 1.5;
    PlugFillRadius2 = 4.5;
    
    PlugFillOffsetYTotal = 0;
    
    BaseX = PlugBaseWidth/2 - PlugBaseRadius;
    BaseY = PlugBaseLength/2 - PlugBaseRadius;
    
    difference() {
    	union() {
    	  linear_extrude(height=PlugBaseThick,center=false,convexity=3)
    		hull() {
    		  translate([-BaseX,-BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		  translate([-BaseX, BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		  translate([ BaseX, BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		  translate([ BaseX,-BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		}
    
    	  translate([PlugFillOffsetX,
    				(PlugFillLength/2 - PlugBaseLength/2 + PlugFillOffsetY),
    				PlugBaseThick])
    		linear_extrude(height=PlugFillThick,center=false,convexity=5)
    		  hull() {
    			translate([0,-(PlugFillLength/2 - PlugFillRadius2),0])
    			  circle(r=PlugFillRadius2,$fn=10);
    			translate([-(PlugFillWidth/2 - PlugFillRadius1),-PlugBaseLength/2,0])
    			  circle(r=PlugFillRadius1,$fn=8);
    			translate([-(PlugFillWidth/2 - PlugFillRadius1),
    					  (PlugFillLength/2 - PlugFillRadius1),0])
    			  circle(r=PlugFillRadius1,$fn=8);
    			translate([(PlugFillWidth/2 - PlugFillRadius1),
    					  (PlugFillLength/2 - PlugFillRadius1),0])
    			  circle(r=PlugFillRadius1,$fn=8);
    			translate([(PlugFillWidth/2 - PlugFillRadius1),-PlugBaseLength/2,0])
    			  circle(r=PlugFillRadius1,$fn=8);
    		  }
    	}
    
    	translate([0,-JackOC/2,-Protrusion])
    	  rotate(360/16) {
    		PolyCyl(Plug3BezelDia,(Plug3BezelThick + Protrusion),8);
    		PolyCyl(Plug3ScrewDia,(PlugBaseThick + PlugFillThick + 2*Protrusion),8);
    	  }
    
    	translate([0,+JackOC/2,-Protrusion])
    	  rotate(360/16) {
    		PolyCyl(Plug2BezelDia,(Plug2BezelThick + Protrusion),8);
    		PolyCyl(Plug2ScrewDia,(PlugBaseThick + PlugFillThick + 2*Protrusion),8);
    	  }
    
    	translate([JackScrewOffsetX,-(PlugBaseLength/2 + JackScrewOffsetY),0])
    	  PolyCyl(JackScrewDia,(PlugBaseThick + PlugFillThick + Protrusion));
    }
    
    }
    
    //-------------------
    // Build things...
    
    ShowPegGrid();
    
    if (Layout == "TT3")
    TinyTrak3();
    
    if (Layout == "Audio")
    AudioInterface();
    
    if (Layout == "DSub")
    DSubMin9();
    
    if (Layout == "Shell")
    CaseShell(CaseOverallLength);
    
    if (Layout == "Top")
    TopPlate();
    
    if (Layout == "Base")
    Base();
    
    if (Layout == "RadioBase")
    RadioBase();
    
    if (Layout == "PlugPlate")
    PlugPlate();
    
    if (Layout == "Contact")
    rotate([180,0,0])
    	Contact();
    
    if (Layout == "Show" || Layout == "Fit") {
    
    translate([0,-ShellLength/2,0]) {
    
    	translate([0,(Layout == "Show")?-ShowGap:0,0])
    	  rotate([90,0,0])
    		color("SandyBrown") Base();
    
    	translate([0,0,0])
    	    color("LightGreen") render() CaseShell();
    
    	translate([-(CaseOverallWidth/2 + 10),50,CaseOverallHeight/2])
    	  rotate([0,-90,0])
    		color("Gold") PlugPlate();
    
    	translate([0,((Layout == "Show")?(ShellLength + ShowGap):ShellLength),0])
    	  rotate([-90,0,0])
    		color("BurlyWood") TopPlate();
    }
    }
    
    if (Layout == "Build1") {
    
    translate([5 + CaseOverallHeight,0,0])
    	rotate([0,0,90])
    	  Base();
    
    translate([-(5 + CaseOverallHeight),0,0])
    	rotate(90)
    	  TopPlate();
    
    }
    
    if (Layout == "Build2") {
    
    translate([0,-CaseOverallHeight/2,ShellLength])
    	rotate([-90,0,0])
    		CaseShell();
    
    }
    
    if (Layout == "Build3") {
    
    translate([0,0,(PlugBaseThick + PlugFillThick)])
    	rotate([180,0,0])
    	  PlugPlate();
    
    }
    
  • Wouxun KG-UV3D GPS+Voice Interface: Electronics

    The latest version of the GPS+Voice electronics for the Wouxun KG-UV3D, which I’m getting ready to build:

    Wouxun KG-UV3D GPS+Voice Schematic
    Wouxun KG-UV3D GPS+Voice Schematic

    A few changes:

    • It runs from those 9 V boosted packs, not 7.4 V direct from their lithium cells
    • U2, the MAX4544 data/voice mux, runs from the shunt-regulated +5 V, not the TT3+ regulator
    • Miscellaneous doc cleanup

    I’m mulling over a capacitor between the TT3+ data output and the earbud, so as to monitor transmissions, but I’m not convinced that’s worthwhile.

    The PCB layout, with wire jumpers on the two inner layers:

    Wouxun KG-UV3D GPS+Voice PCB
    Wouxun KG-UV3D GPS+Voice PCB

    The previous version doesn’t look much different from what this one will become:

    GPS-HT Wouxun interface - brassboard
    GPS-HT Wouxun interface – brassboard

    This will replace the ICOM Z-1A radio and GPS interface on Mary’s bike, which has been working fine for quite a while. That can’t last, so I’m trying to get ahead of the failure curve…

    The Wouxun HT GPS+Audio Interface schematic and PCB layout files, both tucked into a ZIP file with an ODT extension. That is not an OpenDocument file: rename it to remove the ODT extension, then unzip it.

  • Another Dirac Delta: Moen Faucet Repair (?!)

    Can anyone explain how it is that, all of a sudden, Yahoo! Image Search generates 466 hits for my post on repairing our Moen bathroom sink faucet, a sleepy post in my Long Tail that’s been ticking along at one view per day since last December?

    The flash mob was here and gone within the span of an hour:

    View spike for Moen faucet post - 2012-04-05
    View spike for Moen faucet post – 2012-04-05

    Does Yahoo! Image Search have something like a featured image of the hour? I can’t find anything obvious, but I cannot imagine what else would cause that many views of a single page that lacks buck-naked celebrities, jackass stunts, or hideously embarrassing personal revelations.

    Given that all of their “trending image searches” show (typically female) human faces, I doubt that the grubby innards of a faucet would appear in that gallery.

    This has happened before from the same source…