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.

Tag: Improvements

Making the world a better place, one piece at a time

  • Sony NP-FS11 Lithium Battery Rebuilds: 2016

    It seems that two years is about as long as the NP-FS11 batteries last, as shown by the two lower curves from the ones I rebuilt in December 2013 with cells from 2011:

    Sony NP-FS11 2011-2016 Packs
    Sony NP-FS11 2011-2016 Packs

    The two middle curves with those same colors show the “back then” performance of those batteries: they’re shot in both total capacity and terminal voltage.

    I bought enough cells back in 2011 to leave two cells unused until now, which I built into a pack and charged. The green curve in the middle shows the result: those cells haven’t lost anything over the last five (!) years, as their performance still matches the other two batteries when they were new.

    The red curves come from a pair of batteries made with fresh new cells from batteryspace.com. They’re nominally 650 mA·h cells, so the NP-FS11 configuration (two parallel cells) should produce 1300 mA·h; surprisingly, they show 1500 mA·h with a nice voltage curve.

    So, although the 2011 cells work as well as their (now defunct) siblings, that pack can’t deliver the same capacity as the new cells. I expect I’ll rebuild it with 2016 cells in about a year.

    For whatever it’s worth, rebuilding these batteries goes much faster when I don’t have to saw them open. The Kapton tape wrapped around the case halves secures them well enough; there’s no need for fancy gluing.

    NP-FS11 Battery Rebuilds - 2016-03
    NP-FS11 Battery Rebuilds – 2016-03

    Yeah, I should make better labels. It’s hard to form a deep emotional attachment to the poor things, though.

    Here’s a case where something performs better than expected; I don’t always buy cheap junk from the usual eBay vendor…

     

     

     

     

  • OttLite LED Conversion

    Although Mary liked the illumination from her OttLite (an old 13 W fluorescent Folding Task Lamp), neither of us liked its tiny base and tippy nature. It recently fell / was dropped / jumped to its doom, smashing the CFL tube and wreaking havoc on the tiny plastic studs holding its large cast-iron weight and steel base in position. Given that the CFL ballast had started humming a while ago, I took it apart to see whether I could salvage anything from the rubble.

    Remove:

    • Four screws under the fuzzy felt feet
    • One screw under the label on the back
    • A final screw that becomes visible only after disemboweling the hinge assembly by unscrewing the obvious endcaps:
    OttLite LED Conversion - hinge screw
    OttLite LED Conversion – hinge screw

    Pull the hinge end of the white inside panel away from the outer stand at enough of an angle to disengage all three latches holding it to the base, then remove it just enough to let you start cutting wires around the ballast…

    I rebuilt the thing with a pair of 24 V 150 mA warm-white LED panels (good industrial surplus, not the usual cheap eBay crap) powered by a 19 V laptop adapter (from IBM, no less) through a (cheap eBay) boost converter sticky-foam-taped where the fluorescent ballast used to live:

    OttLite LED Conversion - boost supply wiring
    OttLite LED Conversion – boost supply wiring

    The power supply had only two conductors, the central wire surrounded by twisted shielding, and didn’t require a fussy interface. Hooray for simple bulk power supplies; I lopped off the connector and soldered the wires directly to the boost converter.

    The original lamp wiring has a 120 VAC switch inside the hinge that turned the lamp on as you raise the arm holding the CFL tube: exactly what I need for its new use. That eliminated figuring out how to crack the arm apart to rewire it.

    I harvested the base from a(nother) defunct CFL bulb:

    OttLite LED Conversion - harvested CFL base
    OttLite LED Conversion – harvested CFL base

    By soldering wires directly into the pins, I could reuse the existing CFL socket in the lamp arm, the existing wiring, and the switch.

    The LED panels dissipate 3-ish W each:

    OttLite LED Conversion - LED panel layout
    OttLite LED Conversion – LED panel layout

    They’re mounted on a 0.1 inch aluminum sheet from the heap that required exactly one saw cut to fit into the space available, so I defined it to be perfect. The 4-40 screws holding the panels in place continue through the plate and 3/8 inch aluminum standoffs into a quartet of knurled inserts epoxied into eyeballometrically match-drilled holes in the lamp arm:

    OttLite LED Conversion - epoxied threaded inserts
    OttLite LED Conversion – epoxied threaded inserts

    The faint yellowish discoloration from the CFL tube’s heat and UV is much more visible in real life, but nobody will ever see it again. The scrawled blue (+) and (-) marks give the socket polarity; it’s not mechanically polarized and a bit of care is in order. The black rectangle is actually a shiny metal sheet intended to reflect heat from the CFL tube’s base away from the plastic arm.

    I set the boost converter to 23.5 V, at which point the LED panels draw about 100 mA each and get just over uncomfortably warm after an hour or two:

    OttLite LED Conversion - in action
    OttLite LED Conversion – in action

    The panels run 120 °F = 50 °C and the SMD LEDs probably exceed 150 °F = 65 °C. The scant surplus doc touted “No heatsink required” and the single-sided FR4 PCB insulates the LEDs from the aluminum sheet, but I still smeared some heatsink compound behind the panels in the hopes of spreading the heat out a bit.

    I glued the shattered base studs back in place with IPS #3, surrounded them with generous epoxy fillets, plunked the cast iron weight in place atop some waxed paper to mold the epoxy to fit (and let me remove it again, if needs be), screwed everything together, and stuck a foam sheet over the steel base plate. It’s as tippy as before, but at least the LEDs won’t shatter if when it falls. It really needs a larger base; a polycarbonate plate might work, if only I could figure out how to attach it.

    All in all, the lamp looks good and the warm-white LEDs with DC drive don’t produce that horrible fluorescent flicker.

    The lamp now sports a label identifying it as a NisLite; because P-Touch labeler.

  • Knurled Inserts: Epoxy Anchoring

    After taking the incandescent lamp socket off its base, I drilled the tapped (yeah, in plastic) 6-32 holes out to a firm press fit for the knurled 6-32 inserts, buttered the inserts with epoxy, and pressed them firmly in place:

    Lamp Base - epoxy knurled insert
    Lamp Base – epoxy knurled insert

    Fast forward a day and they’re stuck in there like they were glued. You can see a bit of the epoxy around the right rim of the insert; I wiped a bit more off around the other one.

    Putting The Right Amount of epoxy on the insert requires dialing back my “The bigger the blob, the better the job” enthusiasm, but wasn’t all that difficult. It’s certainly more tedious than just ramming the inserts into a printed hole and might actually produce better retention. I doubt that will make the least difference for (almost) anything I build.

    On the whole, they look good…

  • Fluorescent Shoplight Capacitor: Wow!

    You just can’t make this stuff up:

    Fluorescent Shoplights - nasty cap termination
    Fluorescent Shoplights – nasty cap termination

    That shredded plastic can’t possibly be a Good Thing; the endcap contained plenty of loose shreds.

    Perhaps I’m overly critical, but I think the only way these fixtures could have a UL approval certificate was that somebody else didn’t notice their certificate went missing. Most likely, of course, the fixtures sent for approval looked lovely and bore no relation to the junk actually sold to Lowe’s / Home Depot.

    That emerged from the fourth defunct fluorescent fixture I converted to use LED tubes.

  • Raspberry Pi Streaming Radio Player: Marginally Viable Product

    The least horrible way to get events from the keypad turned out to be a simple non-blocking poll from Python’s select library, then sucking the event input queue dry; the main loop now does what might be grandiosely overstated as cooperative multitasking. Well, hey, it reads lines from mplayer’s output pipe and processes keypad events and doesn’t stall (for very long) and that’s multi enough for me.

    It extracts the stream title from the ICY Info line, but I still haven’t bothered with a display. It may well turn out that this thing doesn’t need a display. The stream title will be enclosed in single quotes, but it may also contain non-escaped and non-paired single quotes (a.k.a. apostrophes): the obvious parsing strategy doesn’t work. I expect titles can contain non-escaped semicolons, too, which will kill the algorithm I’m using stone cold dead. Some try - except armor may be appropriate.

    This code does not tolerate a crappy WiFi connection very well at all. I eventually replaced a long-antenna WiFi adapter with an actual Ethernet cable and all the mysterious problems at the far end of the house Went Away. Soooo this code won’t tolerate random network stream dropouts very well, either; we’ll see how poorly that plays out in practice.

    The hackery to monitor / kill / restart / clean up after mplayer and its pipes come directly from seeing what failed, then whacking that mole in the least intrusive manner possible. While it would be better to wrap a nice abstract model around what mplayer is (assumed to be) doing, it’s not at all clear to me that I can build a sufficiently durable model to be worth the effort. Basically, trying to automate a program designed to be human-interactive is always a recipe for disaster.

    The option for the Backspace / Del key lets you do remote debugging by editing the code to just bail out of the loop instead of shut down. Unedited, it’s a power switch: the Pi turns off all the peripherals and shuts itself down. The key_hold conditional means you must press-and-hold that button to kill the power, but don’t run this on your desktop PC, OK?

    Autostarting the program requires one line in /etc/rc.local:

    sudo -u pi python /home/pi/Streamer.py &
    

    AFAICT, using cron with an @REBOOT line has timing issues with the network being available, but I can’t point to any solid evidence that hacking rc.local waits until the network is up, either. So far, so good.

    I make no apologies for any of the streams; I needed streams behind all the buttons and picked stuff from Xiph’s listing. The AAC+ streams from the Public Domain Project give mplayer a bad bellyache; I think its codecs can’t handle the “+” part of AAC+.

    All in all, not bad for a bit over a hundred lines of code, methinks…

    More fiddling will happen, but we need some continuous experience for that; let the music roll!

    The Python program as a GitHub Gist:

    from evdev import InputDevice,ecodes,KeyEvent
    import subprocess32
    import select
    import re
    import sys
    Media = {'KEY_KP7' : ['Classical',['mplayer','-playlist','http://stream2137.init7.net/listen.pls'%5D%5D,
    'KEY_KP8' : ['Jazz',['mplayer','-playlist','http://stream2138.init7.net/listen.pls'%5D%5D,
    'KEY_KP9' : ['WMHT',['mplayer','http://live.str3am.com:2070/wmht1'%5D%5D,
    'KEY_KP4' : ['Dub 1',['mplayer','-playlist','http://dir.xiph.org/listen/2645/listen.m3u'%5D%5D,
    'KEY_KP5' : ['Dub 2',['mplayer','http://streaming207.radionomy.com:80/MiamiClubMusiccom'%5D%5D,
    'KEY_KP6' : ['WAMC',['mplayer','http://pubint.ic.llnwd.net/stream/pubint_wamc'%5D%5D,
    'KEY_KP1' : ['Oldies 1',['mplayer','http://streaming304.radionomy.com:80/keepfree60s'%5D%5D,
    'KEY_KP2' : ['Oldies 2',['mplayer','http://streaming207.radionomy.com:80/1000Oldies'%5D%5D,
    'KEY_KP3' : ['Soft Rock',['mplayer','http://streaming201.radionomy.com:80/SoftRockRadio'%5D%5D,
    'KEY_KP0' : ['Smooth',['mplayer','http://streaming202.radionomy.com:80/The-Smooth-Lounge'%5D%5D
    }
    CurrentKC = 'KEY_KP7'
    Controls = {'KEY_KPSLASH' : '/',
    'KEY_KPASTERISK' : '*',
    'KEY_KPENTER' : ' ',
    'KEY_KPMINUS' : '<',
    'KEY_KPPLUS' : '>'
    }
    # set up event input and polling
    k=InputDevice('/dev/input/keypad')
    kp = select.poll()
    kp.register(k.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    # set up files for mplayer pipes
    lw = open('/tmp/mp.log','w') # mplayer piped output
    lr = open('/tmp/mp.log','r') # … reading that output
    # Start the default stream
    print 'Starting mplayer on',Media[CurrentKC][0]," -> ",Media[CurrentKC][-1][-1]
    p = subprocess32.Popen(Media[CurrentKC][-1],stdin=subprocess32.PIPE,stdout=lw,stderr=subprocess32.STDOUT)
    print ' … running'
    #— Play the streams
    while True:
    # pluck next line from mplayer and decode it
    text = lr.readline()
    if 'ICY Info: ' in text:
    trkinfo = text.split(';')
    for ln in trkinfo:
    if 'StreamTitle' in ln:
    trkhit = re.search(r"StreamTitle='(.*)'",ln)
    TrackName = trkhit.group(1)
    print 'Track name: ', TrackName
    break
    elif 'Exiting…' in text:
    print 'Got EOF / stream cutoff'
    print ' … killing dead mplayer'
    p.kill()
    print ' … flushing pipes'
    lw.truncate(0)
    print ' … discarding keys'
    while [] != kp.poll(0):
    kev = k.read
    print ' … restarting mplayer: ',Media[CurrentKC][0]
    p = subprocess32.Popen(Media[CurrentKC][-1],stdin=subprocess32.PIPE,stdout=lw,stderr=subprocess32.STDOUT)
    print ' … running'
    continue
    # accept pending events from keypad
    if [] != kp.poll(0):
    kev = k.read()
    for e in kev:
    if e.type == ecodes.EV_KEY:
    kc = KeyEvent(e).keycode
    if kc == 'KEY_NUMLOCK':
    continue
    # print 'Got: ',kc
    if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold):
    if True:
    print 'Backspace = shutdown!'
    p = subprocess32.call(['sudo','shutdown','-HP',"now"])
    else:
    print 'BS = bail from main, ssh to restart!'
    sys.exit(0)
    if KeyEvent(e).keystate != KeyEvent.key_down:
    continue
    if kc in Controls:
    print 'Control:', kc
    try:
    p.stdin.write(Controls[kc])
    except Exception as e:
    print "Can't send control: ",e
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subprocess32.Popen(Media[CurrentKC][-1],stdin=subprocess32.PIPE,stdout=lw,stderr=subprocess32.STDOUT)
    print ' … running'
    if kc in Media:
    print 'Switching stream to ',Media[kc][0]," -> ",Media[kc][-1][-1]
    CurrentKC = kc
    print ' … halting player'
    try:
    p.communicate(input='q')
    except Exception as e:
    print 'Perhaps mplayer died?',e
    print ' … killing it for sure'
    p.kill()
    print ' … flushing pipes'
    lw.truncate(0)
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subprocess32.Popen(Media[CurrentKC][-1],stdin=subprocess32.PIPE,stdout=lw,stderr=subprocess32.STDOUT)
    print ' … running'
    print 'Out of loop!'
    view raw Streamer.py hosted with ❤ by GitHub
  • Vacuum Tube LEDs: Fire in the Noval

    Replacing the original Noval socket in the string with the platter-friendly version, bracing the wiring with duct tape, balancing it on my desk, and firing it up:

    Noval socket - red phase
    Noval socket – red phase

    The green phase looks nice, too:

    Noval socket - green phase
    Noval socket – green phase

    Those screws are too big.

    The getter flash covers the entire top of the tube; shining an LED down through the evacuation tip won’t work and even a laser doesn’t do much. That saves me the trouble of trying to create a cap that doesn’t wreck the tube’s good looks.

    I originally planned to use white / natural PETG for the socket, but the more I see of those things, the more I think black is the new white. The sockets should vanish into the background, to let the tubes (and their reflections) carry the show.

    The (yet to be designed) base must vanish under the platter edge, too, which puts a real crimp on its overall height. I’m not sure how to fit an Arduino Pro Mini and an FTDI board beside the existing socket; perhaps this calls for a unified socket-base design held on by those screws, rather than a separate socket inside a base enclosure.

    Even though I know the tubes are inert and cool, I still hesitate before removing them from their sockets with the Neopixels running: you simply do not unplug a hot, powered device!

  • Vacuum Tube LEDs: Noval Tube on a Platter

    Replacing the hex nut traps with knurled insert cylinders slims the ends of the socket:

    Noval Socket - knurled inserts - bottom - Slic3r preview
    Noval Socket – knurled inserts – bottom – Slic3r preview

    Making the raised part of the socket fit the 25 mm ID of a hard drive platter swells the midsection of the socket, but the platter won’t need any machining or punching:

    Noval Socket - knurled inserts - top - Slic3r preview
    Noval Socket – knurled inserts – top – Slic3r preview

    The octal and duodecar sockets will require a punch to open up the platter hole and all sockets require two drilled clearance holes for the screws. Given that I’ll eventually do this on the Sherline, maybe milling the hole for the bigger tubes will be faster & easier than manually punching them.

    I moved the screw centers to 35 mm (from the historically accurate 28 mm) to accommodate the larger center, not that anybody will ever notice, and enlarged the central hole to 7.5 mm (from 5.0 mm) to let more light into the tube base.

    The support structures inside the (now much smaller) knurled insert cylinders might not be strictly necessary, but I left them in place to see how well they built. Which was perfectly, as it turns out, and they popped out with a slight push:

    Noval socket - knurled inserts - support structures
    Noval socket – knurled inserts – support structures

    They’re just the cutest little things (those are 0.100 inch grid squares in the background):

    Noval socket - support structures
    Noval socket – support structures

    Anyhow, the knurled inserts pressed into their holes with a slight shove:

    Noval socket - installing knurled insert
    Noval socket – installing knurled insert

    The chuck jaws were loose on the screw cutoff stud and stopped at the surface, putting the knurled inserts perfectly flush with the socket:

    Noval socket - knurled inserts - installed
    Noval socket – knurled inserts – installed

    The surface looks very slightly distorted around the inserts, although it’s still smooth to the touch, and I think the PETG will slowly relax around the knurls. Even without heat or epoxy, they’re now impossible to pull out with any force I’m willing to apply to the screws threaded into them. Given that the platter screws will (be trying to) pull the inserts through the socket, I think a dry install will suffice for my simple needs.

    Match-mark, drill #27 6-32 clearance holes, and the screws drop right in:

    Noval socket - installed
    Noval socket – installed

    Those stainless steel pan-head 6-32 screws seem a bit large in comparison with the socket. Perhaps I should use 4-40 screws, even though they’re not, ahem, historically accurate.

    The tube pin holes get hand-reamed with a #53 drill = 1.5 mm. That’s a bit over the nominal 1.1 mm pin diameter, but seems to provide both easy insertion and firm retention. For permanent installation, an adhesive would be in order.

    Buff off the fingerprints, stick the tube in place, and it looks pretty good:

    Noval socket - tube on platter
    Noval socket – tube on platter

    Yeah, those screws are too big. Maybe a brace of black M3 socket head screws would look better, despite a complete lack of historicity.

    Now to wire it up and ponder how to build a base.

    The OpenSCAD source code as a GitHub Gist:

    // Vacuum Tube LED Lights
    // Ed Nisley KE4ZNU February 2016
    Layout = "Socket"; // Cap LampBase USBPort Socket(s) (Build)FinCap
    DefaultSocket = "Noval";
    Section = false; // cross-section the object
    Support = 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
    // https://en.wikipedia.org/wiki/Tube_socket#Summary_of_Base_Details
    // punch & screw OC modified for drive platter chassis plate
    // platter = 25 mm ID
    T_NAME = 0; // common name
    T_NUMPINS = 1; // total, with no allowance for keying
    T_PINBCD = 2; // tube pin circle diameter
    T_PINOD = 3; // … diameter
    T_PINLEN = 4; // … length (overestimate)
    T_HOLEOD = 5; // nominal panel hole from various sources
    T_PUNCHOD = 6; // panel hole optimized for inch-size Greenlee punches
    T_TUBEOD = 7; // envelope or base diameter
    T_PIPEOD = 8; // light pipe from LED to tube base
    T_SCREWOC = 9; // mounting screw holes
    // Name pins BCD dia length hole punch env pipe screw
    TubeData = [
    ["Mini7", 8, 9.53, 1.016, 7.0, 16.0, 25.0, 18.0, 5.0, 35.0], // punch 11/16, screw 22.5 OC
    ["Octal", 8, 17.45, 2.36, 10.0, 36.2, (8 + 1)/8 * inch, 32.0, 11.5, 39.0],
    ["Noval", 10, 11.89, 1.1016, 7.0, 22.0, 25.0 , 21.0, 7.5, 35.0], // punch 7/8, screw 28.0 OC
    ["Duodecar", 13, 19.10, 1.05, 9.0, 32.0, (4 + 1)/4 * inch, 38.0, 12.5, 39.0], // aka Compactron
    ];
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Pixel = [7.0,10.0,3.0]; // ID = contact patch, OD = PCB dia, LENGTH = overall thickness
    Nut = [3.5,5.2,7.2]; // socket mounting nut recess — threaded insert
    NutSides = 8;
    BaseShim = 2*ThreadThick; // between pin holes and pixel top
    SocketFlange = 2.0; // rim around socket below punchout
    PanelThick = 1.5; // socket extension through punchout
    FinCutterOD = 1/8 * inch;
    FinCapSize = [(Pixel[OD] + 2*FinCutterOD),30.0,(10.0 + 2*Pixel[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);
    }
    //———————-
    // Tube cap
    CapTube = [4.0,3/16 * inch,10.0]; // brass tube for flying lead to cap LED
    CapSize = [Pixel[ID],(Pixel[OD] + 3.0),(CapTube[OD] + 2*Pixel[LENGTH])];
    CapSides = 6*4;
    module Cap() {
    difference() {
    union() {
    cylinder(d=CapSize[OD],h=(CapSize[LENGTH]),$fn=CapSides); // main cap body
    translate([0,0,CapSize[LENGTH]]) // rounded top
    scale([1.0,1.0,0.65])
    sphere(d=CapSize[OD]/cos(180/CapSides),$fn=CapSides); // cos() fixes slight undersize vs cylinder
    cylinder(d1=(CapSize[OD] + 2*3*ThreadWidth),d2=CapSize[OD],h=1.5*Pixel[LENGTH],$fn=CapSides); // skirt
    }
    translate([0,0,-Protrusion]) // bore for wiring to LED
    PolyCyl(CapSize[ID],(CapSize[LENGTH] + 3*ThreadThick + Protrusion),CapSides);
    translate([0,0,-Protrusion]) // PCB recess with clearance for tube dome
    PolyCyl(Pixel[OD],(1.5*Pixel[LENGTH] + Protrusion),CapSides);
    translate([0,0,(1.5*Pixel[LENGTH] – Protrusion)]) // small step + cone to retain PCB
    cylinder(d1=(Pixel[OD]/cos(180/CapSides)),d2=Pixel[ID],h=(Pixel[LENGTH] + Protrusion),$fn=CapSides);
    translate([0,0,(CapSize[LENGTH] – CapTube[OD]/(2*cos(180/8)))]) // hole for brass tube holding wire loom
    rotate([90,0,0]) rotate(180/8)
    PolyCyl(CapTube[OD],CapSize[OD],8);
    }
    }
    //———————-
    // Heatsink tube cap
    module FinCap() {
    CableOD = 3.5; // cable + braid diameter
    BulbOD = 3.75 * inch; // bulb OD; use 10 inches for flat
    echo(str("Fin Cutter: ",FinCutterOD));
    FinSides = 2*4;
    BulbRadius = BulbOD / 2;
    BulbDepth = BulbRadius – sqrt(pow(BulbRadius,2) – pow(FinCapSize[OD],2)/4);
    echo(str("Bulb OD: ",BulbOD," recess: ",BulbDepth));
    NumFins = floor(PI*FinCapSize[ID] / (2*FinCutterOD));
    FinAngle = 360 / NumFins;
    echo(str("NumFins: ",NumFins," angle: ",FinAngle," deg"));
    difference() {
    union() {
    cylinder(d=FinCapSize[ID],h=FinCapSize[LENGTH],$fn=2*NumFins); // main body
    for (i = [0:NumFins – 1]) // fins
    rotate(i * FinAngle)
    hull() {
    translate([FinCapSize[ID]/2,0,0])
    rotate(180/FinSides)
    cylinder(d=FinCutterOD,h=FinCapSize[LENGTH],$fn=FinSides);
    translate([(FinCapSize[OD] – FinCutterOD)/2,0,0])
    rotate(180/FinSides)
    cylinder(d=FinCutterOD,h=FinCapSize[LENGTH],$fn=FinSides);
    }
    rotate(FinAngle/2) // cable entry boss
    translate([FinCapSize[ID]/2,0,FinCapSize[LENGTH]/2])
    cube([FinCapSize[OD]/4,FinCapSize[OD]/4,FinCapSize[LENGTH]],center=true);
    }
    for (i = [1:NumFins – 1]) // fin inner gullets, omit cable entry side
    rotate(i * FinAngle + FinAngle/2) // joint isn't quite perfect, but OK
    translate([FinCapSize[ID]/2,0,-Protrusion])
    rotate(0*180/FinSides)
    cylinder(d=FinCutterOD/cos(180/FinSides),h=(FinCapSize[LENGTH] + 2*Protrusion),$fn=FinSides);
    translate([0,0,-Protrusion]) // PCB recess
    PolyCyl(Pixel[OD],(1.5*Pixel[LENGTH] + Protrusion),FinSides);
    PolyCyl(Pixel[ID],(FinCapSize[LENGTH] – 3*ThreadThick),FinSides); // bore for LED wiring
    translate([0,0,(FinCapSize[LENGTH] – 3*ThreadThick – 2*CableOD/(2*cos(180/8)))]) // cable inlet
    rotate(FinAngle/2) rotate([0,90,0]) rotate(180/8)
    PolyCyl(CableOD,FinCapSize[OD],8);
    if (BulbOD <= 10.0 * inch) // curve for top of bulb
    translate([0,0,-(BulbRadius – BulbDepth + 2*ThreadThick)]) // … slightly flatten tips
    sphere(d=BulbOD,$fn=16*FinSides);
    }
    }
    //———————-
    // Aperture for USB-to-serial adapter snout
    // These are all magic numbers, of course
    module USBPort() {
    translate([0,28.0])
    rotate([90,0,0])
    linear_extrude(height=28.0)
    polygon(points=[
    [0,0],
    [8.0,0],
    [8.0,4.0],
    // [4.0,4.0],
    [4.0,6.5],
    [-4.0,6.5],
    // [-4.0,4.0],
    [-8.0,4.0],
    [-8.0,0],
    ]);
    }
    //———————-
    // Box for Leviton ceramic lamp base
    module LampBase() {
    Bottom = 3.0;
    Base = [4.0*inch,4.5*inch,20.0 + Bottom];
    Sides = 12*4;
    Retainer = [3.5,11.0,1.0]; // flat fiber washer holding lamp base screws in place
    StudSides = 8;
    StudOC = 3.5 * inch;
    Stud = [Nut[OD], // 6-32 tapped brass insert
    min(15.0,1.5*(Base[ID] – StudOC)/cos(180/StudSides)), // OD = big enough to merge with walls
    (Base[LENGTH] – Retainer[LENGTH])]; // leave room for retainer
    union() {
    difference() {
    rotate(180/Sides)
    cylinder(d=Base[OD],h=Base[LENGTH],$fn=Sides);
    rotate(180/Sides)
    translate([0,0,Bottom])
    cylinder(d=Base[ID],h=Base[LENGTH],$fn=Sides);
    translate([0,-Base[OD]/2,Bottom + 1.2]) // mount on double-sided foam tape
    rotate(0)
    USBPort();
    }
    for (i = [-1,1])
    translate([i*StudOC/2,0,0])
    rotate(180/StudSides)
    difference() {
    cylinder(d=Stud[OD],h=Stud[LENGTH],$fn=StudSides);
    translate([0,0,Bottom])
    PolyCyl(Stud[ID],(Stud[LENGTH] – (Bottom – Protrusion)),6);
    }
    }
    }
    //———————-
    // Tube Socket
    module Socket(Name = DefaultSocket) {
    NumSides = 6*4;
    Tube = search([Name],TubeData,1,0)[0];
    echo(str("Building ",TubeData[Tube][0]," socket"));
    echo(str(" Punch: ",TubeData[ID][T_PUNCHOD]," mm = ",TubeData[ID][T_PUNCHOD]/inch," inch"));
    echo(str(" Screws: ",TubeData[ID][T_SCREWOC]," mm =",TubeData[ID][T_SCREWOC]/inch," inch OC"));
    OAH = Pixel[LENGTH] + BaseShim + TubeData[Tube][T_PINLEN];
    BaseHeight = OAH – PanelThick;
    difference() {
    union() {
    linear_extrude(height=BaseHeight)
    hull() {
    circle(d=(TubeData[Tube][T_PUNCHOD] + 2*SocketFlange),$fn=NumSides);
    for (i=[-1,1])
    translate([i*TubeData[Tube][T_SCREWOC]/2,0])
    circle(d=2.0*Nut[OD],$fn=NumSides);
    }
    cylinder(d=TubeData[Tube][T_PUNCHOD],h=OAH,$fn=NumSides); // boss in chassis punch hole
    }
    for (i=[0:(TubeData[Tube][T_NUMPINS] – 1)]) // tube pins
    rotate(i*360/TubeData[Tube][T_NUMPINS])
    translate([TubeData[Tube][T_PINBCD]/2,0,(OAH – TubeData[Tube][T_PINLEN])])
    rotate(180/4)
    PolyCyl(TubeData[Tube][T_PINOD],(TubeData[Tube][T_PINLEN] + Protrusion),4);
    for (i=[-1,1]) // mounting screw holes & nut traps / threaded inserts
    translate([i*TubeData[Tube][T_SCREWOC]/2,0,-Protrusion]) {
    PolyCyl(Nut[OD],(Nut[LENGTH] + Protrusion),NutSides);
    PolyCyl(Nut[ID],(OAH + 2*Protrusion),NutSides);
    }
    translate([0,0,-Protrusion]) { // LED recess
    PolyCyl(Pixel[OD],(Pixel[LENGTH] + Protrusion),8);
    }
    translate([0,0,(Pixel[LENGTH] – Protrusion)]) { // light pipe
    rotate(180/TubeData[Tube][T_NUMPINS])
    PolyCyl(TubeData[Tube][T_PIPEOD],(OAH + 2*Protrusion),TubeData[Tube][T_NUMPINS]);
    }
    }
    // Totally ad-hoc support structures …
    if (Support) {
    color("Yellow") {
    for (i=[-1,1]) // nut traps
    translate([i*TubeData[Tube][T_SCREWOC]/2,0,(Nut[LENGTH] – ThreadThick)/2])
    for (a=[0:5])
    rotate(a*30 + 15)
    cube([2*ThreadWidth,0.9*Nut[OD],(Nut[LENGTH] – ThreadThick)],center=true);
    if (Pixel[OD] > TubeData[Tube][T_PIPEOD]) // support pipe only if needed
    translate([0,0,(Pixel[LENGTH] – ThreadThick)/2])
    for (a=[0:7])
    rotate(a*22.5)
    cube([2*ThreadWidth,0.9*Pixel[OD],(Pixel[LENGTH] – ThreadThick)],center=true);
    }
    }
    }
    //———————-
    // Build it
    if (Layout == "Cap") {
    if (Section)
    difference() {
    Cap();
    translate([-CapSize[OD],0,CapSize[LENGTH]])
    cube([2*CapSize[OD],2*CapSize[OD],3*CapSize[LENGTH]],center=true);
    }
    else
    Cap();
    }
    if (Layout == "FinCap") {
    if (Section) render(convexity=5)
    difference() {
    FinCap();
    // translate([0,-FinCapSize[OD],FinCapSize[LENGTH]])
    // cube([2*FinCapSize[OD],2*FinCapSize[OD],3*FinCapSize[LENGTH]],center=true);
    translate([-FinCapSize[OD],0,FinCapSize[LENGTH]])
    cube([2*FinCapSize[OD],2*FinCapSize[OD],3*FinCapSize[LENGTH]],center=true);
    }
    else
    FinCap();
    }
    if (Layout == "BuildFinCap")
    translate([0,0,FinCapSize[LENGTH]])
    rotate([180,0,0])
    FinCap();
    if (Layout == "LampBase")
    LampBase();
    if (Layout == "USBPort")
    USBPort();
    if (Layout == "Socket")
    if (Section) {
    difference() {
    Socket();
    translate([-100/2,0,-Protrusion])
    cube([100,50,50],center=false);
    }
    }
    else
    Socket();
    if (Layout == "Sockets") {
    translate([0,50,0])
    Socket("Mini7");
    translate([0,20,0])
    Socket("Octal");
    translate([0,-15,0])
    Socket("Duodecar");
    translate([0,-50,0])
    Socket("Noval");
    }