Skip to content
Noah SpiegelmanMechanical engineering
Back to all projects

Project 05 / 10Mechatronics · Optical communication

Point-to-Point Communication System

EK 210, Boston University · Sep 2025 – Dec 2025

A keypad-to-LCD laser Morse-code link built from two Arduinos, with a servo-steered transmitter.

  • Completed
  • Embedded
  • Mechanical
The finished transmitter, a laser-cut box with a 16-key keypad and a small servo-mounted laser housing on its lid, with the red laser dot lit
Fig. 01The finished transmitter, with the laser on.

Key result

20 / 20

Messages decoded at 1–5 ft

Measured

Distance test with four test messages (cat, abc, dog, hello) at each of five distances from 1 to 5 ft. The system also decoded all 10 required commands at 5+ ft with under 1.5 s of latency.

Technical highlights

  • Typed messages sent across a room as laser-pulsed Morse code and decoded onto an LCD, with no computer involved.
  • Decoded all 10 required commands with 100% accuracy at 5+ ft, with under 1.5 seconds of latency.
  • Parts cost $103.76 against a $200 target, in two fully enclosed, laser-cut units.
My contribution
  • Modelled the receiver from the photoresistor's datasheet with a teammate and bench-tested it
  • Designed the housings in CAD
  • Testing, as part of a team of 4
Tools
Arduino UnoPhotoresistorServo motorTinkercad CircuitsLaser cutting3D printing

Overview

A four-person team project that sends typed messages across a room as laser-pulsed Morse code. A keypad and Arduino encode the message, a servo aims the laser, and a photoresistor receiver decodes it onto an LCD, with no computer involved.

Demonstration of the working system.

Engineering challenge

The client brief called for two units that communicate by light with no laptop involved: a steerable transmitter that sends any of 10 characters or codewords, and a receiver that decodes and displays them. It had to work at 5 ft or more, keep each unit smaller than a shoe box, and run without a reset between messages. We framed the users as responders in remote or network-limited places where Wi-Fi is unavailable.

A pairwise comparison chart ranked reliability and durability first, then ease of use, then cost and size, with a cost target of $200 or less.

Design process

The transmitter takes input from a 16-key keypad. An Arduino Uno turns each character into timed Morse pulses and drives a 650 nm, 5 mW laser in a small housing mounted on an S51 micro servo, which steers the beam.

The receiver has a 20 mm plano-convex lens window on its front, and its photoresistor, in a 10 kΩ voltage divider, sits inside a light-isolating enclosure. A second Arduino times each pulse, separates dots, dashes and gaps, decodes them into characters and shows the message on a 16×2 LCD.

Before building, I worked with a teammate to model the receiver from the photoresistor's datasheet. We calculated the divider output across light levels, then bench-tested it by flashing Morse code with a phone flashlight and reading the decoded output over serial. We also compared a photoresistor with a photodiode and chose the photoresistor for more reliable signal changes, and compared a servo with a stepper and chose the servo for smoother, lower-power aiming.

Analysis & technical details

Send a message by light

Type a short message and send it. The transmitter pulses a laser in Morse code, the receiver times each pulse and gap, classifies dots, dashes and gaps, and decodes them onto its display, the same sequence as the receiver code described in this project.

· · ·16×2 LCDTRANSMITTERRECEIVER

What the photoresistor sees

49 units · 9.8 s

Short bars are dots (1 unit), long bars are dashes (3 units). The receiver times each pulse and each gap, then decodes the pattern.

Press Send to transmit the message.

The timing follows the project's Arduino code: a dot is 1 unit of light and a dash is 3, with 1 unit off between symbols, 3 between letters and 10 between words, and a 200 ms unit. The receiver calls a pulse under 2 units a dot, and a gap of 2.5 units or more the end of a letter and 4.5 or more the end of a word. This demo assumes the receiver's unit matches the transmitter's. The real keypad types letters only, so digits and spaces are extras that the receiver can decode.

Code

Sending a letter as light (transmitter)
int unit = 200;        // Morse timing base (ms)

void Play_Dot_Dash(char sign) {
  if (sign == '.') {
    Laser_On();
    delay(unit);       // dot duration
    Laser_Off();
    delay(unit);       // intra-symbol gap
  }
  else if (sign == '-') {
    Laser_On();
    delay(3 * unit);   // dash duration
    Laser_Off();
    delay(unit);       // intra-symbol gap
  }
}

void Play_Letter(char Letter) {
  if (Letter == ' ') {
    delay(7 * unit);   // word gap
  } else {
    for (int j = 0; j < 26; j++) {
      if (MorseCode[j][5] == Letter) {
        for (int k = 0; k < 5; k++) {
          if (MorseCode[j][k] != 'x') Play_Dot_Dash(MorseCode[j][k]);
        }
        break;
      }
    }
    delay(2 * unit);   // letter gap
  }
}

