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);
}
}