Your avatar

Aijia Yao

MIT EECS • HTMAA 2025

Week 9 - Output Devices

October 2025

This week was mainly about Output Devices.

What I Did

PCB Design for Output Device

Based on the fact that the longest Morse code is 5 symbols (for '0' and '9'), I designed a PCB with 5 RGB LEDs to represent each symbol in the Morse code input. And the patterns will be decoded and encoded based on the international Morse code as shown below:
PCB Design for Output Device
The microcontroller used is XIAO-RP2040, which is compact and suitable for this project.
  • The PCB design and production was done in Week 6.
  • The RGB LEDs are used as indicators for the Morse code pattern.
  • The button is used to trigger the Morse code input.
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)

Testing and Integration

After assembling the output device, I conducted thorough testing to ensure its functionality.

  • I directly adapted the Arduino code from Week 6 to decode the Morse code input from the button presses and display the corresponding patterns on the RGB LEDs. For dots it shines green🟩, for dashes it shines red🟥, and blank with white⬜️.
  • I ensured that the timing for dots, dashes, letter gaps, and word gaps were accurately implemented according to Morse code standards.

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:
  • I modified the Arduino code to read text input from the serial monitor and convert each character into its Morse code representation.
  • The output device then displays the Morse code patterns on the RGB LEDs in sequence, with appropriate timing for letter and word gaps.
Then final Arduino code used is as below:

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


Other Notes

  • Besides Morse code, there're encoding schemes like Baudot Code (ITA-2) and UTF-8 that can be encrypted using this 5-bit pattern.
  • Use of ChatGPT can be found here.