Community project

Tactile Morse Speech Translator

ESP32
Photo of Tactile Morse Speech Translator
Generated with AI

Cherie Mae Cañete

Published October 22, 2026

This project builds a wearable Morse code communication system with two independent ESP32-S3 units that communicate wirelessly via ESP-NOW. One unit features a microphone and speaker for audio feedback, while the glove-mounted unit provides tactile Morse input through vibration motors and button controls, allowing users to send and receive messages in real time.

The guide includes a complete parts list for both units, wiring diagrams for the microphone and amplifier circuits, battery charging setup, and step-by-step assembly instructions for mounting the components comfortably. Firmware is provided to handle Morse encoding, wireless pairing, message queuing, and tactile feedback via the vibration motor.

Wiring diagram

Wiring diagram for Tactile Morse Speech Translator

Gather all the parts

QtyComponent
1

ESP32-S3 Dev Module (Unit A)

The speaker-side ESP32-S3 board that sends audio to the phone and text to the glove.

1

HiLetgo INMP441 I²S Microphone Module

Omnidirectional 24-bit I²S MEMS microphone module based on the TDK InvenSense INMP441 IC. Outputs digital audio over a 3-wire I²S bus (SCK/BCLK, WS/LRCLK, SD/DOUT). Supply 1.8–3.3 V; native 3.3 V operation with no level shifting required. L/R channel select pin: tie to GND for left-channel mono output or VDD for right-channel mono output. Use the ESP32 Arduino core or ESP-IDF I2S peripheral API; no separate PlatformIO library is required.

1

MAX98357A I2S Class-D Mono Amplifier Breakout

I2S-input Class-D mono audio amplifier IC on a compact breakout board. Accepts I2S digital audio input (BCLK, LRC, DIN) and drives a small speaker or transducer directly. No I2C/SPI control bus is needed. The amplifier supply range is 2.5V-5.5V, and the I2S input pins are compatible with 3.3V logic. SD/MODE controls shutdown and channel selection; GAIN selects 3 dB, 6 dB, 9 dB, 12 dB, or 15 dB gain.

1

8Ω Speaker

28 mm, 8 Ω, 1 W

Generic small 8Ω 0.5-3W loudspeaker (~28mm typical). Pair with an I2S amp (MAX98357A) or class-D amp (TPA3116D2) for usable volume; do not drive directly from a GPIO pin. Audio output for music/voice playback.

1

3.7 V 1200 mAh protected LiPo battery

The rechargeable battery that powers the speaker-side wearable.

1

Unit A USB-C LiPo charger module with protection

The USB-C module that safely charges and protects Unit A's LiPo battery.

1

SPST slide power switch (Unit A)

The small slide switch that turns the speaker-side wearable on and off.

1

MCP1700-3302 3.3 V LDO regulator

The low-dropout regulator that makes a safe 3.3 V supply from the LiPo battery.

1

3.7 V 500 mAh protected LiPo battery

The rechargeable battery that powers the glove wearable.

1

Glove USB-C LiPo charger module with protection

The USB-C module that safely charges and protects the glove LiPo battery.

1

SPST slide power switch (glove)

The small slide switch that turns the glove unit on and off.

1

MCP1700-3302 3.3 V LDO regulator

The low-dropout regulator that makes 3.3 V for the ESP32-C3 from the LiPo battery.

1

10 mm 3 V coin ERM vibration motor — DOT

The coin motor on the back of the hand that makes the short Morse dot tap.

1

10 mm 3 V coin ERM vibration motor — DASH

The coin motor at the wrist that makes the longer Morse dash tap.

1

AO3400A

30V 5.7A N-channel enhancement-mode logic-level MOSFET in SOT-23-3L. Vgs(th) typically ~0.7V (max 1.45V at Id=250 uA), fully enhanced at Vgs=2.5V-4.5V. RDS(on) ~26 mOhm at Vgs=4.5V, ~38 mOhm at Vgs=2.5V. Driven directly by 3.3V or 5V microcontroller GPIO/PWM as a low-side switch for loads up to its Id and VDS limits. No firmware library required.

1

AO3400A

