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

  • Bird Box Entrance Reducers: Round 2

    One of the bird box entrance reducers I installed nigh onto a decade ago is still on duty, although downy woodpeckers definitely want a larger hole:

    Bird Box - gray PVC pipe reducer - woodpecker damage
    Bird Box – gray PVC pipe reducer – woodpecker damage

    Another reducer had gone missing over the years, so I made one from a length of PVC pipe:

    Bird Box - PVC pipe reducer - shaping
    Bird Box – PVC pipe reducer – shaping

    It started as 1-½ PVC pipe, 1-⅞ inch actual OD and should fit into a 1-½ hole, so I measured 1.5 × 3.15 around the circumference, bandsawed out the excess, draped it over a 1-½ Forstner bit, toasted it with a heat gun, and squashed it so it’s just a little bit bigger than the (enlarged!) hole in the box.

    Now the entrance is 1-¼ (-ish), just like it should be:

    Bird Box - PVC pipe reducer - installed
    Bird Box – PVC pipe reducer – installed

    The bird box in the front yard has been attracting starlings, in addition to serving as a hawk perch:

    New Coopers Hawks - bird box takeoff whoops
    New Coopers Hawks – bird box takeoff whoops

    The oblong hole required advanced manufacturing techniques:

    Oval Entrance Reducer
    Oval Entrance Reducer

    The front face should be too slick for larger birds and the little ones will zip right into the hole:

    Bird Box - 3D printed entrance reducer
    Bird Box – 3D printed entrance reducer

    The two starlings who’d been evaluating the box seem to have moved on; we doubt they’re now homeless.

    The OpenSCAD source code as a GitHub Gist:

    // Bird Box – oval entrance reducer
    // Ed Nisley KE4ZNU 2020-02-12
    //- Extrusion parameters must match reality!
    // Print with 3 shells and 3 solid layers
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    //———————-
    // Dimensions
    EntranceID = 1.25 * inch;
    BoxHole = [1.5,2.25] * inch;
    BoxWall = 0.75 * inch;
    HoleOC = BoxHole.y – BoxHole.x;
    FlangeWidth = 5.0;
    FlangeThick = 5*ThreadThick;
    $fn = 12*4;
    //——————-
    // Build it
    difference() {
    union() {
    linear_extrude(height=BoxWall + FlangeThick)
    hull()
    for (j=[-1,1])
    translate([0,j*HoleOC/2])
    circle(d=BoxHole.x);
    linear_extrude(height=FlangeThick)
    hull()
    for (j=[-1,1])
    translate([0,j*HoleOC/2])
    circle(d=BoxHole.x + 2*FlangeWidth);
    }
    translate([0,0,-Protrusion])
    cylinder(d=EntranceID,h=2*BoxWall);
    }
  • Kensington Expert Mouse Scroll Ring Fix

    Apparently the newest Kensington Expert “Mouse” trackballs have a hack re-orienting the scroll ring quadrature detector. The picture from my original writeup shows the previous situation:

    Scroll ring IR emitter-detector quadrature pair
    Scroll ring IR emitter-detector quadrature pair

    The quadrature detector, the black block on the left, is oriented with its lens (and, thus, the actual detectors) pointed away from the IR emitter. I thought it might be an assembly screwup, but it’s actually worse: the PCB layout is wrong.

    A note from Tristan in NZ explains the situation:

    So I have a later model than yours. It has a 2nd PCB chunk between where the legs normally would be. Just a floating piece with two holes for the legs, holding the legs from the board […] to the main board.It is also pointing the correct way (with the lens towards the three leg emitter).

    Kensington scroll wheel revision2
    Kensington scroll wheel revision2

    The new quad detector has only three pins and no convex lens, but the active area now faces the emitter across the gap.

    Because the interposer PCB occupies the space previously devoted to the emitter & detector leads, Kensington apparently soldered the new parts directly to the top surface without any clearance:

    It’s like they failed to put through-vias to the rear or didn’t route them to the bottom another way, hence the solder is under the component

    Tristan managed to wreck the detector while attempting to re-solder the intermittent joints, a situation I’m painfully familiar with. He replaced it with a quad detector harvested from a mid-90s optical mouse and it’s back in operation.

    So I think the correct “fix” for the old-style PCBs (without the new interposer) is to unsolder the detector, rotate it so the lens faces the emitter, then somehow rewire the pins to the original pads. This won’t be easy and definitely won’t be pretty, but as long as it’s pointed in the right general direction it should work:

    mine works off axis quite a bit

    Should either of my Expert Mouse trackballs fail, now I know what to do

    Many thanks to Tristan for reporting his findings!

    Update: A note from Alan brings more data to the discussion

  • Video-rated MicroSD Card Status Report

    Having just returned from the fourth ride of the season, it’s worthwhile to note how the MicroSD cards in the cameras are doing.

    The Sony HDR-AS30V helmet camera has been running a 64 GB Sandisk high-endurance video-rated card since late August 2017:

    Sandisk - 64 GB MicroSDXC cards
    Sandisk – 64 GB MicroSDXC cards

    In those 29 calendar months (maybe 20 riding months) I’ve ridden 4500-ish miles at perhaps 12 mph, so call it 375 hr = 22.5 k min. The camera fills a 4 GB file every 22.75 min, so it’s recorded 1000 files = 4 TB, which is 62× its capacity. This is better than the defunct Sandisk Extreme Pro card (3 TB & 50×) and much much better than the Sony cards (1 TB & 15×), although I have caught the camera in RCVR mode maybe twice, which means the card or camera occasionally coughs and reformats itself.

    The Cycliq Fly6 rear camera uses a Sandisk 32 GB card that’s been running flawlessly since late 2017:

    MicroSD 32 GB - Samsung EVO and SanDisk High Endurance
    MicroSD 32 GB – Samsung EVO and SanDisk High Endurance

    The new 16850 lithium cell continues to work fine, too.

    The SJCam M20 rear camera also uses a Sandisk 32 GB high-endurance card and has worked fine since early 2018. An external battery eliminated all the hassle of its feeble internal batteries, although the one that’s been in there has faded to the point of just barely keeping the clock ticking over during winter weeks without rides:

    SJCAM M20 Mount - Tour Easy side view
    SJCAM M20 Mount – Tour Easy side view

    All in all, paying the premium for video-rated MicroSD cards has been worthwhile!

  • Homage Tek CC: Subscripts & Superscripts

    The GCMC typeset() function converts UTF-8 text into a vector list, with Hershey vector fonts sufficing for most CNC projects. The fonts date back to the late 1960s and lack niceties such as superscripts, so the Homage Tektronix Circuit Computer scale legends have a simpler powers-of-ten notation:

    Tek CC - Pilot V5 - plain paper - red blue
    Tek CC – Pilot V5 – plain paper – red blue

    Techies understand upward-pointing carets, but … ick.

    After thinking it over, poking around in the GCMC source code, and sketching alternatives, I ruled out:

    • Adding superscript glyphs to the font tables
    • Writing a text parser with various formatting commands
    • Doing anything smart

    Because I don’t need very many superscripts, a trivial approach seemed feasible. Start by defining the size & position of the superscript characters:

    SuperScale = 0.75;                                       // superscript text size ratio
    SuperOffset = [0mm,0.75 * LegendTextSize.y];            //  ... baseline offset
    

    Half-size characters came out barely readable with 0.5 mm Pilot pens:

    Tek CC - Superscript test - 0.5x
    Tek CC – Superscript test – 0.5x

    They’re legible and might be OK with a diamond drag point.

    They work better at 3/4 scale:

    Tek CC - Superscript test - 0.75x
    Tek CC – Superscript test – 0.75x

    Because superscripts only occur at the end of the scale legends, a truly nasty hack suffices:

    function ArcLegendSuper(Text,Super,Radius,Angle,Orient) {
    
      local tp = scale(typeset(Text,TextFont),LegendTextSize);
    
      tp += scale(typeset(Super,TextFont),LegendTextSize * SuperScale) + SuperOffset + [tp[-1].x,0mm];
    
      local tpa = ArcText(tp,[0mm,0mm],Radius,Angle,TEXT_CENTERED,Orient);
    
      feedrate(TextSpeed);
      engrave(tpa,TravelZ,EngraveZ);
    }
    

    The SuperScale constant shrinks the superscript vectorlist, SuperOffset shifts it upward, and adding [tp[-1].x,0mm] glues it to the end of the normal-size vectorlist.

    Yup, that nasty.

    Creating the legends goes about like you’d expect:

      ArcLegendSuper("pF - picofarad  x10","-12",r,a,INWARD);
    

    Presenting “numeric” superscripts as text keeps the option open for putting non-numeric stuff up there, which seemed easier than guaranteeing YAGNI.

    A similar hack works for subscripts:

    Tek CC - Subscript test - 0.75x
    Tek CC – Subscript test – 0.75x

    With even more brutal code:

      Sub_C = scale(typeset("C",TextFont),LegendTextSize * SubScale) + SubOffset;
    
    <<< snippage >>>
    
        tp = scale(typeset("←----- τ",TextFont),LegendTextSize);
        tp += Sub_C + [tp[-1].x,0mm];
        tp += scale(typeset(" Scale -----→",TextFont),LegendTextSize) + [tp[-1].x,0mm];
    

    The hackage satisfied the Pareto Principle, so I’ll declare victory and move on.

  • Filament Spool Sidewall

    A new spool of retina-burn orange PETG snagged when the takeup guide let the filament fall off the inboard side and the extruder tightened the loops around the spool holder. I carefully unwound the loops without removing the spool to ensure I didn’t introduce a crossover, scraped the bird’s next off the platform, and restarted the print.

    After undoing the second snag, I added a crude spool sidewall:

    Makergear M2 - filament spool sidewall
    Makergear M2 – filament spool sidewall

    It’s decidedly unlovely, but I was in a hurry to get a PCB holder printed and ready for use. Worked perfectly!

    I’ve rarely had a problem with any other spools and I don’t know what’s new-and-different with this one.

  • CNC 3018XL: Arduino + Protoneer CNC

    If the truth be known, I wanted to do this as soon as I discovered the CAMtool V3.3 board hardwired the DRV8825 PCBs in 1:32 microstep mode:

    CNC 3018XL - Protoneer atop Arduino - installed
    CNC 3018XL – Protoneer atop Arduino – installed

    The Protoneer CNC board has jumpers, so selecting 1:8 microstep mode is no big deal.

    As before, I epoxied another row of pins along the I/O header for Makerbot-style endstops:

    Protoneer endstop power mod
    Protoneer endstop power mod

    I’ll probably regret not adding pins along the entire row, but, unlike the MPCNC, the CNC 3018XL won’t ever have hard limit switches. I plugged the Run-Hold switch LEDs into an unused +5 V pin and moved on.

    I modified the DRV8825 driver PCBs for fast decay mode:

    DRV8825 PCB - Fast Decay Mode wire
    DRV8825 PCB – Fast Decay Mode wire

    Then set the current to a bit over 1 A:

    3018XL - Protoneer setup - Z 1 mm
    3018XL – Protoneer setup – Z 1 mm

    Six hours later I hauled the once-again-functional CNC 3018XL to my presentation for the ACM:

    Spirograph - intricate sample plot - detail
    Spirograph – intricate sample plot – detail

    Memo to Self: Time to get another Prontoneer board …

  • SK6812 RGBW Test Fixture: Row-by-Row Color Mods

    The vacuum tube LED firmware subtracts the minimum value from the RGB channels of the SK6812 RGBW LEDs and displays it in the white channel, thereby reducing the PWM value of the RGB LEDs by their common “white” component. The main benefit is reducing the overall power by about two LEDs. More or less, kinda sorta.

    I tweaked the SK6812 test fixture firmware to show how several variations of the basic RGB colors appear:

          for (int col=0; col < NUMCOLS ; col++) {              // for each column
            byte Value[PIXELSIZE];                              // figure first row colors
            for (byte p=0; p < PIXELSIZE; p++) {                //  ... for each color in pixel
              Value[p] = StepColor(p,-col*Pixels[p].TubePhase);
            }
            // just RGB
            int row = 0;
            uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],0);
            strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    
            byte MinWhite = min(min(Value[RED],Value[GREEN]),Value[BLUE]);
    
            // only common white
            UniColor = strip.Color(0,0,0,MinWhite);
            strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    
            // RGB minus common white + white
            UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,MinWhite);
            strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    
             // RGB minus common white
            UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,0);
            strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    
            // inverse RGB
            UniColor = strip.Color(255 - Value[RED],255 - Value[GREEN],255 - Value[BLUE],0);
            strip.setPixelColor(col + NUMCOLS*row++,UniColor);

    Which looks like this:

    SK6812 Test Fixture - RGBW color variations - diffuser
    SK6812 Test Fixture – RGBW color variations – diffuser

    The pure RGB colors appear along the bottom row, with the variations proceeding upward to the inverse RGB in the top row. The dust specks show it’s actually in focus.

    The color variations seem easier to see without the diffuser:

    SK6812 Test Fixture - RGBW color variations - bare LEDs
    SK6812 Test Fixture – RGBW color variations – bare LEDs

    The white LEDs are obviously “warm white”, which seems not to make much difference.

    Putting a jumper from D2 to the adjacent (on an Nano, anyway) ground pin selects the original pattern, removing the jumper displays the modified pattern:

    SK6812 test fixture - pattern jumper
    SK6812 test fixture – pattern jumper

    For whatever it’s worth, those LEDs have been running at full throttle for two years with zero failures!

    The Arduino source code as a GitHub Gist:

    // SK6812 RGBW LED array exerciser
    // Ed Nisley – KE4ANU – February 2017
    // 2020-01-25 add row-by-row color modifications
    #include <Adafruit_NeoPixel.h>
    //———-
    // Pin assignments
    const byte PIN_NEO = A3; // DO – data out to first Neopixel
    const byte PIN_HEARTBEAT = 13; // DO – Arduino LED
    const byte PIN_SELECT = 2; // DI – pattern select input
    //———-
    // Constants
    #define UPDATEINTERVAL 20ul
    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 100
    // phase difference between LEDs for slowest color
    #define BASEPHASE (PI/16.0)
    // LEDs in each row
    #define NUMCOLS 5
    // number of rows
    #define NUMROWS 5
    #define NUMPIXELS (NUMCOLS * NUMROWS)
    #define PINDEX(row,col) (row*NUMCOLS + col)
    //———-
    // Globals
    // instantiate the Neopixel buffer array
    Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMPIXELS, PIN_NEO, NEO_GRBW + NEO_KHZ800);
    uint32_t FullWhite = strip.Color(255,255,255,255);
    uint32_t FullOff = strip.Color(0,0,0,0);
    struct pixcolor_t {
    byte Prime;
    unsigned int NumSteps;
    unsigned int Step;
    float StepSize;
    float TubePhase;
    byte MaxPWM;
    };
    // colors in each LED
    enum pixcolors {RED, GREEN, BLUE, WHITE, PIXELSIZE};
    struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
    unsigned long MillisNow;
    unsigned long MillisThen;
    //– 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
    // printf("C: %d Phi: %d Value: %d\r\n",Color,(int)(Phi*180.0/PI),Value);
    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
    pinMode(PIN_SELECT,INPUT_PULLUP);
    Serial.begin(57600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    printf("WS2812 / SK6812 array exerciser\r\nEd Nisley – KE4ZNU – February 2017\r\n");
    /// set up Neopixels
    strip.begin();
    strip.show();
    // lamp test: run a brilliant white dot along the length of the strip
    printf("Lamp test: walking white\r\n");
    strip.setPixelColor(0,FullWhite);
    strip.show();
    delay(250);
    for (int i=1; i<NUMPIXELS; i++) {
    digitalWrite(PIN_HEARTBEAT,HIGH);
    strip.setPixelColor(i-1,FullOff);
    strip.setPixelColor(i,FullWhite);
    strip.show();
    digitalWrite(PIN_HEARTBEAT,LOW);
    delay(250);
    }
    strip.setPixelColor(NUMPIXELS – 1,FullOff);
    strip.show();
    delay(250);
    // fill the array, row by row
    printf(" … fill\r\n");
    for (int i=NUMROWS-1; i>=0; i–) { // for each row
    digitalWrite(PIN_HEARTBEAT,HIGH);
    for (int j=NUMCOLS-1; j>=0 ; j–) {
    strip.setPixelColor(PINDEX(i,j),FullWhite);
    strip.show();
    delay(100);
    }
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    // clear to black, column by column
    printf(" … clear\r\n");
    for (int j=NUMCOLS-1; j>=0; j–) { // for each column
    digitalWrite(PIN_HEARTBEAT,HIGH);
    for (int i=NUMROWS-1; i>=0; i–) {
    strip.setPixelColor(PINDEX(i,j),FullOff);
    strip.show();
    delay(100);
    }
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    delay(1000);
    // set up the color generators
    MillisNow = MillisThen = millis();
    printf("First random number: %ld\r\n",random(10));
    Pixels[RED].Prime = 3;
    Pixels[GREEN].Prime = 5;
    Pixels[BLUE].Prime = 7;
    Pixels[WHITE].Prime = 11;
    printf("Primes: (%d,%d,%d,%d)\r\n",
    Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime,Pixels[WHITE].Prime);
    unsigned int PixelSteps = (unsigned int) ((BASEPHASE / TWO_PI) *
    RESOLUTION * (unsigned int) max(max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime),Pixels[WHITE].Prime));
    printf("Pixel phase offset: %d deg = %d steps\r\n",(int)(BASEPHASE*(360.0/TWO_PI)),PixelSteps);
    Pixels[RED].MaxPWM = 255;
    Pixels[GREEN].MaxPWM = 255;
    Pixels[BLUE].MaxPWM = 255;
    Pixels[WHITE].MaxPWM = 32;
    for (byte c=0; c < PIXELSIZE; c++) {
    Pixels[c].NumSteps = RESOLUTION * (unsigned int) Pixels[c].Prime;
    Pixels[c].Step = (3*Pixels[c].NumSteps)/4;
    Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // in radians per step
    Pixels[c].TubePhase = PixelSteps * Pixels[c].StepSize; // radians per tube
    printf("c: %d Steps: %5d Init: %5d",c,Pixels[c].NumSteps,Pixels[c].Step);
    printf(" PWM: %3d Phi %3d deg\r\n",Pixels[c].MaxPWM,(int)(Pixels[c].TubePhase*(360.0/TWO_PI)));
    }
    }
    //——————
    // Run the mood
    void loop() {
    MillisNow = millis();
    if ((MillisNow – MillisThen) > UpdateMS) {
    digitalWrite(PIN_HEARTBEAT,HIGH);
    unsigned int AllSteps = 0;
    for (byte c=0; c < PIXELSIZE; c++) { // step to next increment in each color
    if (++Pixels[c].Step >= Pixels[c].NumSteps) {
    Pixels[c].Step = 0;
    printf("Color %d steps %5d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow – MillisThen));
    }
    AllSteps += Pixels[c].Step; // will be zero only when all wrap at once
    }
    if (0 == AllSteps) {
    printf("Grand cycle at: %ld\r\n",MillisNow);
    }
    if (digitalRead(PIN_SELECT)) {
    for (int col=0; col < NUMCOLS ; col++) { // for each column
    byte Value[PIXELSIZE]; // figure first row colors
    for (byte p=0; p < PIXELSIZE; p++) { // … for each color in pixel
    Value[p] = StepColor(p,-col*Pixels[p].TubePhase);
    }
    // just RGB
    int row = 0;
    uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],0);
    strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    byte MinWhite = min(min(Value[RED],Value[GREEN]),Value[BLUE]);
    // only common white
    UniColor = strip.Color(0,0,0,MinWhite);
    strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    // RGB minus common white + white
    UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,MinWhite);
    strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    // RGB minus common white
    UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,0);
    strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    // inverse RGB
    UniColor = strip.Color(255 – Value[RED],255 – Value[GREEN],255 – Value[BLUE],0);
    strip.setPixelColor(col + NUMCOLS*row++,UniColor);
    }
    }
    else {
    for (int k=0; k < NUMPIXELS; k++) { // for each pixel
    byte Value[PIXELSIZE];
    for (byte c=0; c < PIXELSIZE; c++) { // … for each color
    Value[c] = StepColor(c,-k*Pixels[c].TubePhase); // figure new PWM value
    // Value[c] = (c == RED && Value[c] == 0) ? Pixels[c].MaxPWM : Value[c]; // flash highlight for tracking
    }
    uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],Value[WHITE]);
    strip.setPixelColor(k,UniColor);
    }
    }
    strip.show();
    MillisThen = MillisNow;
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    }