Vacuum Tube LEDs: Now With Morse Code

Adding Mark Fickett’s non-blocking Morse Arduino library turns the tubes into transmitters:

21HB5A on platter - orange green
21HB5A on platter – orange green

The plate cap LED blinks the message in orange, while both LEDs continue to slowly change color as before.

You define a Morse sender object (C++, yo!) by specifying its output pin and code speed in words per minute, dump a string into it, then call a continuation function fast enough to let it twiddle the output bit for each pulse. Obviously, the rate at which the callback happens determines the timing granularity.

However, setting a knockoff Neopixel to a given color requires more than just a binary signal on an output pin. The continuation function returns false when it’s done with the message, after which you can initialize and send another message. There’s no obvious (to me, anyhow) way to get timing information out of the code.

The easiest solution: called the Morse continuation function at the top of the main loop, read its output pin to determine when a dit or dah is active, then set the plate cap color accordingly:

LEDMorseSender Morse(PIN_MORSE, (float)MORSE_WPM);
...
Morse.setup();
Morse.setMessage(String("       cq cq cq de ke4znu       "));
PrevMorse = ThisMorse = digitalRead(PIN_MORSE);
...
if (!Morse.continueSending()) {
  Morse.startSending();
}
ThisMorse = digitalRead(PIN_MORSE);
...
if (ThisMorse) {             // if Morse output high, overlay
    strip.setPixelColor(PIXEL_MORSE,MorseColor);
}
PrevMorse = ThisMorse;
strip.show();               // send out precomputed colors
...
<<compute colors for next iteration as usual>>

I use the Entropy library to seed the PRNG, then pick three prime numbers for the sine wave periods (with an ugly hack to avoid matching periods):

uint32_t rn = Entropy.random();
...
randomSeed(rn);
...

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

printf("Primes: (%d,%d,%d)\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime);

In the spirit of “Video or it didn’t happen”: YouTube!

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
#include <Adafruit_NeoPixel.h>
#include <morse.h>
#include <Entropy.h>
//----------
// Pin assignments
const byte PIN_NEO = A3; // DO - data out to first Neopixel
const byte PIN_HEARTBEAT = 13; // DO - Arduino LED
#define PIN_MORSE 12
//----------
// Constants
#define PIXELS 2
#define PIXEL_MORSE 1
#define MORSE_WPM 10
#define UPDATEINTERVAL 50ul
const unsigned long UpdateMS = UPDATEINTERVAL - 1ul; // update LEDs only this many ms apart (minus loop() overhead)
// number of steps per cycle, before applying prime factors
#define RESOLUTION 250
// want to randomize the startup a little?
#define RANDOMIZE true
//----------
// Globals
// instantiate the Neopixel buffer array
Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXELS, PIN_NEO, NEO_GRB + NEO_KHZ800);
uint32_t FullWhite = strip.Color(255,255,255);
uint32_t FullOff = strip.Color(0,0,0);
uint32_t MorseColor = strip.Color(255,191,0);
struct pixcolor_t {
byte Prime;
unsigned int NumSteps;
unsigned int Step;
float StepSize;
byte MaxPWM;
};
unsigned int PlatterSteps;
byte PrimeList[] = {3,5,7,13,19,29};
// colors in each LED
enum pixcolors {RED, GREEN, BLUE, 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
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
return Value;
}
//-- 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\r\nEd Nisley - KE4ZNU - September 2016\r\n");
Entropy.initialize(); // start up entropy collector
// set up Neopixels
strip.begin();
strip.show();
// lamp test: a brilliant white flash
printf("Lamp test: flash white\r\n");
for (byte i=0; i<3 ; 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);
}
// set up real random numbers
uint32_t rn = Entropy.random();
if (RANDOMIZE) {
printf("Preloading LED array with seed: %08lx\r\n",rn);
randomSeed(rn);
}
else {
printf("Start not randomized\r\n");
}
printf("First random number: %ld\r\n",random(10));
// set up the color generators
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);
printf("Primes: (%d,%d,%d)\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime);
Pixels[RED].MaxPWM = 255;
Pixels[GREEN].MaxPWM = 255;
Pixels[BLUE].MaxPWM = 255;
for (byte c=0; c < PIXELSIZE; c++) {
Pixels[c].NumSteps = RESOLUTION * (unsigned int) Pixels[c].Prime;
Pixels[c].Step = RANDOMIZE ? random(Pixels[c].NumSteps) : (3*Pixels[c].NumSteps)/4;
Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // in radians per step
printf("c: %d Steps: %d Init: %d",c,Pixels[c].NumSteps,Pixels[c].Step);
printf(" PWM: %d\r\n",Pixels[c].MaxPWM);
}
// set up Morse generator
printf("Morse %d wpm\n",MORSE_WPM);
Morse.setup();
Morse.setMessage(String(" cq cq cq de ke4znu "));
PrevMorse = ThisMorse = digitalRead(PIN_MORSE);
MillisNow = MillisThen = millis();
}
//------------------
// Run the mood
void loop() {
if (!Morse.continueSending()) {
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 (ThisMorse) { // if Morse output high, overlay
strip.setPixelColor(PIXEL_MORSE,MorseColor);
}
PrevMorse = ThisMorse;
strip.show(); // send out precomputed colors
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));
}
}
byte Value[PIXELSIZE];
for (byte c=0; c < PIXELSIZE; c++) { // ... for each color
Value[c] = StepColor(c,0.0); // figure new PWM value
}
UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
for (int j=0; j < strip.numPixels(); j++) { // fill all LEDs with color
strip.setPixelColor(j,UniColor);
}
MillisThen = MillisNow;
digitalWrite(PIN_HEARTBEAT,LOW);
}
}
view raw TubeMorse.ino hosted with ❤ by GitHub