30V 5.7A N-channel enhancement-mode logic-level MOSFET in SOT-23-3L. Vgs(th) typically ~0.7V (max 1.45V at Id=250 uA), fully enhanced at Vgs=2.5V-4.5V. RDS(on) ~26 mOhm at Vgs=4.5V, ~38 mOhm at Vgs=2.5V. Driven directly by 3.3V or 5V microcontroller GPIO/PWM as a low-side switch for loads up to its Id and VDS limits. No firmware library required.

1

Push Button

Yes

Momentary push button switch

1

Push Button

No

Momentary push button switch

1

Push Button

Please repeat

Momentary push button switch

1

Push Button

Thank you

Momentary push button switch

1

100 ohm gate resistor — DOT motor

100 Ω

The resistor between the ESP32-C3 pin and the DOT motor MOSFET gate.

1

100 ohm gate resistor — DASH motor

100 Ω

The resistor between the ESP32-C3 pin and the DASH motor MOSFET gate.

1

100 kilohm gate pulldown — DOT motor

100 kΩ

The resistor that keeps the DOT motor off while the ESP32-C3 starts.

1

100 kilohm gate pulldown — DASH motor

100 kΩ

The resistor that keeps the DASH motor off while the ESP32-C3 starts.

1

1N5819 flyback diode — DOT motor

1N5819

The diode that safely absorbs the voltage spike from the DOT vibration motor.

1

1N5819 flyback diode — DASH motor

1N5819

The diode that safely absorbs the voltage spike from the DASH vibration motor.

1

1 microfarad ceramic capacitor — Unit A regulator input

1 µF, 6.3 V or higher

The capacitor that keeps Unit A's regulator input steady.

1

1 microfarad ceramic capacitor — Unit A regulator output

1 µF, 6.3 V or higher

The capacitor that keeps Unit A's 3.3 V supply steady.

1

1 microfarad ceramic capacitor — glove regulator input

1 µF, 6.3 V or higher

The capacitor that keeps the glove regulator input steady.

1

1 microfarad ceramic capacitor — glove regulator output

1 µF, 6.3 V or higher

The capacitor that keeps the glove 3.3 V supply steady.

1

Unit A USB-C charge socket

The USB-C socket where Unit A's charging cable plugs in.

1

Glove USB-C charge socket

The USB-C socket where the glove charging cable plugs in.

Assemble it in 5 steps

1. Build the two protected battery supplies

With both switches OFF, connect each LiPo battery BAT+ and BAT− to its own charger B+ and B− pads. Connect charger OUT+ through that unit's slide switch. Send the switched Unit A line to a 3.3 V regulator, and send the switched glove line both to its 3.3 V regulator and to the two motor positive leads.

  • Never connect a LiPo backwards or charge it without the protection/charger module; reversed battery wiring can overheat or damage the cells.

2. Wire the microphone and speaker unit

Connect Unit A GPIO12 to SCK/BCLK on both audio modules, GPIO13 to WS/LRC on both, INMP441 SD to GPIO14, and GPIO15 to MAX98357A DIN. Connect the microphone L/R pin to GND. Connect MAX98357A SPK+ and SPK− directly to the two speaker tabs; do not connect either speaker tab to ground.

  • Keep microphone wires short and place the microphone opening away from the speaker to reduce feedback.

3. Build each motor switch

Put each coin motor's negative lead on its AO3400A Drain and the Source on GND. Place a 1N5819 across each motor: striped cathode to motor positive, unstriped anode to the Drain/motor negative. Put a 100 ohm resistor from GPIO3 to the DOT Gate and GPIO4 to the DASH Gate, then place a 100 kilohm resistor from each Gate to GND.

  • Make sure the striped end of each flyback diode goes to battery positive; reversing it creates a battery short when the motor turns on.

4. Add the glove controls

Connect one side of every tactile button to GND. Connect the other sides to GPIO0 for Yes, GPIO1 for No, GPIO5 for Please repeat, and GPIO6 for Thank you. The firmware turns on each pin's internal pull-up, so no separate button resistor is needed.

5. Mount it comfortably

