Vacuum Tube Lights: Duodecar Rebuild

You’ll recall the LED atop the 21HB5A tube failed, shortly after replacing the bottom LED and rewiring the ersatz plate lead, which led me to rebuild the whole thing with SK6812 RGBW LEDs. So I printed all the plastic parts again, because the duodecar tube socket’s pin circle can fit into a hard drive platter’s unmodified 25 mm hole, then drilled another platter to suit:

Duodecar disk drilling
Duodecar disk drilling

The hole under the drill fits the 3.5 mm stereo socket for the ersatz plate lead, so it’s bigger than before.

I’ve switched from Arduino Pro Minis with a separate USB converter to Arduino Nanos with an on-board CH340 USB chip, because the fake FTDI chips on the converters are a continuing aggravation:

21HB5A base - interior
21HB5A base – interior

Adding those wire slots to the sockets definitely helps tidy things up; the wires no longer need a crude cable tie anchoring them to the socket mounting screws.

I wanted to drive the LEDs from the A7 pin, rather than the A3 pin I’d been using on the Pro Minis, to keep the wires closer together, but it turns out that A6 and A7 can’t become digital output pins. So I used A5, although I may come to regret the backward incompatibility.

In any event, the 21HB5A tube looks spiffy with its new LEDs in full effect:

21HB5A with RBGBW LEDs - cyan violet phase
21HB5A with RBGBW LEDs – cyan violet phase

I dialed the white LED PWM down to 32, making the colors somewhat pastel, rather than washed-out.

The Arduino source code as a GitHub Gist:

// Neopixel mood lighting for vacuum tubes
// Ed Nisley - KE4ANU - June 2016
// September 2016 - Add Morse library and blinkiness
// October 2016 - Set random colors at cycle end
// March 2017 - RGBW SK6812 LEDs
#include <Adafruit_NeoPixel.h>
#include <morse.h>
#include <Entropy.h>
//----------
// Pin assignments
const byte PIN_NEO = A5; // DO - data out to first Neopixel
const byte PIN_HEARTBEAT = 13; // DO - Arduino LED
#define PIN_MORSE 12
//----------
// Constants
// number of pixels
#define PIXELS 2
// index of the Morse output pixel and how fast it sends
boolean Send_Morse = false;
#define PIXEL_MORSE (PIXELS - 1)
#define MORSE_WPM 10
// lag between adjacent pixel, degrees of slowest period
#define PIXELPHASE 45
// update LEDs only this many ms apart (minus loop() overhead)
#define UPDATEINTERVAL 50ul
#define UPDATEMS (UPDATEINTERVAL - 1ul)
// 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);
uint32_t FullWhite = strip.Color(255,255,255,255);
uint32_t FullOff = strip.Color(0,0,0,0);
uint32_t MorseColor;
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};
// 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;
unsigned long MillisNow;
unsigned long MillisThen;
// Morse code
char * MorseText = " cq cq cq de ke4znu";
LEDMorseSender Morse(PIN_MORSE, (float)MORSE_WPM);
uint8_t PrevMorse, ThisMorse;
//-- Figure PWM based on current state
byte StepColor(byte Color, float Phi) {
byte Value;
Value = (Pixels[Color].MaxPWM / 2.0) * (1.0 + sin(Pixels[Color].Step * Pixels[Color].StepSize + Phi));
// Value = (Value) ? Value : Pixels[Color].MaxPWM; // flash at dimmest points for debug
return Value;
}
//-- 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);
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 = 32;
unsigned int PhaseSteps = (unsigned int) ((PIXELPHASE / 360.0) *
RESOLUTION * (unsigned int) max(max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime),Pixels[WHITE].Prime));
printf("Pixel phase offset: %d deg = %d steps\r\n",(int)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: %d Init: %d Phase: %d 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
Serial.begin(57600);
fdevopen(&s_putc,0); // set up serial output for printf()
printf("Vacuum Tube Mood Light - RGBW\r\nEd Nisley - KE4ZNU - March 2017\r\n");
Entropy.initialize(); // start up entropy collector
// set up pixels
strip.begin();
strip.show();
// lamp test: a brilliant white flash
printf("Lamp test: flash white\r\n");
for (byte i=0; i<5 ; i++) {
for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with white
strip.setPixelColor(j,FullWhite);
}
strip.show();
delay(500);
for (int j=0; j < strip.numPixels(); j++) { // fill LEDs with black
strip.setPixelColor(j,FullOff);
}
strip.show();
delay(500);
}
// 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();
// set up Morse generator
Morse.setup();
Morse.setMessage(String(MorseText));
MorseColor = strip.Color(255,random(32,64),random(16),0);
PrevMorse = ThisMorse = digitalRead(PIN_MORSE);
printf("Morse enabled: %d at %d wpm color: %08lx\n [%s]\r\n",Send_Morse,MORSE_WPM,MorseColor,MorseText);
MillisNow = MillisThen = millis();
}
//------------------
// Run the mood
void loop() {
if (!Morse.continueSending()) {
printf("Restarting Morse message\r\n");
Morse.startSending();
}
ThisMorse = digitalRead(PIN_MORSE);
MillisNow = millis();
if (((MillisNow - MillisThen) >= UPDATEMS) || // time for color change?
(PrevMorse != ThisMorse)) { // Morse output bit changed?
digitalWrite(PIN_HEARTBEAT,HIGH);
if (Send_Morse && ThisMorse) { // if Morse output high, overlay flash
strip.setPixelColor(PIXEL_MORSE,MorseColor);
}
PrevMorse = ThisMorse;
strip.show(); // send out precomputed colors
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 %d steps %d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow - MillisThen));
}
else {
CycleRun = true; // this color is still cycling
}
}
// If all cycles have completed, reset the color generators
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));
}
UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],Value[WHITE]);
strip.setPixelColor(i,UniColor);
}
MillisThen = MillisNow;
digitalWrite(PIN_HEARTBEAT,LOW);
}
}
view raw TubeMorse.ino hosted with ❤ by GitHub

One thought on “Vacuum Tube Lights: Duodecar Rebuild

Comments are closed.