AI Morse Code Communication Using Eye-Blink Recognition + ESP32 + IoT Webpage + n8n + AI Agent + Telegram Voice Alerts + Google Sheets + ThingSpeak
This project can be built as a complete assistive IoT communication system in which a user communicates by blinking their eyes. The ESP32 detects blink duration, converts the blink pattern into Morse code, decodes Morse into text, and sends the result through Wi-Fi to an n8n automation server.
n8n then acts as the orchestration/agent layer: it can interpret the message, determine whether it is an emergency, record the event in Google Sheets, update ThingSpeak, and send a Telegram text and/or voice alert.
The ESP32 can simultaneously host a local webpage showing the current blink state, Morse code, decoded message, Wi-Fi status and system statistics. ESP32 supports Wi-Fi station mode and can serve HTTP pages directly, making this architecture practical. Espressif Systems
1. Project title
AI-Powered Eye Blink Morse Communication and Agentic IoT Alert System
Technologies
- ESP32
- IR eye-blink sensor
- Morse-code recognition
- Wi-Fi
- ESP32 Web Server
- n8n automation
- n8n AI Agent
- Telegram Bot
- Telegram voice notifications
- Google Sheets
- ThingSpeak
- Optional OpenAI/Gemini/other LLM
- Optional text-to-speech service
- HTML/CSS/JavaScript dashboard
2. Project objective
The main objective is to allow a person to communicate without using:
- keyboard
- touchscreen
- microphone
- physical switches
- conventional speech
The user communicates through short and long eye closures.
For example:
Short blink = .
Long blink = -
Therefore:
... = S
--- = O
... = S
produces:
SOS
The system can then send:
User blink pattern
↓
Morse decoder
↓
"SOS"
↓
ESP32
↓
Wi-Fi
↓
n8n
↓
AI Agent
↓
Emergency detected
↓
Telegram
├── Text alert
└── Voice alert
At the same time:
ESP32 → ThingSpeak
ESP32/n8n → Google Sheets
ESP32 → Web Dashboard
3. Complete system architecture
┌──────────────────────┐
│ USER/EYE │
│ │
│ Short blink . │
│ Long blink - │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ IR EYE-BLINK SENSOR │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ ESP32 │
│ │
│ Blink detection │
│ Timing engine │
│ Morse encoder/decoder│
│ Wi-Fi │
│ HTTP Web Server │
└───────┬───────┬──────┘
│ │
┌────────────┘ └─────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ ESP32 Webpage │ │ ThingSpeak │
│ │ │ │
│ Blink status │ │ Morse events │
│ Morse │ │ Message │
│ Text │ │ Statistics │
│ Wi-Fi │ └─────────────────┘
└──────────────────┘
│
│ HTTPS POST
▼
┌─────────────────────┐
│ n8n │
│ │
│ Webhook │
│ Validation │
│ Data processing │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ AI AGENT │
│ │
│ Interpret message │
│ Emergency detection │
│ Priority │
│ Response generation │
└───────┬───────┬─────┘
│ │
┌─────────────┘ └─────────────┐
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Google Sheets │ │ Telegram Bot │
│ │ │ │
│ Timestamp │ │ Text alert │
│ Morse │ │ Voice alert │
│ Message │ │ Emergency │
│ Priority │ └─────────────────┘
└──────────────────┘
n8n is particularly suitable here because it provides workflow automation, integrations and AI functionality. n8n Documentation
4. How the eye-blink communication works
The most important part is converting the user's eye closure into Morse.
We define:
| Action | Meaning |
|---|---|
| Short eye closure | . |
| Long eye closure | - |
| Medium pause | End of Morse character |
| Long pause | End of word |
| Very long pause | End of message |
Example:
Blink Blink Blink
. . .
= S
Another example:
Long Long Long
- - -
= O
Therefore:
... --- ...
becomes:
SOS
5. Recommended blink timing
Start with these values:
BLINK < 450 ms → DOT
BLINK 450–1200 ms → DASH
PAUSE 700–1500 ms → END OF CHARACTER
PAUSE >1500 ms → SPACE
PAUSE >3000 ms → MESSAGE COMPLETE
These are starting values, not universal medical/physiological values.
Different users will have different blinking speeds, so the final version should include a calibration mode.
A better implementation is:
User performs 5 normal short blinks
↓
ESP32 measures duration
↓
Calculate average
↓
DOT threshold = calibrated short blink
DASH threshold = approximately 2–3 × dot duration
6. Hardware components
Required
| Component | Quantity |
|---|---|
| ESP32 DevKit | 1 |
| IR eye-blink sensor | 1 |
| LED | 1 |
| 220 Ω resistor | 1 |
| Push button | 1 |
| Breadboard | 1 |
| Jumper wires | Several |
| USB cable | 1 |
| 5 V USB power supply | 1 |
Optional
- OLED 0.96-inch I2C display
- Buzzer
- second LED
- battery
- enclosure
- wearable/head-mounted sensor
- emergency cancel button
7. Eye-blink sensor
A simple implementation can use an IR reflective eye-blink sensor.
Conceptually:
USER'S EYE
┌───────────┐
│ │
│ 👁 │
│ │
└─────┬─────┘
│
IR sensor
┌────┴────┐
│ IR LED │
│ + │
│ receiver│
└────┬────┘
│
▼
ESP32
When the eyelid changes the reflected IR level, the sensor output changes.
Important
If your sensor provides an analog output, use an ESP32 ADC input and determine a threshold experimentally.
If your module provides a digital output, connect its digital output to a GPIO.
The software below assumes:
Sensor HIGH = eye closed
If your module behaves oppositely, change:
#define EYE_CLOSED_STATE HIGH
to:
#define EYE_CLOSED_STATE LOW
8. ESP32 schematic
A simple connection is:
IR EYE BLINK SENSOR
┌───────────────┐
│ │
│ VCC ──────────┼──────── 3.3V
│ GND ──────────┼──────── GND
│ OUT ──────────┼────┐
└───────────────┘ │
│
▼
GPIO 27
┌────────┐
│ ESP32 │
│ │
GPIO27 │ │
│ │
GPIO2 ──────┤ LED │
│ │
GPIO25 ─────┤ BUTTON │
│ │
│ WiFi │
└────┬───┘
│
│ Internet
▼
Wi-Fi Router
│
┌────────────────────┼─────────────────┐
▼ ▼ ▼
n8n ThingSpeak Browser
9. Detailed wiring
IR sensor
Sensor VCC → ESP32 3.3V
Sensor GND → ESP32 GND
Sensor OUT → GPIO27
Status LED
GPIO2 → 220Ω → LED anode
LED cathode → GND
Emergency/reset button
GPIO25 → Push button → GND
The firmware can use:
pinMode(BUTTON_PIN, INPUT_PULLUP);
so no external pull-up resistor is necessary.
10. Software architecture
The ESP32 firmware contains five major modules:
┌─────────────────────────────┐
│ ESP32 FIRMWARE │
├─────────────────────────────┤
│ 1. Sensor reader │
│ 2. Blink timing engine │
│ 3. Morse decoder │
│ 4. Wi-Fi communication │
│ 5. Web server/dashboard │
└─────────────────────────────┘
11. Morse lookup table
The ESP32 needs to convert:
.- → A
-... → B
-.-. → C
...
The complete table is:
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 --..
Numbers can also be supported:
0 -----
1 .----
2 ..---
3 ...--
4 ....-
5 .....
6 -....
7 --...
8 ---..
9 ----.
12. Complete ESP32 firmware
The following is a practical baseline implementation.
Change the Wi-Fi and n8n settings before uploading.
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// =====================================================
// USER CONFIGURATION
// =====================================================
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_PASSWORD";
// n8n webhook
const char* N8N_WEBHOOK =
"https://YOUR-N8N-DOMAIN/webhook/eye-morse";
// ThingSpeak
const char* THINGSPEAK_API_KEY =
"YOUR_THINGSPEAK_WRITE_KEY";
const char* THINGSPEAK_URL =
"https://api.thingspeak.com/update";
// =====================================================
// GPIO
// =====================================================
#define EYE_SENSOR_PIN 27
#define LED_PIN 2
#define BUTTON_PIN 25
#define EYE_CLOSED_STATE HIGH
// =====================================================
// TIMING
// =====================================================
unsigned long DOT_MAX = 450;
unsigned long DASH_MAX = 1200;
unsigned long CHAR_GAP = 1000;
unsigned long WORD_GAP = 1800;
unsigned long MESSAGE_GAP = 3000;
// =====================================================
// WEB SERVER
// =====================================================
WebServer server(80);
// =====================================================
// STATE
// =====================================================
bool eyeClosed = false;
bool previousEyeState = false;
unsigned long blinkStart = 0;
unsigned long lastBlinkEnd = 0;
unsigned long lastActivity = 0;
String currentMorse = "";
String currentMessage = "";
unsigned long blinkCount = 0;
unsigned long messageCount = 0;
// =====================================================
// MORSE TABLE
// =====================================================
struct MorseEntry {
const char* code;
char letter;
};
MorseEntry morseTable[] = {
{".-", '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'}
};
const int MORSE_TABLE_SIZE =
sizeof(morseTable) / sizeof(morseTable[0]);
// =====================================================
// MORSE DECODER
// =====================================================
char decodeMorse(String code) {
for (int i = 0; i < MORSE_TABLE_SIZE; i++) {
if (code.equals(morseTable[i].code)) {
return morseTable[i].letter;
}
}
return '?';
}
// =====================================================
// COMPLETE CHARACTER
// =====================================================
void finishCharacter() {
if (currentMorse.length() == 0) {
return;
}
char decoded = decodeMorse(currentMorse);
currentMessage += decoded;
Serial.print("Morse: ");
Serial.print(currentMorse);
Serial.print(" -> ");
Serial.println(decoded);
currentMorse = "";
}
// =====================================================
// SEND MESSAGE TO N8N
// =====================================================
void sendToN8N() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected");
return;
}
HTTPClient http;
http.begin(N8N_WEBHOOK);
http.addHeader("Content-Type", "application/json");
StaticJsonDocument<1024> doc;
doc["device_id"] = "ESP32-EYE-001";
doc["morse"] = currentMorse;
doc["message"] = currentMessage;
doc["blink_count"] = blinkCount;
doc["timestamp"] = millis();
String payload;
serializeJson(doc, payload);
Serial.println("Sending to n8n:");
Serial.println(payload);
int response =
http.POST(payload);
Serial.print("n8n response: ");
Serial.println(response);
http.end();
}
// =====================================================
// SEND DATA TO THINGSPEAK
// =====================================================
void sendThingSpeak() {
if (WiFi.status() != WL_CONNECTED) {
return;
}
HTTPClient http;
String url =
String(THINGSPEAK_URL) +
"?api_key=" + THINGSPEAK_API_KEY +
"&field1=" + String(blinkCount) +
"&field2=" + String(currentMessage.length()) +
"&field3=" + String(currentMessage);
http.begin(url);
int response = http.GET();
Serial.print("ThingSpeak response: ");
Serial.println(response);
http.end();
}
// =====================================================
// FINISH MESSAGE
// =====================================================
void finishMessage() {
finishCharacter();
if (currentMessage.length() == 0) {
return;
}
Serial.println("======================");
Serial.print("FINAL MESSAGE: ");
Serial.println(currentMessage);
Serial.println("======================");
messageCount++;
sendToN8N();
sendThingSpeak();
currentMessage = "";
blinkCount = 0;
}
// =====================================================
// PROCESS BLINK
// =====================================================
void processBlink(unsigned long duration) {
blinkCount++;
Serial.print("Blink duration: ");
Serial.println(duration);
if (duration <= DOT_MAX) {
currentMorse += ".";
Serial.println("DOT");
} else if (duration <= DASH_MAX) {
currentMorse += "-";
Serial.println("DASH");
} else {
Serial.println("Blink too long");
}
lastActivity = millis();
}
// =====================================================
// WEBPAGE
// =====================================================
String webpage() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width,initial-scale=1">
<title>AI Eye Morse IoT</title>
<style>
body {
font-family: Arial;
background: #101827;
color: white;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: auto;
}
.card {
background: #1d2939;
padding: 20px;
margin-bottom: 15px;
border-radius: 15px;
}
.title {
font-size: 28px;
color: #38bdf8;
}
.value {
font-size: 35px;
color: #22c55e;
word-wrap: break-word;
}
.status {
font-size: 18px;
}
button {
padding: 15px 25px;
border: 0;
border-radius: 10px;
background: #ef4444;
color: white;
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="title">
AI Eye Blink Morse Communication
</div>
<p>
ESP32 + n8n + AI Agent + Telegram
</p>
</div>
<div class="card">
<div>Eye status</div>
<div id="eye" class="value">
Loading...
</div>
</div>
<div class="card">
<div>Current Morse</div>
<div id="morse" class="value">
-
</div>
</div>
<div class="card">
<div>Decoded Message</div>
<div id="message" class="value">
-
</div>
</div>
<div class="card">
<div>Statistics</div>
<p>Blinds: <span id="blinks">0</span></p>
<p>Messages: <span id="messages">0</span></p>
</div>
<div class="card">
<button onclick="resetSystem()">
RESET MESSAGE
</button>
</div>
</div>
<script>
async function updateData() {
const response =
await fetch('/status');
const data =
await response.json();
document.getElementById('eye')
.innerText = data.eye;
document.getElementById('morse')
.innerText = data.morse || '-';
document.getElementById('message')
.innerText = data.message || '-';
document.getElementById('blinks')
.innerText = data.blinks;
document.getElementById('messages')
.innerText = data.messages;
}
async function resetSystem() {
await fetch('/reset');
updateData();
}
setInterval(updateData, 500);
updateData();
</script>
</body>
</html>
)rawliteral";
return html;
}
// =====================================================
// WEB ROUTES
// =====================================================
void handleRoot() {
server.send(
200,
"text/html",
webpage()
);
}
void handleStatus() {
StaticJsonDocument<512> doc;
doc["eye"] =
eyeClosed ? "CLOSED" : "OPEN";
doc["morse"] =
currentMorse;
doc["message"] =
currentMessage;
doc["blinks"] =
blinkCount;
doc["messages"] =
messageCount;
String response;
serializeJson(doc, response);
server.send(
200,
"application/json",
response
);
}
void handleReset() {
currentMorse = "";
currentMessage = "";
blinkCount = 0;
server.send(
200,
"text/plain",
"RESET"
);
}
// =====================================================
// WIFI
// =====================================================
void connectWiFi() {
WiFi.begin(
WIFI_SSID,
WIFI_PASSWORD
);
Serial.print("Connecting WiFi");
while (
WiFi.status() != WL_CONNECTED
) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("WiFi connected");
Serial.print("ESP32 IP: ");
Serial.println(
WiFi.localIP()
);
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
pinMode(
EYE_SENSOR_PIN,
INPUT
);
pinMode(
LED_PIN,
OUTPUT
);
pinMode(
BUTTON_PIN,
INPUT_PULLUP
);
connectWiFi();
server.on(
"/",
handleRoot
);
server.on(
"/status",
handleStatus
);
server.on(
"/reset",
handleReset
);
server.begin();
Serial.println(
"Web server started"
);
}
// =====================================================
// MAIN LOOP
// =====================================================
void loop() {
server.handleClient();
// ---------------------------------------------
// RESET BUTTON
// ---------------------------------------------
if (
digitalRead(BUTTON_PIN) == LOW
) {
currentMorse = "";
currentMessage = "";
delay(300);
Serial.println(
"System reset"
);
}
// ---------------------------------------------
// READ EYE SENSOR
// ---------------------------------------------
bool state =
digitalRead(EYE_SENSOR_PIN)
== EYE_CLOSED_STATE;
// Eye just closed
if (
state &&
!previousEyeState
) {
eyeClosed = true;
blinkStart = millis();
digitalWrite(
LED_PIN,
HIGH
);
Serial.println(
"Eye CLOSED"
);
}
// Eye just opened
if (
!state &&
previousEyeState
) {
eyeClosed = false;
unsigned long duration =
millis() - blinkStart;
digitalWrite(
LED_PIN,
LOW
);
processBlink(duration);
lastBlinkEnd = millis();
Serial.println(
"Eye OPEN"
);
}
previousEyeState = state;
// ---------------------------------------------
// HANDLE PAUSES
// ---------------------------------------------
if (
!eyeClosed &&
lastBlinkEnd > 0
) {
unsigned long pause =
millis() - lastBlinkEnd;
if (
currentMorse.length() > 0 &&
pause > CHAR_GAP
) {
finishCharacter();
lastBlinkEnd = millis();
}
if (
currentMessage.length() > 0 &&
currentMorse.length() == 0 &&
pause > MESSAGE_GAP
) {
finishMessage();
lastBlinkEnd = 0;
}
}
}
13. Important improvement to the firmware
For a production-quality version, I recommend separating:
blink recognition
↓
Morse state machine
↓
message assembly
↓
network transmission
rather than sending network requests directly from timing-sensitive code.
This prevents Wi-Fi delays from affecting blink detection.
A better architecture is:
Sensor task
↓
Event queue
↓
Morse task
↓
Message queue
↓
Wi-Fi task
On an ESP32, these can eventually be implemented using FreeRTOS tasks.
14. ESP32 webpage
When the ESP32 connects to Wi-Fi, Serial Monitor displays something like:
WiFi connected
ESP32 IP: 192.168.1.105
Web server started
Open:
http://192.168.1.105
on a phone or computer connected to the same network.
The dashboard displays:
┌────────────────────────────────────────┐
│ AI Eye Blink Morse Communication │
├────────────────────────────────────────┤
│ Eye status │
│ │
│ OPEN │
├────────────────────────────────────────┤
│ Current Morse │
│ │
│ .- │
├────────────────────────────────────────┤
│ Decoded Message │
│ │
│ A │
├────────────────────────────────────────┤
│ Statistics │
│ │
│ Blinks: 12 │
│ Messages: 3 │
├────────────────────────────────────────┤
│ │
│ RESET MESSAGE │
│ │
└────────────────────────────────────────┘
15. n8n architecture
The cloud automation should look like this:
ESP32
│
│ POST JSON
▼
┌─────────────┐
│ Webhook │
└──────┬──────┘
│
▼
┌─────────────┐
│ Validate │
│ JSON │
└──────┬──────┘
│
▼
┌─────────────┐
│ Normalize │
│ data │
└──────┬──────┘
│
▼
┌─────────────┐
│ AI Agent │
└──────┬──────┘
│
┌────────┼─────────┐
│ │ │
▼ ▼ ▼
Emergency Normal Invalid
│ │ │
▼ ▼ ▼
Telegram Telegram Log
Voice Text
│
├──────────────┐
▼ ▼
Google Sheets ThingSpeak
16. Create the n8n webhook
In n8n:
- Create a new workflow.
- Add Webhook.
- Select:
- HTTP Method:
POST
- HTTP Method:
- Set path:
eye-morse
The URL becomes approximately:
https://YOUR-N8N-DOMAIN/webhook/eye-morse
Put this URL into the ESP32:
const char* N8N_WEBHOOK =
"https://YOUR-N8N-DOMAIN/webhook/eye-morse";
For security, do not expose an unauthenticated production webhook without validation.
Add a secret header/token such as:
X-Device-Token: YOUR_SECRET
and validate it in n8n.
17. JSON sent by ESP32
The ESP32 should send:
{
"device_id": "ESP32-EYE-001",
"morse": "... --- ...",
"message": "SOS",
"blink_count": 15,
"timestamp": 123456
}
A better production payload is:
{
"device_id": "ESP32-EYE-001",
"user_id": "USER001",
"morse": "... --- ...",
"message": "SOS",
"blink_count": 15,
"signal_quality": 0.96,
"battery": 87,
"firmware": "1.0.0",
"timestamp": "2026-10-08T10:17:00+05:30"
}
18. n8n workflow
Create these nodes:
Webhook
↓
Validate Device
↓
Normalize Data
↓
AI Agent
↓
IF Emergency?
├──────────────┐
│ YES │ NO
▼ ▼
Emergency Normal
Telegram Telegram
│ │
└──────┬───────┘
▼
Google Sheets
│
▼
ThingSpeak
│
▼
Webhook Response
n8n has a built-in Telegram integration capable of sending messages and audio files, among other Telegram operations. n8n Documentation
19. AI Agent design
The AI Agent should not perform the basic Morse decoding.
That should remain deterministic on the ESP32.
Instead:
ESP32
↓
Reliable Morse decoding
↓
Text
↓
AI Agent
↓
Meaning/context/emergency classification
This is much safer than asking an LLM to interpret raw blink timing.
20. AI Agent system prompt
Use a prompt similar to:
You are the communication assistant for an eye-blink Morse
communication device.
The incoming message was generated by a deterministic Morse
decoder running on an ESP32.
Your tasks are:
1. Interpret the decoded message.
2. Classify it as:
NORMAL
URGENT
EMERGENCY
UNKNOWN
3. Generate a concise human-readable explanation.
4. Never invent information that was not contained in the
incoming message.
5. If the message indicates immediate danger, classify it as
EMERGENCY.
6. Return valid JSON only.
Required JSON format:
{
"classification": "NORMAL",
"priority": 1,
"message": "Decoded communication",
"alert_text": "Human readable notification",
"voice_text": "Text suitable for voice notification"
}
Example input:
SOS
Possible output:
{
"classification": "EMERGENCY",
"priority": 10,
"message": "SOS",
"alert_text": "Emergency SOS signal received from the eye-blink communication device.",
"voice_text": "Emergency. SOS signal received from the eye-blink communication device."
}
21. Why use AI Agent instead of simple IF logic?
A normal rule system could detect:
SOS → Emergency
HELP → Emergency
But an AI Agent can interpret:
I NEED HELP
or:
I AM NOT FEELING WELL
or:
PLEASE CALL MY FAMILY
and categorize them.
However, the AI should be treated as an interpretation/orchestration layer, not the sole safety mechanism.
For critical applications, deterministic emergency rules should run before/alongside the AI.
For example:
IF message contains "SOS"
OR message contains "HELP"
OR message contains "EMERGENCY"
↓
Immediate emergency route
Then AI can add context.
22. Google Sheets database
Create a spreadsheet:
EyeBlinkCommunication
Create columns:
| Timestamp | Device | Morse | Message | Classification | Priority | Alert | Signal |
|---|---|---|---|---|---|---|---|
| 2026-10-08 10:17 | ESP32-EYE-001 | ...---... | SOS | EMERGENCY | 10 | Emergency | 0.96 |
| 2026-10-08 10:20 | ESP32-EYE-001 | .... . .-.. .--. | HELP | EMERGENCY | 10 | Help required | 0.94 |
Google Sheets supports appending values to the next row of a logical table. (Google for Developers)
In n8n, add:
Google Sheets
↓
Append Row
Map:
Timestamp
Device ID
Morse
Message
Classification
Priority
Alert text
Signal quality
23. ThingSpeak setup
Create a ThingSpeak channel.
Suggested fields:
Field 1 = Blink Count
Field 2 = Message Length
Field 3 = Signal Quality
Field 4 = Emergency Level
Field 5 = Message Code
ThingSpeak supports updating channel fields through HTTP GET/POST requests. MathWorks+1
Example:
https://api.thingspeak.com/update
with:
api_key=YOUR_WRITE_KEY
field1=15
field2=3
field3=0.96
field4=10
The ThingSpeak API returns an entry ID on a successful update; a failed update returns 0. MathWorks
24. ThingSpeak dashboard
You can create charts for:
Blink activity
Blink count
│
20 │ ╭╮
15 │ ╭───╯╰╮
10 │───╯ ╰──╮
5 │ ╰
0 └─────────────────
Time →
Emergency level
10 ┤ █
8 ┤ █
6 ┤ █
4 ┤
2 ┤
0 ┤ █ █ █ █
ThingSpeak is designed to store and visualize IoT channel data and supports REST and MQTT update methods. MathWorks
25. Telegram notification
The n8n workflow should send two notifications.
Text
Example:
🚨 EYE-BLINK EMERGENCY ALERT
Device: ESP32-EYE-001
Morse:
... --- ...
Message:
SOS
Priority:
CRITICAL
Please check the user immediately.
Telegram's Bot API supports sendMessage, and Telegram also provides sendVoice for voice messages. Telegram+1
26. Telegram voice alert
The voice pipeline is:
AI Agent
│
│ voice_text
▼
Text-to-Speech
│
▼
MP3/OGG audio
│
▼
Telegram
│
▼
Caregiver's phone
For example:
AI:
"Emergency. SOS signal received from
the eye-blink communication device."
TTS converts that to audio.
Then n8n sends the audio through Telegram.
The n8n Telegram integration supports sending audio files. n8n Documentation
27. Recommended n8n voice workflow
AI Agent
│
▼
voice_text
│
▼
┌─────────────────┐
│ Text-to-Speech │
└────────┬────────┘
│
▼
Audio file
│
▼
┌─────────────────┐
│ Telegram Node │
│ Send Audio/Voice│
└────────┬────────┘
│
▼
CAREGIVER
You can implement TTS using whichever supported provider you prefer; the important interface is:
text → audio binary → Telegram
28. Telegram emergency logic
I recommend this decision system:
MESSAGE
│
▼
┌─────────────┐
│ Normalize │
└──────┬──────┘
│
▼
Contains emergency keyword?
/ \
YES NO
│ │
▼ ▼
EMERGENCY AI Agent
│ │
▼ ▼
Immediate CLASSIFY
Telegram │
▼
NORMAL/URGENT
This gives you two layers:
Fast deterministic emergency detection
+
AI interpretation
29. Complete n8n workflow concept
The workflow can be represented as:
┌────────────────────────┐
│ 1. WEBHOOK │
│ POST /eye-morse │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ 2. AUTHENTICATION │
│ Validate device token │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ 3. DATA VALIDATION │
│ Check message fields │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ 4. CODE NODE │
│ Normalize text │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ 5. EMERGENCY RULE │
│ SOS / HELP / etc. │
└───────┬────────┬───────┘
│ │
YES NO
│ │
│ ▼
│ ┌─────────────┐
│ │ AI AGENT │
│ └──────┬──────┘
│ │
│ ▼
│ CLASSIFY
│
└──────────┬──────────
│
▼
┌────────────┐
│ Telegram │
│ Text │
└─────┬──────┘
│
▼
┌────────────┐
│ TTS │
└─────┬──────┘
│
▼
┌────────────┐
│ Telegram │
│ Voice │
└─────┬──────┘
│
┌────────┴────────┐
▼ ▼
Google Sheets ThingSpeak
│ │
└────────┬────────┘
▼
Webhook Response
30. n8n Code node
After the Webhook, a Code node can normalize the incoming data.
const input = $json;
const message =
String(input.message || "")
.trim()
.toUpperCase();
const morse =
String(input.morse || "")
.trim();
let classification = "NORMAL";
let priority = 1;
const emergencyWords = [
"SOS",
"EMERGENCY",
"HELP",
"DANGER",
"SAVE ME"
];
for (const word of emergencyWords) {
if (message.includes(word)) {
classification = "EMERGENCY";
priority = 10;
break;
}
}
return [
{
json: {
device_id:
input.device_id || "UNKNOWN",
morse,
message,
blink_count:
Number(input.blink_count || 0),
classification,
priority,
received_at:
new Date().toISOString()
}
}
];
31. AI Agent input
Pass something like:
Device:
{{$json.device_id}}
Decoded Morse:
{{$json.morse}}
Decoded message:
{{$json.message}}
Initial classification:
{{$json.classification}}
Priority:
{{$json.priority}}
The AI Agent returns structured information.
32. Example normal communication
User performs:
.... . .-.. .-.. ---
ESP32 decodes:
HELLO
n8n receives:
{
"message": "HELLO"
}
AI Agent:
{
"classification": "NORMAL",
"priority": 1,
"alert_text": "Normal communication received: HELLO."
}
Telegram:
ℹ️ Eye-blink communication
Device: ESP32-EYE-001
Message:
HELLO
Google Sheets:
HELLO | NORMAL | 1
ThingSpeak:
Normal communication event
33. Example emergency communication
User sends:
... --- ...
ESP32:
SOS
n8n:
SOS
AI:
EMERGENCY
Telegram:
🚨 EMERGENCY ALERT
SOS received from ESP32-EYE-001.
Immediate attention required.
Voice:
"Emergency. SOS signal received from the eye blink communication device."
Google Sheets:
Timestamp
Device
Morse
Message
EMERGENCY
10
ThingSpeak:
Emergency level = 10
34. End-to-end data flow
USER
│
│ Blink
▼
IR Sensor
│
│ Digital signal
▼
ESP32
│
│ Measure duration
▼
Blink classifier
│
├── 250 ms → .
└── 800 ms → -
│
▼
Morse buffer
│
│ ... --- ...
▼
Morse decoder
│
▼
"SOS"
│
├─────────────────────┐
│ │
▼ ▼
ESP32 webpage Wi-Fi POST
│
▼
n8n
│
▼
AI Agent
│
┌────────┼────────┐
│ │ │
▼ ▼ ▼
Telegram Sheets ThingSpeak
│
▼
TTS
│
▼
Telegram Voice
│
▼
CAREGIVER
35. State machine
A robust implementation should use the following state machine:
┌─────────────┐
│ IDLE │
└──────┬──────┘
│
eye closes
│
▼
┌─────────────┐
│ BLINKING │
└──────┬──────┘
│
eye opens
│
▼
┌─────────────┐
│ CLASSIFY │
└──────┬──────┘
│
┌─────┴─────┐
│ │
short long
│ │
▼ ▼
. -
│ │
└─────┬─────┘
▼
┌─────────────┐
│ MORSE BUFFER│
└──────┬──────┘
│
pause detected
│
▼
Decode letter
│
▼
MESSAGE BUFFER
│
long pause
│
▼
MESSAGE END
│
▼
Wi-Fi/n8n
36. Calibration mode
This is one of the most important improvements.
At startup:
ESP32:
CALIBRATION MODE
Please perform 5 short blinks.
The user performs:
blink
blink
blink
blink
blink
ESP32 calculates:
average = 270 ms
Then:
DOT_MAX = 400 ms
DASH_MIN = 400 ms
DASH_MAX = 1000 ms
The user then performs several long blinks.
ESP32 learns:
short blink average
long blink average
and calculates the optimal boundary.
37. Better classification algorithm
Instead of fixed thresholds:
duration < 450 → dot
duration > 450 → dash
use:
short_mean = 280 ms
long_mean = 820 ms
boundary =
(short_mean + long_mean) / 2
boundary = 550 ms
Then:
< 550 ms → DOT
> 550 ms → DASH
This makes the system much more user-specific.
38. False blink rejection
Normal involuntary blinking can create unwanted dots.
Therefore add a minimum intentional blink duration.
For example:
< 100 ms
↓
Ignore
Then:
100–550 ms
↓
DOT
550–1500 ms
↓
DASH
This should be calibrated experimentally rather than treated as a universal value.
39. Double-blink command
You can also reserve special commands.
For example:
Blink Blink quickly
could mean:
BACKSPACE
Triple blink:
Blink Blink Blink
could mean:
CLEAR
Long hold:
Very long eye closure
could mean:
SEND
This can make the system much more usable.
40. Example user interface
The webpage can eventually contain:
╔══════════════════════════════════════╗
║ AI EYE MORSE COMMUNICATOR ║
╠══════════════════════════════════════╣
║ ║
║ Eye status: 🟢 OPEN ║
║ ║
║ Current blink: 285 ms ║
║ ║
║ Morse: ... --- ... ║
║ ║
║ Message: SOS ║
║ ║
║ Signal quality: 96% ║
║ Wi-Fi: Connected ║
║ n8n: Online ║
║ ThingSpeak: Online ║
║ ║
║ [ SEND ] [ CLEAR ] ║
║ ║
╠══════════════════════════════════════╣
║ SYSTEM STATISTICS ║
║ ║
║ Blinks: 48 ║
║ Messages: 7 ║
║ Emergencies: 2 ║
╚══════════════════════════════════════╝
41. Security architecture
Do not expose the ESP32 directly to the public Internet.
Use:
ESP32
│
│ outbound HTTPS
▼
n8n
rather than:
Internet
│
▼
ESP32
Recommended:
ESP32
↓
HTTPS
↓
n8n Webhook
↓
Secret validation
↓
AI
The ESP32 should contain only the credentials it actually needs.
Never publish:
Wi-Fi password
n8n secret
ThingSpeak write API key
Telegram bot token
AI API key
in GitHub.
42. Telegram security
Your Telegram bot token must remain secret.
Use n8n credentials rather than hard-coding:
BOT_TOKEN
inside ESP32 firmware.
The ESP32 doesn't need Telegram access.
Instead:
ESP32 → n8n → Telegram
This is considerably cleaner.
43. Why n8n should handle Telegram
Bad architecture:
ESP32
├── Telegram
├── Google
├── ThingSpeak
├── AI
└── TTS
This creates a very complicated embedded device.
Better architecture:
ESP32
│
│
n8n API
│
┌────────┼────────┐
│ │ │
AI Telegram Sheets
│
TTS
│
ThingSpeak
ESP32 only needs to handle:
Sensor
Morse
Wi-Fi
Web UI
while n8n handles cloud orchestration.
44. Agentic IoT architecture
This can be described as an Agentic IoT system because the device is not merely transmitting telemetry.
The chain is:
SENSE
↓
UNDERSTAND
↓
DECIDE
↓
ACT
↓
RECORD
↓
NOTIFY
Specifically:
SENSE
Eye blink
↓
UNDERSTAND
Morse decoder + AI Agent
↓
DECIDE
Normal / Urgent / Emergency
↓
ACT
Telegram alert
↓
RECORD
Google Sheets
↓
VISUALIZE
ThingSpeak
↓
RESPOND
Voice notification
45. Suggested final project features
For a strong academic/engineering project, implement these stages.
Level 1 — Basic
Eye sensor
↓
ESP32
↓
Morse decoder
↓
Serial Monitor
Level 2 — IoT
ESP32
↓
Wi-Fi
↓
Webpage
Level 3 — Cloud
ESP32
↓
n8n
↓
Google Sheets
Level 4 — IoT analytics
ESP32
↓
ThingSpeak
↓
Charts
Level 5 — AI
n8n
↓
AI Agent
↓
Classification
Level 6 — Emergency automation
AI Agent
↓
Telegram
↓
Voice alert
Level 7 — Advanced
Calibration
+
signal quality
+
FreeRTOS
+
offline queue
+
battery monitoring
+
authentication
+
OTA updates
46. Recommended final workflow
The finished project should operate like this:
┌───────────────┐
│ USER │
│ 👁 │
└───────┬───────┘
│
Blink eyes
│
▼
┌───────────────┐
│ IR SENSOR │
└───────┬───────┘
│
▼
┌───────────────┐
│ ESP32 │
│ │
│ Timing │
│ Morse │
│ Wi-Fi │
│ Web server │
└───┬─────┬─────┘
│ │
┌───────┘ └────────┐
▼ ▼
Webpage ThingSpeak
│ │
│ │
└─────────┐ ┌───────┘
▼ ▼
┌─────────┐
│ n8n │
└────┬────┘
│
▼
┌───────────┐
│ AI AGENT │
└─────┬─────┘
│
┌────────┼─────────┐
▼ ▼ ▼
Telegram Sheets Dashboard
│
▼
TTS
│
▼
🔊 VOICE ALERT
│
▼
CAREGIVER
47. Example complete demonstration
User wants to communicate:
"I NEED HELP"
The user enters Morse through eye blinks:
.. -. . . -.. .... . .-.. .--.
ESP32 converts it to:
I NEED HELP
ESP32 sends:
{
"device_id": "ESP32-EYE-001",
"morse": ".. -. . . -.. .... . .-.. .--.",
"message": "I NEED HELP"
}
n8n receives it.
The emergency rules see:
HELP
AI Agent returns:
{
"classification": "EMERGENCY",
"priority": 10,
"message": "I NEED HELP",
"alert_text": "The user has requested immediate help.",
"voice_text": "Emergency. The user has requested immediate help."
}
Then:
Google Sheets
↓
Event stored
ThingSpeak
↓
Emergency = 10
Telegram
↓
Text alert
TTS
↓
Audio
Telegram
↓
Voice alert
The caregiver receives:
🚨 EMERGENCY
The user has requested immediate help.
Device: ESP32-EYE-001
Priority: CRITICAL
and a voice message:
"Emergency. The user has requested immediate help."
48. Testing procedure
Test 1 — Sensor
Open Serial Monitor.
Expected:
Eye CLOSED
Eye OPEN
Blink duration: 280
DOT
Test 2 — Dash
Perform a deliberately longer blink.
Expected:
Blink duration: 850
DASH
Test 3 — Letter
Perform:
.-
Expected:
A
Test 4 — Word
Perform:
... --- ...
Expected:
SOS
Test 5 — Webpage
Open:
http://ESP32-IP
Expected:
Eye: OPEN
Morse: ...
Message: SOS
Test 6 — n8n
Send test JSON:
{
"device_id": "TEST",
"morse": "... --- ...",
"message": "SOS",
"blink_count": 9
}
Expected:
Webhook
↓
AI
↓
Telegram
↓
Google Sheets
↓
ThingSpeak
49. Test matrix
| Test | Input | Expected |
|---|---|---|
| Eye open | No blink | No event |
| Short blink | 250 ms | . |
| Long blink | 800 ms | - |
.- |
A | A |
... |
S | S |
--- |
O | O |
... --- ... |
SOS | Emergency |
| HELP | Morse | Emergency |
| HELLO | Morse | Normal |
| Invalid Morse | .-.--.- |
?/reject |
| Wi-Fi lost | Blink | Local buffering |
| Wi-Fi restored | Buffered data | Upload |
| n8n offline | Message | Queue/retry |
| Telegram failure | Alert | Retry/log |
50. Offline operation
A good version should continue working without Internet.
Internet available?
YES
↓
Send immediately
NO
↓
Store locally
↓
Continue detecting blinks
↓
Wi-Fi restored
↓
Upload queued messages
For example:
struct MessageRecord {
String morse;
String message;
unsigned long timestamp;
};
A production version could use ESP32 Preferences or LittleFS for persistent storage.
51. Reliability improvements
For a serious prototype, add:
- blink calibration
- sensor filtering
- debounce
- intentional-blink detection
- local message queue
- HTTPS
- authentication
- watchdog timer
- OTA firmware updates
- battery monitoring
- signal-quality calculation
- retry mechanism
- duplicate-message protection
- local OLED display
- emergency physical cancel button
52. Important limitation
An eye-blink communication prototype should not be described as a certified medical or emergency-response device unless it has gone through the appropriate medical-device engineering, validation, cybersecurity, reliability and regulatory processes.
For an academic/demo project, describe it as:
An AI-assisted IoT prototype for eye-blink-based Morse communication and remote notification.
That is both technically accurate and safer.
53. Recommended project folder structure
AI-Eye-Morse-IoT/
│
├── ESP32/
│ ├── eye_morse.ino
│ ├── config.h
│ └── README.md
│
├── n8n/
│ ├── workflow.json
│ ├── ai_prompt.txt
│ └── README.md
│
├── WebDashboard/
│ ├── index.html
│ ├── style.css
│ └── app.js
│
├── Documentation/
│ ├── architecture.md
│ ├── schematic.md
│ ├── testing.md
│ └── user_manual.md
│
└── README.md
54. Project abstract for documentation
Abstract
AI-Powered Eye Blink Morse Communication and Agentic IoT Alert System is an assistive communication prototype designed to enable users to transmit messages using intentional eye blinks. An IR-based eye-blink sensor detects the duration of eye closures and an ESP32 microcontroller converts short and long blinks into Morse-code symbols. The ESP32 decodes the Morse sequence into alphanumeric text and provides a local web dashboard for real-time monitoring.
Using Wi-Fi, decoded messages are transmitted to an n8n automation workflow. The workflow acts as an intelligent IoT orchestration layer, validating incoming device data, passing communication to an AI Agent for semantic interpretation and priority classification, recording events in Google Sheets, updating ThingSpeak IoT visualizations, and generating Telegram notifications. Emergency messages can trigger both text and text-to-speech voice alerts through Telegram.
The architecture combines embedded sensing, edge processing, IoT communication, cloud automation, AI-assisted decision making, data logging and real-time notification into a single system. The project demonstrates how an ESP32 can serve as an intelligent edge device while n8n provides the automation and AI orchestration layer.
55. Advantages
Hardware advantages
- Low-cost
- ESP32 is widely available
- Wi-Fi built in
- Small form factor
- Low power
- Easy to prototype
Software advantages
- Open architecture
- Easy to modify
- n8n provides visual workflows
- Google Sheets provides simple logging
- ThingSpeak provides IoT visualization
- Telegram provides instant notification
- AI provides semantic interpretation
Communication advantages
The user doesn't need:
keyboard
mouse
touchscreen
microphone
speech
Only intentional eye movements are required.
56. Future enhancements
The project can eventually become much more advanced.
Camera-based eye tracking
Instead of an IR sensor:
ESP32-CAM / camera
↓
Eye landmark detection
↓
Blink detection
↓
Morse
This could distinguish:
left wink
right wink
both eyes blink
double blink
long blink
Bidirectional communication
The caregiver could send a message back:
Caregiver
↓
Telegram
↓
n8n
↓
ESP32
↓
OLED
↓
User
For example:
CARE:
"Are you okay?"
ESP32 displays:
ARE YOU OK?
57. Advanced agentic architecture
The ultimate architecture could be:
┌───────────────────┐
│ EYE-BLINK USER │
└─────────┬─────────┘
│
▼
┌────────────┐
│ ESP32 │
│ Edge AI/IoT│
└─────┬──────┘
│
MQTT/HTTPS
│
▼
┌────────────┐
│ n8n │
│ Orchestrator│
└─────┬──────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
AI Agent Database IoT Cloud
│
┌────────┼────────┐
│ │ │
▼ ▼ ▼
Telegram TTS Google Sheets
│
▼
Caregiver
This is the strongest way to present the project as Agentic IoT + AI + automation, rather than simply an ESP32 sensor project.
58. Official documentation
For implementation, the most useful references are:
- ESP32 Arduino Wi-Fi documentation — Wi-Fi station/AP functionality and ESP32 web-server examples.
- n8n documentation — workflows, integrations and AI functionality.
- n8n Telegram node documentation — Telegram message/audio integration.
- Telegram Bot API — Telegram bot messaging API.
- ThingSpeak Write Data API — HTTP GET/POST channel updates.
- ThingSpeak IoT documentation — REST/MQTT IoT data architecture.
- Google Sheets API append documentation — appending communication records to a spreadsheet.
Final recommended architecture
For your project, I would use this exact division of responsibility:
┌─────────────────────────────────────────────────────┐
│ EDGE DEVICE │
│ │
│ IR Eye Sensor → ESP32 → Morse Decoder │
│ │ │
│ ├── Local Web Dashboard │
│ │ │
│ └── HTTPS │
└─────────────────────────┬───────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ n8n │
│ │
│ Webhook → Validation → Emergency Rules → AI Agent │
│ │ │
│ ┌────────────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ Google Sheets ThingSpeak Telegram│
│ │ │
│ ▼ │
│ TTS Voice│
└─────────────────────────────────────────────────────┘
This gives you a complete ESP32 + AI + n8n + Agentic IoT + Telegram + Voice + Google Sheets + ThingSpeak project rather than only a blink sensor.
The next practical step is to build it in this order: (1) sensor → (2) blink timing → (3) Morse decoder → (4) ESP32 webpage → (5) n8n webhook → (6) Google Sheets → (7) ThingSpeak → (8) Telegram text → (9) TTS/Telegram voice → (10) AI Agent → (11) calibration and reliability improvements.
Content diagnostic: content_references
{"count":1,"examples":[{"type":"grouped_webpages","sourceIndex":3,"shape":{"name":"missing","data":"present","dil":"missing"}}]}

No comments:
Post a Comment