Tour Easy 1 W Amber Running Light: Holder and First Light

Wrapping a left-side ball mount around the PVC case produced a holder:

Fairing 1 W LED Mount - Left side - show view
Fairing 1 W LED Mount – Left side – show view

Which looks like this in real life:

1 W Amber Running Light - installed front
1 W Amber Running Light – installed front

The support structure under the arch required a bit more cleanup than it got, so the clamp didn’t quite close around the ball on the first full test:

1 W Amber Running Light - installed side
1 W Amber Running Light – installed side

Both the phone camera and the eyeballometer report the 1 W amber LED isn’t quite as bright as the 400 lumen Anker flashlight on its low setting:

1 W Amber Running Light - First Light
1 W Amber Running Light – First Light

Stir the unusual (for a bike) amber color together with some blinkiness, though, and it’s definitely attention-getting.

The OpenSCAD source code as a GitHub Gist:

// Tour Easy Fairing Flashlight Mount
// Ed Nisley KE4ZNU - July 2017
// August 2017 -
// August 2020 - add reinforcing columns under mount cradle
// August 2021 - 1 W Amber LED
/* [Build Options] */
FlashName = "1WLED"; // [AnkerLC40,AnkerLC90,J5TactV2,InnovaX5,Sidemarker,Clearance,Laser,1WLED]
Component = "BallClamp"; // [Ball, BallClamp, Mount, Plates, Bracket, Complete]
Layout = "Build"; // [Build, Show]
Support = true;
MountSupport = true;
/* [Hidden] */
ThreadThick = 0.25; // [0.20, 0.25]
ThreadWidth = 0.40; // [0.40]
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.01; // [0.01, 0.1]
HoleWindage = 0.2;
/* [Fairing Mount] */
Side = "Right"; // [Right,Left]
ToeIn = -10; // inward from ahead
Tilt = 20; // upward from forward (M=20 E=10)
Roll = 0; // outward from top
//- Screws and inserts
/* [Hidden] */
ID = 0;
OD = 1;
LENGTH = 2;
/* [Hidden] */
ClampInsert = [3.0,4.2,8.0];
ClampScrew = [3.0,5.9,35.0]; // thread dia, head OD, screw length
ClampScrewWasher = [3.0,6.75,0.5];
ClampScrewNut = [3.0,6.1,4.0]; // nyloc nut
/* [Hidden] */
F_NAME = 0;
F_GRIPOD = 1;
F_GRIPLEN = 2;
LightBodies = [
["AnkerLC90",26.6,48.0],
["AnkerLC40",26.6,55.0],
["J5TactV2",25.0,30.0],
["InnovaX5",22.0,55.0],
["Sidemarker",15.0,20.0],
["Clearance",50.0,20.0],
["Laser",10.0,30.0],
["1WLED",25.4,40.0],
];
//- Fairing Bracket
// Magic numbers taken from the actual fairing mount
/* [Hidden] */
inch = 25.4;
BracketHoleOD = 0.25 * inch; // 1/4-20 bolt holes
BracketHoleOC = 1.0 * inch; // fairing hole spacing
// usually 1 inch, but 15/16 on one fairing
Bracket = [48.0,16.3,3.6 - 0.6]; // fairing bracket end plate overall size
BracketHoleOffset = (3/8) * inch; // end to hole center
BracketM = 3.0; // endcap arc height
BracketR = (pow(BracketM,2) + pow(Bracket[1],2)/4) / (2*BracketM); // ... radius
//- Base plate dimensions
Plate = [100.0,30.0,6*ThreadThick + Bracket[2]];
PlateRad = Plate[1]/4;
RoundEnds = true;
echo(str("Base plate thick: ",Plate[2]));
//- Select flashlight data from table
echo(str("Flashlight: ",FlashName));
FlashIndex = search([FlashName],LightBodies,1,0)[F_NAME];
//- Set ball dimensions
BallWall = 5.0; // max ball wall thickness
echo(str("Ball wall: ",BallWall));
BallOD = IntegerMultiple(LightBodies[FlashIndex][F_GRIPOD] + 2*BallWall,1.0);
echo(str(" OD: ",BallOD));
BallLength = IntegerMultiple(min(sqrt(pow(BallOD,2) - pow(LightBodies[FlashIndex][F_GRIPOD],2)) - 2*4*ThreadThick,
LightBodies[FlashIndex][F_GRIPLEN]),1.0);
echo(str(" length: ",BallLength));
BallSides = 8*4;
//- Set clamp ring dimensions
//ClampOD = 50;
ClampOD = BallOD + 2*5;
echo(str("Clamp OD: ",ClampOD));
ClampLength = min(20.0,0.75*BallLength);
echo(str(" length: ",ClampLength));
ClampScrewOC = IntegerMultiple((ClampOD + BallOD)/2,1);
echo(str(" screw OC: ",ClampScrewOC));
TiltMirror = (Side == "Right") ? [0,0,0] : [0,1,0];
//- Adjust hole diameter to make the size come out right
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);
}
//- Fairing Bracket
// This part of the fairing mount supports the whole flashlight mount
// Centered on screw hole
module Bracket() {
linear_extrude(height=Bracket[2],convexity=2)
difference() {
translate([(Bracket[0]/2 - BracketHoleOffset),0,0])
offset(delta=ThreadWidth)
intersection() {
square([Bracket[0],Bracket[1]],center=true);
union() {
for (i=[-1,0,1]) // middle circle fills gap
translate([i*(Bracket[0]/2 - BracketR),0])
circle(r=BracketR);
}
}
circle(d=BracketHoleOD/cos(180/8),$fn=8); // dead center at the origin
}
}
//- General plate shape
// Centered in the middle of the plate
module PlateBlank() {
difference() {
intersection() {
translate([0,0,Plate[2]/2]) // select upper half of spheres
cube(Plate,center=true);
hull()
if (RoundEnds)
for (i=[-1,1])
translate([i*(Plate[0]/2 - PlateRad),0,0])
resize([Plate[1]/2,Plate[1],2*Plate[2]])
sphere(r=PlateRad); // nice round ends!
else
for (i=[-1,1], j=[-1,1])
translate([i*(Plate[0]/2 - PlateRad),j*(Plate[1]/2 - PlateRad),0])
resize([2*PlateRad,2*PlateRad,2*Plate[2]])
sphere(r=PlateRad); // nice round corners!
}
translate([BracketHoleOC,0,-Protrusion]) // punch screw holes
PolyCyl(BracketHoleOD,2*Plate[2],8);
translate([-BracketHoleOC,0,-Protrusion])
PolyCyl(BracketHoleOD,2*Plate[2],8);
}
}
//- Inner plate
module InnerPlate() {
difference() {
PlateBlank();
translate([-BracketHoleOC,0,Plate[2] - Bracket[2] + Protrusion]) // punch fairing bracket
Bracket();
}
}
//- Outer plate
// With optional legend for orientation and parameters
module OuterPlate(Legend = true) {
TextRotate = (Side == "Left") ? 0 : 180;
difference() {
PlateBlank();
if (Legend)
mirror([0,1,0])
translate([0,0,-Protrusion])
linear_extrude(height=3*ThreadThick + Protrusion) {
translate([BracketHoleOC + 15,0,0])
text(text=">>>",size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([-BracketHoleOC,8,0]) rotate(TextRotate)
text(text=str("Toe ",ToeIn),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([-BracketHoleOC,-8,0]) rotate(TextRotate)
text(text=str("Tilt ",Tilt),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([BracketHoleOC,-8,0]) rotate(TextRotate)
text(text=Side,size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([BracketHoleOC,8,0]) rotate(TextRotate)
text(text=str("Roll ",Roll),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([0,0,0])
rotate(90)
text(text="KE4ZNU",size=4,spacing=1.20,font="Arial",halign="center",valign="center");
}
}
}
//- Slotted ball around flashlight
// Print with brim to ensure adhesion!
module SlotBall() {
NumSlots = 8*2; // must be even, half cut from each end
SlotWidth = 2*ThreadWidth;
SlotBaseThick = 10*ThreadThick; // enough to hold finger ends together
RibLength = (BallOD - LightBodies[FlashIndex][F_GRIPOD])/2;
translate([0,0,(Layout == "Build") ? BallLength/2 : 0])
rotate([0,(Layout == "Show") ? 90 : 0,0])
difference() {
intersection() {
sphere(d=BallOD,$fn=2*BallSides); // basic ball
cube([2*BallOD,2*BallOD,BallLength],center=true); // trim to length
}
translate([0,0,-LightBodies[FlashIndex][F_GRIPOD]])
rotate(180/BallSides)
PolyCyl(LightBodies[FlashIndex][F_GRIPOD],2*BallOD,BallSides); // remove flashlight body
for (i=[0:NumSlots/2 - 1]) { // cut slots
a=i*(2*360/NumSlots);
SlotCutterLength = LightBodies[FlashIndex][F_GRIPOD];
rotate(a)
translate([SlotCutterLength/2,0,SlotBaseThick])
cube([SlotCutterLength,SlotWidth,BallLength],center=true);
rotate(a + 360/NumSlots)
translate([SlotCutterLength/2,0,-SlotBaseThick])
cube([SlotCutterLength,SlotWidth,BallLength],center=true);
}
}
color("Yellow")
if (Support && (Layout == "Build")) {
for (i=[0:NumSlots-1]) {
a = i*360/NumSlots;
rotate(a + 180/NumSlots)
translate([(LightBodies[FlashIndex][F_GRIPOD] + RibLength)/2 + ThreadWidth,0,BallLength/(2*4)])
cube([RibLength,2*ThreadWidth,BallLength/4],center=true);
}
}
}
//- Clamp around flashlight ball
BossLength = ClampScrew[LENGTH] - 1*ClampScrewWasher[LENGTH];
BossOD = ClampInsert[OD] + 2*(6*ThreadWidth);
module BallClamp(Section="All") {
difference() {
union() {
intersection() {
sphere(d=ClampOD,$fn=BallSides); // exterior ball clamp
cube([ClampLength,2*ClampOD,2*ClampOD],center=true); // aiming allowance
}
hull()
for (j=[-1,1])
translate([0,j*ClampScrewOC/2,-BossLength/2])
cylinder(d=BossOD,h=BossLength,$fn=6);
}
sphere(d=(BallOD + 1*ThreadThick),$fn=BallSides); // interior ball with minimal clearance
for (j=[-1,1]) {
translate([0,j*ClampScrewOC/2,-ClampOD]) // screw clearance
PolyCyl(ClampScrew[ID],2*ClampOD,6);
translate([0,j*ClampScrewOC/2, // insert clearance
-0*(BossLength/2 - ClampInsert[LENGTH] - 3*ThreadThick) + Protrusion])
rotate([0,180,0])
PolyCyl(ClampInsert[OD],2*ClampOD,6);
translate([0,j*ClampScrewOC/2, // insert transition
-(BossLength/2 - ClampInsert[LENGTH] - 3*ThreadThick)])
cylinder(d1=ClampInsert[OD]/cos(180/6),d2=ClampScrew[ID],h=6*ThreadThick,$fn=6);
}
if (Section == "Top")
translate([0,0,-ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
else if (Section == "Bottom")
translate([0,0,ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
}
color("Yellow")
if (Support) { // ad-hoc supports
NumRibs = 6;
RibLength = 0.5 * BallOD;
RibWidth = 1.9*ThreadWidth;
SupportOC = ClampLength / NumRibs;
if (Section == "Top") // base plate for adhesion
translate([0,0,ThreadThick])
cube([ClampLength + 6*ThreadWidth,RibLength,2*ThreadThick],center=true);
else if (Section == "Bottom")
translate([0,0,-ThreadThick])
cube([ClampLength + 6*ThreadWidth,RibLength,2*ThreadThick],center=true);
render(convexity=2*NumRibs)
intersection() {
sphere(d=BallOD - 0*ThreadWidth); // cut at inner sphere OD
cube([ClampLength + 2*ThreadWidth,RibLength,BallOD],center=true);
if (Section == "Top") // select only desired section
translate([0,0,ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
else if (Section == "Bottom")
translate([0,0,-ClampOD/2])
cube([2*ClampOD,2*ClampOD,ClampOD],center=true);
union() { // ribs for E-Z build
for (j=[-1,0,1])
translate([0,j*SupportOC,0])
cube([ClampLength,RibWidth,1.0*BallOD],center=true);
for (i=[0:NumRibs]) // allow NumRibs + 1 to fill the far end
translate([i*SupportOC - ClampLength/2,0,0])
rotate([0,90,0])
cylinder(d=BallOD - 2*ThreadThick,
h=RibWidth,$fn=BallSides,center=true);
}
}
}
}
//- Mount between fairing plate and flashlight ball
// Build with support for bottom of clamp screws!
module Mount() {
MountShift = [ClampOD*sin(ToeIn/2),0,ClampOD/2];
OuterPlate();
mirror(TiltMirror) {
intersection() {
translate(MountShift)
rotate([-Roll,ToeIn,Tilt])
BallClamp("Bottom");
translate([0,0,Plate.x/2 + 3*ThreadThick])
cube(Plate.x,center=true);
}
if (MountSupport) // anchor outer corners at worst overhang
color("Yellow") {
RibWidth = 1.9*ThreadWidth;
SupportOC = 0.1 * ClampLength;
intersection() {
difference() {
rotate([0,0,Tilt])
translate([(ClampOD - BallOD)*sin(ToeIn/2),0,3*ThreadThick]) // Z = avoid legends
for (i=[-4.5,-2.5,0,2.0,4.5])
translate([i*SupportOC - 0.0,0,(5 + Plate[2])/2])
cube([RibWidth,0.7*ClampOD,(5 + Plate[2])],center=true);
translate(MountShift)
rotate([-Roll,ToeIn,Tilt])
sphere(d=ClampOD - 2*ThreadWidth,$fn=BallSides);
}
translate([0,0,ClampOD/2])
cube([Plate.x,Plate.y,ClampOD],center=true);
}
}
}
}
//- Build things
if (Component == "Bracket")
Bracket();
if (Component == "Ball")
SlotBall();
if (Component == "BallClamp")
if (Layout == "Show")
BallClamp("All");
else if (Layout == "Build")
BallClamp("Top");
if (Component == "Mount")
Mount();
if (Component == "Plates") {
translate([0,0.7*Plate[1],0])
InnerPlate();
translate([0,-0.7*Plate[1],0])
OuterPlate(Legend = false);
}
if (Component == "Complete") {
OuterPlate();
mirror(TiltMirror) {
translate([0,0,ClampOD/2 + BossOD*abs(sin(ToeIn))]) {
rotate([-Roll,ToeIn,Tilt])
SlotBall();
rotate([-Roll,ToeIn,Tilt])
BallClamp();
}
}
}

Tour Easy 1 W Amber Running Light: Firmware

Rather than conjure a domain specific language to blink an LED, it’s easier to use Morse code:

Herewith, Arduino source code using Mark Fickett’s Morse library to blink an amber running light:

// Tour Easy Running Light
// Ed Nisley - KE4ZNU
// September 2021

#include <morse.h>

#define PIN_OUTPUT	13

LEDMorseSender Morser(PIN_OUTPUT,(float)10.0);

void setup()
{
	Morser.setup();

    Morser.setMessage(String("qst de ke4znu "));
    Morser.sendBlocking();

//    Morser.setWPM((float)3.0);
    Morser.setSpeed(50);
	Morser.setMessage(String("s   "));
}

void loop()
{
	if (!Morser.continueSending())
		Morser.startSending();

}

Bonus: a trivially easy ID string.

A dit time of 50 ms produces a brief flash that’s probably about as fast as it can be, given that the regulator must ramp the LED current up from zero after its Enable input goes high. In round numbers, a 50ms dit corresponds to 24 WPM Morse.

Each of the three blanks after the “s” produces a seven element word space to keep the blinks from running together.

Sending “b ” (two blanks) with a 75 ms dit time may be more noticeable. You should tune for maximum conspicuity on your rides.

1 W Amber Running Light - installed front
1 W Amber Running Light – installed front

On our first ride, Mary got a friendly wave from a motorcyclist, an approving toot from a driver, and several “you go first” gestures at intersections.

Works for us …

Tour Easy 1 W Amber Running Light: Circuitry

With the internal slab attached to the 1 W LED heatsink, some double-sided foam tape affixes an Arduino Nano to one side of the slab:

1 W LED Running Light - Arduino side
1 W LED Running Light – Arduino side

The MP1584 regulator and its 2.2 Ω current-sensing resistor (tacked down with acrylic adhesive) go on the other side:

1 W LED Running Light - Regulator side
1 W LED Running Light – Regulator side

The Arduino and regulator draw power from the Bafang motor controller’s 6.3 V headlight circuit. The 2.2 Ω resistor sets the LED current to 360 mA = 900 mW. The blue wire connects the Arduino’s default LED output pin (D13) to the regulator’s Enable input (pin 2) to allow programmatic blinkiness.

The end view shows everything Barely Fits™:

1 W LED Running Light - internal assembly
1 W LED Running Light – internal assembly

All it needs is a rear cover of some sort …

Tour Easy 1 W Amber Running Light: Internal Plate

A semi-scaled doodle laying out an Arduino Nano and the MP1584 regulator board suggested they might fit behind the heatsink with the 1 W LED:

Amber running light - board layout doodle - side
Amber running light – board layout doodle – side

A somewhat more detailed doodle of the end view prompted me to bore the PVC pipe out to 23 mm:

Amber running light - board layout doodle - end
Amber running light – board layout doodle – end

The prospect of designing a 3D printed holder for the boards suggested Quality Shop Time combined with double-stick foam tape would ensure a better outcome.

So I bandsawed the remains of a chunky angle bracket into a pair of rectangles, flycut All The Sides to square them up, and tapped a pair of M3 holes along one edge of each:

1 W LED Running Light - baseplate tapping
1 W LED Running Light – baseplate tapping

The other long edges got the V groove that killed the Sherline’s Y axis nut:

Sherline Y-Axis Nut Mishap - setup
Sherline Y-Axis Nut Mishap – setup

The groove holds a length of 4 mm OD (actually 5/32 inch, but don’t tell anybody) brass tubing:

1 W LED Running Light - baseplate trial fit
1 W LED Running Light – baseplate trial fit

The M3 button head screws are an admission of defeat, as I could see no way of controlling the width + thickness of the aluminum slabs to get a firm push fit in the PVC tube. The screws let me tune for best picture after everything else settled out.

A little more machining opened up the top of the groove:

1 W LED Running Light - baseplate dry assembly
1 W LED Running Light – baseplate dry assembly

A short M3 button head screw (with its head turned down to 4 mm) drops into the slot and holds the slab to the threaded hole in the LED heatsink. The long screw is holding the threaded insert in place for this dry fit.

I doodled a single long screw through the whole thing, but having it fall off the heatsink when taking the rear cover off seemed like a Bad Idea™. An M3 button head screw uses a 2 mm hex key that fits neatly through the threaded insert, thereby making it work.

Butter it up with epoxy, scrape off the excess, and let things cure:

1 W LED Running Light - baseplate curing
1 W LED Running Light – baseplate curing

This was obviously made up as I went along …

Arduino MEGA Debugging LEDs

Kibitzing on a project involving an Arduino Mega (properly MEGA, but who cares?) with plenty of spare I/O pins led me to slap together a block of LEDs:

Arduino Mega Debugging LEDs
Arduino Mega Debugging LEDs

The excessive lead length on the 330 Ω resistors will eventually anchor scope probes syncing on / timing interesting program events.

Not that you have any, but they’re antique HP HDSP-4836 tuning indicators: RRYYGGYYRR. If you were being fussy, you might use 270 Ω resistors on the yellow LEDs to brighten them up.

A simple test program exercises the LEDs:

/*
  Debugging LED outputs for Mega board
  Ed Nisley - KE4ZNU
  Plug the board into the Digital Header pins 34-52 and GND 
*/

byte LowLED = 34;
byte HighLED = 52;
byte ThisLED = LowLED;

//-----
void setup() {
  pinMode(LED_BUILTIN,OUTPUT);
  
  for (byte p = LowLED; p <= HighLED; p+=2)
    pinMode(p, OUTPUT);

//  Serial.begin(9600);
}

// -----
void loop() {
  digitalWrite(LED_BUILTIN,HIGH);
  
  digitalWrite(ThisLED, HIGH);
  delay(100);
  digitalWrite(ThisLED, LOW);
 // delay(500);

  ThisLED = (ThisLED < HighLED) ? (ThisLED + 2) : LowLED;

//  Serial.println(ThisLED);

  digitalWrite(LED_BUILTIN,LOW);
}

Nothing fancy, but it ought to come in handy at some point.

KeyboardIO Atreus: RGB LED Firmware

Having wired a WS2812 RGB LED into my KeyboardIO Atreus, lighting it up requires some QMK firmware configuration. It’s easiest to set up a “new” keymap based on the QMK Atreus files, as described in the QMK startup doc:

qmk new-keymap -kb keyboardio/atreus -km ednisley

Obviously, you’ll pick a different keymap name than I did. All the files mentioned below will reside in the new subdirectory, which starts out with only a keymap.c file copied from the default layout.

The rules.mk file enables RGB Lighting, as well as Auto Shift and Tap Dance:

AUTO_SHIFT_ENABLE = yes			# allow automagic shifting
TAP_DANCE_ENABLE = yes			# allow multi-tap keys

RGBLIGHT_ENABLE = yes			# addressable LEDs

If you had different hardware, you could specify the driver with a WS2812_DRIVER option.

QMK can also control single-color LEDs with PWM (a.k.a. backlighting), and per-key RGB LEDs (a.k.a. RGB Matrix). These functions, their configuration / controls / data, and their documentation overlap and intermingle to the extent that I spent most of my time figuring out what not to include.

Some configuration happens in the config.h file:

#define RGB_DI_PIN B2
#define RGBLED_NUM 1

// https://github.com/qmk/qmk_firmware/blob/master/docs/ws2812_driver.md
//#define WS2812_TRST_US 280
//#define WS2812_BYTE_ORDER WS2812_BYTE_ORDER_GRB

#define RGBLIGHT_LAYERS
#define RGBLIGHT_EFFECT_RGB_TEST
#define RGBLIGHT_LIMIT_VAL 63

#define NO_DEBUG
#define NO_PRINT

The first two lines describe a single WS2812 RGB LED wired to pin B2 (a.k.a. MOSI) of the Atmel 32U4 microcontroller. The default Reset duration and Byte Order values work for the LED I used

Protip: swapping the order from GRB to RGB is a quick way to discover if the firmware actually writes to the LED, even before you get anything else working: it’ll be red with the proper setting and green with the wrong one.

Dialing the maximum intensity down works well with a bright LED shining directly at your face from a foot away.

Turning on RGBLIGHT_LAYERS is what makes this whole thing happen. The RGBLIGHT_EFFECT_RGB_TEST option enables a simple test animation at the cost of a few hundred bytes of code space; remove that line after everything works.

The last two lines remove the debugging facilities; as always with microcontroller projects, there’s enough room for either your code or the debugger required to get it running, but not both.

With those files set up, the keymap.c file does the heavy lifting:

// Modified from the KeyboardIO layout
// Ed Nisley - KE4ZNU

#include QMK_KEYBOARD_H

enum layer_names {
    _BASE,
    _SHIFTS,
    _FUNCS,
    _NLAYERS
};

// Tap Dance

enum {
    TD_SPC_ENT,
};

qk_tap_dance_action_t tap_dance_actions[] = {
    [TD_SPC_ENT] = ACTION_TAP_DANCE_DOUBLE(KC_SPC, KC_ENT),
};


// Layer lighting

// Undefine this to enable simple test mode
// Also put #define RGBLIGHT_EFFECT_RGB_TEST in config.h

#define LED_LL

#ifdef LED_LL

const rgblight_segment_t PROGMEM ll_0[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_WHITE} );
const rgblight_segment_t PROGMEM ll_1[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_MAGENTA} );
const rgblight_segment_t PROGMEM ll_2[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_CYAN} );
const rgblight_segment_t PROGMEM ll_3[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_BLUE} );
const rgblight_segment_t PROGMEM ll_4[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_GREEN} );
const rgblight_segment_t PROGMEM ll_5[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_RED} );
const rgblight_segment_t PROGMEM ll_6[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_YELLOW} );

const rgblight_segment_t* const PROGMEM ll_layers[] = RGBLIGHT_LAYERS_LIST(
    ll_0,ll_1,ll_2,ll_3,ll_4,ll_5,ll_6
);

#endif

void keyboard_post_init_user(void) {

#ifdef LED_LL
    rgblight_layers = ll_layers;
    rgblight_set_layer_state(0, 1);
#else
    rgblight_enable_noeeprom();
    rgblight_mode_noeeprom(RGBLIGHT_MODE_RGB_TEST);
//    rgblight_mode_noeeprom(RGBLIGHT_MODE_BREATHING + 3);
#endif

}


#ifdef LED_LL

layer_state_t layer_state_set_user(layer_state_t state) {
    for (uint8_t i=0 ; i < _NLAYERS; i++)
        rgblight_set_layer_state(i, layer_state_cmp(state, i));

    return state;
}
#endif


// Key maps

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [_BASE] = LAYOUT(                             // base layer for typing
    KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,                      KC_Y,    KC_U,    KC_I,    KC_O,    KC_P    ,
    KC_A,    KC_S,    KC_D,    KC_F,    KC_G,                      KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN ,
    KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_GRV,  KC_LALT, KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH ,
    LT(_FUNCS,KC_ESC), KC_TAB, KC_LGUI,  KC_BSPC, KC_LSFT,  KC_LCTL, KC_ENT , TD(TD_SPC_ENT),  MO(_SHIFTS), KC_MINS, KC_QUOT, KC_BSLS),

  [_SHIFTS] = LAYOUT(                           // shifted chars and numpad
    KC_EXLM, KC_AT,   KC_UP,   KC_DLR,  KC_PERC,                  KC_PGUP, KC_7,    KC_8,   KC_9, KC_HOME,
    KC_LPRN, KC_LEFT, KC_DOWN, KC_RGHT, KC_RPRN,                  KC_PGDN, KC_4,    KC_5,   KC_6, KC_END,
    KC_LBRC, KC_RBRC, KC_HASH, KC_LCBR, KC_RCBR, KC_CIRC, KC_AMPR,KC_ASTR, KC_1,    KC_2,   KC_3, KC_PLUS,
    KC_NO  , KC_INS,  KC_LGUI, KC_DEL , KC_BSPC, KC_LCTL, KC_LALT,KC_SPC,  KC_TRNS, KC_DOT, KC_0, KC_EQL ),

  [_FUNCS] = LAYOUT(                            // function keys
    KC_INS,  KC_HOME, KC_UP,   KC_END,  KC_PGUP,                   KC_UP,   KC_F7,   KC_F8,   KC_F9,   KC_F10  ,
    KC_DEL,  KC_LEFT, KC_DOWN, KC_RGHT, KC_PGDN,                   KC_DOWN, KC_F4,   KC_F5,   KC_F6,   KC_F11  ,
    KC_NO,   KC_VOLU, KC_NO,   KC_NO,   RESET,   _______, _______, KC_NO,   KC_F1,   KC_F2,   KC_F3,   KC_F12  ,
    KC_NO,   KC_VOLD, KC_LGUI, KC_LSFT, KC_BSPC, KC_LCTL, KC_LALT, KC_SPC,  TO(_BASE), KC_PSCR, KC_SLCK, KC_PAUS )
};

Undefine LED_LL to enable the test mode, compile, flash, and the LED should cycle red / green / blue forever; you also need the RGB_TEST option in the config.h file.

Define LED_LL and layer lighting should then Just Work™, with the LED glowing:

  • White for the basic layer with all the letters
  • Magenta with the Fun key pressed
  • Cyan with the Esc key pressed

The key map code defines colors for layers that don’t yet exist, but it should get you started.

For convenience, I wadded all three QMK files into a GitHub Gist.

The LED is kinda subtle:

Atreus keyboard - LED installed
Atreus keyboard – LED installed

As you might expect, figuring all that out took much longer than for you to read about it, but now I have a chance of remembering what I did.

KeyboardIO Atreus: RGB LED Installation

Having scouted out the territory inside the KeyboardIO Atreus, adding an LED requires taking it completely apart to drill a hole in the aluminum faceplate:

Atreus keyboard - panel drilling
Atreus keyboard – panel drilling

Reattaching the plate to the PCB with only three screws allows marking the hole position on the PCB, which is much easier than pretending to derive the position from first principles:

Atreus keyboard - LED marking
Atreus keyboard – LED marking

Despite appearances, I traced the hole with a mechanical pencil: black graphite turns shiny silvery gray against matte black soldermask. Also, the PCB trace is off-center, not the hole.

Overlay the neighborhood with Kapton tape to protect the PCB from what comes next:

Atreus keyboard - Kapton tape

Snip a WS2812 RGB LED from a strip, stick it in place with eyeballometric alignment over the target, and wire it up:

Atreus keyboard - LED wiring
Atreus keyboard – LED wiring

Despite the terrible reliability of WS2812 RGB LEDs mounted on PCB carriers, a different set on a meter of high-density flex tape have worked reasonably well when not thermally stressed, so I’ll assume this one arrived in good order.

Aligning the LED directly under the hole required a few iterations:

Atreus keyboard - LED positioning
Atreus keyboard – LED positioning

The iridescent green patch is a diffraction pattern from the controller chip’s internal circuitry.

The data comes from MOSI, otherwise known as B2, down in the lower left corner:

Atmel 32U4 - JTAG pins
Atmel 32U4 – JTAG pins

Actually lighting the LED now becomes a simple matter of software QMK firmware.