October 2025
This week was mainly about Output Devices.

| Component | Specification |
|---|---|
| Microcontroller | XIAO-RP2040(details here) |
| LED | NeoPixel Compatible LED (WS2812B, see datasheet) |
| Button | Tactile switch (TS04-66-43-BK-260-SMT, see datasheet) |
After assembling the output device, I conducted thorough testing to ensure its functionality.
The video above demonstrates the output device in action, where button presses correspond to Morse code inputs, and the RGB LEDs display the respective patterns.
Then I extended the functionality to allow serial input from the computer to be displayed on the output device:
#include <Adafruit_NeoPixel.h>
#define BUTTON_PIN D0
#define LED_PIN D7
#define NUMPIXELS 5
Adafruit_NeoPixel pixels(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
// ---- Morse timing (for button input) ----
#define WPM 10
const unsigned long UNIT_MS = 1200UL / WPM;
const unsigned long DOT_DASH_THRESH_MS = (unsigned long)(2.5 * UNIT_MS);
const unsigned long LETTER_GAP_MS = (unsigned long)(3.5 * UNIT_MS);
const unsigned long WORD_GAP_MS = (unsigned long)(7.0 * UNIT_MS);
const unsigned long DEBOUNCE_MS = 25;
// ---- Serial playback timing ----
const unsigned long SERIAL_LETTER_GAP_MS = 600UL; // 0.6s between letters
bool lastLevel = HIGH;
unsigned long lastEdgeMs = 0;
unsigned long pressStartMs = 0;
unsigned long lastReleaseMs = 0;
String currentMorse;
// ---- Morse table ----
struct MorseEntry { const char* code; char ch; };
const MorseEntry table[] PROGMEM = {
{".-", 'A'}, {"-...", 'B'}, {"-.-.", 'C'}, {"-..", 'D'}, {".", 'E'},
{"..-.", 'F'}, {"--.", 'G'}, {"....", 'H'}, {"..", 'I'}, {".---", 'J'},
{"-.-", 'K'}, {".-..", 'L'}, {"--", 'M'}, {"-.", 'N'}, {"---", 'O'},
{".--.", 'P'}, {"--.-", 'Q'}, {".-.", 'R'}, {"...", 'S'}, {"-", 'T'},
{"..-", 'U'}, {"...-", 'V'}, {".--", 'W'}, {"-..-", 'X'}, {"-.--", 'Y'},
{"--..", 'Z'},
{"-----",'0'}, {".----",'1'}, {"..---",'2'}, {"...--",'3'}, {"....-",'4'},
{".....",'5'}, {"-....",'6'}, {"--...",'7'}, {"---..",'8'}, {"----.",'9'},
};
char decode(const String& code) {
for (auto &e : table) if (code == e.code) return e.ch;
return '?';
}
String encode(char ch) {
char up = toupper((unsigned char)ch);
for (auto &e : table) if (up == e.ch) return String(e.code);
return String("");
}
// Colors
inline uint32_t C_RED() { return pixels.Color(30, 0, 0); }
inline uint32_t C_GRN() { return pixels.Color(0, 30, 0); }
inline uint32_t C_WHT() { return pixels.Color(24, 24, 24); }
// Render a 5-LED pattern for the given morse code string.
// Dot = green, Dash = red, remaining up to 5 = white.
void showLetterPattern(const String& code) {
for (uint8_t i = 0; i < NUMPIXELS; ++i) {
uint32_t col = C_WHT(); // default white for blanks
if (i < (uint8_t)code.length()) {
char sym = code[i];
if (sym == '.') col = C_GRN();
else if (sym == '-') col = C_RED();
}
pixels.setPixelColor(i, col);
}
pixels.show();
}
// Word gap: all off (visual separator)
void showWordGap() {
pixels.clear();
pixels.show();
}
// Rainbow color wheel with lower brightness
uint32_t wheel(byte pos) {
float scale = 0.1; // brightness scale (0.0–1.0)
byte r, g, b;
if (pos < 85) {
r = pos * 3; g = 255 - pos * 3; b = 0;
} else if (pos < 170) {
pos -= 85;
r = 255 - pos * 3; g = 0; b = pos * 3;
} else {
pos -= 170;
r = 0; g = pos * 3; b = 255 - pos * 3;
}
return pixels.Color(r * scale, g * scale, b * scale);
}
// Quick rainbow flash, then turn everything off
void rainbowFlash(uint8_t cycles = 2, uint8_t step = 8, uint16_t stepDelay = 12) {
for (uint8_t c = 0; c < cycles; ++c) {
for (uint16_t j = 0; j < 256; j += step) {
for (uint16_t i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, wheel((i * 256 / NUMPIXELS + j) & 255));
}
pixels.show();
delay(stepDelay);
}
}
pixels.clear();
pixels.show(); // turn off all when done
}
void endLetter() {
if (currentMorse.isEmpty()) return;
char ch = decode(currentMorse);
Serial.print(ch);
showLetterPattern(currentMorse);
currentMorse = "";
}
// ---------- Serial playback state ----------
String serial_input_buffer; // accumulates incoming chars until newline
String serial_playback; // the string currently being played
size_t serial_index = 0; // index into serial_playback
unsigned long serial_next_at = 0;
bool serial_playing = false;
void maybeStartSerialPlaybackFromInput() {
while (Serial.available() > 0) {
char c = (char)Serial.read();
if (c == '\r') continue;
if (c == '\n') {
serial_playback = serial_input_buffer;
serial_input_buffer = "";
serial_index = 0;
serial_playing = (serial_playback.length() > 0);
serial_next_at = 0; // start immediately
if (serial_playing) {
Serial.print(F("\n[PLAY] "));
Serial.println(serial_playback);
}
} else {
serial_input_buffer += c;
}
}
}
void serviceSerialPlayback() {
if (!serial_playing) return;
unsigned long now = millis();
if (serial_next_at != 0 && now < serial_next_at) return;
if (serial_index >= serial_playback.length()) {
// Finished showing the entire line -> do rainbow + OFF
rainbowFlash(); // quick celebratory flash
serial_playing = false; // stop playback
return;
}
char ch = serial_playback[serial_index++];
if (ch == ' ') {
showWordGap();
serial_next_at = now + SERIAL_LETTER_GAP_MS; // still wait 2s on spaces
return;
}
String code = encode(ch);
if (code.length() > 0) {
Serial.print((char)toupper((unsigned char)ch));
showLetterPattern(code);
} else {
showWordGap(); // unsupported char -> blank
}
serial_next_at = now + SERIAL_LETTER_GAP_MS; // 2s between letters
}
// ------------------------------------------
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pixels.begin();
pixels.clear(); pixels.show();
Serial.println("Morse ready: button input + Serial text playback.");
Serial.println("Type a line (A–Z, 0–9, spaces) and press Enter to play.");
}
void loop() {
unsigned long now = millis();
int level = digitalRead(BUTTON_PIN);
// --- Button input handling ---
if (level != lastLevel && (now - lastEdgeMs) > DEBOUNCE_MS) {
lastEdgeMs = now;
if (level == LOW) {
pressStartMs = now;
} else {
unsigned long dur = now - pressStartMs;
lastReleaseMs = now;
if (dur < DOT_DASH_THRESH_MS) { Serial.print('.'); currentMorse += '.'; }
else { Serial.print('-'); currentMorse += '-'; }
}
lastLevel = level;
}
if (lastLevel == HIGH && lastReleaseMs != 0) {
unsigned long idle = now - lastReleaseMs;
if (idle >= LETTER_GAP_MS && currentMorse.length() > 0) {
endLetter();
lastReleaseMs = now; // avoid immediate word-gap trigger
}
if (idle >= WORD_GAP_MS) {
Serial.print(' ');
showWordGap();
lastReleaseMs = now;
}
}
// --- Serial input & playback ---
maybeStartSerialPlaybackFromInput();
serviceSerialPlayback();
}