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: Electronics Workbench

Electrical & Electronic gadgets

  • SJCAM M20 Camera: Tour Easy Seat Mount

    The general idea is to replace this:

    M20 in waterproof case - Tour Easy seat
    M20 in waterproof case – Tour Easy seat

    With this:

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

    Thereby solving two problems:

    • Pitifully small battery capacity
    • Wobbly camera support

    The battery is an Anker PowerCore 13000 Power Bank plugged into the M20’s USB port. Given that SJCAM’s 1 A·h batteries barely lasted for a typical hour of riding, the 13 A·h PowerCore will definitely outlast my legs. The four blue dots just ahead of the strap around the battery show it’s fully charged and the blue light glowing through the case around the M20 indicates it’s turned on.

    The solid model has four parts:

    SJCAM M20 Mount - Fit layout
    SJCAM M20 Mount – Fit layout

    Which, as always, incorporates improvements based on the actual hardware on the bike.

    A strap-and-buckle belt harvested from a defunct water pack holds the battery into the cradle and the cradle onto the rack, with a fuzzy velcro strip stuck to the bottom to prevent sliding:

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

    The shell around the camera is basically a box minus the camera:

    SJCAM M20 Mount - Show - shell
    SJCAM M20 Mount – Show – shell

    The shell builds as three separate slabs, with the center section having cutouts ahead of the camera’s projections to let it slide into place:

    SJCAM M20 Mount - Show - shell sections
    SJCAM M20 Mount – Show – shell sections

    The new shell version is 30.5 mm thick, so a 40 mm screw will stick out maybe 5 mm beyond the nylon locknut. I trust the screws will get lost in the visual noise of the bike.

    A peg sticking out behind the USB jack anchors the cable in place:

    SJCAM M20 Mount - Show - shell sections - USB side
    SJCAM M20 Mount – Show – shell sections – USB side

    The front slab and center top have curves matching the M20 case:

    SJCAM M20 Mount - Show - shell sections - button side
    SJCAM M20 Mount – Show – shell sections – button side

    The camera model has a tidy presentation option:

    SJCAM M20 Mount - Show - M20 body
    SJCAM M20 Mount – Show – M20 body

    And an ugly option to knock the protruberances out of the shell:

    SJCAM M20 Mount - Show - M20 body - knockouts
    SJCAM M20 Mount – Show – M20 body – knockouts

    The square-ish post on the base fits into an angled socket in the clamp around the seat rail:

    SJCAM M20 Mount - Show - clamp
    SJCAM M20 Mount – Show – clamp

    The numbers correspond to the “Look Angle” of the socket pointing the camera toward overtaking traffic. The -20° in the first clamp shows a bit too much rack:

    SJCAM M20 Mount - first ride - traffic - 2019-02-06
    SJCAM M20 Mount – first ride – traffic – 2019-02-06

    It may not matter, though, as sometimes you want to remember what’s on the right:

    SJCAM M20 Mount - first ride - 2019-02-06
    SJCAM M20 Mount – first ride – 2019-02-06

    FWIW, the track veering off onto the grass came from a fat-tire bike a few days earlier. Most of the rail trail had cleared by the time we tried it, with some ice and snow in rock cuts and shaded areas.

    Contrary to the first picture, I later remounted the camera under the seat rail with its top side downward. The M20 has a “rotate video” mode for exactly that situation, which I forgot to turn off in the fancy new mount, so I rotated the pix afterward.

    A 3 mm screw extends upward through the hole in the socket to meet a threaded brass insert epoxied into the shell base, as shown in the uglified M20 model. Despite appearances, the hole is perpendicular to both the socket and the shell, so you can tweak the Look Angle without reprinting the shell.

    All in all, the mount works well. We await better riding weather …

    The OpenSCAD source code as a GitHub Gist:

    // SJCAM M20 Camera Mount for Tour Easy seat back rail
    // Ed Nisley – KE4ZNU
    // 2019-02
    /* [Layout Options] */
    Layout = "Fit"; // [Show,Fit,Build]
    Part = "Shell"; // [Cradle,Shell,Clamp,ShellSections,M20,Interposer,Battery,Buttons]
    LookAngle = [0,5,-25]; // camera angle, looking backwards
    /* [Extrusion Parameters] */
    ThreadWidth = 0.40;
    ThreadThick = 0.25;
    HoleWindage = 0.2;
    Protrusion = 0.1;
    //—–
    // Dimensions
    /* [Hidden] */
    ID = 0;
    OD = 1;
    LENGTH = 2;
    ClampScrew = [5.0,10.0,50.0]; // ID=thread OD=washer LENGTH=total
    ClampInsert = [5.0,7.5,10.5]; // brass insert
    MountScrew = [3.0,7.0,23]; // ID=thread OD=washer LENGTH=tune to fit clamp arch
    MountInsert = [3.0,4.95,8.0]; // ID=screw OD, OD=knurl dia
    EmbossDepth = 2*ThreadThick + Protrusion; // recess depth + Protrusion beyond surface
    DebossHeight = EmbossDepth; // text height + Protrusion into part
    Projection = 10; // stick-out to punch through shell sides & suchlike
    SupportColor = "Yellow";
    FadeColor = "Green";
    FadeAlpha = 0.25;
    //—–
    // Useful routines
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,
    h=Height,
    $fn=Sides);
    }
    //—–
    // M20 Camera
    // Looks backwards from seat = usual right-hand coordinates work fine
    // X parallel to bike frame, Y parallel to seat strut, Z true vertical
    M20 = [24.5,40.5,54.0];
    M20tm = 4.0; // chord height at top of case
    M20tr = (pow(M20tm,2) + pow(M20.y,2)/4) / (2*M20tm); // … radius
    echo(str("Top radius: ",M20tr));
    M20TopSides = 3*3*4;
    echo(str(" … sides: ",M20TopSides));
    M20fm = 1.0; // chord height at front of case
    M20fr = (pow(M20fm,2) + pow(M20.y,2)/4) / (2*M20fm); // … radius
    echo(str("Front radius: ",M20fr));
    M20FrontSides = ceil(M20fr / M20tr * M20TopSides); // make arc sides match up
    echo(str(" … sides: ",M20FrontSides));
    Lens = [19.0,22.5,5.5]; // ID=optical element, OD=tube
    LensBezel = [23.0,24.5,2.5]; // ID=lens tube, OD=bezel
    LensOffset = [-M20fm,0,41.5]; // bottom of case to lens centerline
    LensCap = [Lens[OD],24.5,4.5]; // silicone lens cap
    Spkr = [0.75,M20.y,14.3]; // speaker recess below LCD
    Switch = [8.0,1.0,38.0]; // selection switches
    SwitchOffset = [9.0,0,0]; // from rear to center of switches
    Jack = [10.0,0.1,36.0]; // jack and MicroSD card access, slightly enlarged
    JackOffset = [10.0,0,30.0]; // rear, bottom to center of jack block
    USB = [JackOffset.x – Jack.x/2,20.0,10.0]; // strut under USB plug
    USBOffset = [0,0,33.5]; // bottom to center of jack
    SDCard = [2.0,0.1,12.0]; // SD Card slot
    SDOffset = [9.0,0,20.0]; // bottom, rear to center of slot
    Button = [8.5,10.5,M20tm]; // ID = button, OD = bezel
    ButtonOC = 18.0; // on-center Y separation, assume X centered
    Screen = [0.1,31,24]; // LCD on rear face
    ScreenOffset = [0,0,33];
    BarLEDs = [0.1 + M20fm,12.0,5.0]; // Bar LEDs on front face
    BarLEDsOffset = [-M20fm,0,12.5];
    PwrLED = [3.5,3.5,0.1 + M20tm]; // power LED on top
    PwrLEDOffset = [2.5,0,0];
    RearLEDs = [1.0,2.0,0.1]; // charge and power LED openings above LCD
    RearLEDsOffset = [0,13.0/2,M20tm + 3.0]; // .. from top center of case
    module Buttons(KO) {
    for (j = [-1,1])
    translate([0,j*ButtonOC/2,0]) {
    cylinder(d=Button[OD],h=Button[LENGTH],$fn=12);
    if (KO)
    translate([0,0,M20tm])
    cylinder(d1=Button[OD],d2=1.5*Button[OD],h=Button.z,$fn=12);
    }
    }
    module M20Shape(Knockout = false) {
    difference() {
    intersection() {
    translate([0,0,M20.z/2 – M20tr]) // top curve
    rotate([0,90,0]) rotate(180/M20TopSides)
    cylinder(r=M20tr,h=2*(M20.x + Protrusion),$fn=M20TopSides,center=true);
    translate([M20.x/2 – M20fr,0,0])
    rotate(180/M20FrontSides)
    cylinder(r=M20fr,h=2*M20.z,$fn=M20FrontSides,center=true);
    cube(M20,center=true);
    }
    translate([Spkr.x/2 – M20.x/2 – Protrusion,0,Spkr.z/2 – Protrusion/2 – M20.z/2])
    cube(Spkr + [Protrusion,2*Protrusion,Protrusion],center=true);
    }
    translate([M20.x/2,0,-M20.z/2] + LensOffset)
    rotate([0,90,0])
    cylinder(d=Lens[OD] + HoleWindage,h=(Knockout ? Projection : Lens[LENGTH]),$fn=4*4*3,center=false);
    translate([M20.x/2 + M20fm/2,0,-M20.z/2] + LensOffset) // lens bezel
    rotate([0,90,0])
    cylinder(d1=LensBezel[OD],d2=Lens[OD],h=LensBezel[LENGTH],$fn=4*4*3,center=false);
    translate([-M20.x/2 + SwitchOffset.x, // side switches
    -(Switch.y + M20.y – Protrusion)/2,
    0])
    cube(Switch + [0,Protrusion,0] + (Knockout ? [0,Projection,0] : [0,0,0]),center=true);
    if (Knockout)
    translate([(M20.x/2 – M20fm)/2,-M20.y/2,0]) // side switch slide-in clearance
    cube([M20.x/2 – M20fm,2*Switch.y,Switch.z],center=true);
    translate([-M20.x/2 + JackOffset.x,
    (Jack.y + M20.y – Protrusion)/2,
    JackOffset.z – M20.z/2])
    cube(Jack + [0,Protrusion,0] + (Knockout ? [0,Projection,0] : [0,0,0]),center=true);
    translate([0,0,M20.z/2 – M20tm]) // top control buttons
    Buttons(Knockout);
    if (Knockout)
    translate([(M20.x – M20fm)/4,0,M20.z/2 – M20tm + Button[LENGTH]/2]) // slide-in button clearance
    cube([(M20.x – M20fm)/2,ButtonOC + Button[OD],Button[LENGTH]],center=true);
    translate([-(M20.x + Screen.x – Protrusion)/2,0,-M20.z/2] + ScreenOffset)
    cube(Screen + [Protrusion,0,0] + (Knockout ? [Projection,0,0] : [0,0,0]),center=true);
    for (j = [-1,1])
    translate([-M20.x/2 + Protrusion,j*RearLEDsOffset.y,M20.z/2 – RearLEDsOffset.z])
    rotate([0,-90,0]) rotate(180/6)
    PolyCyl(RearLEDs[OD],Knockout ? Projection : RearLEDs[LENGTH],6);
    translate([M20.x/2 + BarLEDs.x/2,0,-M20.z/2] + BarLEDsOffset)
    cube(BarLEDs + (Knockout ? [Projection,0,0] : [0,0,0]),center=true);
    translate([0,0,M20.z/2 – M20tm] + PwrLEDOffset)
    rotate(180/8)
    PolyCyl(PwrLED[OD],(Knockout ? Projection : PwrLED[LENGTH]),8);
    if (Knockout) {
    translate([0,0,-M20.z/2])
    rotate([180,0,0]) { // mounting screw
    PolyCyl(MountScrew[ID],MountScrew[LENGTH],6);
    translate([0,0,MountScrew[LENGTH] – Protrusion])
    PolyCyl(MountScrew[OD],MountScrew[ID] + 4*ThreadThick,6); // SHCS head is about 1 ID long
    }
    translate([0,0,-(M20.z/2 + MountInsert[LENGTH] + 4*ThreadWidth – Protrusion)])
    PolyCyl(MountInsert[OD],MountInsert[LENGTH] + 4*ThreadWidth,6); // insert inside Interposer
    }
    }
    //—–
    // Shell
    // Wraps around camera
    NomWall = 3.0;
    ShellWall = [IntegerMultiple(NomWall,ThreadThick),
    IntegerMultiple(NomWall,ThreadWidth),
    IntegerMultiple(NomWall,ThreadWidth)];
    ShellRadius = ShellWall.x;
    ShellSides = 8;
    ShellOA = M20 + 2*ShellWall;
    echo(str("Shell OA: ",ShellOA));
    Interposer = [M20.x – M20fm,M20.x – M20fm,10.0]; // if you can't be smart, be square
    module Shell() {
    Screw = [3.0,6.75,30]; // ID=thread OD=washer LENGTH
    ScrewClear = 1.0; // additional washer clearance
    ScrewSides = 8;
    ScrewOC = M20 + [0,Screw[ID]/cos(180/ScrewSides),Screw[ID]/cos(180/ScrewSides)]; // use PolyCyl hole dia, ignore .x value
    difference() {
    union() {
    hull()
    for (i=[-1,1], j=[-1,1], k=[-1,1])
    translate([i*(ShellOA.x – 2*ShellRadius)/2,
    j*(ShellOA.y – 2*ShellRadius)/2,
    k*(ShellOA.z – 2*ShellRadius)/2])
    sphere(r=ShellRadius/cos(180/ShellSides),$fn=ShellSides); // fix low-poly approx radius
    for (j=[-1,1], k=[-1,1]) // screw bosses, full length
    translate([0,j*ScrewOC.y/2,k*ScrewOC.z/2])
    rotate([0,90,0]) rotate(180/ScrewSides)
    cylinder(d=Screw[OD] + ScrewClear,h=ShellOA.x,center=true,$fn=ScrewSides);
    translate([-(ShellOA.x – USB.x – ShellWall.x)/2, // USB plug support strut
    (M20.y + USB.y)/2 – ShellRadius,
    -M20.z/2] + USBOffset)
    hull()
    for (i=[-1,1], j=[-1,1], k=[-1,1])
    translate([i*(USB.x + ShellWall.x – 2*ShellRadius)/2,
    j*(USB.y – 2*ShellRadius)/2,
    k*(USB.z – 2*ShellRadius)/2])
    rotate(0*180/ShellSides) rotate([90,0,90])
    sphere(r=ShellRadius/cos(180/ShellSides),$fn=ShellSides);
    translate([-M20fm/2,0,-ShellOA.z/2 – Interposer.z + Protrusion/2])
    InterposerShape(Embiggen = false);
    }
    render(convexity=4) // remove camera shape from interior
    M20Shape(Knockout = true);
    for (j=[-1,1], k=[-1,1]) // screw bores
    translate([-ShellOA.x,j*ScrewOC.y/2,k*ScrewOC.z/2])
    rotate([0,90,0]) rotate(180/ScrewSides)
    PolyCyl(Screw[ID],2*ShellOA.x,ScrewSides);
    translate([ShellOA.x/2 – ThreadThick + Protrusion/2,0,-5]) // recess for legend
    cube([EmbossDepth,ShellOA.y – 12,7],center=true);
    translate([0,(M20.y + 1.5*SDCard.z)/2 + ThreadWidth,-M20.z/2 + SDOffset.z])
    resize([M20.x,0,0])
    sphere(d=1.5*SDCard.z,$fn=24);
    }
    translate([ShellOA.x/2 – DebossHeight,0,-5])
    rotate([90,0,90])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text="KE4ZNU",size=5,spacing=1.20,font="Arial:style:Bold",halign="center",valign="center");
    // Totally ad-hoc support structures
    if (false)
    color(SupportColor) {
    for (j=[-1,1], k=[0,1])
    translate([-ShellOA.x/2 + Screw[LENGTH],j*ShellOA.y/2,k*ShellOA.z])
    rotate([0,90,0])
    SupportScrew(Dia=Screw[OD] + ScrewClear,Length=ShellOA.x – Screw[LENGTH],Num=ScrewSides);
    }
    }
    // Generate support structure for screw boss
    module SupportScrew(Dia,Length,Num = 6) {
    for (a=[0 : 360/Num : 360/2])
    rotate(a)
    translate([0,0,(Length + ThreadThick)/2])
    cube([Dia – 2*ThreadWidth,2*ThreadWidth,Length – ThreadThick],center=true);
    }
    // Generate interposer block
    // Origin at center bottom surface for E-Z rotation
    module InterposerShape(Embiggen = false) {
    translate([0,0,Interposer.z/2])
    if (Embiggen) {
    minkowski() {
    cube(Interposer,center=true);
    cube(HoleWindage,center=true);
    }
    }
    else
    cube(Interposer + [-Protrusion,0,Protrusion],center=true); // avoid slivers, merge with shell
    }
    // Cut shell sections for printing
    // "Front" = lens end, toward +X direction
    // origin centered on M20.xyz and ShellOA.xyz
    module ShellSection(Section="Front") {
    if (Section == "Front") // include front curve
    intersection() {
    Shell();
    translate([ShellOA.x – (M20fm + ShellWall.x),0,0])
    cube([ShellOA.x,2*ShellOA.y,2*ShellOA.z],center=true);
    }
    else if (Section == "Center") // exclude front curve for E-Z printing
    intersection() {
    Shell();
    translate([-M20fm/2,0,0])
    cube([M20.x – M20fm,2*ShellOA.y,2*ShellOA.z],center=true);
    }
    else if (Section == "Back") // flush with LCD on rear face
    intersection() {
    Shell();
    translate([-ShellOA.x + (ShellWall.x),0,0])
    cube([ShellOA.x,2*ShellOA.y,2*ShellOA.z],center=true);
    }
    }
    //—–
    // Clamp
    // Grips seat frame rail
    // Uses shell rounding values for tidiness
    // Adjust MountScrew[LENGTH] to put head more-or-less flush with clamp arch
    RailOD = 20.0; // slightly elliptical in bent section
    RailSides = 2*3*4;
    ClampOA = [60.0,40.0,ClampScrew[LENGTH]]; // set clamp size to avoid weird screw spacing
    echo(str("Clamp OA: ",ClampOA));
    ClampOffset = 0.0; // raise clamp to allow more room for mount
    ClampTop = ClampOA.z/2 + ClampOffset;
    InsertCap = 6*ThreadThick; // fill layers atop inserts
    Kerf = 2.0;
    module Clamp(Support = false) {
    RibThick = 2*ThreadWidth;
    NumRibs = IntegerMultiple(ceil(ClampOA.y / 4.0),2); // space ribs roughly 4 mm apart
    RibSpace = ClampOA.y / NumRibs;
    echo(str("Ribs: ",NumRibs," spaced: ",RibSpace));
    ClampScrewOC = IntegerMultiple(ClampOA.x – ClampScrew[OD] – 10*ThreadWidth,1.0);
    echo(str("ClampScrew OC: ",ClampScrewOC));
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1], k=[-1,1])
    translate([i*(ClampOA.x – 2*ShellRadius)/2,
    j*(ClampOA.y – 2*ShellRadius)/2,
    k*(ClampOA.z – 2*ShellRadius)/2 + ClampOffset])
    sphere(r=ShellRadius/cos(180/ShellSides),$fn=ShellSides);
    cube([2*ClampOA.x,2*ClampOA.y,Kerf],center=true); // split across middle
    rotate([90,0,0]) // seat rail
    cylinder(d=RailOD,h=2*ClampOA.y,$fn=RailSides,center=true);
    for (i=[-1,1]) // clamp inserts
    translate([i*ClampScrewOC/2,0,0])
    rotate(180/6)
    PolyCyl(ClampInsert[OD],ClampTop – InsertCap,6);
    for (i=[-1,1]) // clamp screw clearance
    translate([i*ClampScrewOC/2,0,-(ClampOA.z/2 – ClampOffset) – InsertCap])
    rotate(180/6)
    PolyCyl(ClampScrew[ID],ClampOA.z,6);
    translate([0,0,ClampTop + 0.7*Interposer.z]) // mounting bolt hole
    rotate(LookAngle)
    translate([0,0,ShellOA.z/2]) {
    M20Shape(Knockout = true);
    translate([0,0,-ShellOA.z/2 – Interposer.z])
    InterposerShape(Embiggen = true);
    }
    translate([ClampOA.x/2 – (EmbossDepth – Protrusion)/2, // recess for LookAngle.z
    0,
    ClampOA.z/4 + ClampOffset])
    cube([EmbossDepth,17,8],center=true);
    translate([0.3*ClampOA.x, // recess for LookAngle.z
    -(ClampOA.y/2 – (EmbossDepth – Protrusion)/2),
    ClampOA.z/4 + ClampOffset])
    cube([10,EmbossDepth,8],center=true);
    translate([0,0,-ClampOA.z/2 + (EmbossDepth – Protrusion)/2]) // recess bottom legend
    cube([35,10,EmbossDepth],center=true);
    }
    translate([ClampOA.x/2 – DebossHeight,0,ClampOA.z/4 + ClampOffset]) // LookAngle.z legend
    rotate([90,0,90])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text=str(LookAngle.z),size=6,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    translate([0.3*ClampOA.x,-ClampOA.y/2 + DebossHeight + Protrusion/2,ClampOA.z/4 + ClampOffset]) // LookAngle.y legend
    rotate([90,0,00])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text=str(LookAngle.y),size=6,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    translate([0,0,-ClampOA.z/2])
    linear_extrude(height=DebossHeight,convexity=20)
    mirror([0,1,0])
    text(text="KE4ZNU",size=5,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    if (Support) {
    difference() {
    color(SupportColor)
    union() {
    for (j=[-NumRibs/2:NumRibs/2])
    translate([0,j*RibSpace,0])
    rotate([90,0,0])
    cylinder(d=RailOD – 2*ThreadThick,h=RibThick,$fn=2*3*4,center=true);
    cube([RailOD – 4*ThreadWidth,NumRibs*RibSpace,Kerf + 2*ThreadThick],center=true);
    }
    cube([2*ClampOA.x,2*ClampOA.y,Kerf],center=true); // split across middle
    }
    }
    }
    //—–
    // Battery
    // Based on Anker PowerCore, simplified shapes
    // Includes port & button punchouts
    Battery = [97.5,80.0,22.5]; // X=length, Y includes rounded edges, Z = Y dia
    module BatteryShape() {
    USB = [Projection,38,10]; // clearance around USB output ports
    USBOffset = [0,25.5,0]; // from -Y edge to center of USB block
    ChargeBtn = [11.0 + 5.0,10,5.0 + 5.0]; // charge level check button, enlarged
    Btnc = ChargeBtn.z; // figure button recess into battery curve
    Btnr = Battery.z/2;
    Btnm = Btnr – sqrt(pow(Btnr,2) – pow(Btnc,2)/4);
    ChargeBtnOffset = [17.0,0,0]; // from +X edge to center, centered on Z
    BatterySides = 2*3*4;
    hull()
    for (j=[-1,1])
    translate([0,j*(Battery.y – Battery.z)/2,0])
    rotate([0,90,0])
    cylinder(d=Battery.z,h=Battery.x,$fn=BatterySides,center=true);
    translate([(Battery.x + USB.x)/2 – Protrusion,-Battery.y/2 + USBOffset.y,0])
    cube(USB,center=true);
    translate([Battery.x/2 – ChargeBtnOffset.x,Battery.y/2 + ChargeBtn.y/2 – 2*Btnm,0])
    cube(ChargeBtn,center=true);
    }
    //—–
    // Battery cradle
    RackWidth = 89.0; // flat width between rack rails
    CradleWall = [4.0,4.0,3.0]; // wall thickness
    CradleRadius = 2.0; // corner rounding
    CradlePad = 0.5; // cushion around battery
    BatteryBase = CradleWall.z + CradlePad; // actual bottom surface of battery
    CradleOA = [Battery.x + 2*CradleWall.x,
    min((Battery.y + 2*CradleWall.y),RackWidth),
    BatteryBase + Battery.z/3];
    echo(str("Cradle OA: ",CradleOA));
    module Cradle() {
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1]) { // box with tidy rounded corners
    translate([i*(CradleOA.x/2 – CradleRadius),
    j*(CradleOA.y/2 – CradleRadius),
    1*(CradleOA.z – CradleRadius)])
    sphere(r=CradleRadius,$fn=6);
    translate([i*(CradleOA.x/2 – CradleRadius),
    j*(CradleOA.y/2 – CradleRadius),
    0*(CradleOA.z/2 – CradleRadius)])
    cylinder(r=CradleRadius,h=CradleOA.z/2,$fn=6);
    }
    translate([0,0,Battery.z/2 + BatteryBase]) // minus the battery
    minkowski(convexity=3) { // … slightly embiggened
    BatteryShape();
    cube(2*CradlePad,center=true);
    }
    if (false) // reveal insets for debug
    translate([0,0,-Protrusion])
    cube(CradleOA + [0,0,CradleOA.z],center=false);
    translate([0,0,CradleWall.z – ThreadThick + Protrusion/2]) // recess top legend
    cube([55,20,EmbossDepth],center=true);
    translate([0,0,(EmbossDepth – Protrusion)/2]) // recess bottom legend
    cube([70,15,EmbossDepth],center=true);
    }
    translate([0,4.0,CradleWall.z – DebossHeight – Protrusion])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text="PowerCore",size=6,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    translate([0,-4.0,CradleWall.z – DebossHeight – Protrusion])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text="13000",size=6,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    linear_extrude(height=DebossHeight,convexity=20)
    mirror([0,1,0])
    text(text="KE4ZNU",size=10,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    }
    //—–
    // Build things
    // Layouts for design & tweaking
    if (Layout == "Show")
    if (Part == "Battery")
    BatteryShape();
    else if (Part == "Buttons")
    Buttons();
    else if (Part == "Interposer")
    InterposerShape(Embiggen = false);
    else if (Part == "Shell")
    Shell();
    else if (Part == "M20")
    M20Shape(Knockout = false);
    else if (Part == "ShellSections") {
    translate([ShellOA.x,0,0])
    ShellSection(Section="Front");
    translate([0,0,0])
    ShellSection(Section="Center");
    translate([-ShellOA.x,0,0])
    ShellSection(Section="Back");
    }
    else if (Part == "Clamp") {
    Clamp(Support = false);
    color(FadeColor,FadeAlpha)
    rotate([90,0,0])
    cylinder(d=RailOD,h=2*ClampOA.y,$fn=RailSides,center=true);
    }
    else if (Part == "Cradle") {
    Cradle();
    translate([0,0,Battery.z/2 + CradleWall.z])
    color(FadeColor,FadeAlpha)
    BatteryShape();
    }
    // Build layouts for top-level parts
    if (Layout == "Build")
    if (Part == "Cradle")
    Cradle();
    else if (Part == "Clamp") {
    translate([0,0.7*ClampOA.y,0])
    difference() {
    translate([0,0,-Kerf/2])
    Clamp(Support = true);
    translate([0,0,-ClampOA.z])
    cube(2*ClampOA,center=true);
    }
    translate([0,-0.7*ClampOA.y,-0])
    difference() {
    translate([0,0,-Kerf/2])
    rotate([0,180,0])
    Clamp(Support = true);
    translate([0,0,-ClampOA.z])
    cube(2*ClampOA,center=true);
    }
    }
    else if (Part == "Shell") {
    translate([0,-1.2*ShellOA.y,ShellOA.x/2])
    rotate([0,90,180])
    ShellSection(Section="Front");
    translate([0,0,M20.x/2])
    rotate([0,-90,0])
    ShellSection(Section="Center");
    translate([0,1.4*ShellOA.y,ShellOA.x/2])
    rotate([0,-90,180])
    ShellSection(Section="Back");
    }
    // Ad-hoc arrangement to see how it all goes together
    if (Layout == "Fit") {
    rotate(180) {
    Cradle();
    translate([0,0,Battery.z/2 + CradleWall.z])
    color(FadeColor,FadeAlpha)
    BatteryShape();
    }
    translate([0,-100,0]) {
    Clamp();
    color(FadeColor,FadeAlpha)
    rotate([90,0,0])
    cylinder(d=RailOD,h=2*ClampOA.y,$fn=RailSides,center=true);
    }
    translate([0,-100,(ClampOA.z + ShellOA.z)/2 + Interposer.z])
    translate([0,0,-ShellOA.z/2 + Interposer.z])
    rotate(LookAngle)
    translate([0,0,ShellOA.z/2]) {
    Shell();
    color(FadeColor,FadeAlpha)
    M20Shape(Knockout = false);
    }
    }

  • “5 W” G4 COB LED Specsmanship

    A bag of G4 COB LEDs arrived from halfway around the planet:

    G4 COB LEDs - 15 and 18 LED modules
    G4 COB LEDs – 15 and 18 LED modules

    Those are “5 W” and “4 W” cool white modules, respectively, with another set of 4 W warm white looking pretty much the same. There’s no provision for heatsinking, which makes the wattage seem suspect; halogen G4 bulbs run around 20 W, for whatever that’s worth.

    The silicone overlay becomes nearly transparent when seen through an ordinary desktop document scanner:

    Circular 12V COB 18 LED panel
    Circular 12V COB 18 LED panel

    Highlighting the PCB copper pours shows 18 LEDs arranged in three series groups of six LEDs in parallel:

    Circular 12V COB 18 LED panel - copper layout
    Circular 12V COB 18 LED panel – copper layout

    The “smart IC” touted in the writeup turns out to be a bridge rectifier for AC or DC power:

    G4 COB LED - 18 LED - components
    G4 COB LED – 18 LED – components

    The SMD resistors on all 15 modules measure 27.6 Ω, more or less, and seem randomly oriented face-up or face-down. I assume that one is face-down; maybe it’s just unlabeled on both sides.

    Back of the envelope: there’s no way it will dissipate 5 W. The bridge drops 1.4 V = 2×0.7, the LEDs drop maybe 9 V, leaving the resistor with 1.6 V to pass all of 60 mA, so call it 700 mW.

    Some measurements:

    G4 COB LED measurements
    G4 COB LED measurements

    With 12 VDC applied to the pins, the bridge drops 1.6 V, the LEDs 8.2 V, and the resistor 2.2 V, with 80 mA through the whole affair dissipating just under 1 W.

    Huh.

    Cranking the supply until the current hits 200 mA puts 15.7 V across the pins for a total dissipation of 3.1 W, burning 1.7 W in the LEDs and 1.1 W in the resistor.

    Cranking the supply to 21.3 V drives 410 mA, dissipates just under 9 W total, produces a curl of rosin smoke from the PCB, and maybe delaminates the silicone around some of the LEDs.

    OK, now I have a crash test dummy.

    Given complete control over the application, I’ll strip everything off the PCB and bond it to a heatsink of some sort. With 6 LEDs in parallel, 120 mA (6 × 20 mA) total current might be reasonable and 200 mA (6 × 30 mA) probably won’t kill the things outright. Plus, I have spares.

    An external 18 Ω resistor should suffice. Perhaps a pair of 6 Ω SMD resistors on the PCB, with fine-tuning through an external resistor. Call it 250 mW apiece: don’t use little bitty SMD resistors.

  • NP-BX1 Lithium Batteries: DOT-01

    A quartet of DOT01 NP-BX1 batteries arrived:

    Dot01 NP-BX1 - new 2019-02
    Dot01 NP-BX1 – new 2019-02

    The dotted lines show the results from late 2015 for a pair of then-new Wasabi NP-BX1 batteries, so the DOT-01 batteries look about the same. The F battery barely lasted to the halfway point of our most recent bike ride and the G battery now resides in the blinky-and-glowy pile.

    I’d be unsurprised to discover all the myraid “different” NP-BX1 batteries all come from the same factory. Unlike the Wasabi batteries, these lack date codes, which seems like an extra-cost option you don’t get on the low end.

  • 2015 Subaru Forester Battery

    With the intent of being able to find a picture of the battery in our 2015 Subaru Forester when I need it:

    The manual says the “battery type” is 55D23L, with a 48 A·h capacity.

    Here in the US, we measure a battery’s physical size with “Group Size” numbers which have no relation with JIS numbers, despite some overlapping or similar numeric values. The money quote:

    Definition of Group Size: The Battery Council International (BCI) assigns numbers and letters to common battery types. These numbers and letters are standards for maximum container size, location and type of terminal and special container features.

    So, it’s random. Choose a retailer, feed in the automobile year / make / model, and discover I need a Group 35 battery.

    The label includes “390 CCA”, which is the Cold Cranking Amps rating:

    The rating refers to the number of amps a 12-volt battery can deliver at 0°F for 30 seconds while maintaining a voltage of at least 7.2 volts

    So, if you’re building an automotive gadget and expect the battery to deliver something like 12 V, you’re wrong. Bonus protip: look up “load dump” to get an idea of the highest voltage.

    The “20 HR 48 Ah” specifies the Reserve Capacity:

    Amp Hour or C20 is an indicator of how much energy is stored in a battery. It is the energy a battery can deliver continuously for 20 hours at 80°F without falling below 10.5 volts.

    So a constant load of 2.4 A would do the trick, should you leave a few lights on overnight during the summer. In wintertime, you’re on your own.

    Because hell hath no fury like that of an unjustified assumption, the terminals are on the top surface toward the rear, with the positive lug on the left when you’re standing at the front bumper. That may be the “L” in “D23L”.

    Long ago, I ran afoul of an automotive battery which required knowing the terminal chirality and, of course, I bought the wrong one. Now I have a picture!

  • Makergear M2: Z-Axis Platform Sensor Switch, Replacement Thereof

    After nearly four years of dangling a bare millimeter above the nozzle, the lever on the relocated Z-Axis switch finally snagged a stray thread and got bent out of shape. I un-bent it, but finally decided it was time to get more air between the nozzle and the switch actuator.

    The small shim reduces the actuation distance:

    file:///mnt/bulkdata/Cameras/2019/Shop Projects/Makergear M2/Z-Axis Switch/IMG_20190204_185300 - M2 Z-Axis - microswitch exterior
    file:///mnt/bulkdata/Cameras/2019/Shop Projects/Makergear M2/Z-Axis Switch/IMG_20190204_185300 – M2 Z-Axis – microswitch exterior

    Prying the ends outward with a thumbnail releases a pair of snaps and the cover pops off to reveal the innards:

    M2 Z-Axis - microswitch interior
    M2 Z-Axis – microswitch interior

    The spring-loaded innards will launch themselves into the far corners of your shop, so be gentle as you slide the lever out and reinstall the side plate with a pair of clicks.

    I filed the screw holes in my homebrew brass angle plate into slots, so as to get some adjustability, remounted the switch on the X-axis gantry, and tuned for best clearance:

    M2 Z-Axis - bare microswitch vs nozzle
    M2 Z-Axis – bare microswitch vs nozzle

    It looks a bit more canted than it really is.

    There’s about 1.6 mm of Z-axis distance between the nozzle and the switch, which should suffice for another few years.

    The view from the front shows a slight angle, too:

    M2 Z-Axis - activated
    M2 Z-Axis – activated

    There’s a millimeter or so below the nuts holding the X-axis linear slide in place, because the original 18 mm M3 SHCS are now 16 mm long (having shotgunned the metric SHCS and BHCS situation some time ago) and the washers are gone.

    They’re all nylon lock nuts except for the one just to the left of the switch, providing barely enough clearance for the Powerpole connectors on the hotrod platform:

    M2 Z-Axis - platform connector clearance
    M2 Z-Axis – platform connector clearance

    With the nozzle off the platform to the far right side, Z-axis homing proceeded normally. Manually jogging to Z=+5.0 mm left 2.6 mm of air under the nozzle, so I reset the offset in EEPROM to -2.4 = (2.6 – 5.0) mm:

    M206 Z-2.4
    M500

    The first calibration square came out at 2.91 mm, so I changed the offset to -2.3 mm, got a 2.80 mm square with a firmly squished first layer, changed it to -2.5 mm, and got a 3.00 mm square for my efforts.

    An array of five squares showed the platform remains level to within +0.05 / -0.07 mm:

    M2 Platform Alignment Check - 2019-02-06
    M2 Platform Alignment Check – 2019-02-06

    I defined it to be Good Enough™ and quit while I was ahead.

    The bottom two squares in the left pile have squished first layers. The rest look just fine:

    M2 Z-Axis - switch offset calibration squares
    M2 Z-Axis – switch offset calibration squares

    The whole set-and-test process required about 45 minutes, most of which was spent waiting for the platform to reach 90 °C in the 14 °C Basement Laboratory.

    Done!

  • Kenmore 158: COB LED Light Bar

    With the Juki TL-2010Q all lit up, it seemed reasonable to apply the same technique to the Kenmore 158 sewing machine a few feet away:

    Kenmore 158 COB LED - installed
    Kenmore 158 COB LED – installed

    In an ideal world, I’d match the COB LED module to the opening under the machine’s arm, but module length isn’t a free variable, so it sticks out a bit on both sides.

    As you can see from the reflections on the base, this machine already has LEDs over the needle and in the endcap:

    Needle LEDs - bottom
    Needle LEDs – bottom

    They run from a 12 VDC 18 W power supply with an adjustable boost converter producing 18 V for the nominally 21 V LEDs:

    Needle LEDs power supply - interior
    Needle LEDs power supply – interior

    I replaced the coaxial power plug with a DE-9 connector:

    Kenmore 158 COB LED - power supply
    Kenmore 158 COB LED – power supply

    The 1/4 inch QD connectors on the AC power are marginally OK in this situation, as they’re tucked under the sewing table out of harm’s way. The other end of the AC line cord burrows into the sewing machine’s guts and isn’t easily removed, so this was the least-awful place for a connection.

    The LED connector pinout:

    Kenmore 158 COB LED - Power supply DE-9 pinout
    Kenmore 158 COB LED – Power supply DE-9 pinout

    The black cable comes from my lifetime supply of lovely supple flexible 28-ish AWG 9-conductor serial cables with molded-on male connectors.

    I used some silver-plated / Teflon-insulated coaxial cable for the COB LED wiring. It burrows into the guts of the machine through a gap above the presser foot lift lever, then joins up with similar cables from the other LEDs routed through the (grossly oversized) heatsink fins:

    Kenmore 158 COB LED - endcap wire routing
    Kenmore 158 COB LED – endcap wire routing

    The cables meet the repurposed serial cable inside the arm, following the original route of the 120 VAC wires formerly lighting the glowworm incandescent bulb in the endcap:

    Kenmore 158 COB LED - machine assembly
    Kenmore 158 COB LED – machine assembly

    What’s not obvious in that picture: the cables pass under two stamped steel guides and through two stamped steel clamps, each secured to the frame by a cheese head screw in a tapped hole. They definitely don’t make ’em like they used to!

    A 2.0 Ω ballast resistor produced the right amount of light, dropping 780 mV to run the LEDs at 390 mA and burning 300 mW. This supply produces 12.0 V at that current, so the COB LEDs run at 11.2 V and dissipate only 4.4 W.

    The lower output voltage (compared to the supply on the Juki) is probably the result of the higher load from the SMD LEDs lighting up the area around the needle. We cranked up their voltage to match the COB LEDs, so they’re surely conducting more than the original (guesstimated) 50 mA apiece = 300 mA total. I have no convenient (pronounced “easy”) way to measure either their current or voltage; when the light’s good, it’s all good.

    The other Kenmore 158 machines will eventually get the same treatment, but not right now.

  • Juki TL-2010Q: COB LED Light Levels

    The COB LED module claims to run at 12 V and 6 W, so it expects to draw 500 mA. First pass measurements showed 500 mA happened at 11.6 V:

    Juki TL-2010Q COB LED - ballast resistor test
    Juki TL-2010Q COB LED – ballast resistor test

    The 12 VDC supply actually produced 12.1 V at 500 mA, so a 1 Ω 1/2 W resistor should produce the right current:

    Juki TL-2010Q COB LED - heatsink endcap - internal connections
    Juki TL-2010Q COB LED – heatsink endcap – internal connections

    Which it did, but the Customer Base judged 6 W to be far too much light. A 2.7 Ω resistor seemed too dim, so we settled on 2.2 Ω:

    Juki TL-2010Q COB LED - 2.2 ohm header
    Juki TL-2010Q COB LED – 2.2 ohm header

    For the record, a 2.2 Ω resistor drops 980 mV and dissipates 440 mW, probably too close to its 500 mW rating. The supply produces 12.2 VDC at 450 mA, so the LEDs run at 11.2 V and dissipate 5 W; the heatsink remains pleasantly warm to the touch.

    The hot melt glue anchoring the pin header won’t win any prizes, but it sticks like glue to the Kapton tape and, in any event, there’s not much to go wrong in there.

    A cardboard cover hides the ugly details:

    Juki TL-2010Q COB LED - installed
    Juki TL-2010Q COB LED – installed

    And then It Just Works™:

    Juki TL-2010Q COB LED - installed - rear view
    Juki TL-2010Q COB LED – installed – rear view

    As evidenced by the glove fingertips, she does a lot of sewing and I’m glad I can shed some light on the subject …