Sew or bond the DOT motor on the back of the hand and the DASH motor about 3 to 4 cm away at the wrist. Put the C3, drivers, battery, and strain-relieved wires in a soft back-of-hand or wrist pouch. Keep LiPo cells flat, protected from sharp bends, and removable for inspection.

  • Test motor locations with loose tape before sewing; the two sensations must be easy to tell apart.

Review all connections

1. Connections between "unit_a_s3" and "ESP32"

Functionunit_a_s3ESP32
power3V3 → MCP1700-3302 3.3 V LDO regulator OUTEXT
groundGND → Unit A common groundGND
digitalGPIO12_BCLK → HiLetgo INMP441 I²S Microphone Module SCKEXT
digitalGPIO12_BCLK → MAX98357A I2S Class-D Mono Amplifier Breakout BCLKEXT
digitalGPIO13_LRCLK → HiLetgo INMP441 I²S Microphone Module WSEXT
digitalGPIO13_LRCLK → MAX98357A I2S Class-D Mono Amplifier Breakout LRCEXT
digitalGPIO15_AMP_DOUT → MAX98357A I2S Class-D Mono Amplifier Breakout DINEXT

2. Connections between "mic" and "ESP32"

FunctionmicESP32
dataSD → ESP32-S3 Dev Module (Unit A) GPIO14_MIC_DINEXT
powerVDD → ESP32-S3 Dev Module (Unit A) 3V3EXT
groundGND → GroundGND
groundL/R → Select left microphone channelGND

3. Connections between "amp" and "ESP32"

FunctionampESP32
powerVIN → ESP32-S3 Dev Module (Unit A) 3V3EXT
groundGND → GroundGND
digitalSD → ESP32-S3 Dev Module (Unit A) 3V3EXT
dataSPK+ → 8Ω Speaker POSEXT
dataSPK- → 8Ω Speaker NEGEXT

4. Connections between "a_battery" and "ESP32"

Functiona_batteryESP32
powerBAT+ → Unit A USB-C LiPo charger module with protection B+EXT
groundBAT- → Unit A USB-C LiPo charger module with protection B-EXT

5. Connections between "a_charger" and "ESP32"

Functiona_chargerESP32
groundGND → Common groundGND
powerOUT+ → SPST slide power switch (Unit A) INEXT
groundOUT- → Protected battery groundGND

6. Connections between "a_switch" and "ESP32"

Functiona_switchESP32
powerOUT → MCP1700-3302 3.3 V LDO regulator INEXT

7. Connections between "a_regulator" and "ESP32"

Functiona_regulatorESP32
groundGND → Regulator groundGND

8. Connections between "a_in_cap" and "ESP32"

Functiona_in_capESP32
powerPositive → MCP1700-3302 3.3 V LDO regulator INEXT
groundNegative → Ground returnGND

9. Connections between "a_out_cap" and "ESP32"

Functiona_out_capESP32
powerPositive → MCP1700-3302 3.3 V LDO regulator OUTEXT
groundNegative → Ground returnGND

10. Connections between "glove_battery" and "ESP32"

Functionglove_batteryESP32
powerBAT+ → Glove USB-C LiPo charger module with protection B+EXT
groundBAT- → Glove USB-C LiPo charger module with protection B-EXT

11. Connections between "glove_charger" and "ESP32"

Functionglove_chargerESP32
groundGND → Common groundGND
powerOUT+ → SPST slide power switch (glove) INEXT
groundOUT- → Protected battery groundGND

12. Connections between "glove_switch" and "ESP32"

Functionglove_switchESP32
powerOUT → MCP1700-3302 3.3 V LDO regulator INEXT
powerOUT → 10 mm 3 V coin ERM vibration motor — DOT M+EXT
powerOUT → 10 mm 3 V coin ERM vibration motor — DASH M+EXT

13. Connections between "glove_regulator" and "ESP32"

Functionglove_regulatorESP32
powerOUT → ESP32-C3 regulated power3V3
groundGND → Regulator groundGND

14. Connections between "g_in_cap" and "ESP32"

Functiong_in_capESP32
powerPositive → MCP1700-3302 3.3 V LDO regulator INEXT
groundNegative → Ground returnGND

15. Connections between "g_out_cap" and "ESP32"