Each letter is looked up in a table that stores its dots and dashes (an x marks an unused slot), then played by switching the laser on for 1 unit for a dot or 3 for a dash, each followed by 1 unit off. The unit is 200 ms. After a letter the code waits 2 more units, so letters are 3 units apart, and a space waits 7 more on top of that, which makes the gap between words 10 units.

Typing with a phone-style keypad (transmitter)
// T9 LETTER INPUT
if (key >= '2' && key <= '9') {
  int num = key - '0';
  if (lastKey == num && (now - lastKeyPress) < 800) {
    pressCount++;
    if (pressCount >= (int)strlen(T9Letters[num])) pressCount = 0;
  } else {
    pressCount = 0;
  }
  lastKey = num;
  lastKeyPress = now;
  // ...
}

// CONFIRM LETTER
if (key == '#') {
  // ...
  To_Transmit += letter;
  // ...
}

// TRANSMIT MESSAGE
if (key == '*') {
  for (int i = 0; i < To_Transmit.length(); i++) {
    Play_Letter(To_Transmit[i]);
  }
  To_Transmit = "";
}

The 16-key keypad works like an old phone. Keys 2 to 9 each carry three or four letters, and pressing the same key again within 0.8 seconds steps to the next one. # adds the chosen letter to the message and * sends it. Keys A and B turn the servo that aims the laser 10° at a time, within 0 to 180°, and D recentres it. In the later version of the code, 0 locks the laser on so it can be aimed at the receiver.

Telling dots, dashes and gaps apart (receiver)
uint8_t SYMBOL_MAX_X10 = 25;     // 2.5× dot => symbol gap
uint8_t LETTER_MAX_X10 = 45;     // 4.5× dot => letter gap (else word)

// ON -> OFF : dot or dash
if (dur < (2UL*DOT_UNIT)) {
  if (curLen<6) { currentSymbols[curLen++]='.'; Serial.print('.'); }
} else {
  if (curLen<6) { currentSymbols[curLen++]='-'; Serial.print('-'); }
}

uint8_t classifyGap_x10(uint32_t offMs, uint16_t dot){
  uint32_t units_x10 = (offMs*10UL)/(uint32_t)dot;
  if (units_x10 < SYMBOL_MAX_X10) return 0;
  if (units_x10 < LETTER_MAX_X10) return 1;
  return 2;
}

The receiver times how long the light stays on: under 2 dot-lengths is a dot and anything longer is a dash. When the light comes back on, it measures how long it was off, in tenths of a dot-length: under 2.5 dots is only the gap between symbols, under 4.5 ends a letter, and longer ends a word. The collected dots and dashes are looked up and the letter is shown on the LCD, and a letter is also finished if the light stays off long enough.

Turning a noisy light sensor into on and off (receiver)
emaRaw = emaRaw + (raw - emaRaw)/EMA_RAW_ALPHA;
if (abs(emaRaw - baseline) < (int)max(marginOn, marginOff)*2){
  baseline = baseline + (emaRaw - baseline)/BASELINE_ALPHA;
}

bool nowOn = computeOnHys(emaRaw, isOn);

bool computeOnHys(int smoothedRaw, bool prevOn){
  int thrOn, thrOff;
  if (!INVERT_ON_LOGIC){
    thrOn  = (FIXED_THRESHOLD>0) ? FIXED_THRESHOLD : (baseline + marginOn);
    thrOff = (FIXED_THRESHOLD>0) ? FIXED_THRESHOLD : (baseline + marginOff);
    if (!prevOn) return smoothedRaw > thrOn;
    else         return smoothedRaw > thrOff;
  }
  // ...
}

The photoresistor's readings are smoothed with a moving average. At start-up the receiver measures the ambient light with no signal (it asks you to cover the sensor) and uses that as a baseline, which then drifts slowly with the room's light. The light counts as on once the smoothed reading rises 25 above the baseline and stays on until it falls back to within 15 of it, so flicker near the threshold doesn't create false pulses. Pulses shorter than 60 ms are ignored.

Fabrication & assembly

We designed the housings in CAD and laser-cut the panels, which join with interlocking finger joints. We tested each subsystem on its own before integrating, and checked the power budget for a 9 V battery.

Testing & results

The system decoded all 10 required commands with 100% accuracy at distances of 5+ ft, with under 1.5 seconds of latency. In a distance test, 20 of 20 messages (cat, abc, dog and hello, at 1 to 5 ft) decoded correctly. The transmitter steers through 90° under motor control, and both units are fully enclosed.

The parts cost $103.76 against the $200 target. Estimated battery life on a 9 V battery is about 5 hours for the receiver and about 2 hours for the transmitter.

Problems & lessons learned

Lessons learned: Morse timing has to be tuned to the sensor's response time, and a few degrees of misalignment dropped the signal completely. Servo current spikes occasionally reset the Arduino in early tests, which taught us to budget power and consider capacitors or separate rails. Testing subsystems separately made debugging much faster.

What I'd improve next

Next steps we identified: a QWERTY keyboard for faster typing, a weatherproof housing, and a lens setup that gives the receiver a wider target.