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

  • 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");
    }
  • Command-Line CD Ripping, Redux

    A slight improvement to my two-step manual CD ripping process, with the intent of avoiding any thoughts about abcde:

    cdparanoia -B -v ; eject cdrom
    

    Ejecting the CD after cdparanoid finishes with it provides a visual cue for the next step.

    Set up the disk number and maximum number of tracks, then unleash lame:

    d=1
    tm=19
    for t in $(seq -w 1 $tm) ; do lame --preset tape --tt "D${d}:T${t}" --ta "Michael Lewis" --tl "The Big Short" --tn "${t}/${tm}" --tg "Audio Book" --add-id3v2 track${t}.cdda.wav D${d}-${t}.mp3 ; done
    rm track*
    

    The $(seq -w 1 $tm) expansion generates a list of zero-filled numbers for the tracks.

    There’s surely a one-liner to extract $tm, the maximum track number, from the track* files, but I’ll leave that for later.

    You can increment the disk number with let "d++" if the ripping goes smoothly. If not, that’s fraught with peril, because you (well, I) will do it once too often.

    Iterate for each CD in the set, washing & primping as needed for good results.

    And that’s that, at least for a while…

  • Raspberry Pi Streaming Radio Player: Minimum Viable Product

    With the numeric keypad producing events, and the USB audio box producing sound, the next steps involve starting mplayer through Python’s subprocess interface and feeding keystrokes into it.

    There’s not much to it:

    As much hardware doc as you need:

    RPi Streaming Player - first lashup
    RPi Streaming Player – first lashup

    The green plug leads off to a set of decent-quality PC speakers with far more bass drive than seems absolutely necessary in this context. The usual eBay vendor bungled an order for the adapter between the RCA line-out jacks and the 3.5 mm plug that will avoid driving the speakers from the UCA202’s headphone monitor output; I doubt that will make any audible difference. If you need an adapter with XLR female to 1/4 inch mono, let me know…

    The keypad labels provide all the UI documentation there is:

    Numeric Keypad - stream labels
    Numeric Keypad – stream labels

    The Python source code as a GitHub Gist:

    from evdev import InputDevice,ecodes,KeyEvent
    import subprocess32
    Media = {'KEY_KP7' : ['mplayer','http://relay.publicdomainproject.org:80/classical.aac'%5D,
    'KEY_KP8' : ['mplayer','http://relay.publicdomainproject.org:80/jazz_swing.aac'%5D,
    'KEY_KP9' : ['mplayer','http://live.str3am.com:2070/wmht1'%5D,
    'KEY_KP6' : ['mplayer','http://pubint.ic.llnwd.net/stream/pubint_wamc'%5D,
    'KEY_KP1' : ['mplayer','-playlist','http://dir.xiph.org/listen/5423257/listen.m3u'%5D,
    'KEY_KP2' : ['mplayer','-playlist','http://dir.xiph.org/listen/5197460/listen.m3u'%5D,
    'KEY_KP3' : ['mplayer','-playlist','http://dir.xiph.org/listen/5372471/listen.m3u'%5D,
    'KEY_KP0' : ['mplayer','-playlist','http://dir.xiph.org/listen/5420157/listen.m3u'%5D
    }
    Controls = {'KEY_KPSLASH' : '/',
    'KEY_KPASTERISK' : '*',
    'KEY_KPDOT' : ' '
    }
    k=InputDevice('/dev/input/keypad')
    print 'Starting mplayer'
    p = subprocess32.Popen(Media['KEY_KP7'],stdin=subprocess32.PIPE)
    print ' … running'
    for e in k.read_loop():
    if (e.type == ecodes.EV_KEY) and (KeyEvent(e).keystate == 1):
    kc = KeyEvent(e).keycode
    if kc == 'KEY_NUMLOCK':
    continue
    print "Got: ",kc
    if kc == 'KEY_BACKSPACE':
    print 'Backspace = shutdown!'
    p = subprocess32.call(['sudo','halt'])
    break
    if kc in Controls:
    print 'Control:', kc
    p.stdin.write(Controls[kc])
    if kc in Media:
    print 'Switching stream to ',Media[kc]
    print ' … halting'
    p.communicate(input='q')
    print ' … restarting'
    p = subprocess32.Popen(Media[kc],stdin=subprocess32.PIPE)
    print ' … running'
    print "Out of loop!"

    The Media dictionary relates keycodes with the command line parameters required to fire mplayer at the streaming stations. With that running, the Controls dictionary turns keycodes into mplayer keyboard controls.

    There’s no display: you have no idea what’s going on. I must start the program manually through an ssh session and can watch mplayer‘s console output.

    Poking the Halt button forcibly halts the RPi, after which you squeeze the Reset button to reboot the thing. There’s no indication that it’s running, other than sound coming out of the speakers, and no way to tell it fell of the rails other than through the ssh session.

    The loop blocks on events, so it can’t also extract stream titles from the (not yet implemented) mplayer stdout pipe / file and paste them on the (missing) display; that’s gotta go.

    There’s a lot not to like about all that, of course, but it’s in the tradition of getting something working to discover how it fails and, in this case, how it sounds, which is even more important.

  • Raspberry Pi: USB Keypad Via evdev

    The general idea is to use keystrokes plucked from a cheap numeric keypad to control mplayer, with the intent of replacing some defunct CD players and radios and suchlike. The keypads look about like you’d expect:

    Numeric keypads
    Numeric keypads

    The keypad layouts are, of course, slightly different (19 vs 18 keys!) and they behave differently with regard to their NumLock state, but at least they produce the same scancodes for the corresponding keys. The black (wired) keypad has a 000 button that sends three 0 events in quick succession, which isn’t particularly useful in this application.

    With the appropriate udev rule in full effect, this Python program chews its way through incoming events and reports only the key-down events that will eventually be useful:

    from evdev import InputDevice,ecodes,KeyEvent
    k=InputDevice('/dev/input/keypad')
    for e in k.read_loop():
    if (e.type == ecodes.EV_KEY) and (KeyEvent(e).keystate == 1):
    if (KeyEvent(e).keycode == 'KEY_NUMLOCK'):
    continue # we don't care about the NumLock state
    else:
    print KeyEvent(e).scancode, KeyEvent(e).keycode

    Pressing the keys on the white keypad in an obvious sequence produces the expected result:

    82 KEY_KP0
    79 KEY_KP1
    80 KEY_KP2
    81 KEY_KP3
    75 KEY_KP4
    76 KEY_KP5
    77 KEY_KP6
    71 KEY_KP7
    72 KEY_KP8
    73 KEY_KP9
    98 KEY_KPSLASH
    55 KEY_KPASTERISK
    14 KEY_BACKSPACE
    74 KEY_KPMINUS
    78 KEY_KPPLUS
    96 KEY_KPENTER
    83 KEY_KPDOT
    

    Observations

    • KeyEvent(e).keycode is a string: 'KEY_KP0'
    • e.type is numeric, so just compare against evcodes.EV_KEY
    • KeyEvent(e).scancode is the numeric key identifier
    • KeyEvent(e).keystate = 1 for the initial press
    • Those KeyEvent(e).key_down/up/hold values don’t change

    If you can type KEY_KP0 correctly, wrapping it in quotes isn’t such a big stretch, so I don’t see much point to running scancodes through ecodes.KEY[KeyEvent(e).scancode] just to compare the enumerations.

    I’m surely missing something Pythonic, but I don’t get the point of attaching key_down/up/hold constants to the key event class. I suppose that accounts for changed numeric values inside inherited classes, but … sheesh.

    Anyhow, that loop looks like a good starting point.

  • Improved Lip Balm / Lipstick Holder

    Mary asked for a less angular version of the Lip Balm Holder, which gave me a chance to practice my list comprehension:

    Improved Lipstick and Balm Holder
    Improved Lipstick and Balm Holder

    You hand the OpenSCAD program a list of desired tube diameters in the order you want them, the program plunks the first one (ideally, the largest diameter) in the middle, arranges the others around it counterclockwise from left to right, then slips a lilypad under each tube.

    As long as you don’t ask for anything egregiously stupid, the results look reasonably good:

    Improved Lipstick and Balm Holder - 8 tubes
    Improved Lipstick and Balm Holder – 8 tubes

    As before, each tube length is 1.5 times its diameter; the lipsticks / balms fit loosely and don’t flop around.

    Given the tube diameters and the wall thickness, list comprehensions simplify creating lists of the radii from the center tube to each surrounding tube, the center-to-center distances between each of the outer tubes, and the angles between successive tubes:

    // per-tube info, first element forced to 0 to make entries match RawDia vector indexes
    
    Radius = [0, for (i=[1:NumTubes-1]) (TubeRad[0] + TubeRad[i] + Wall)];			// Tube[i] distance to center pointRadius = [0, for (i=[1:NumTubes-1]) (TubeRad[0] + TubeRad[i] + Wall)];			// Tube[i] distance to center point
    echo(str("Radius: ",Radius));
    
    CtrToCtr = [0, for (i=[1:NumTubes-2]) (TubeRad[i] + TubeRad[i+1] + Wall)];		// Tube[i] distance to Tube[i+1]
    echo(str("CtrToCtr: ",CtrToCtr));
    
    Angle = [0, for (i=[1:NumTubes-2]) acos((pow(Radius[i],2) + pow(Radius[i+1],2) - pow(CtrToCtr[i],2)) / (2 * Radius[i] * Radius[i+1]))];
    echo(str("Angle: ",Angle));
    
    TotalAngle = sumv(Angle,len(Angle)-1);
    echo(str("TotalAngle: ",TotalAngle));
    

    The angles come from the oblique triangle solution when you know all three sides (abc) and want the angle (C) between a and b:

    C = arccos( (a2 + b2 - c2) / (2ab) )

    Peering down inside, the Slic3r preview shows the lily pads are the tops of squashed spheres:

    Improved Lipstick and Balm Holder - Slic3r preview
    Improved Lipstick and Balm Holder – Slic3r preview

    The pads are 2.0 times the tube diameter, which seemed most pleasing to the eye. They top out at 2.0 mm thick, which might make the edges too thin for comfort.

    Update: Here’s what it looks like with a convex hull wrapped around all the lilypads:

    Improved Lipstick and Balm Holder - hulled base
    Improved Lipstick and Balm Holder – hulled base

    I’m awaiting reports from My Spies concerning the typical diameter(s) of lipstick tubes, then I’ll run off a prototype and see about the lily pad edges.

    The OpenSCAD source code as a GitHub gist:

    // Lipstick and Balm Tube Holder
    // Ed Nisley KE4ZNU – February 2016
    //- Extrusion parameters – must match reality!
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1;
    HoleWindage = 0.2;
    //——
    // Dimensions
    RawDia = [26,21,19,17,19,21]; // actual tube diameters in desired order, center = largest first
    NumTubes = len(RawDia);
    Clearance = 2.0;
    TubeDia = [for (i=[0:NumTubes-1]) (RawDia[i] + Clearance)]; // actual tube diameters
    TubeRad = TubeDia / 2;
    echo(str("NumTubes: ",NumTubes));
    Wall = 2.0;
    BaseThick = 2.0;
    BaseFactor = 2.0;
    NumSides = 8*4;
    // per-tube info, first element forced to 0 to make entries match RawDia vector indexes
    Radius = [0, for (i=[1:NumTubes-1]) (TubeRad[0] + TubeRad[i] + Wall)]; // Tube[i] distance to center point
    echo(str("Radius: ",Radius));
    CtrToCtr = [0, for (i=[1:NumTubes-2]) (TubeRad[i] + TubeRad[i+1] + Wall)]; // Tube[i] distance to Tube[i+1]
    echo(str("CtrToCtr: ",CtrToCtr));
    Angle = [0, for (i=[1:NumTubes-2]) acos((pow(Radius[i],2) + pow(Radius[i+1],2) – pow(CtrToCtr[i],2)) / (2 * Radius[i] * Radius[i+1]))];
    echo(str("Angle: ",Angle));
    TotalAngle = sumv(Angle,len(Angle)-1);
    echo(str("TotalAngle: ",TotalAngle));
    //———————-
    // Useful routines
    // vector sum cribbed from doc
    function sumv(v,i,s=0) = (i==s ? v[i] : v[i] + sumv(v,i-1,s));
    //———————-
    //- Build it
    for (i=[0:NumTubes-1])
    rotate(90 – TotalAngle/2 + sumv(Angle, (i>0) ? (i-1) : 0))
    translate([Radius[i],0,0]) {
    resize([0,0,2*BaseThick]) // bases
    difference() {
    sphere(r=BaseFactor*TubeRad[i],$fn=NumSides);
    translate([0,0,-BaseFactor*TubeDia[i]])
    cube(2*BaseFactor*TubeDia[i],center=true);
    }
    difference() { // tubes
    cylinder(r=TubeRad[i] + Wall,h=1.5*TubeDia[i] + BaseThick,$fn=NumSides);
    cylinder(d=TubeDia[i],h=1.5*TubeDia[i] + BaseThick + Protrusion,$fn=NumSides);
    }
    }
  • Security By Obscurity: Not In Full Effect

    The library kiosk that handles paying your overdue book fines:

    Fine payment kiosk with driver info
    Fine payment kiosk with driver info

    Now, you’d need to know a few things about what’s going on inside, but I’d say they’re pretty much rolling out the welcome mat for you to find those things out…

    Wanna bet it’s running Windows, just like all the electronic voting machines?

  • UDEV Rules for Cheap Numeric Keypads

    I’m thinking of numeric keypads as control panels for Raspberry Pi projects like a simpleminded streaming radio player, so:

    Numeric keypads
    Numeric keypads

    Sorta like wedding pictures: you can expose for the groom-in-black or the bride-in-white, but not both at the same time.

    The wireless keypad does not have a slot for the USB radio: put ’em in a bag to keep ’em together when not in use.

    The general idea is to create a standard name (/dev/input/keypad) for either keypad when it gets plugged in, so the program need not figure out the device name from first principles. This being an embedded system, I can ensure only one keypad will be plugged in at any one time.

    The wired keypad has an odd name that makes a certain perverse sense:

    cat /proc/bus/input/devices
    ... snippage ...
    I: Bus=0003 Vendor=13ba Product=0001 Version=0110
    N: Name="HID 13ba:0001"
    P: Phys=usb-20980000.usb-1.4/input0
    S: Sysfs=/devices/platform/soc/20980000.usb/usb1/1-1/1-1.4/1-1.4:1.0/0003:13BA:0001.0007/input/input6
    U: Uniq=
    H: Handlers=sysrq kbd event0
    B: PROP=0
    B: EV=120013
    B: KEY=10000 7 ff800000 7ff febeffdf f3cfffff ffffffff fffffffe
    B: MSC=10
    B: LED=7
    

    It’s a single-function device, so this rule in /etc/udev/rules.d/KeyPad.rules suffices:

    ATTRS{name}=="HID 13ba:0001", SYMLINK+="input/keypad"
    

    Using the Vendor and Device ID strings (13ba:0001) might make more sense.

    The wireless keypad isn’t nearly that easy, because it reports for duty as both a keyboard and a mouse:

    cat /proc/bus/input/devices
    ... snippage ...
    I: Bus=0003 Vendor=062a Product=4101 Version=0110
    N: Name="MOSART Semi. 2.4G Keyboard Mouse"
    P: Phys=usb-20980000.usb-1.4/input0
    S: Sysfs=/devices/platform/soc/20980000.usb/usb1/1-1/1-1.4/1-1.4:1.0/0003:062A:4101.0008/input/input7
    U: Uniq=
    H: Handlers=sysrq kbd event0
    B: PROP=0
    B: EV=120013
    B: KEY=10000 7 ff9f207a c14057ff febeffdf ffefffff ffffffff fffffffe
    B: MSC=10
    B: LED=7
    
    I: Bus=0003 Vendor=062a Product=4101 Version=0110
    N: Name="MOSART Semi. 2.4G Keyboard Mouse"
    P: Phys=usb-20980000.usb-1.4/input1
    S: Sysfs=/devices/platform/soc/20980000.usb/usb1/1-1/1-1.4/1-1.4:1.1/0003:062A:4101.0009/input/input8
    U: Uniq=
    H: Handlers=kbd mouse0 event2
    B: PROP=0
    B: EV=1f
    B: KEY=3f 3007f 0 0 0 0 483ffff 17aff32d bf544446 0 0 1f0001 130f93 8b17c000 677bfa d941dfed 9ed680 4400 0 10000002
    B: REL=1c3
    B: ABS=1f01 0
    B: MSC=10
    

    That may be because the 0x06a2 Vendor ID was cloned (that’s pronounced “ripped-off”) from Creative Labs. My guess is they ripped the entire chipset, because the 0x4101 device ID came from a Creative Labs wireless keyboard + mouse:

    lsusb
    ... snippage ...
    Bus 001 Device 011: ID 062a:4101 Creative Labs
    ... snippage ...
    

    Because it’s a dual-mode wireless device, we need more information to create the corresponding udev rule. The keyboard part appears (on this boot) as event0, which we find thusly:

    ll /dev/input/by-id
    total 0
    lrwxrwxrwx 1 root root 9 Feb  5 17:39 usb-Burr-Brown_from_TI_USB_Audio_CODEC-event-if03 -> ../event1
    lrwxrwxrwx 1 root root 9 Feb  5 17:39 usb-MOSART_Semi._2.4G_Keyboard_Mouse-event-kbd -> ../event0
    lrwxrwxrwx 1 root root 9 Feb  5 17:39 usb-MOSART_Semi._2.4G_Keyboard_Mouse-if01-event-mouse -> ../event2
    lrwxrwxrwx 1 root root 9 Feb  5 17:39 usb-MOSART_Semi._2.4G_Keyboard_Mouse-if01-mouse -> ../mouse0
    

    Some spelunking suggests using the environment variables set up by the default udev rules, which we find thusly:

    udevadm test /sys/class/input/event0
    ... vast snippage ...
    .INPUT_CLASS=kbd
    ACTION=add
    DEVLINKS=/dev/input/by-id/usb-MOSART_Semi._2.4G_Keyboard_Mouse-event-kbd /dev/input/by-path/platform-20980000.usb-usb-0:1.4:1.0-event-kbd
    DEVNAME=/dev/input/event0
    DEVPATH=/devices/platform/soc/20980000.usb/usb1/1-1/1-1.4/1-1.4:1.0/0003:062A:4101.0012/input/input17/event0
    ID_BUS=usb
    ID_INPUT=1
    ID_INPUT_KEY=1
    ID_INPUT_KEYBOARD=1
    ID_MODEL=2.4G_Keyboard_Mouse
    ID_MODEL_ENC=2.4G\x20Keyboard\x20Mouse
    ID_MODEL_ID=4101
    ID_PATH=platform-20980000.usb-usb-0:1.4:1.0
    ID_PATH_TAG=platform-20980000_usb-usb-0_1_4_1_0
    ID_REVISION=0108
    ID_SERIAL=MOSART_Semi._2.4G_Keyboard_Mouse
    ID_TYPE=hid
    ID_USB_DRIVER=usbhid
    ID_USB_INTERFACES=:030101:030102:
    ID_USB_INTERFACE_NUM=00
    ID_VENDOR=MOSART_Semi.
    ID_VENDOR_ENC=MOSART\x20Semi.
    ID_VENDOR_ID=062a
    MAJOR=13
    MINOR=64
    SUBSYSTEM=input
    ... more snippage ...
    

    So when that vendor and device appear with ID_INPUT_KEYBOARD set, we can create a useful symlink using this rule in /etc/udev/rules.d/KeyPad.rules:

    ATTRS{idVendor}=="062a", ATTRS{idProduct}=="4101", ENV{ID_INPUT_KEYBOARD}=="1", SYMLINK+="input/keypad"
    

    Because only one keypad will be plugged in at any one time, the /etc/udev/rules.d/KeyPad.rules file can contain both rules:

    ATTRS{name}=="HID 13ba:0001", SYMLINK+="input/keypad"
    ATTRS{idVendor}=="062a", ATTRS{idProduct}=="4101", ENV{ID_INPUT_KEYBOARD}=="1", SYMLINK+="input/keypad"
    

    Reload the rules and fire them off:

    sudo udevadm control --reload
    sudo udevadm trigger
    

    And then It Just Works:

    ll /dev/input/by-id
    total 0
    lrwxrwxrwx 1 root root 9 Feb  5 17:39 usb-Burr-Brown_from_TI_USB_Audio_CODEC-event-if03 -> ../event1
    lrwxrwxrwx 1 root root 9 Feb  5 19:03 usb-MOSART_Semi._2.4G_Keyboard_Mouse-event-kbd -> ../event0
    lrwxrwxrwx 1 root root 9 Feb  5 19:03 usb-MOSART_Semi._2.4G_Keyboard_Mouse-if01-event-mouse -> ../event2
    lrwxrwxrwx 1 root root 9 Feb  5 19:03 usb-MOSART_Semi._2.4G_Keyboard_Mouse-if01-mouse -> ../mouse0ll /dev/input
    
    ll /dev/input
    total 0
    drwxr-xr-x 2 root root     120 Feb  5 19:03 by-id
    drwxr-xr-x 2 root root     120 Feb  5 19:03 by-path
    crw-rw---- 1 root input 13, 64 Feb  5 19:03 event0
    crw-rw---- 1 root input 13, 65 Feb  5 17:39 event1
    crw-rw---- 1 root input 13, 66 Feb  5 19:03 event2
    lrwxrwxrwx 1 root root       6 Feb  5 19:03 keypad -> event0
    crw-rw---- 1 root input 13, 63 Feb  5 17:39 mice
    crw-rw---- 1 root input 13, 32 Feb  5 19:03 mouse0
    

    My configuration hand is strong

    Note: Once again, I manually restored the source code after the WordPress “improved” editor shredded it by replacing all the double-quote and greater-than symbols inside the “protected” sourcecode blocks with their HTML-escaped equivalents. Some breakage may remain and, as always, WP can shred sourcecode blocks even if I don’t edit the post. They’ve (apparently) banned me from contacting Support, because of an intemperate rant based on years of having them ignore this (and other) problems. I didn’t expect any real help, so this isn’t much of a step backwards in terms of actual support …