Functiong_out_capESP32
powerPositive → Output stabilizer3V3
groundNegative → Ground returnGND

16. Connections between "motor_dot" and "ESP32"

Functionmotor_dotESP32
powerM- → AO3400A Drain (D)EXT

17. Connections between "motor_dash" and "ESP32"

Functionmotor_dashESP32
powerM- → AO3400A Drain (D)EXT

18. Connections between "fet_dot" and "ESP32"

Functionfet_dotESP32
groundSource (S) → Ground returnGND

19. Connections between "fet_dash" and "ESP32"

Functionfet_dashESP32
groundSource (S) → Ground returnGND

20. Connections between "dot_diode" and "ESP32"

Functiondot_diodeESP32
powerAnode → AO3400A Drain (D)EXT
powerCathode → 10 mm 3 V coin ERM vibration motor — DOT M+EXT

21. Connections between "dash_diode" and "ESP32"

Functiondash_diodeESP32
powerAnode → AO3400A Drain (D)EXT
powerCathode → 10 mm 3 V coin ERM vibration motor — DASH M+EXT

22. Connections between "dot_gate_r" and "ESP32"

Functiondot_gate_rESP32
digitalA → DOT PWM signalGPIO 3
digitalB → AO3400A Gate (G)EXT

23. Connections between "dash_gate_r" and "ESP32"

Functiondash_gate_rESP32
digitalA → DASH PWM signalGPIO 4
digitalB → AO3400A Gate (G)EXT

24. Connections between "dot_pulldown" and "ESP32"

Functiondot_pulldownESP32
digitalA → AO3400A Gate (G)EXT
groundB → Ground returnGND

25. Connections between "dash_pulldown" and "ESP32"

Functiondash_pulldownESP32
digitalA → AO3400A Gate (G)EXT
groundB → Ground returnGND

26. Connections between "button_yes" and "ESP32"

Functionbutton_yesESP32
groundGND → Ground returnGND
digitalSIGNAL → Yes reply inputGPIO 0

27. Connections between "button_no" and "ESP32"

Functionbutton_noESP32
groundGND → Ground returnGND
digitalSIGNAL → No reply inputGPIO 1

28. Connections between "button_repeat" and "ESP32"

Functionbutton_repeatESP32
groundGND → Repeat reply inputGND
digitalSIGNAL → Please repeat reply inputGPIO 5

29. Connections between "button_thanks" and "ESP32"

Functionbutton_thanksESP32
groundGND → Ground returnGND
digitalSIGNAL → Thank you reply inputGPIO 6

30. Connections between "a_charge_port" and "ESP32"

Functiona_charge_portESP32
power5V → Unit A USB-C LiPo charger module with protection USB-C 5VEXT
groundGND → USB groundGND

31. Connections between "g_charge_port" and "ESP32"

Functiong_charge_portESP32
power5V → Glove USB-C LiPo charger module with protection USB-C 5VEXT
groundGND → USB groundGND

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>

