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

  • Copying All! The! Files! (Except Some)

    All our data files spin around on a nearly full 1 TB drive in a “file server”, a grandiosely overqualified and dirt-cheap off-lease Dell Optiplex desktop sitting in the basement. It’s been running headless and unattended for the last half-dozen years and is badly in need of replacement, so I must copy all its files to a newer, even more overqualified, and equally cheap off-lease Optiplex.

    Copying the files from the /mnt/music collection on the existing server to the identically named directory on the new server proceeds thusly:

    sudo mount -o ro fileserver.local:/mnt/music /mnt/nfs
    sudo rsync -ahu --progress --log-file=/tmp/music.log \
     --exclude="/lost+found" \
     --exclude=".Trash*" \
     --exclude=".dtrash*" \
     --delete \
     /mnt/nfs/ /mnt/music

    Mount the existing collection (from the old server) in read-only mode to avoid heartache subsequent to confusion. It could happen.

    The first time through, add a -n option for a dry run, then inspect the log file for surprises.

    The various --exclude options avoid copying trashed-but-not-yet-deleted files from the various trash directories maintained by various file handlers. In the process of sorting this out, I learned the DigiKam photo manager creates a .dtrash directory holding deleted files for each of its Album listings, appearing down near the bottom of the top-level album wherein you’ve quasi-deleted photos via “Move to Trash”.

    The --delete option removes files on the destination (new disk) if they’re not on the source (old disk). I started this migration earlier this year, before the world fell apart, and have moved / consolidated / renamed various directories & files in the interim, so deleting the previous copies from their old locations makes the destination match the source.

    So far, so good …

  • Bathroom Door Retainer: Bigger and Stronger

    Bathroom Door Retainer: Bigger and Stronger

    After three years, the retainer holding the front bathroom door open against winds blowing through the house on stormy days finally fractured, right at the top of the towel rack where you’d expect it:

    Bathroom Door Retainer - fractured
    Bathroom Door Retainer – fractured

    I was all set to add reinforcing pins and whatnot, then came to my senses and just made the whole thing a few millimeters larger:

    Bathroom Door Retainer - stronger
    Bathroom Door Retainer – stronger

    Customer feedback indicates white blends better with the background.

    I made a few minor tweaks to the original design, including slightly larger bumps to hold it against the towel bar that, regrettably, put corresponding gouges into the bar. Who knew they used such soft plastic back in the day?

    The OpenSCAD source code as a GitHub Gist:

    // Bathroom Door Retainer
    // Ed Nisley KE4ZNU – May 2017
    // 2020-07 beef up, particularly at top of bar
    Layout = "Show"; // [Show, Build]
    //——-
    //- Extrusion parameters must match reality!
    /* [Hidden] */
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    //——-
    // Dimensions
    /* [Dimensions] */
    TowelBarSide = 20.5; // towel bar across flat side
    TowelBarAngle = 45; // rotation of top flat from horizontal
    BumpOD = 2.0; // retaining ball
    DoorOffset = 14.0; // from towel bar to door
    DoorThick = 37.0;
    WallThick = 8.0; // minimum wall thickness
    PlateThick = 4.0; // … slab
    RetainerDepth = 15.0; // thickness of retaining notch
    NumSides = 6*4;
    CornerRad = WallThick;
    BarClipOD = TowelBarSide*sqrt(2) + 2*WallThick;
    BarClipRad = BarClipOD/2;
    OAH = RetainerDepth + PlateThick;
    module LatchPlan() {
    union() {
    linear_extrude(height=OAH,convexity=4)
    difference() {
    union() {
    circle(d=BarClipOD,$fn=NumSides);
    hull()
    for (i=[0,1], j=[0,1])
    translate([i*(BarClipRad + DoorOffset + DoorThick + WallThick – CornerRad),j*(BarClipRad – CornerRad)])
    circle(r=CornerRad,$fn=4*4);
    }
    rotate(TowelBarAngle) // towel bar shape
    square(size=TowelBarSide,center=true);
    translate([0,-TowelBarSide/sqrt(2)]) // make access slot
    rotate(-TowelBarAngle)
    square(size=[2*TowelBarSide,TowelBarSide],center=false);
    }
    for (a=[0:180:360])
    rotate(a + TowelBarAngle)
    translate([TowelBarSide/2,0,OAH/2])
    rotate([90,0,45])
    sphere(d=BumpOD,$fn=4*3);
    }
    }
    module Latch() {
    difference() {
    LatchPlan();
    translate([BarClipRad + DoorOffset,-BarClipRad/2,-Protrusion])
    cube([DoorThick,BarClipOD,RetainerDepth + Protrusion],center=false);
    }
    }
    //——-
    // Build it!
    if (Layout == "Show") {
    Latch();
    }
    if (Layout == "Build") {
    translate([0,0,OAH])
    rotate([180,0,0])
    Latch();
    }

    Done!

  • Manjaro Linux vs. Dell Latitude E7250 Bluetooth

    Manjaro Linux vs. Dell Latitude E7250 Bluetooth

    Although the Dell Latitude E7250 allegedly had Bluetooth capability and the Blueman applet tried connecting to my Bluetooth headsets, the connection aways failed and nothing worked. There’s a WLAN module stuck in an M.2 socket inside the laptop providing both WiFi and Bluetooth:

    Dell E7250 - DW1560 card in place
    Dell E7250 – DW1560 card in place

    A bit of searching suggested the driver wasn’t loading properly, which became obvious after I knew where to look:

    dmesg | grep -i blue
    … snippage …
    [    5.678610] Bluetooth: hci0: BCM20702A1 (001.002.014) build 1572
    [    5.678851] bluetooth hci0: Direct firmware load for brcm/BCM20702A1-0a5c-216f.hcd failed with error -2
    [    5.678853] Bluetooth: hci0: BCM: Patch brcm/BCM20702A1-0a5c-216f.hcd not found
    [   10.854607] Bluetooth: RFCOMM TTY layer initialized
    [   10.854613] Bluetooth: RFCOMM socket layer initialized
    [   10.854619] Bluetooth: RFCOMM ver 1.11

    Without having the proper firmware / patch loaded, the module won’t work, even though the TTY / socket layers know it’s present, which explains why Blueman did everything except actually connect to the headsets.

    More searching suggested you must extract the firmware HEX file from the Windows driver. Feeding the Service Tag into the Dell support site, then feeding “Bluetooth” and “Windows 8.1, 64-bit” (preinstalled on the laptop) into the Drivers & Downloads tab gets you the relevant EXE file: Dell Wireless 1550/1560 Wi-Fi and Bluetooth Driver. It turns out to be a self-extracting ZIP file (in Windows, anyway), so unzip it all by yourself:

    unzip Network_Driver_5DFVH_WN32_6.30.223.262_A03.EXE

    This produces a blizzard of HEX files in the newly created Drivers/production/Windows8.1-x64 directory. Each firmware HEX file is keyed to the USB Product Code identifying the unique USB gadget, found with lsusb:

    lsusb
    … snippage …
    Bus 002 Device 003: ID 0a5c:216f Broadcom Corp. BCM20702A0 Bluetooth
    … snippage …

    The DW1560 apparently has a USB RAM interface, with the specific HEX file identified in the CopyList stanza of the INF file corresponding to that USB Product Code:

    grep -i -A 5  ramusb216f.copylist Drivers/production/Windows8.1-x64/bcbtums-win8x64-brcm.inf
    [RAMUSB216F.CopyList]
    bcbtums.sys
    btwampfl.sys
    BCM20702A1_001.002.014.1443.1572.hex
    … snippage …

    However, the Linux firmware loader needs a different file format with a different name, mashed together from the HEX file, USB Vendor, and USB Product codes:

    hex2hcd -o BCM20702A1-0a5c-216f.hcd BCM20702A1_001.002.014.1443.1572.hex

    The converted firmware file goes where the loader expected to find it:

    sudo cp BCM20702A1-0a5c-216f.hcd /lib/firmware/brcm/

    Whereupon next reboot sorted things out:

    dmesg | grep -i blue
    [    6.024838] Bluetooth: Core ver 2.22
    [    6.024868] Bluetooth: HCI device and connection manager initialized
    [    6.024872] Bluetooth: HCI socket layer initialized
    [    6.024874] Bluetooth: L2CAP socket layer initialized
    [    6.024881] Bluetooth: SCO socket layer initialized
    [    6.100796] Bluetooth: BNEP (Ethernet Emulation) ver 1.3
    [    6.100800] Bluetooth: BNEP filters: protocol multicast
    [    6.100804] Bluetooth: BNEP socket layer initialized
    [    6.157114] Bluetooth: hci0: BCM: chip id 63
    [    6.158125] Bluetooth: hci0: BCM: features 0x07
    [    6.176119] Bluetooth: hci0: BCM20702A
    [    6.177114] Bluetooth: hci0: BCM20702A1 (001.002.014) build 0000
    [    7.031228] Bluetooth: hci0: BCM20702A1 (001.002.014) build 1572
    [    7.047177] Bluetooth: hci0: DW1560 Bluetooth 4.0 LE
    [   13.141854] Bluetooth: RFCOMM TTY layer initialized
    [   13.141865] Bluetooth: RFCOMM socket layer initialized
    [   13.141872] Bluetooth: RFCOMM ver 1.11

    The firmware may be in one of the myriad Bluetooth packages not installed by default, so perhaps identifying & installing the proper package would sidestep the hocus-pocus.

    Maybe next time?

    Now I can wear my Bose Hearphones in Zoom sessions with the E7250, because my Pixel 3a phone heats up almost to the gets-bendy level while thrashing its battery to death.

  • JPG Recovery From a Camera FAT Filesystem

    JPG Recovery From a Camera FAT Filesystem

    You can do it by hand, as I used to, or use recoverjpeg:

    dmesg | tail
    cd /tmp
    sudo dcfldd if=/dev/sde1 of=pix.bin bs=1M count=100
    recoverjpeg pix.bin 
    ristretto image00*
    

    Nothing prizewinning, but better than no picture at all:

    Garage Robin - recovered image
    Garage Robin – recovered image

    Note that you start by copying a reasonable chunk of the partition from the Memory Stick / (micro)SD Card first, to prevent a bad situation from getting worse.

    Now I can remember the easy way the next time around this block …

  • Solid Modeling: Support Puzzle

    Solid Modeling: Support Puzzle

    I’ve been putting this type of support structure inside screw holes & suchlike for years:

    Browning Hi-Power Magazine Block - solid model - Generic 1 - support detail
    Browning Hi-Power Magazine Block – solid model – Generic 1 – support detail

    It’s basically a group of small rectangles rotated around the hole’s axis and about one thread thickness shorter than the overhanging interior.

    I’ve found that incorporating exactly the right support structure eliminates Slic3r’s weird growths, eases removal, and generally works better all around.

    So doing this for the baseplate of the Glass Tile frame came naturally:

    Glass Tile Frame - octagonal support
    Glass Tile Frame – octagonal support

    This OpenSCAD snippet plunks one of those asterisks in each of four screw holes:

      if (Support)
        color("Yellow")
          for (i=[-1,1], j=[-1,1])
            translate([i*InsertOC.x/2,j*InsertOC.y/2,0])
              for (a=[0:45:135])
                  rotate(a)
                    translate([0,0,(Screw[LENGTH] - ThreadThick)/2])
                      cube([Screw[OD] - 2*ThreadWidth,2*ThreadWidth,Screw[LENGTH] - ThreadThick],center=true);

    The “cubes” overlap in the middle, with no completely coincident faces or common edges, so it’s 2-manifold. Slic3r, however, produces a weird time estimate whenever the model includes those structures:

    Slic3r - NaN time estimate
    Slic3r – NaN time estimate

    NaN stands for Not A Number and means something horrible has happened in the G-Code generation. Fortunately, the G-Code worked perfectly and produced the desired result, but I’m always uneasy when Something Seems Wrong.

    Messing around with the code produced a slightly different support structure:

    Glass Tile Frame - quad support
    Glass Tile Frame – quad support

    The one thread thick square on the bottom helps glue the structure to the platform and four ribs work just as well as eight in the octagonal hole:

      Fin = [Screw[OD]/2 - 1.5*ThreadWidth,2*ThreadWidth,ScrewRecess - ThreadThick];
      if (Inserts && SupportInserts)
        color("Yellow")
          for (i=[-1,1], j=[-1,1])
            translate([i*InsertOC.x/2,j*InsertOC.y/2,0]) {
              rotate(180/8)
                cylinder(d=6*ThreadWidth,h=ThreadThick,$fn=8);
              for (a=[0:90:360])
                  rotate(a)
                    translate([Fin.x/2 + ThreadWidth/2,0,(ScrewRecess - ThreadThick)/2])
                      cube(Fin,center=true);
            }

    Which changed the NaN time estimates into actual numbers.

    One key difference may be the small hole in the middle. The four ribs (not two!) now overlap by one thread width around the hole, so they’re not quite coincident and Slic3r produces a tidy model:

    Glass Tile Frame - quad support - Slic3r
    Glass Tile Frame – quad support – Slic3r

    The hole eliminates a smear of infill from the center, which may have something to do with the improvement.

    In any event, I have an improved copypasta recipe for the next screw holes in need of support, even if I don’t understand why it’s better.

  • Glass Tiles: Matrix for SK6812 PCBs

    Glass Tiles: Matrix for SK6812 PCBs

    Tweaking the glass tile frame for press-fit SK6812 PCBs in the bottom of the array cells:

    Glass Tile Frame - cell array - openscad
    Glass Tile Frame – cell array – openscad

    Which looks like this with the LEDs and brass inserts installed:

    Glass Tile - 2x2 array - interior
    Glass Tile – 2×2 array – interior

    The base holds an Arduino Nano with room for wiring under the cell array:

    Glass Tile Frame - base - openscad
    Glass Tile Frame – base – openscad

    Which looks like this after it’s all wired up:

    Glass Tile - 2x2 array - wiring
    Glass Tile – 2×2 array – wiring

    The weird colors showing through the inserts are from the LEDs. The red thing in the upper left is a silicone insulation snippet. Yes, that’s hot-melt glue holding the Arduino Nano in place and preventing the PCBs from getting frisky.

    Soak a handful of glass tiles overnight in paint stripper:

    Glass Tiles - paint stripper soak
    Glass Tiles – paint stripper soak

    Whereupon the adhesive slides right off with the gentle application of a razor scraper. Rinse carefully, dry thoroughly, and snap into place.

    Tighten the four M3 SHCS and it’s all good:

    Glass Tile - 2x2 array - operating
    Glass Tile – 2×2 array – operating

    So far, I’ve had two people tell me they don’t know what it is, but they want one:

    Glass Tile - various versions
    Glass Tile – various versions

    The OpenSCAD Customizer lets you set the array size:

    Glass Tile Frame - 3x3 - press-fit SK6812 LEDs
    Glass Tile Frame – 3×3 – press-fit SK6812 LEDs

    However, just because you can do something doesn’t mean you should:

    Glass Tile Frame - 6x6 cell array - openscad
    Glass Tile Frame – 6×6 cell array – openscad

    Something like this might be interesting:

    Glass Tile Frame - 2x6 cell array - openscad
    Glass Tile Frame – 2×6 cell array – openscad

    In round numbers, printing the frame takes about an hour per cell, so a 2×2 array takes three hours and 3×3 array runs around seven hours. A 6×6 frame is just not happening.

    The OpenSCAD source code as a GitHub Gist:

    // Illuminated Tile Grid
    // Ed Nisley – KE4ZNU
    // 2020-05
    /* [Configuration] */
    Layout = "Build"; // [Cell,CellArray,MCU,Base,Show,Build]
    Shape = "Square"; // [Square, Pyramid, Cone]
    Cells = [2,2];
    CellDepth = 15.0;
    Inserts = true;
    SupportInserts = true;
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1; // make holes end cleanly
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Tile = [25.0 + 0.1,25.0 + 0.1,4.0];
    WallThick = 4*ThreadWidth;
    FloorThick = 3.0;
    Flange = [2*ThreadWidth,2*ThreadWidth,0]; // ridge supporting tile
    Separator = [3*ThreadWidth,3*ThreadWidth,Tile.z – 1]; // between tiles
    Screw = [3.0,6.0,3.5]; // M3 SHCS, OD=head, LENGTH=head
    Insert = [3.0,4.2,8.0]; // threaded brass insert
    ScrewRecess = Screw[LENGTH] + 4*ThreadThick;
    LEDPCB = [9.6,9.6,2.9]; // round SK6812, squared-off sides
    LED = [5.0 + 2*HoleWindage,5.0 + 2*HoleWindage,1.3];
    LEDOffset = [0.0,0.0,0.0]; // if offset from PCB center
    CellOAL = [Tile.x,Tile.y,0] + Separator + [0,0,CellDepth] + [0,0,FloorThick];
    ArrayOAL = [Cells.x*CellOAL.x,Cells.y*CellOAL.y,CellOAL.z]; // just the LED cells
    BlockOAL = ArrayOAL + [2*WallThick,2*WallThick,0]; // LED cells + exterior wall
    echo(str("Block OAL: ",BlockOAL));
    InsertOC = ArrayOAL – [Insert[OD],Insert[OD],0] – [WallThick,WallThick,0];
    echo(str("Insert OC: ",InsertOC));
    TapeThick = 1.0;
    Arduino = [44.0,18.0,8.0 + TapeThick]; // Arduino Nano to top of USB Mini-B plug
    USBPlug = [15.0,11.0,9.0]; // USB Mini-B plug insulator
    USBOffset = [0,0,5.0]; // offset from PCB base
    WiringSpace = 3.5;
    WiringBay = [(Cells.x – 1)*CellOAL.x + LEDPCB.x,(Cells.y – 1)*CellOAL.y + LEDPCB.x,WiringSpace];
    PlateOAL = [BlockOAL.x,BlockOAL.y,FloorThick + Arduino.z + WiringSpace]; // allow wiring above Arduino
    echo(str("Base Plate: ",PlateOAL));
    echo(str("Screw length: ",(PlateOAL.z – ScrewRecess) + Insert.z/2," to ",(PlateOAL.z – ScrewRecess) + Insert.z));
    LegendRecess = 1*ThreadThick;
    //————————
    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);
    }
    //———————–
    // Base and optics in single tile
    module LEDCone() {
    hull() {
    translate([0,0,CellDepth + Tile.z/2])
    cube(Tile – 2*[Flange.x,Flange.y,0],center=true);
    if (Shape == "Square") {
    translate([0,0,LEDPCB.z/2])
    cube([Tile.x,Tile.y,LEDPCB.z] – 2*[Flange.x,Flange.y,0],center=true);
    }
    else if (Shape == "Pyramid") {
    translate([0,0,LEDPCB.z/2])
    cube(LEDPCB,center=true);
    }
    else if (Shape == "Cone") {
    translate([0,0,LEDPCB.z/2])
    cylinder(d=1.0*LEDPCB.x,h=LED.z,center=true);
    }
    else {
    echo(str("Whoopsie! Invalid Shape: ",Shape));
    cube(5);
    }
    }
    }
    // One complete LED cell
    module LEDCell() {
    difference() {
    translate([0,0,CellOAL.z/2])
    cube(CellOAL + [Protrusion,Protrusion,0],center=true); // force overlapping adjacent sides!
    translate([0,0,CellOAL.z – Separator.z + Tile.z/2])
    cube(Tile,center=true);
    translate([0,0,LEDPCB.z])
    LEDCone();
    // cube([LED.x,LED.y,CellOAL.z],center=true);
    translate(-LEDOffset + [0,0,-CellOAL.z/2])
    rotate(180/8)
    PolyCyl(LEDPCB.x,CellOAL.z,8);
    }
    }
    // The whole array of cells
    module CellArray() {
    difference() {
    union() {
    translate([CellOAL.x/2 – Cells.x*CellOAL.x/2,CellOAL.y/2 – Cells.y*CellOAL.y/2,0])
    for (i=[0:Cells.x – 1], j=[0:Cells.y – 1])
    translate([i*CellOAL.x,j*CellOAL.y,0])
    LEDCell();
    if (Inserts) // bosses
    for (i=[-1,1], j=[-1,1])
    translate([i*InsertOC.x/2,j*InsertOC.y/2,0])
    rotate(180/8)
    cylinder(d=Insert[OD] + 2*WallThick,h=Insert[LENGTH],$fn=8);
    }
    if (Inserts) // holes
    for (i=[-1,1], j=[-1,1])
    translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion])
    rotate(180/8)
    PolyCyl(Insert[OD],Insert[LENGTH] + FloorThick + Protrusion,8);
    }
    difference() {
    translate([0,0,CellOAL.z/2])
    cube(BlockOAL,center=true);
    translate([0,0,CellOAL.z])
    cube(ArrayOAL + [0,0,2*CellOAL.z],center=true);
    }
    }
    // Arduino bounding box
    // Origin at center bottom of PCB
    module Controller() {
    union() {
    translate([0,0,Arduino.z/2])
    cube(Arduino,center=true);
    translate([Arduino.x/2 – Protrusion,-USBPlug.y/2,USBOffset.z + TapeThick – USBPlug.z/2])
    cube(USBPlug + [Protrusion,0,0],center=false);
    }
    }
    // Baseplate
    module BasePlate() {
    difference() {
    translate([0,0,PlateOAL.z/2])
    cube(PlateOAL,center=true);
    translate([PlateOAL.x/2 – Arduino.x/2 – 2*WallThick,0,FloorThick])
    Controller();
    translate([PlateOAL.x/2 – Arduino.x/2 – 2*WallThick,0,FloorThick + PlateOAL.z/2])
    cube([Arduino.x – 2*2.0,WiringBay.y,PlateOAL.z],center=true); // cutouts beside MCU
    translate([0,0,PlateOAL.z – WiringBay.z + PlateOAL.z/2 – Protrusion])
    cube([PlateOAL.x – 2*WallThick,WiringBay.y,PlateOAL.z],center=true); // cutout above MCU
    translate([0,0,PlateOAL.z – WiringBay.z + PlateOAL.z/2 – Protrusion])
    cube([WiringBay.x,PlateOAL.y – 2*WallThick,PlateOAL.z],center=true); // cutout above MCU
    if (Inserts)
    for (i=[-1,1], j=[-1,1])
    translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion])
    rotate(180/8) {
    PolyCyl(Screw[ID],2*PlateOAL.z,8);
    PolyCyl(Screw[OD],ScrewRecess + Protrusion,8);
    }
    cube([45,17.0,2*LegendRecess],center=true);
    }
    linear_extrude(height=2*LegendRecess) {
    translate([0,1])
    rotate(-0*90) mirror([1,0,0])
    text(text="Ed Nisley",size=6,font="Arial:style:Bold",halign="center");
    translate([0,-6.5])
    rotate(-0*90) mirror([1,0,0])
    text(text="softsolder.com",size=4.5,font="Arial:style:Bold",halign="center");
    }
    Fin = [Screw[OD]/2 – 1.5*ThreadWidth,2*ThreadWidth,ScrewRecess – ThreadThick];
    if (Inserts && SupportInserts)
    color("Yellow")
    for (i=[-1,1], j=[-1,1])
    translate([i*InsertOC.x/2,j*InsertOC.y/2,0]) {
    rotate(180/8)
    cylinder(d=6*ThreadWidth,h=ThreadThick,$fn=8);
    for (a=[0:90:360])
    rotate(a)
    translate([Fin.x/2 + ThreadWidth/2,0,(ScrewRecess – ThreadThick)/2])
    cube(Fin,center=true);
    }
    }
    //———————–
    // Build things
    if (Layout == "Cell")
    LEDCell();
    else if (Layout == "CellArray")
    CellArray();
    else if (Layout == "MCU")
    Controller();
    else if (Layout == "Base")
    BasePlate();
    else if (Layout == "Show") {
    translate([0,0,3*PlateOAL.z])
    CellArray();
    BasePlate();
    translate([PlateOAL.x/2 – Arduino.x/2 – 2*WallThick,0,FloorThick])
    color("Orange",0.3)
    Controller();
    }
    else if (Layout == "Build") union() {
    translate([0,0.6*BlockOAL.y,0])
    CellArray();
    translate([0,-0.6*BlockOAL.x,0])
    rotate(90)
    BasePlate();
    }

  • Glass Tiles: Glow vs. Flash Firmware

    Glass Tiles: Glow vs. Flash Firmware

    Although it’s not obvious in a still picture, the firmware now supports both the continuously changing colors of the Nissan fog lamp (mashed with tweaks from the vacuum tube lights) and the randomly changing colors from the LED matrix, both using SK6812 LEDs rather than the failing WS2812 modules:

    Glass Tile - glow vs flash
    Glass Tile – glow vs flash

    Flash is a misnomer, as the tiles simply change from one color to the next, but I’ve never been adept at picking catchy names. In any event, the glass tiles on the left show nice pastel shades, in contrast to the bright primary(-ish) colors appearing on the right.

    The colors are random numbers from 1 to 7, because 0 produces a somewhat ugly dark cell. The SK6812 modules have a white LED in addition to the RGB LEDs in the WS2812 modules, so I replace the “additive white” R+G+B color with the more-or-less true white (warm, for these modules) LED.

    The new color goes into a cell picked at random (0 through 3, for 2×2 frames), except if the cell already holds the same color, whereupon a simple XOR flips the colors, except if the cell is already full-on white, whereupon it becomes half-on white to avoid going completely dark.

    The glass tiles must change colors at a much slower pace than the 8×8 LED matrix, because there are so few cells; a random delay between 500 ms and 6 s seems about right.

    They look really great in a dim room!

    The Arduino source code as a GitHub Gist:

    // Neopixel lighting for glass tiles
    // Ed Nisley – KE4ANU – May 2020
    #include <Adafruit_NeoPixel.h>
    #include <Entropy.h>
    //———-
    // Pin assignments
    const byte PIN_NEO = A3; // DO – data to first Neopixel
    const byte PIN_MODE = 2; // DI – select mode
    const byte PIN_SPEED = 3; // DI – select speed
    const byte PIN_SELECT = 4; // DO – drive adjacent pins low
    const byte PIN_HEARTBEAT = 13; // DO – Arduino LED
    //———-
    // Constants
    // number of pixels
    #define PIXELS 9
    // lag between adjacent pixels in degrees of slowest period
    #define PIXELPHASE 45
    // update LEDs only this many ms apart (minus loop() overhead)
    #define UPDATEINTERVAL 50ul
    // number of steps per cycle, before applying prime factors
    #define RESOLUTION 500
    //———-
    // Globals
    // instantiate the Neopixel buffer array
    Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXELS, PIN_NEO, NEO_GRBW + NEO_KHZ800);
    struct pixcolor_t {
    unsigned int Prime;
    unsigned int NumSteps;
    unsigned int Step;
    float StepSize;
    float Phase;
    byte MaxPWM;
    };
    unsigned int PlatterSteps;
    byte PrimeList[] = {3,5,7,13,19,29};
    unsigned int MaxTileTime;
    // colors in each LED
    enum pixcolors {RED, GREEN, BLUE, WHITE, PIXELSIZE};
    struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
    uint32_t UniColor;
    enum dispmode {GLOW, FLASH}; // based on input pin
    unsigned long UpdateMS;
    unsigned long MillisNow;
    unsigned long MillisThen;
    //– Select three unique primes for the color generator function
    // Then compute all the step parameters based on those values
    void SetColorGenerators(void) {
    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);
    do {
    Pixels[WHITE].Prime = PrimeList[random(sizeof(PrimeList))];
    } while (Pixels[WHITE].Prime == Pixels[RED].Prime ||
    Pixels[WHITE].Prime == Pixels[GREEN].Prime ||
    Pixels[WHITE].Prime == Pixels[BLUE].Prime);
    if (!digitalRead(PIN_SPEED)) { // force fast for debugging
    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);
    Pixels[RED].MaxPWM = 255;
    Pixels[GREEN].MaxPWM = 255;
    Pixels[BLUE].MaxPWM = 255;
    Pixels[WHITE].MaxPWM = 255;
    unsigned int PhaseSteps = (unsigned int) ((PIXELPHASE / 360.0) *
    RESOLUTION * (unsigned int) max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime));
    printf("Pixel phase offset: %d deg = %d steps\r\n",(int)PIXELPHASE,PhaseSteps);
    for (byte c=0; c < PIXELSIZE; c++) {
    Pixels[c].NumSteps = RESOLUTION * Pixels[c].Prime; // steps per cycle
    Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // radians per step
    Pixels[c].Step = random(Pixels[c].NumSteps); // current step
    Pixels[c].Phase = PhaseSteps * Pixels[c].StepSize;; // phase in radians for this color
    printf(" c: %d Steps: %5d Init: %5d Phase: %3d deg",c,Pixels[c].NumSteps,Pixels[c].Step,(int)(Pixels[c].Phase * 360.0 / TWO_PI));
    printf(" PWM: %d\r\n",Pixels[c].MaxPWM);
    }
    }
    //– 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_MODE,INPUT_PULLUP);
    pinMode(PIN_SPEED,INPUT_PULLUP);
    pinMode(PIN_SELECT,OUTPUT);
    digitalWrite(PIN_SELECT,LOW); // drive adjacent pins
    Serial.begin(57600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    printf("\r\nAlgorithmic Art Light – Glass Tiles\r\nEd Nisley – KE4ZNU – May 2020\r\n");
    printf("Display mode: %s\r\n",digitalRead(PIN_MODE) == GLOW ? "Glow" : "Flash");
    printf("Speed: %s\r\n",digitalRead(PIN_SPEED) ? "Normal" : "Override");
    Entropy.initialize(); // start up entropy collector
    // set up pixels
    strip.begin();
    strip.show();
    // lamp test
    printf("Lamp test: flash full-on colors\r\n");
    uint32_t FullRGBW = strip.Color(255,255,255,255);
    uint32_t FullRGB = strip.Color(255,255,255,0);
    uint32_t FullR = strip.Color(255,0,0,0);
    uint32_t FullG = strip.Color(0,255,0,0);
    uint32_t FullB = strip.Color(0,0,255,0);
    uint32_t FullW = strip.Color(0,0,0,255);
    uint32_t FullOff = strip.Color(0,0,0,0);
    uint32_t TestColors[] = {FullR,FullG,FullB,FullRGB,FullW,FullRGBW,FullOff};
    for (byte i=0; i < sizeof(TestColors)/sizeof(uint32_t) ; i++) {
    printf(" color: %08lx\r\n",TestColors[i]);
    for (int j=0; j < strip.numPixels(); j++) {
    strip.setPixelColor(j,TestColors[i]);
    }
    strip.show();
    delay(1000);
    }
    // while (1) {continue;}; // all LEDs constant for burn-in testing
    // get an actual random number
    uint32_t rn = Entropy.random();
    printf("Random seed: %08lx\r\n",rn);
    randomSeed(rn);
    // set up the color generators
    SetColorGenerators();
    MaxTileTime = (digitalRead(PIN_SPEED) ? 6 : 1) * (1000.0 / UPDATEINTERVAL);
    UpdateMS = UPDATEINTERVAL;
    MillisNow = MillisThen = millis();
    }
    //——————
    // Run the mood
    void loop() {
    MillisNow = millis();
    if ((MillisNow – MillisThen) >= UpdateMS) { // time for color change?
    digitalWrite(PIN_HEARTBEAT,HIGH);
    if (digitalRead(PIN_MODE) == GLOW) {
    boolean CycleRun = false; // check to see if all cycles have ended
    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 %5d steps %5d at %8ld delta %8ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow – MillisThen));
    }
    else {
    CycleRun = true; // this color is still cycling
    }
    }
    if (!CycleRun) {
    printf("All cycles ended: setting new color generator values\r\n");
    SetColorGenerators();
    }
    for (int i=0; i < strip.numPixels(); i++) { // for each pixel
    byte Value[PIXELSIZE];
    for (byte c=0; c < PIXELSIZE; c++) { // … for each color
    Value[c] = (Pixels[c].MaxPWM / 2.0) * (1.0 + sin(Pixels[c].Step * Pixels[c].StepSize – i*Pixels[c].Phase));
    }
    byte WhiteBias = min(min(Value[RED],Value[GREEN]),Value[BLUE]); // hack to reduce power
    UniColor = strip.Color((Value[RED] – WhiteBias) * Pixels[RED].MaxPWM/255,
    (Value[GREEN] – WhiteBias) * Pixels[GREEN].MaxPWM/255,
    (Value[BLUE] – WhiteBias) * Pixels[BLUE].MaxPWM/255,
    WhiteBias * Pixels[WHITE].MaxPWM/255);
    strip.setPixelColor(i,UniColor);
    }
    }
    else {
    byte c = random(1,8); // exclude 0 = all off, to avoid darkness
    printf("Color %d ",c);
    byte r = c & 0x04 ? 0xff : 0;
    byte g = c & 0x02 ? 0xff : 0;
    byte b = c & 0x01 ? 0xff : 0;
    byte w = 0;
    if (c == 7) { // use white LED instead of R+G+B
    r = g = b = 0;
    w = 0xff;
    }
    UniColor = strip.Color(r, g, b, w);
    byte i = random(strip.numPixels());
    printf("at %d ",i);
    if (UniColor == strip.getPixelColor(i)) { // flip color
    printf("^ ");
    if (w) { // white becomes dim
    w = 0x7f;
    UniColor = strip.Color(r, g, b, w);
    }
    else
    UniColor ^= 0xffffff00l; // other colors flip
    }
    else {
    printf(" ");
    }
    strip.setPixelColor(i,UniColor);
    UpdateMS = random(10,MaxTileTime) * UPDATEINTERVAL; // pick time for next update
    printf("delay: %6ld ms\r\n",UpdateMS);
    }
    strip.show(); // send out precomputed colors
    MillisThen = MillisNow;
    digitalWrite(PIN_HEARTBEAT,LOW);
    }
    }
    view raw GlassTiles.ino hosted with ❤ by GitHub