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

  • Vacuum Tube LEDs: Now With Morse Code

    Adding Mark Fickett’s non-blocking Morse Arduino library turns the tubes into transmitters:

    21HB5A on platter - orange green
    21HB5A on platter – orange green

    The plate cap LED blinks the message in orange, while both LEDs continue to slowly change color as before.

    You define a Morse sender object (C++, yo!) by specifying its output pin and code speed in words per minute, dump a string into it, then call a continuation function fast enough to let it twiddle the output bit for each pulse. Obviously, the rate at which the callback happens determines the timing granularity.

    However, setting a knockoff Neopixel to a given color requires more than just a binary signal on an output pin. The continuation function returns false when it’s done with the message, after which you can initialize and send another message. There’s no obvious (to me, anyhow) way to get timing information out of the code.

    The easiest solution: called the Morse continuation function at the top of the main loop, read its output pin to determine when a dit or dah is active, then set the plate cap color accordingly:

    LEDMorseSender Morse(PIN_MORSE, (float)MORSE_WPM);
    ...
    Morse.setup();
    Morse.setMessage(String("       cq cq cq de ke4znu       "));
    PrevMorse = ThisMorse = digitalRead(PIN_MORSE);
    ...
    if (!Morse.continueSending()) {
      Morse.startSending();
    }
    ThisMorse = digitalRead(PIN_MORSE);
    ...
    if (ThisMorse) {             // if Morse output high, overlay
        strip.setPixelColor(PIXEL_MORSE,MorseColor);
    }
    PrevMorse = ThisMorse;
    strip.show();               // send out precomputed colors
    ...
    <<compute colors for next iteration as usual>>
    

    I use the Entropy library to seed the PRNG, then pick three prime numbers for the sine wave periods (with an ugly hack to avoid matching periods):

    uint32_t rn = Entropy.random();
    ...
    randomSeed(rn);
    ...
    
    Pixels[RED].Prime = PrimeList[random(sizeof(PrimeList))];
    
    do {
      Pixels[GREEN].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[RED].Prime == Pixels[GREEN].Prime);
    
    do {
      Pixels[BLUE].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[BLUE].Prime == Pixels[RED].Prime ||
            Pixels[BLUE].Prime == Pixels[GREEN].Prime);
    
    printf("Primes: (%d,%d,%d)\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime);
    

    In the spirit of “Video or it didn’t happen”: YouTube!

    The Arduino source code as a GitHub Gist:

    // Neopixel mood lighting for vacuum tubes
    // Ed Nisley – KE4ANU – June 2016
    // September 2016 – Add Morse library and blinkiness
    #include <Adafruit_NeoPixel.h>
    #include <morse.h>
    #include <Entropy.h>
    //———-
    // Pin assignments
    const byte PIN_NEO = A3; // DO – data out to first Neopixel
    const byte PIN_HEARTBEAT = 13; // DO – Arduino LED
    #define PIN_MORSE 12
    //———-
    // Constants
    #define PIXELS 2
    #define PIXEL_MORSE 1
    #define MORSE_WPM 10
    #define UPDATEINTERVAL 50ul
    const unsigned long UpdateMS = UPDATEINTERVAL – 1ul; // update LEDs only this many ms apart (minus loop() overhead)
    // number of steps per cycle, before applying prime factors
    #define RESOLUTION 250
    // want to randomize the startup a little?
    #define RANDOMIZE true
    //———-
    // Globals
    // instantiate the Neopixel buffer array
    Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXELS, PIN_NEO, NEO_GRB + NEO_KHZ800);
    uint32_t FullWhite = strip.Color(255,255,255);
    uint32_t FullOff = strip.Color(0,0,0);
    uint32_t MorseColor = strip.Color(255,191,0);
    struct pixcolor_t {
    byte Prime;
    unsigned int NumSteps;
    unsigned int Step;
    float StepSize;
    byte MaxPWM;
    };
    unsigned int PlatterSteps;
    byte PrimeList[] = {3,5,7,13,19,29};
    // colors in each LED
    enum pixcolors {RED, GREEN, BLUE, PIXELSIZE};
    struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
    uint32_t UniColor;
    unsigned long MillisNow;
    unsigned long MillisThen;
    // Morse code
    LEDMorseSender Morse(PIN_MORSE, (float)MORSE_WPM);
    uint8_t PrevMorse, ThisMorse;
    //– Figure PWM based on current state
    byte StepColor(byte Color, float Phi) {
    byte Value;
    Value = (Pixels[Color].MaxPWM / 2.0) * (1.0 + sin(Pixels[Color].Step * Pixels[Color].StepSize + Phi));
    // Value = (Value) ? Value : Pixels[Color].MaxPWM; // flash at dimmest points
    return Value;
    }
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //——————
    // Set the mood
    void setup() {
    pinMode(PIN_HEARTBEAT,OUTPUT);
    digitalWrite(PIN_HEARTBEAT,LOW); // show we arrived
    Serial.begin(57600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    printf("Vacuum Tube Mood Light\r\nEd Nisley – KE4ZNU – September 2016\r\n");
    Entropy.initialize(); // start up entropy collector
    // set up Neopixels
    strip.begin();
    strip.show();
    // lamp test: a brilliant white flash
    printf("Lamp test: flash white\r\n");
    for (byte i=0; i<3 ; i++) {
    for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with white
    strip.setPixelColor(j,FullWhite);
    }
    strip.show();
    delay(500);
    for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with black
    strip.setPixelColor(j,FullOff);
    }
    strip.show();
    delay(500);
    }
    // set up real random numbers
    uint32_t rn = Entropy.random();
    if (RANDOMIZE) {
    printf("Preloading LED array with seed: %08lx\r\n",rn);
    randomSeed(rn);
    }
    else {
    printf("Start not randomized\r\n");
    }
    printf("First random number: %ld\r\n",random(10));
    // set up the color generators
    Pixels[RED].Prime = PrimeList[random(sizeof(PrimeList))];
    do {
    Pixels[GREEN].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[RED].Prime == Pixels[GREEN].Prime);
    do {
    Pixels[BLUE].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[BLUE].Prime == Pixels[RED].Prime ||
    Pixels[BLUE].Prime == Pixels[GREEN].Prime);
    printf("Primes: (%d,%d,%d)\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime);
    Pixels[RED].MaxPWM = 255;
    Pixels[GREEN].MaxPWM = 255;
    Pixels[BLUE].MaxPWM = 255;
    for (byte c=0; c < PIXELSIZE; c++) {
    Pixels[c].NumSteps = RESOLUTION * (unsigned int) Pixels[c].Prime;
    Pixels[c].Step = RANDOMIZE ? random(Pixels[c].NumSteps) : (3*Pixels[c].NumSteps)/4;
    Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // in radians per step
    printf("c: %d Steps: %d Init: %d",c,Pixels[c].NumSteps,Pixels[c].Step);
    printf(" PWM: %d\r\n",Pixels[c].MaxPWM);
    }
    // set up Morse generator
    printf("Morse %d wpm\n",MORSE_WPM);
    Morse.setup();
    Morse.setMessage(String(" cq cq cq de ke4znu "));
    PrevMorse = ThisMorse = digitalRead(PIN_MORSE);
    MillisNow = MillisThen = millis();
    }
    //——————
    // Run the mood
    void loop() {
    if (!Morse.continueSending()) {
    Morse.startSending();
    }
    ThisMorse = digitalRead(PIN_MORSE);
    MillisNow = millis();
    if (((MillisNow – MillisThen) > UpdateMS) || // time for color change?
    (PrevMorse != ThisMorse)) { // Morse output bit changed?
    digitalWrite(PIN_HEARTBEAT,HIGH);
    if (ThisMorse) { // if Morse output high, overlay
    strip.setPixelColor(PIXEL_MORSE,MorseColor);
    }
    PrevMorse = ThisMorse;
    strip.show(); // send out precomputed colors
    for (byte c=0; c < PIXELSIZE; c++) { // compute next increment for each color
    if (++Pixels[c].Step >= Pixels[c].NumSteps) {
    Pixels[c].Step = 0;
    printf("Cycle %d steps %d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow – MillisThen));
    }
    }
    byte Value[PIXELSIZE];
    for (byte c=0; c < PIXELSIZE; c++) { // … for each color
    Value[c] = StepColor(c,0.0); // figure new PWM value
    }
    UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
    for (int j=0; j < strip.numPixels(); j++) { // fill all LEDs with color
    strip.setPixelColor(j,UniColor);
    }
    MillisThen = MillisNow;
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    }
    view raw TubeMorse.ino hosted with ❤ by GitHub
  • Makergear M2: Spring-Loaded Extruder Feed Gear

    When I installed the new fine-tooth filament drive gear (wheel, whatever) in the M2, I ran some numbers that suggested replacing the fixed-position screw with a (more-or-less-)constant-force spring. Some recent discussions on the M2 forum suggest, at least to me, that the drive gear is, indeed, less forgiving of filament diameter variations, drive housing wear, and suchlike than the chunkier old gear.

    Having recently bought an assortment of longer M4 screws, I finally got around to installing an appropriate spring from the Big Box o’ Springs and another washer to capture it:

    Makergear M2 - spring-loaded filament drive
    Makergear M2 – spring-loaded filament drive

    Before doing anything, I measured the gap between the filament drive body (on the left) and the lever arm (on the right) holding the idler bearing: 21 mil = 0.53 mm.

    I don’t have a number for the spring constant; it’s rather stiff.

    After installing the spring, I cranked the screw to restore the same gap as before, which should mean the spring is exerting roughly the same force on the arm as the fixed-position screw.

    The general idea: the spring allows the flexible arm to move as the filament diameter changes, while maintaining roughly the same pressure on the drive gear, thus producing nearly the same depth-of-engagement grooves in the filament. Maintaining “the same pressure” requires the motion to be relatively small compared to the spring preload distance, which seems reasonable with ±0.1 mm diameter variations and maybe 5 mm of preload.

    The new filament drive gear hasn’t ever stripped out (after that initial finger fumble), so this will be more of a test to verify that the spring doesn’t make the situation worse.

  • Rewiring a Baofeng Battery Eliminator

    An aftermarket “battery eliminator” for Baofeng UV-5R radios costs under seven bucks delivered:

    Baofeng Battery Eliminator - overview
    Baofeng Battery Eliminator – overview

    That label seemed … odd:

    Baofeng Battery Eliminator - Li-ion Label
    Baofeng Battery Eliminator – Li-ion Label

    The OEM battery, tucked inside a case that’s for all intents and purposes identical to this one, sports an 1800 mA·h rating that I regarded as mmmm optimistic; I’d expect maybe 1000 mA·h, tops. From what I can tell, the 3800 mA·h label should go on an extended-capacity “big” battery that wraps around the bottom of the radio. Maybe the factory produced a pallet of mis-labeled small packs that they couldn’t fob off on actual customers with a straight face and couldn’t justify the labor to peel-and-stick the proper labels.

    Anyhow, it’s not a battery.

    The circuitry inside shows considerably more fit & finish than I expected:

    Baofeng Battery Eliminator - interior
    Baofeng Battery Eliminator – interior

    It’s not clear how effective that heatsink could be, given that it’s trapped inside a compact plastic enclosure snugged against the radio’s metal chassis, but it’s a nice touch. Two layers of foam tape anchor the terminals at the top and hold the heatsink / LM7808-class TO-220 regulator in place.

    Although I wanted the DC input to come from the side, rather than the bottom, so the radio could stand up, the pack simply isn’t thick enough to accommodate the jack in that orientation. I drilled out the existing wire hole to fit a coaxial power plug and deployed my own foam tape:

    Baofeng Battery Eliminator - rewired interior
    Baofeng Battery Eliminator – rewired interior

    Replacing the foam tape at the top holds the bent-brass (?) terminals in more-or-less the proper orientation, with Genuine 3M / Scotch Plaid adding a festive touch. A groove in the other half of the shell captures the free ends of those terminals, so they’re not flopping around in mid-air.

    The jack fits an old-school 7.5 V transformer wall wart that produces 11 V open-circuit. It’s probably still a bit too high with the UV-5R’s minimal receive-only load, but I refuse to worry.

    Now KE4ZNU-10 won’t become a lithium fire in the attic stairwell…

    While I had the hood up, I used Chirp to gut the radio’s stored frequencies / channels / memories and set 144.39 in Memory 0 as the only non-zero value. With a bit of luck, that will prevent it from crashing and jamming a randomly chosen frequency outside the amateur bands…

  • Vacuum Tube LEDs: Improved Sockets

    All the sockets now sport channels in the bottom to capture the braid to the plate cap (whether or not the tube has a plate cap) and the wiring from the Arduino:

    Vacuum Tube Lights - Octal Socket - solid model
    Vacuum Tube Lights – Octal Socket – solid model

    The Slic3r preview shows the detail a bit better:

    Vaccum Tube Lights - Octal Socket - Slic3r preview
    Vaccum Tube Lights – Octal Socket – Slic3r preview

    The boss around the pins is now 25 mm OD and snaps neatly into the unpunched hub hole of a hard drive platter:

    0D3 Octal - 25 mm socket OD in platter
    0D3 Octal – 25 mm socket OD in platter

    I moved the mounting holes to 42 mm OC to give the button heads on those screws a bit more clearance from the base.

    Moving the knockoff Neopixel up to the top of the pipe leading to the tube base dramatically increases the amount of light going into the tube envelope:

    0D3 Octal - 25 mm socket - raised LED
    0D3 Octal – 25 mm socket – raised LED

    You can just barely see a strip of foam tape holding the LED PCB (loosely) into the too-large hole.

    The OpenSCAD source code also produces the improved base clamp; to get a socket, just set Layout = "Socket" and away you go. It doesn’t yet have the reduced-diameter hole down the middle; that’s in the nature of fine tuning.

  • Vacuum Tube LEDs: Milling a 0D3 Spigot the Right Way

    Now, with the 0D3 tube properly clamped and aligned in the Sherline mill:

    OD3 Octal - V-block clamp
    OD3 Octal – V-block clamp

    I can slowly run an end mill down onto the spigot:

    0D3 Octal - milling spigot
    0D3 Octal – milling spigot

    Eventually converting the whole post into black dust in the vacuum cleaner:

    0D3 Octal - milled spigot
    0D3 Octal – milled spigot

    That was completely uneventful, which is pretty much the whole point of good fixturing, isn’t it?

    Applying the vacuum cleaner while milling seems to have kept the dust out of the base, although I’m not sure I can pull that trick off every time.

  • Improved Octal Tube Base Clamp

    In order to clamp the tube in a V-block, the clamp must position the tube’s centerline so the envelope will clear the V groove, thusly:

    OD3 Octal - V-block clamp
    OD3 Octal – V-block clamp

    The clamp now extends into the V-block and surrounds the entire Bakelite tube base:

    Octal base compression clamp - Slic3r preview
    Octal base compression clamp – Slic3r preview

    The little divot captures the clamp screw and the slot lets the whole affair compress just enough to firmly squeeze the entire tube base.

    The tube data table now includes columns for the envelope OD and the base OD, although only the 0D3 (and similar) Octal tubes in my collection have a bulging envelope and a smaller base. You can build clamps for cylindrical glass tubes if you like; I don’t vouch for the accuracy of the table contents.

    For whatever it’s worth, the 6SN7GTB tube I started with has a 32 mm Bakelite base and the 0D3 tube has a 29 mm base. That should probably justify two separate entries in the table, but I’m making this up as I go along.

    The OpenSCAD source code as a GitHub Gist:

    // Vacuum Tube LED Lights
    // Ed Nisley KE4ZNU February … September 2016
    Layout = "TubeClamp"; // Cap LampBase USBPort Bushings
    // Socket(s) Cap (Build)FinCap Platter[Base|Fixture]
    // TubeClamp PlatterParts
    DefaultSocket = "Octal";
    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
    // CD = 15 mm ID with raised ring at 37 mm, needs screw head clearance
    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 (must also clear evacuation tip / spigot)
    T_HOLEOD = 5; // nominal panel hole from various sources
    T_PUNCHOD = 6; // panel hole optimized for inch-size Greenlee punches
    T_BASEOD = 7; // base OD
    T_BULBOD = 8; // glass envelope OD
    T_PIPEOD = 9; // light pipe from LED to tube base (clear evac tip / spigot)
    T_SCREWOC = 10; // mounting screw holes
    T_PLATECAP = 11; // nonzero to print a plate cap
    // Name pins BCD dia length hole punch base bulb pipe screw cap
    TubeData = [
    ["Mini7", 8, 9.53, 1.016, 7.0, 16.0, 25.0, 18.0, 18.0, 5.0, 35.0, 0], // punch 11/16, screw 22.5 OC
    // ["Octal", 8, 17.45, 2.36, 11.0, 36.2, (8 + 1)/8 * inch, 32.0, 38.1, 11.5, 47.0, 1], // screw 39.0 OC, base 32 or 39
    ["Octal", 8, 17.45, 2.36, 11.0, 36.2, 25.0, 29.0, 38.1, 11.5, 42.0, 1], // platter + 4 mm screws
    ["Noval", 10, 11.89, 1.1016, 7.0, 22.0, 25.0, 21.0, 21.0, 7.5, 35.0, 0], // punch 7/8, screw 28.0 OC
    ["Magnoval", 10, 17.45, 1.27, 9.0, 29.7, (4 + 1)/4 * inch, 46.0, 46.0, 12.4, 38.2, 0], // similar to Novar
    // ["Duodecar", 13, 19.10, 1.05, 9.0, 32.0, (4 + 1)/4 * inch, 38.0, 38.0, 12.5, 47.0, 1], // screw was 39.0 OC
    ["Duodecar", 13, 19.10, 1.05, 9.0, 25.0, 25.0, 38.0, 38.0, 12.5, 42.0, 1], // fit un-punched drive platter
    ];
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Pixel = [7.0,10.0,3.0]; // ID = contact patch, OD = PCB dia, LENGTH = overall thickness
    PixelRecessHeight = 1.55*Pixel[LENGTH]; // enough of a recess to allow for tube top curvature
    SocketNut = // socket mounting: threaded insert or nut recess
    // [3.5,5.2,7.2] // 6-32 insert
    [4.0,6.0,5.9] // 4 mm short insert
    ;
    NutSides = 8;
    SocketShim = 2*ThreadThick; // between pin holes and pixel top
    SocketFlange = 1.5; // 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])];
    USBPCB =
    // [28,16,6.5] // small Sparkfun knockoff
    [36,18 + 1,5.8 + 0.4] // Deek-Robot fake FTDI with ISP header
    ;
    Platter = [25.0,95.0,1.26]; // hard drive platter dimensions
    PlatterSides = 8*4; // polygon approximation
    //———————-
    // 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(d=(FixDia + HoleWindage),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] + 2.0),(CapTube[OD] + 2.0*Pixel[LENGTH])];
    CapSides = 8*4;
    SkirtOD = CapSize[OD] + 4*ThreadWidth;
    CapTubeHeight = (CapSize[LENGTH] + PixelRecessHeight)/2;
    CapTubeBossOD = 1*ThreadWidth + 2*(CapTubeHeight – PixelRecessHeight)/cos(180/8);
    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=SkirtOD,d2=CapSize[OD],h=PixelRecessHeight,$fn=CapSides); // skirt
    translate([0,-SkirtOD/2,CapTubeHeight]) // boss around brass tube
    rotate([-90,0,0])
    rotate(180/8)
    cylinder(d=CapTubeBossOD,h=CapTube[LENGTH],$fn=8);
    }
    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],(PixelRecessHeight + Protrusion),CapSides);
    translate([0,0,(PixelRecessHeight – Protrusion)]) // small step + cone to retain PCB
    cylinder(d1=(Pixel[OD]/cos(180/CapSides) + HoleWindage),d2=Pixel[ID],h=(Pixel[LENGTH] + Protrusion),$fn=CapSides);
    translate([0,0,CapTubeHeight]) // 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],(PixelRecessHeight + 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,USBPCB[0]])
    rotate([90,0,0])
    linear_extrude(height=USBPCB[0])
    polygon(points=[
    [0,0],
    [USBPCB[1]/2,0],
    [USBPCB[1]/2,0.5*USBPCB[2]],
    [USBPCB[1]/3,USBPCB[2]],
    [-USBPCB[1]/3,USBPCB[2]],
    [-USBPCB[1]/2,0.5*USBPCB[2]],
    [-USBPCB[1]/2,0],
    ]);
    }
    //———————-
    // Box for Leviton ceramic lamp base
    module LampBase() {
    Insert = [3.5,5.2,7.2]; // 6-32 brass insert to match standard electrical screws
    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 = [Insert[OD], // insert for socket screws
    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);
    }
    }
    }
    //———————-
    // Base for hard drive platters
    module PlatterBase(TubeName = DefaultSocket) {
    PCB =
    [36,18,3] // Arduino Pro Mini
    ;
    Tube = search([TubeName],TubeData,1,0)[0];
    SocketHeight = Pixel[LENGTH] + SocketShim + TubeData[Tube][T_PINLEN] – PanelThick;
    echo(str("Base for ",TubeData[Tube][0]," socket"));
    Overhang = 5.5; // platter overhangs base by this much
    Bottom = 4*ThreadThick;
    Base = [(Platter[OD] – 3*Overhang), // smaller than 3.5 inch Sch 40 PVC pipe…
    (Platter[OD] – 2*Overhang),
    2.0 + max(PCB[1],(2.0 + SocketHeight + USBPCB[2])) + Bottom];
    Sides = 24*4;
    echo(str(" Height: ",Base[2]," mm"));
    Insert = // platter mounting: threaded insert or nut recess
    // [3.5,5.2,7.2] // 6-32 insert
    [3.7,5.0,8.0] // 3 mm – long insert
    ;
    NumStuds = 4;
    StudSides = 8;
    Stud = [Insert[OD], // insert for socket screws
    2*Insert[OD], // OD = big enough to merge with walls
    Base[LENGTH]]; // leave room for retainer
    StudBCD = floor(Base[OD] – Stud[OD]/cos(180/StudSides));
    echo(str("Platter screw BCD: ",StudBCD," mm"));
    PCBInset = Base[ID]/2 – sqrt(pow(Base[ID]/2,2) – pow(PCB[0],2)/4);
    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 PCB on foam tape
    rotate(0)
    USBPort();
    }
    for (a = [0:(NumStuds – 1)]) // platter mounting studs
    rotate(180/NumStuds + a*360/(NumStuds))
    translate([StudBCD/2,0,0])
    difference() {
    rotate(180/(2*StudSides))
    cylinder(d=Stud[OD],h=Stud[LENGTH],$fn=2*StudSides);
    translate([0,0,Bottom])
    rotate(180/StudSides)
    PolyCyl(Stud[ID],(Stud[LENGTH] – (Bottom – Protrusion)),StudSides);
    }
    intersection() { // microcontroller PCB mounting plate
    rotate(180/Sides)
    cylinder(d=Base[OD],h=Base[LENGTH],$fn=Sides);
    translate([-PCB[0]/2,(Base[ID]/2 – PCBInset),0])
    cube([PCB[0],Base[OD]/2,Base[LENGTH]],center=false);
    }
    difference() {
    intersection() { // totally ad-hoc bridge around USB opening
    rotate(180/Sides)
    cylinder(d=Base[OD],h=Base[LENGTH],$fn=Sides);
    translate([-1.25*USBPCB[1]/2,-(Base[ID]/2),0])
    cube([1.25*USBPCB[1],2.0,Base[LENGTH]],center=false);
    }
    translate([0,-Base[OD]/2,Bottom + 1.2]) // mount PCB on foam tape
    rotate(0)
    USBPort();
    translate([0,-(Base[ID]/2 – 2.0 + 1*ThreadWidth),Bottom – 3*ThreadThick]) // legend
    rotate([90,0,180])
    linear_extrude(height=1*ThreadWidth + Protrusion) {
    translate([0,(Base[LENGTH] – 5.5),0])
    text(text=TubeName,size=4,font="Arial:style=Bold",halign="center");
    // translate([0,(Base[LENGTH] – 8.5),0])
    // text(text=str("BCD ",StudBCD),size=2,font="Arial",halign="center");
    translate([0,(Base[LENGTH] – 11),0])
    text(text="KE4ZNU",size=3,font="Arial",halign="center");
    }
    }
    }
    }
    //———————-
    // Drilling fixture for disk platters
    module PlatterFixture() {
    StudOC = [1.16*inch,1.16*inch]; // Sherline tooling plate screw spacing
    StudClear = 5.0;
    AlignOffset = 100;
    AlignBar = [3*ThreadWidth,10.0,3*ThreadThick];
    BasePlate = [(20 + StudOC[0]*ceil(Platter[OD] / StudOC[0])),(Platter[OD] + 10),7.0];
    PlateRound = 10.0; // corner radius
    difference() {
    hull() // basic block
    for (i=[-1,1], j=[-1,1])
    translate([i*(BasePlate[0]/2 – PlateRound),j*(BasePlate[1]/2 – PlateRound),0])
    cylinder(r=PlateRound,h=BasePlate[2],$fn=4*4);
    for (i=[-1,1], j=[-1,1]) // index marks
    translate([i*AlignOffset/2,j*AlignOffset/2,BasePlate[2] – 2*ThreadThick])
    cylinder(d=1.5,h=1,$fn=6);
    for (i=[-1,1])
    translate([i*(AlignOffset + AlignBar[0])/2,0,(BasePlate[2] – AlignBar[2]/2 + Protrusion/2)])
    cube(AlignBar + [0,0,Protrusion],center=true);
    for (j=[-1,1])
    translate([0,j*(AlignOffset + AlignBar[0])/2,(BasePlate[2] – AlignBar[2]/2 + Protrusion/2)])
    rotate(90)
    cube(AlignBar + [0,0,Protrusion],center=true);
    for (a=[0:90:270])
    rotate(a)
    translate([(AlignBar[1]/2 + AlignBar[0]/2),0,(BasePlate[2] – AlignBar[2]/2 + Protrusion/2)])
    cube(AlignBar + [0,-Protrusion,Protrusion],center=true);
    for (i=[-1,1], j=[-1,0,1]) // holes for tooling plate studs
    translate([i*StudOC[0]*ceil(Platter[OD] / StudOC[0])/2,j*StudOC[0],-Protrusion])
    PolyCyl(StudClear,BasePlate[2] + 2*Protrusion,6);
    translate([0,0,-Protrusion]) // center clamp hole
    PolyCyl(StudClear,BasePlate[2] + 2*Protrusion,6);
    translate([0,0,BasePlate[2] – Platter[LENGTH]]) // disk locating recess
    rotate(180/PlatterSides)
    linear_extrude(height=(Platter[LENGTH] + Protrusion),convexity=2)
    difference() {
    circle(d=(Platter[OD] + 1),$fn=PlatterSides);
    circle(d=Platter[ID],$fn=PlatterSides);
    }
    translate([0,0,BasePlate[2] – 4.0]) // drilling recess
    rotate(180/PlatterSides)
    linear_extrude(height=(4.0 + Protrusion),convexity=2)
    difference() {
    circle(d=(Platter[OD] – 10),$fn=PlatterSides);
    circle(d=(Platter[ID] + 10),$fn=PlatterSides);
    }
    }
    }
    //———————-
    // 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[Tube][T_PUNCHOD]," mm = ",TubeData[Tube][T_PUNCHOD]/inch," inch"));
    echo(str(" Screws: ",TubeData[Tube][T_SCREWOC]," mm =",TubeData[Tube][T_SCREWOC]/inch," inch OC"));
    OAH = Pixel[LENGTH] + SocketShim + TubeData[Tube][T_PINLEN];
    BaseHeight = OAH – PanelThick;
    difference() {
    union() {
    linear_extrude(height=BaseHeight) // base outline
    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*SocketNut[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(SocketNut[OD],(SocketNut[LENGTH] + Protrusion),NutSides);
    PolyCyl(SocketNut[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]);
    }
    for (i=[-1,1]) // cable retaining slots
    translate([i*(Pixel[OD] + TubeData[Tube][T_SCREWOC])/4,0,(Pixel[LENGTH] – Protrusion)/2])
    cube([Pixel[LENGTH],TubeData[Tube][T_SCREWOC],(Pixel[LENGTH] + Protrusion)],center=true);
    }
    // Totally ad-hoc support structures …
    if (Support) {
    color("Yellow") {
    for (i=[-1,1]) // nut traps
    translate([i*TubeData[Tube][T_SCREWOC]/2,0,(SocketNut[LENGTH] – ThreadThick)/2])
    for (a=[0:5])
    rotate(a*30 + 15)
    cube([2*ThreadWidth,0.9*SocketNut[OD],(SocketNut[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);
    }
    }
    }
    //———————-
    // Greenlee punch bushings
    module PunchBushing(Name = DefaultSocket) {
    PunchScrew = 9.5;
    BushingThick = 3.0;
    Tube = search([Name],TubeData,1,0)[0];
    echo(str("Building ",TubeData[Tube][0]," bushing"));
    NumSides = 6*4;
    difference() {
    union() {
    cylinder(d=Platter[ID],h=BushingThick,$fn=NumSides);
    cylinder(d=TubeData[Tube][T_PUNCHOD],h=(BushingThick – Platter[LENGTH]),$fn=NumSides);
    }
    translate([0,0,-Protrusion])
    PolyCyl(PunchScrew,5.0,8);
    }
    }
    //———————-
    // Tube clamp
    module TubeClamp(Name = DefaultSocket) {
    Tube = search([Name],TubeData,1,0)[0];
    echo(str("Building ",TubeData[Tube][0]," clamp"));
    ClampWidth = 37.0; // inside of clamp arch
    ClampLength = 20; // along tube base
    ClampScrew = [6.0,7.8,6.0]; // nose of clamp screw
    ClampBlock = [4*ThreadWidth + TubeData[Tube][T_BULBOD],
    4*ThreadWidth + TubeData[Tube][T_BULBOD],
    ClampLength];
    difference() {
    union() {
    intersection() {
    translate([0,0,ClampBlock[2]/2])
    rotate(45)
    cube(ClampBlock,center=true); // V-block sides
    translate([0,-ClampWidth/2,ClampBlock[2]/2])
    cube([ClampWidth,ClampWidth,ClampBlock[2]],center=true); // clamp sides
    }
    intersection() {
    cylinder(d=ClampWidth,h=ClampBlock[2]);
    translate([0,ClampWidth/4,ClampBlock[2]/2])
    cube([ClampWidth,ClampWidth/2,ClampBlock[2]],center=true); // clamp sides
    }
    }
    translate([0,0,-Protrusion]) // remove tube base (remains centered)
    cylinder(d=TubeData[Tube][T_BASEOD],h=(ClampLength + 2*Protrusion));
    translate([0,(ClampWidth/2 + TubeData[Tube][T_BASEOD]/2)/2,ClampBlock[LENGTH]/3])
    rotate([-90,0,0])
    PolyCyl(ClampScrew[ID],1*ClampScrew[LENGTH],6); // clamp screw recess
    translate([0,-(6*ThreadWidth)/2,-Protrusion])
    cube([ClampWidth,6*ThreadWidth,(ClampLength + 2*Protrusion)]); // clamp relief slot
    }
    }
    //———————-
    // 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 == "PlatterBase")
    PlatterBase();
    if (Layout == "PlatterParts") {
    Tube = search([DefaultSocket],TubeData,1,0)[0];
    echo(str("Parts for ",TubeData[Tube][T_NAME]," assembly"));
    PlatterBase();
    translate([0.25*Platter[OD],-0.6*Platter[OD],0])
    rotate(0)
    Socket();
    if (TubeData[Tube][T_PLATECAP])
    for (i=[-1,1])
    translate([(-0.25*Platter[OD] – i*Pixel[OD]),-0.6*Platter[OD],0])
    rotate(i*90)
    Cap();
    }
    if (Layout == "PlatterFixture")
    PlatterFixture();
    if (Layout == "USBPort")
    USBPort();
    if (Layout == "TubeClamp")
    TubeClamp();
    if (Layout == "Bushings")
    PunchBushing();
    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");
    translate([0,-85,0])
    Socket("Magnoval");}
  • Maloney Road Repaving

    The Wappinger DPW laid asphalt along Maloney Rd, from side to side and end to end (well, to the end of their jurisdiction at the Lagrange town boundary). We passed the crew putting down the first layer on the westbound side:

    Maloney Road Paving - 2016-09-14
    Maloney Road Paving – 2016-09-14

    A few days later, they were doing the final layer on that side as we approached the Rail Trail entrance:

    Maloney Road Paving - 2016-09-17
    Maloney Road Paving – 2016-09-17

    Sometimes, good things happen out there on the roads!

    [Update: Vedran points to a Youtube video of paving:

    Paving Operations

    By the looks of it they are from (almost) your neck of the woods (NYCDOT). They have a mighty impressive machine going but if you watch the lower right corner for about 10 seconds you’ll spot them paving right over a manhole cover :) Guess no matter how smart the tech, users will always find a way.

    I’ve seen that done, too, but a guy should immediately dig out the cover (using the paint marks on the curb to find it) and taper the edges. That way, the paving machine produces a smooth surface along the street and the cover isn’t (shouldn’t be!) too deeply recessed.

    Sometimes they just spraypaint a circle over the buried cover and wait until somebody must go into that hole before digging it out. That makes a nice, smooth paving job, but eventually produces a steep-walled pit in the pavement which enlarges and crumbles into gravel.

    They should add a ring to the manhole to bring the cover flush with the new surface, but nobody (except the WDPW above!) does that around here until after the third or fourth paving job. Until then, it’s just like a pothole with a slick metallic bottom …

    /update]