#define DOT_PIN 3
#define DASH_PIN 4
#define YES_PIN 0
#define NO_PIN 1
#define REPEAT_PIN 5
#define THANKS_PIN 6
constexpr uint8_t ESPNOW_CHANNEL=6, QUEUE_SIZE=192;
enum Kind:uint8_t{HELLO=1,TEXT=2,ACK=3,REPLY=4};
struct __attribute__((packed)) Packet { uint8_t kind; uint16_t seq; uint8_t len; char text[200]; };
char q[QUEUE_SIZE]; uint8_t head=0,tail=0; bool active=false,dash=false; uint32_t nextAt=0; uint16_t dotMs=120,lastSeq=0; bool keywordsOnly=false; uint8_t peer[6]; bool paired=false;
const char* morse(char c){ switch(c){case 'A':return ".-";case 'B':return "-...";case 'C':return "-.-.";case 'D':return "-..";case 'E':return ".";case 'F':return "..-.";case 'G':return "--.";case 'H':return "....";case 'I':return "..";case 'J':return ".---";case 'K':return "-.-";case 'L':return ".-..";case 'M':return "--";case 'N':return "-.";case 'O':return "---";case 'P':return ".--.";case 'Q':return "--.-";case 'R':return ".-.";case 'S':return "...";case 'T':return "-";case 'U':return "..-";case 'V':return "...-";case 'W':return ".--";case 'X':return "-..-";case 'Y':return "-.--";case 'Z':return "--..";case '0':return "-----";case '1':return ".----";case '2':return "..---";case '3':return "...--";case '4':return "....-";case '5':return ".....";case '6':return "-....";case '7':return "--...";case '8':return "---..";case '9':return "----.";case '.':return ".-.-.-";case ',':return "--..--";case '?':return "..--..";case '/':return "-..-.";case '-':return "-....-";default:return "";} }
void push(char c){uint8_t n=(head+1)%QUEUE_SIZE;if(n==tail)tail=(tail+1)%QUEUE_SIZE;q[head]=c;head=n;}
bool pop(char &c){if(tail==head)return false;c=q[tail];tail=(tail+1)%QUEUE_SIZE;return true;}
void sendPacket(uint8_t kind,uint16_t seq,const char* text=""){if(!paired)return;Packet p={};p.kind=kind;p.seq=seq;p.len=min((size_t)200,strlen(text));memcpy(p.text,text,p.len);esp_now_send(peer,(uint8_t*)&p,sizeof(p.kind)+sizeof(p.seq)+sizeof(p.len)+p.len);}
void onRecv(const esp_now_recv_info_t* info,const uint8_t* data,int len){if(len<4)return;Packet p={};memcpy(&p,data,min(len,(int)sizeof(p)));memcpy(peer,info->src_addr,6);paired=true;if(p.kind==TEXT){sendPacket(ACK,p.seq);if(p.seq==lastSeq)return;lastSeq=p.seq;for(uint8_t i=0;i<p.len;i++){char c=toupper((unsigned char)p.text[i]);if(!keywordsOnly || strchr(" YESNOHELPSTOPREPEATTHANKYOU",c))push(c);}}}
void schedule(){static const char* symbols="";static uint8_t i=0;uint32_t now=millis();if(now<nextAt)return;if(active){ledcWrite(DOT_PIN,0);ledcWrite(DASH_PIN,0);active=false;nextAt=now+dotMs;return;}if(symbols&&symbols[i]){dash=symbols[i++]== '-';ledcWrite(dash?DASH_PIN:DOT_PIN,180);active=true;nextAt=now+(dash?3*dotMs:dotMs);return;}char c; if(!pop(c))return;if(c==' '){nextAt=now+7*dotMs;return;}symbols=morse(c);i=0;if(*symbols)nextAt=now+2*dotMs;}
void buttons(){static uint32_t down[4]={};static bool held[4]={};int pins[4]={YES_PIN,NO_PIN,REPEAT_PIN,THANKS_PIN};for(int i=0;i<4;i++){bool low=!digitalRead(pins[i]);if(low&&!down[i])down[i]=millis();if(low&&down[i]&&!held[i]&&millis()-down[i]>900){held[i]=true;if(i==2){head=tail=0;}if(i==3){dotMs=dotMs==240?80:dotMs+20;}}if(!low&&down[i]){if(!held[i]){const char* r[]={"YES","NO","PLEASE_REPEAT","THANK_YOU"};sendPacket(REPLY,0,r[i]);}down[i]=0;held[i]=false;}}}
void setup(){for(int p:{YES_PIN,NO_PIN,REPEAT_PIN,THANKS_PIN})pinMode(p,INPUT_PULLUP);ledcAttach(DOT_PIN,180,8);ledcAttach(DASH_PIN,180,8);WiFi.mode(WIFI_STA);esp_wifi_set_channel(ESPNOW_CHANNEL,WIFI_SECOND_CHAN_NONE);if(esp_now_init()!=ESP_OK)return;esp_now_register_recv_cb(onRecv);esp_now_peer_info_t b={};memset(b.peer_addr,0xFF,6);b.channel=ESPNOW_CHANNEL;b.ifidx=WIFI_IF_STA;esp_now_add_peer(&b);Packet h={};h.kind=HELLO;esp_now_send(b.peer_addr,(uint8_t*)&h,4);}
void loop(){schedule();buttons();delay(1);}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik