AI Smart Mountain Road Safety System with Fog Detection
Complete IoT + ESP32 + AI Agent + n8n Automation + Telegram Voice Alerts + Google Sheets + ThingSpeak Project Documentation
1. Project overview
The AI Smart Mountain Road Safety System is an IoT-based prototype designed to detect dangerous fog and poor visibility on mountain roads, monitor environmental conditions, identify potential road hazards, and automatically send warnings to drivers or road monitoring personnel.
The system uses an ESP32 microcontroller to collect sensor readings and send them over Wi-Fi to an automation and cloud platform. An n8n workflow processes the readings, applies decision rules and AI-assisted analysis, logs the results to Google Sheets, updates ThingSpeak charts, and triggers Telegram text or voice notifications.
The project combines embedded electronics, IoT communications, cloud data storage, AI-assisted decision-making, and automated alerts into one system.
ESP32 — sensor and control unit
Reads fog-related optical measurements, temperature, humidity, and optional road hazard sensors. Publishes readings to the cloud.
Fog detection and environmental sensing
Measures optical transmission or scattering, together with temperature and humidity, to estimate hazardous low-visibility conditions.
n8n — automation and AI agent
Receives telemetry, classifies risk, runs an AI analysis when useful, and coordinates logging and notifications.
Telegram — warning delivery
Sends road safety alerts, with optional generated audio announcements for a monitoring group or control room.
Google Sheets and ThingSpeak — monitoring
Google Sheets stores event history and AI predictions; ThingSpeak provides time-series sensor charts and remote monitoring.
Important safety note: This is a prototype design. A low-cost optical sensor can detect changes associated with fog, but it does not automatically provide a calibrated road-visibility distance. Real mountain-road deployment requires suitable visibility instrumentation, weatherproof enclosures, field calibration, reliable communications, and independently validated warning logic. The prototype must not be the sole safety mechanism for a public road.
2. System objectives
-
Detect increasing fog and potentially hazardous visibility conditions.
-
Measure temperature and relative humidity.
-
Monitor optional vehicle approach or obstacle distance.
-
Classify the road condition as Normal, Caution, Danger, or Sensor Fault.
-
Send automated Telegram notifications to road operators.
-
Generate spoken warning messages for remote monitoring.
-
Record sensor data, alerts, and energy predictions in Google Sheets.
-
Display historical sensor readings on ThingSpeak.
-
Use AI to summarize conditions and forecast energy consumption.
-
Provide a web dashboard for live monitoring and system status.
3. System architecture
Mountain road sensor station
Optical visibility sensor · DHT22 · Optional distance sensor · Power monitor
GPIO / I²C / ADC
ESP32 firmware
Read · Validate · Filter · Classify · Publish HTTPS/HTTP telemetry
Wi-Fi or local gateway
n8n automation server
Webhook → validation → rule engine → AI analysis → alert routing
Telegram
Text + voice alerts
Google Sheets
Events + predictions
ThingSpeak
Sensor history
Web dashboard
Current status + trends
End-to-end data flow
-
Sensors collect environmental and optical data.
-
ESP32 validates the readings and calculates a filtered fog indicator.
-
The firmware assigns an initial safety level.
-
Telemetry is transmitted to an n8n webhook.
-
n8n checks the payload and evaluates thresholds and recent readings.
-
If enabled, an AI agent creates a structured explanation or energy estimate.
-
n8n records the event in Google Sheets and sends sensor values to ThingSpeak.
-
If a warning condition is confirmed, Telegram sends a text and optionally an audio file.
-
The web dashboard displays current status and historical trends.
4. Components list
The following is a practical prototype bill of materials. The optical visibility sensor is the most important design choice; a generic rain sensor or ordinary IR obstacle sensor is not a calibrated fog sensor.
Indicative prototype quantities and costs in India; prices vary by supplier and sensor specification.
|
Component |
Qty. |
Purpose |
Indicative cost |
|---|---|---|---|
|
ESP32 DevKit V1 |
1 |
Main controller and Wi-Fi |
₹400–800 |
|
DHT22 / AM2302 |
1 |
Temperature and humidity |
₹250–500 |
|
Optical fog/visibility sensor |
1 |
Fog or optical transmission measurement |
₹1,500–15,000+ |
|
Optical emitter and receiver, if building a sensor |
1 set |
Optical attenuation measurement |
₹300–1,500 |
|
HC-SR04 ultrasonic sensor |
1 optional |
Nearby obstacle demonstration |
₹100–250 |
|
INA219 current/voltage sensor |
1 optional |
Power consumption measurement |
₹150–350 |
|
OLED I²C display |
1 optional |
Local status and readings |
₹150–350 |
|
LEDs, resistors, buzzer |
1 set |
Local warning indication |
₹100–250 |
|
Breadboard and jumper wires |
1 set |
Prototype wiring |
₹150–300 |
|
Regulated 5 V supply |
1 |
Power source |
₹200–500 |
|
Weather-resistant enclosure |
1 |
Outdoor protection |
₹500–2,000 |
A prototype using a basic optical detector may cost approximately ₹3,000–8,000. A properly calibrated visibility sensor, outdoor enclosure, and dependable power and communication hardware can increase the total considerably.
Recommended sensor strategy
-
Optical visibility: Use a calibrated visibility sensor, preferably one that reports meteorological optical range (MOR) in metres. If using a homemade optical sensor, treat its reading as a relative fog index until calibrated.
-
Temperature/humidity: DHT22 for a basic prototype; a better outdoor-rated sensor can be used for deployment.
-
Obstacle distance: HC-SR04 is for controlled demonstrations, not a reliable outdoor vehicle detection system in heavy rain or fog.
-
Power monitoring: INA219 measures voltage and current on a suitable DC power rail. It does not directly measure every power source or AC mains consumption.
-
Optional location: GPS can associate alerts with a road segment or station.
5. Circuit schematic diagram
This is a functional wiring schematic for an ESP32 DevKit, DHT22, analog optical sensor module, INA219, LEDs and buzzer.
5.1 Pin connection table
|
Module |
Module pin |
ESP32 connection |
|---|---|---|
|
DHT22 |
VCC |
3.3 V |
|
DHT22 |
GND |
GND |
|
DHT22 |
DATA |
GPIO 4 |
|
Optical sensor with analog output |
VCC |
According to module specification |
|
Optical sensor |
GND |
GND |
|
Optical sensor |
Analog OUT |
GPIO 34 through a safe voltage interface |
|
INA219 |
VCC |
3.3 V |
|
INA219 |
GND |
GND |
|
INA219 |
SDA |
GPIO 21 |
|
INA219 |
SCL |
GPIO 22 |
|
Green LED |
Anode through 220–330 Ω |
GPIO 25 |
|
Red LED |
Anode through 220–330 Ω |
GPIO 26 |
|
Buzzer driver |
Input |
GPIO 27 |
|
Optional HC-SR04 |
TRIG |
GPIO 18 |
|
Optional HC-SR04 |
ECHO |
GPIO 19 through a voltage divider |
Electrical precautions
-
ESP32 GPIOs are not 5 V tolerant. Verify the optical sensor's output voltage and use a divider or suitable level shifter when necessary.
-
Some analog sensor modules have an adjustable digital output as well as an analog output. Use the analog output only if it provides a safe voltage range for the ESP32 ADC.
-
The HC-SR04 Echo pin is typically 5 V; use a resistor divider before connecting it to the ESP32.
-
Use a transistor or MOSFET driver for buzzers that draw more current than a GPIO can supply.
-
Keep outdoor sensor wiring protected from moisture, lightning-induced surges, and electromagnetic interference.
5.2 Optical visibility sensor concept
A simple transmissive sensor places an emitter and receiver at a known distance. As fog droplets scatter and absorb light, the received optical intensity changes.
For an ideal transmissometer, the extinction coefficient can be estimated as
where:
-
TT is the received-to-reference optical transmission ratio.
-
LL is the optical path length in metres.
-
ββ is the extinction coefficient in m−1m−1.
A commonly used meteorological visibility relationship is
where VV is visibility in metres, under the assumptions associated with the selected contrast threshold.
This calculation requires a suitable optical system, reference measurement, alignment, contamination control, and calibration. A single uncalibrated analog reading cannot be converted directly into trustworthy visibility distance.
6. Software and account setup
Install or create the following:
-
Arduino IDE with ESP32 board support.
-
Arduino libraries:
DHT sensor library,Adafruit Unified Sensor, andArduinoJson. -
An n8n instance, hosted or self-hosted, with HTTPS for production.
-
A Telegram bot created through BotFather.
-
A Google Sheet with a configured Google Sheets credential in n8n.
-
A ThingSpeak account with a channel and write API key.
-
An AI provider credential, if using an AI agent or language model.
-
Optional text-to-speech (TTS) service for generating spoken audio files.
For a first prototype, use one sensor station and one Telegram destination. Add multiple stations after the entire data pipeline works reliably.
7. ESP32 firmware
This example sends sensor telemetry to n8n. It uses an analog optical sensor as a relative indicator, not a calibrated visibility instrument. The threshold values below are starting points for testing and must be tuned to the sensor.
7.1 Configure the firmware
Replace these example settings with your own:
-
Wi-Fi SSID and password.
-
HTTPS webhook URL from your n8n instance.
-
Optical sensor calibration constants.
-
Your selected fog and humidity thresholds.
For deployment, do not publish actual credentials or leave the webhook unprotected.
7.2 ESP32 source code
Install the libraries listed above through Arduino Library Manager.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <math.h>
// ---------- User configuration ----------
const char* WIFI_SSID = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* N8N_WEBHOOK =
"https://YOUR_N8N_HOST/webhook/mountain-road";
// Pins
constexpr int DHT_PIN = 4;
constexpr int OPTICAL_PIN = 34;
constexpr int GREEN_LED = 25;
constexpr int RED_LED = 26;
constexpr int BUZZER_PIN = 27;
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
// Tune after sensor calibration.
// These values are illustrative ADC thresholds, not visibility in metres.
constexpr int FOG_CAUTION_ADC = 1700;
constexpr int FOG_DANGER_ADC = 2300;
constexpr float HIGH_HUMIDITY = 90.0;
constexpr unsigned long SAMPLE_INTERVAL_MS = 10000;
unsigned long lastSample = 0;
uint32_t sequenceNumber = 0;
float filteredOptical = NAN;
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED &&
millis() - start < 20000) {
delay(500);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.print("Wi-Fi connected: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("Wi-Fi unavailable; will retry.");
}
}
int readOpticalADC() {
// Average samples to reduce random noise.
uint32_t total = 0;
const int samples = 16;
for (int i = 0; i < samples; i++) {
total += analogRead(OPTICAL_PIN);
delay(5);
}
return total / samples;
}
String classifyRisk(int optical, float humidity,
bool sensorValid) {
if (!sensorValid) return "SENSOR_FAULT";
// Confirm the direction of the ADC response for your sensor.
// This example assumes higher ADC = greater fog indication.
if (optical >= FOG_DANGER_ADC &&
humidity >= HIGH_HUMIDITY) {
return "DANGER";
}
if (optical >= FOG_CAUTION_ADC) {
return "CAUTION";
}
return "NORMAL";
}
void sendTelemetry(int optical, float temperature,
float humidity, bool sensorValid,
const String& risk) {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Skipping upload: Wi-Fi offline");
return;
}
WiFiClientSecure client;
// For quick testing only. Production must validate the
// server certificate or use a trusted CA certificate.
client.setInsecure();
HTTPClient http;
http.setTimeout(8000);
if (!http.begin(client, N8N_WEBHOOK)) {
Serial.println("Failed to initialize HTTP");
return;
}
http.addHeader("Content-Type", "application/json");
JsonDocument doc;
doc["station_id"] = "MOUNTAIN_STATION_01";
doc["sequence"] = ++sequenceNumber;
doc["uptime_ms"] = millis();
doc["optical_adc"] = optical;
doc["temperature_c"] = temperature;
doc["humidity_pct"] = humidity;
doc["sensor_valid"] = sensorValid;
doc["risk"] = risk;
// Relative indicator, not calibrated visibility.
doc["visibility_m"] = nullptr;
String payload;
serializeJson(doc, payload);
int code = http.POST(payload);
Serial.print("HTTP status: ");
Serial.println(code);
if (code > 0) {
Serial.println(http.getString());
} else {
Serial.println(http.errorToString(code));
}
http.end();
}
void setup() {
Serial.begin(115200);
pinMode(GREEN_LED, OUTPUT);
pinMode(RED_LED, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(GREEN_LED, LOW);
digitalWrite(RED_LED, LOW);
digitalWrite(BUZZER_PIN, LOW);
analogReadResolution(12);
dht.begin();
connectWiFi();
}
void loop() {
if (millis() - lastSample < SAMPLE_INTERVAL_MS) {
delay(20);
return;
}
lastSample = millis();
int optical = readOpticalADC();
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
bool sensorValid =
!isnan(temperature) &&
!isnan(humidity) &&
temperature >= -40 && temperature <= 80 &&
humidity >= 0 && humidity <= 100;
// Low-pass filter optical readings.
if (isnan(filteredOptical)) {
filteredOptical = optical;
} else {
filteredOptical =
0.75f * filteredOptical + 0.25f * optical;
}
String risk = classifyRisk(
(int)filteredOptical, humidity, sensorValid);
bool danger = risk == "DANGER";
bool normal = risk == "NORMAL";
digitalWrite(RED_LED, danger ? HIGH : LOW);
digitalWrite(GREEN_LED, normal ? HIGH : LOW);
digitalWrite(BUZZER_PIN, danger ? HIGH : LOW);
Serial.printf(
"Optical=%d Temp=%.1f C RH=%.1f%% Risk=%s\n",
optical, temperature, humidity, risk.c_str()
);
sendTelemetry(
optical, temperature, humidity, sensorValid, risk
);
}
7.3 Firmware implementation notes
-
Some optical modules produce a lower ADC value in fog. Reverse the comparison logic if that is what your hardware measures.
-
The example activates the buzzer while the reading is in the danger category. Add a latching state and hysteresis to avoid repeated on/off transitions near a threshold.
-
The code sends a sample every ten seconds. For real use, consider a different interval, offline buffering, retry limits, and a persistent sequence number.
-
setInsecure()disables certificate verification. Replace it with CA-based TLS verification before exposing the system to real networks. -
The webhook should require authentication, such as a secret header, and reject malformed or excessively large requests.
8. n8n workflow design
The n8n workflow is the central automation layer. It should validate readings and make deterministic safety decisions before using an AI model.
1. Webhook
Receive JSON from ESP32
2. Validate and normalize
Check fields, ranges and station identity
3. Rule-based risk classification
Normal / Caution / Danger / Sensor Fault
4A. Warning branch
Telegram text and optional voice
4B. Logging branch
Google Sheets + ThingSpeak
5. AI-assisted analysis
Explain the readings and estimate future power use
8.1 Required n8n nodes
|
Node |
Configuration |
|---|---|
|
Webhook |
POST path |
|
Code |
Validate input and calculate risk |
|
IF / Switch |
Branch by risk level |
|
Google Sheets |
Append event or telemetry row |
|
HTTP Request |
Send measurements to ThingSpeak |
|
Telegram |
Send warning text |
|
TTS HTTP Request |
Generate an audio file, if supported by the chosen service |
|
Telegram |
Send generated audio or voice message |
|
AI Agent / LLM |
Optional analysis and power prediction |
|
Error workflow |
Report failures and service outages |
Use a production webhook URL, not the temporary test URL, after activating the workflow.
8.2 Example n8n Code node
This code validates the incoming payload and derives a risk category. It assumes the incoming JSON has the fields sent by the ESP32 firmware.
const input = $json.body ?? $json;
const optical = Number(input.optical_adc);
const temperature = Number(input.temperature_c);
const humidity = Number(input.humidity_pct);
const valid =
input.station_id === "MOUNTAIN_STATION_01" &&
input.sensor_valid === true &&
Number.isFinite(optical) &&
Number.isFinite(temperature) &&
Number.isFinite(humidity) &&
optical >= 0 && optical <= 4095 &&
temperature >= -40 && temperature <= 80 &&
humidity >= 0 && humidity <= 100;
let risk = "SENSOR_FAULT";
let message = "Sensor data is invalid or incomplete.";
if (valid) {
if (optical >= 2300 && humidity >= 90) {
risk = "DANGER";
message =
"Potentially hazardous fog indication. Verify road visibility and activate the approved warning procedure.";
} else if (optical >= 1700) {
risk = "CAUTION";
message =
"Fog indicator elevated. Increase monitoring and verify visibility.";
} else {
risk = "NORMAL";
message = "No configured fog threshold exceeded.";
}
}
return [{
json: {
station_id: input.station_id ?? "UNKNOWN",
sequence: input.sequence ?? null,
received_at: new Date().toISOString(),
optical_adc: optical,
temperature_c: temperature,
humidity_pct: humidity,
risk,
message,
valid,
alert_required:
risk === "DANGER" || risk === "SENSOR_FAULT"
}
}];
This is the minimum classification logic. For a stronger implementation, add rolling averages, sensor-health diagnostics, alert deduplication, a rate limit, and a rule to keep an alert active until the hazard has been cleared for a sustained period.
8.3 Importable n8n workflow JSON
The following starter workflow implements the core telemetry pipeline: receive the ESP32 POST request, classify the readings, and return a JSON response. It is a foundation to import and test before adding credential-dependent integrations.
{
"name": "Mountain Road Safety - ESP32 Ingestion",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "mountain-road",
"responseMode": "responseNode",
"options": {}
},
"id": "10000000-0000-4000-8000-000000000001",
"name": "ESP32 Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [200, 300],
"webhookId": "mountain-road-ingest"
},
{
"parameters": {
"jsCode": "const input = $json.body ?? $json;\nconst optical = Number(input.optical_adc);\nconst temperature = Number(input.temperature_c);\nconst humidity = Number(input.humidity_pct);\nconst valid = input.station_id === 'MOUNTAIN_STATION_01' && input.sensor_valid === true && Number.isFinite(optical) && Number.isFinite(temperature) && Number.isFinite(humidity) && optical >= 0 && optical <= 4095 && temperature >= -40 && temperature <= 80 && humidity >= 0 && humidity <= 100;\nlet risk = 'SENSOR_FAULT';\nlet message = 'Invalid or incomplete sensor data.';\nif (valid) {\n if (optical >= 2300 && humidity >= 90) {\n risk = 'DANGER';\n message = 'Potentially hazardous fog indication. Verify visibility and activate the approved warning procedure.';\n } else if (optical >= 1700) {\n risk = 'CAUTION';\n message = 'Elevated fog indicator. Verify visibility and increase monitoring.';\n } else {\n risk = 'NORMAL';\n message = 'No configured fog threshold exceeded.';\n }\n}\nreturn [{json:{station_id:input.station_id ?? 'UNKNOWN',sequence:input.sequence ?? null,received_at:new Date().toISOString(),optical_adc:optical,temperature_c:temperature,humidity_pct:humidity,risk,message,valid,alert_required:risk === 'DANGER' || risk === 'SENSOR_FAULT'}}];"
},
"id": "10000000-0000-4000-8000-000000000002",
"name": "Validate and Classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [440, 300]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 200
}
},
"id": "10000000-0000-4000-8000-000000000003",
"name": "Return Classification",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [700, 300]
}
],
"connections": {
"ESP32 Webhook": {
"main": [
[
{
"node": "Validate and Classify",
"type": "main",
"index": 0
}
]
]
},
"Validate and Classify": {
"main": [
[
{
"node": "Return Classification",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
How to import it
-
Copy the complete JSON into a file named
mountain-road-workflow.json. -
Open n8n and create a workflow.
-
Choose the workflow import option and select the JSON file.
-
Save the workflow and test the Webhook node using its test URL.
-
Activate the workflow and use the production webhook URL in the ESP32 firmware.
This JSON is an ingestion starter, not the complete connected deployment. Telegram, Google Sheets, ThingSpeak and TTS require their respective credentials and nodes. Add those after confirming that the starter correctly classifies incoming sensor data.
8.4 Add the integrations in n8n
After the Validate and Classify node, connect the following branches.
-
Google Sheets: Append one row for each accepted telemetry event.
-
ThingSpeak: Send the sensor fields through an HTTP Request node.
-
Telegram: Use an IF or Switch node to trigger notifications only for the configured alert categories.
-
TTS: Pass a short, controlled warning message to a text-to-speech service, then deliver the returned audio file.
-
AI analysis: Send a summarized set of recent readings to the AI model. Do not allow model output to override the deterministic danger classification.
For production, configure an error workflow and retain a separate log of failed cloud operations.
9. Telegram bot setup
Step 1 — Create the bot
-
Open Telegram and search for the verified
@BotFatheraccount. -
Send
/newbot. -
Enter a display name, such as
Mountain Road Safety. -
Choose a unique username ending in
bot. -
Save the token securely. Anyone with the token can control the bot.
Step 2 — Find the chat ID
-
Open your new bot and press Start.
-
Send a test message.
-
Retrieve the chat ID using the Telegram Bot API
getUpdatesmethod, or use a Telegram Trigger node in n8n. -
For a group, add the bot and send a message after adding it. Ensure it has the permissions required by the group.
Do not expose your bot token in public source code, screenshots, or a public Git repository.
Step 3 — Configure Telegram in n8n
Create Telegram credentials in n8n using the bot token. Add a Telegram node configured to send a text message.
Example message:
Mountain Road Safety Bot
🚨 MOUNTAIN ROAD FOG WARNING Station: MOUNTAIN_STATION_01 Condition: DANGER Optical indicator: 2450 ADC Humidity: 94% Temperature: 16.8 °C Action: Verify actual visibility and follow the approved road-warning procedure. Time: 20:42 IST
Illustrative notification. The measurements are examples, not live sensor readings.Use expressions to populate the message from the validated JSON output. Avoid sending a notification every time a danger reading arrives; create a deduplication rule based on station, risk state and time.
10. Google Sheets integration
Google Sheets is useful for historical data, audit records, energy predictions and maintenance reporting.
10.1 Create the spreadsheet
Create a workbook named Mountain Road Safety Monitoring.
Add these tabs:
Tab 1: Telemetry
|
Column |
Field |
|---|---|
|
A |
Timestamp |
|
B |
Station ID |
|
C |
Sequence |
|
D |
Optical ADC |
|
E |
Visibility metres |
|
F |
Temperature °C |
|
G |
Humidity % |
|
H |
Risk |
|
I |
Sensor valid |
|
J |
Power watts |
|
K |
Energy Wh |
Tab 2: Alerts
-
Timestamp
-
Station ID
-
Risk level
-
Alert text
-
Telegram delivery status
-
Voice delivery status
-
Acknowledged by
-
Resolution timestamp
Tab 3: EnergyForecast
-
Timestamp
-
Station ID
-
Measured power (W)
-
Measured energy (Wh)
-
Predicted energy (Wh)
-
Forecast horizon
-
Model version
-
Prediction error
10.2 Connect n8n to Google Sheets
-
Configure a Google Sheets OAuth credential in n8n.
-
Authorize access to the intended spreadsheet.
-
Add a Google Sheets node and select the document and sheet tab.
-
Choose Append Row.
-
Map each validated JSON field to its corresponding column.
-
Test with several samples, including invalid sensor data and a danger event.
Do not store passwords, API keys, Telegram tokens or other secrets in the sheet. Restrict spreadsheet access and define a retention policy for operational data.
11. ThingSpeak cloud dashboard
ThingSpeak provides time-series charts for IoT telemetry and is suitable for viewing trends across fog indicators, temperature, humidity and power consumption.
11.1 Create a channel
Create a channel named Mountain Road Safety Station 01 with these fields:
|
ThingSpeak field |
Data |
|---|---|
|
Field 1 |
Optical ADC |
|
Field 2 |
Temperature °C |
|
Field 3 |
Relative humidity % |
|
Field 4 |
Risk code |
|
Field 5 |
Measured power W |
|
Field 6 |
Cumulative energy Wh |
|
Field 7 |
Visibility metres, only if calibrated |
|
Field 8 |
Sensor health code |
Use numeric values for chart fields. One possible risk encoding is 0 = Normal, 1 = Caution, 2 = Danger, 3 = Sensor Fault.
11.2 Configure the channel
-
Create a ThingSpeak account and channel.
-
Enable the fields you plan to record.
-
Copy the channel's Write API Key.
-
Store it in an n8n credential or environment variable.
-
Use an n8n HTTP Request node to publish data.
-
Create time-series visualizations in the channel's private or public dashboard, as appropriate.
11.3 Example ThingSpeak HTTP request
Configure the HTTP Request node as follows:
-
Method:
GETorPOST. -
Endpoint:
https://api.thingspeak.com/update. -
Send the write key as
api_key. -
Map the fields to
field1throughfield8.
For example, the request body for a POST can contain:
api_key=YOUR_WRITE_API_KEY
field1=2450
field2=16.8
field3=94
field4=2
field5=3.6
field6=12.4
Those values are illustrative. Map real measurements using n8n expressions rather than hardcoding them. A successful update returns an entry ID; a zero or failed response should be treated as a logging failure.
Check the current ThingSpeak channel update limits for your account and plan before selecting the upload interval. Avoid sending duplicate updates more frequently than permitted.
12. AI-powered fog analysis
The AI component should explain sensor trends and help forecast energy consumption. The primary hazard classification must remain rule-based and independently testable.
Recommended two-stage design
Stage A — Deterministic safety engine
Validate measurements, check sensor health, apply calibrated thresholds, filter noise, and trigger the required safety response.
Stage B — AI analysis agent
Summarize recent trends, explain why a warning was triggered, identify suspicious sensor behaviour, and forecast station energy consumption.
Example AI agent prompt
Use this as the system instruction for an AI node connected to the validated data:
You are an assistant for a mountain-road environmental monitoring station.
Use only the supplied measurements and historical data.
Never invent sensor readings or claim that a sensor-derived
optical indicator is a calibrated visibility distance.
Explain trends in temperature, humidity, optical readings,
and power consumption.
If the rule-based system reports DANGER or SENSOR_FAULT,
preserve that status. Do not downgrade or cancel an alert.
Return a concise JSON object with:
- summary
- reported_risk
- observed_trend
- sensor_quality_concerns
- predicted_energy_wh
- prediction_horizon
- recommended_operator_check
Recommendations are advisory. Do not claim that the road is
safe without independently validated visibility measurements.
13. AI power consumption prediction logic
Power forecasting helps estimate the energy required to operate the ESP32, sensors, communications hardware and optional warning devices. It can also help estimate battery life for remote mountain stations.
13.1 Measure actual power
With a suitable INA219 or equivalent monitor, measure voltage VV and current II:
Power is measured in watts. Energy consumed over time is measured in watt-hours:
where ΔtiΔti is the interval in seconds between measurements.
For example, if a station draws an average of 1.2 W for 24 hours:
For a battery system, account for regulator losses, battery usable capacity, temperature effects, self-discharge and any solar charging.
13.2 Start with a baseline prediction
Before training an AI model, establish a simple baseline:
That is, use the rolling average of daily energy as the next-day forecast. A stronger model can include:
-
Previous hourly and daily energy use.
-
Time of day and day of week.
-
Wi-Fi transmission frequency.
-
Fog warning and buzzer activation duration.
-
Ambient temperature.
-
Solar charging and battery voltage, where applicable.
13.3 Example forecasting code for n8n
This Code node estimates energy use using recent power measurements. The forecast is a baseline projection, not a trained AI model. Feed it a consistent series of samples from Google Sheets or another time-series store.
AI Smart Mountain Road Safety System with Fog Detection
This project builds an AI-powered, IoT-based mountain-road safety system using an ESP32. It detects fog, poor visibility, vehicles, obstacles, gas/smoke, and environmental conditions; activates local road warnings; uploads live data to ThingSpeak; logs events in Google Sheets; and uses an n8n AI Agent to send intelligent Telegram voice/text alerts.
ThingSpeak supports device-to-cloud data through REST or MQTT, and a free account updates a channel at most every 15 seconds, so the ESP32 should normally publish at 15–60 second intervals.[in.mathworks][uk.mathworks]
Project Overview
What the system does
- Measures visibility/fog using an IR obstacle sensor or laser/IR distance sensor.
- Measures temperature, humidity, gas/smoke, light level, rain, and vibration.
- Detects a vehicle or obstacle approaching a dangerous curve using an ultrasonic/IR sensor.
- Turns on high-intensity LEDs, buzzer, and warning display during fog or danger.
- Sends sensor data to ThingSpeak for a live cloud dashboard.
- Sends event data to n8n, where an AI Agent classifies risk and generates a warning.
- Sends Telegram text + voice alerts to road authorities or drivers.
- Stores every alert and reading in Google Sheets for historical analysis.
- Predicts likely power consumption using sensor activity, alert frequency, and weather conditions.
System roles
| Layer | Technology | Purpose |
|---|---|---|
| Edge device | ESP32 | Reads sensors, controls actuators, publishes data |
| Local warning | LEDs, buzzer, relay | Warns approaching vehicles immediately |
| Cloud dashboard | ThingSpeak | Stores and visualizes road conditions |
| Automation | n8n | Receives events, runs AI logic, triggers alerts |
| AI layer | OpenAI/other LLM in n8n | Risk classification and alert message generation |
| Notifications | Telegram Bot | Text and voice alerts |
| Records | Google Sheets | Event history and audit log |
| Optional control | Telegram commands | Request status, reset alarm, change mode |
n8n provides Telegram message operations including Send Audio, and Google Sheets supports Append Row and Append or Update Row, which are ideal for automated event logging.[docs.n8n][docs.n8n]
Architecture
┌──────────────────────────────┐
│ Mountain Road │
└──────────────┬───────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
│ Fog / Visibility│ │ Vehicle / │ │ Environment │
│ IR/LDR Sensor │ │ Obstacle Sensor │ │ DHT22, MQ-2, │
└───────┬────────┘ └────────┬────────┘ │ Rain, Vibration │
│ │ └────────┬────────┘
└───────────────┬───────┴───────────────────────┘
│
┌─────────▼─────────┐
│ ESP32 │
│ Edge AI Logic │
└───┬───────────┬───┘
│ │
┌──────────▼──┐ ┌───▼─────────────┐
│ Local Alert │ │ Wi-Fi / HTTPS │
│ LED+Buzzer │ └───┬─────────────┘
└─────────────┘ │
│
┌────────────────┼─────────────────┐
│ │ │
┌────────▼───────┐ ┌──────▼───────┐ ┌───────▼────────┐
│ ThingSpeak │ │ n8n │ │ Google Sheets │
│ Cloud Dashboard│ │ AI Agent │ │ Event Log │
└────────────────┘ └──────┬───────┘ └────────────────┘
│
┌──────────▼──────────┐
│ Telegram Voice + │
│ Text Notifications │
└─────────────────────┘
Components List
| Component | Quantity | Purpose |
|---|---|---|
| ESP32 DevKit V1 / WROOM-32 | 1 | Main Wi-Fi microcontroller |
| DHT22 or DHT11 | 1 | Temperature and humidity |
| MQ-2 gas/smoke sensor | 1 | Smoke, LPG, combustible gas detection |
| LDR module | 1 | Ambient light / night detection |
| IR obstacle sensor | 1–2 | Fog/low-visibility and vehicle detection |
| HC-SR04 ultrasonic sensor | 1 | Distance to obstacle/vehicle |
| Rain sensor module | 1 | Wet-road detection |
| SW-420 vibration sensor | 1 | Landslide/vibration detection |
| PIR motion sensor | 1 | Optional human/animal movement detection |
| 5V relay module | 1 | Controls high-power warning lights |
| Red LEDs / 12V warning lamps | 2–4 | Fog/danger warning |
| Yellow LED | 1 | Caution mode |
| Green LED | 1 | Safe mode |
| Active buzzer | 1 | Immediate audible alarm |
| OLED display, 0.96-inch I2C | 1 | Local status display |
| Breadboard and jumper wires | 1 set | Prototyping |
| 5V 2A adapter or 18650 battery + TP4056 | 1 | Power supply |
| Voltage divider / AMS1117 3.3V | As needed | Sensor level compatibility |
| Enclosure, IP65 box | 1 | Outdoor protection |
Pin Connection Plan
Use GPIO pins that are safe during boot. Avoid GPIO0, GPIO2, GPIO12, and GPIO15 for sensors or relays where possible.
| ESP32 Pin | Connected Device | Notes |
|---|---|---|
| 3V3 | DHT22 VCC, LDR VCC, IR VCC, OLED VCC | 3.3 V sensors |
| VIN / 5V | MQ-2 VCC, relay VCC, HC-SR04 VCC | Use 5 V where required |
| GND | Common ground | All modules must share ground |
| GPIO4 | DHT22 DATA | Add 10 kΩ pull-up to 3.3 V for DHT22 |
| GPIO34 | MQ-2 A0 | ADC input |
| GPIO35 | LDR A0 | ADC input |
| GPIO32 | Rain sensor A0 | ADC input |
| GPIO33 | Vibration sensor DO | Digital input |
| GPIO25 | IR fog sensor DO | LOW usually means object detected |
| GPIO26 | IR vehicle sensor DO | Vehicle/obstacle detection |
| GPIO27 | HC-SR04 TRIG | Output |
| GPIO14 | HC-SR04 ECHO | Use voltage divider: 5 V to 3.3 V |
| GPIO18 | Green LED | Safe condition |
| GPIO19 | Yellow LED | Caution |
| GPIO21 | Red LED | Danger |
| GPIO22 | Buzzer | Alarm |
| GPIO23 | Relay IN | High-power warning lamp |
| GPIO5 / GPIO23 | OLED SDA / SCL | I2C display; choose available pins |
Circuit Schematic Diagram
ESP32 DevKit
┌──────────────────────────────┐
│ 3V3 GND GPIO4 GPIO34 │
│ 5V GPIO25 GPIO35 │
│ GPIO26 GPIO32 │
│ GPIO27 GPIO33 │
│ GPIO14 GPIO18-23 │
└───┬────┬────┬────┬────┬──────┘
│ │ │ │ │
┌──────────────┘ │ │ │ └────────────────┐
│ │ │ │ │
┌─────▼─────┐ ┌────────▼──┐ │ ┌──▼──────────┐ ┌────────▼──────┐
│ DHT22 │ │ IR Fog │ │ │ HC-SR04 │ │ Relay Module │
│ VCC->3V3 │ │ VCC->3V3 │ │ │ VCC->5V │ │ VCC->5V │
│ DATA->GPIO4│ │ DO->GPIO25│ │ │ TRIG->GPIO27│ │ IN ->GPIO23 │
│ GND->GND │ │ GND->GND │ │ │ ECHO->GPIO14│ │ GND->GND │
└───────────┘ └───────────┘ │ │ GND->GND │ └──────┬────────┘
│ └─────────────┘ │
┌─────────────┐ ┌──────────┐ │ │
│ MQ-2 Sensor │ │ LDR │ │ ┌─────▼─────┐
│ VCC->5V │ │ VCC->3V3 │ │ │ 12V Lamp │
│ A0 ->GPIO34 │ │ A0->GPIO35│ │ │ via relay │
│ GND->GND │ │ GND->GND │ │ └───────────┘
└─────────────┘ └──────────┘ │
│
┌─────────────┐ ┌──────────┐ │
│ Rain Sensor │ │ Vibration│ │
│ A0 ->GPIO32 │ │ DO->GPIO33│ │
└─────────────┘ └──────────┘ │
│
┌────────────▼───────────┐
│ LEDs: Green GPIO18 │
│ Yellow GPIO19 │
│ Red GPIO21 │
│ Buzzer GPIO22 │
└────────────────────────┘
Important: HC-SR04 ECHO outputs 5 V. Connect it to ESP32 through a voltage divider, for example 1 kΩ from ECHO to GPIO14 and 2 kΩ from GPIO14 to GND.
Safety Logic
The ESP32 calculates a Road Risk Score from multiple sensors:
Risk = W_f F + W_v V + W_g G + W_r R + W_l L + W_s S
Where:
F= fog/low visibility levelV= vehicle/obstacle proximity riskG= gas/smoke riskR= rain riskL= landslide/vibration riskS= night/low-light riskWvalues are weights
Example weights:
| Parameter | Weight |
|---|---|
| Fog | 0.35 |
| Obstacle/vehicle | 0.25 |
| Rain | 0.15 |
| Gas/smoke | 0.10 |
| Vibration/landslide | 0.10 |
| Night | 0.05 |
Decision levels
| Risk Score | Status | Local Action | Cloud Action |
|---|---|---|---|
| 0–29 | SAFE | Green LED | Normal data upload |
| 30–59 | CAUTION | Yellow LED + slow beep | n8n caution alert |
| 60–84 | DANGER | Red LED + buzzer | n8n danger alert + Sheets log |
| 85–100 | EMERGENCY | Red LED + relay lamp + continuous buzzer | Emergency Telegram voice alert |
ESP32 Source Code
Install these Arduino libraries:
WiFi.hHTTPClient.hDHT sensor library by AdafruitAdafruit Unified SensorAdafruit SSD1306Adafruit GFXArduinoJson
/*
* AI Smart Mountain Road Safety System with Fog Detection
* ESP32 + n8n + ThingSpeak + Google Sheets + Telegram Voice Alerts
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>
// ---------------- Wi-Fi ----------------
const char* WIFI_SSID = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
// ---------------- ThingSpeak ----------------
const char* THINGSPEAK_HOST = "api.thingspeak.com";
const char* THINGSPEAK_WRITE_KEY = "YOUR_THINGSPEAK_WRITE_KEY";
// ---------------- n8n Webhook ----------------
const char* N8N_WEBHOOK_URL = "https://YOUR_N8N_DOMAIN/webhook/mountain-road-alert";
// ---------------- Pins ----------------
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define MQ2_PIN 34
#define LDR_PIN 35
#define RAIN_PIN 32
#define VIBRATION_PIN 33
#define FOG_IR_PIN 25
#define VEHICLE_IR_PIN 26
#define TRIG_PIN 27
#define ECHO_PIN 14
#define GREEN_LED 18
#define YELLOW_LED 19
#define RED_LED 21
#define BUZZER_PIN 22
#define RELAY_PIN 23
#define OLED_SDA 5
#define OLED_SCL 23
// ---------------- Thresholds ----------------
int fogThreshold = 500; // Adjust after calibration
int gasThreshold = 700; // MQ-2 ADC threshold
int rainThreshold = 2000; // Lower value may mean wet, depending on module
int vibrationThreshold = 1; // Digital HIGH means vibration
int obstacleDistanceCm = 100; // Danger distance
int safeThreshold = 29;
int cautionThreshold = 59;
int dangerThreshold = 84;
// ---------------- Objects ----------------
DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
unsigned long lastThingSpeak = 0;
unsigned long lastN8NAlert = 0;
const unsigned long THINGSPEAK_INTERVAL = 20000; // 20 seconds
const unsigned long ALERT_COOLDOWN = 60000; // 1 minute
float temperature = 0;
float humidity = 0;
int gasValue = 0;
int lightValue = 0;
int rainValue = 0;
bool vibration = false;
bool fogDetected = false;
bool vehicleDetected = false;
long distanceCm = 999;
int riskScore = 0;
String riskLevel = "SAFE";
void setup() {
Serial.begin(115200);
pinMode(GREEN_LED, OUTPUT);
pinMode(YELLOW_LED, OUTPUT);
pinMode(RED_LED, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
pinMode(FOG_IR_PIN, INPUT);
pinMode(VEHICLE_IR_PIN, INPUT);
pinMode(VIBRATION_PIN, INPUT);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
digitalWrite(GREEN_LED, LOW);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(RED_LED, LOW);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(RELAY_PIN, LOW);
dht.begin();
Wire.begin(OLED_SDA, OLED_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED not found");
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Mountain Road Safety");
display.println("Starting...");
display.display();
connectWiFi();
}
void loop() {
readSensors();
calculateRisk();
controlLocalAlerts();
updateDisplay();
unsigned long now = millis();
if (now - lastThingSpeak >= THINGSPEAK_INTERVAL) {
sendToThingSpeak();
lastThingSpeak = now;
}
if (riskScore > cautionThreshold && now - lastN8NAlert >= ALERT_COOLDOWN) {
sendToN8N();
lastN8NAlert = now;
}
delay(1000);
}
void connectWiFi() {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connected. IP: ");
Serial.println(WiFi.localIP());
}
void readSensors() {
temperature = dht.readTemperature();
humidity = dht.readHumidity();
gasValue = analogRead(MQ2_PIN);
lightValue = analogRead(LDR_PIN);
rainValue = analogRead(RAIN_PIN);
vibration = digitalRead(VIBRATION_PIN) == HIGH;
fogDetected = digitalRead(FOG_IR_PIN) == LOW;
vehicleDetected = digitalRead(VEHICLE_IR_PIN) == LOW;
distanceCm = readUltrasonicDistance();
}
long readUltrasonicDistance() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH, 30000);
if (duration == 0) {
return 999;
}
return duration * 0.034 / 2;
}
void calculateRisk() {
int fogRisk = 0;
int vehicleRisk = 0;
int gasRisk = 0;
int rainRisk = 0;
int vibrationRisk = 0;
int nightRisk = 0;
if (fogDetected || lightValue < fogThreshold) {
fogRisk = 100;
} else if (lightValue < fogThreshold + 300) {
fogRisk = 50;
}
if (vehicleDetected || distanceCm < obstacleDistanceCm) {
vehicleRisk = 100;
} else if (distanceCm < obstacleDistanceCm * 2) {
vehicleRisk = 50;
}
if (gasValue > gasThreshold) {
gasRisk = 100;
} else if (gasValue > gasThreshold * 0.7) {
gasRisk = 50;
}
if (rainValue < rainThreshold) {
rainRisk = 100;
} else if (rainValue < rainThreshold + 500) {
rainRisk = 50;
}
if (vibration) {
vibrationRisk = 100;
}
if (lightValue < 800) {
nightRisk = 100;
}
riskScore =
(fogRisk * 0.35) +
(vehicleRisk * 0.25) +
(rainRisk * 0.15) +
(gasRisk * 0.10) +
(vibrationRisk * 0.10) +
(nightRisk * 0.05);
riskScore = constrain(riskScore, 0, 100);
if (riskScore <= safeThreshold) {
riskLevel = "SAFE";
} else if (riskScore <= cautionThreshold) {
riskLevel = "CAUTION";
} else if (riskScore <= dangerThreshold) {
riskLevel = "DANGER";
} else {
riskLevel = "EMERGENCY";
}
}
void controlLocalAlerts() {
digitalWrite(GREEN_LED, LOW);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(RED_LED, LOW);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(RELAY_PIN, LOW);
if (riskLevel == "SAFE") {
digitalWrite(GREEN_LED, HIGH);
} else if (riskLevel == "CAUTION") {
digitalWrite(YELLOW_LED, HIGH);
digitalWrite(BUZZER_PIN, HIGH);
delay(100);
digitalWrite(BUZZER_PIN, LOW);
} else if (riskLevel == "DANGER") {
digitalWrite(RED_LED, HIGH);
digitalWrite(BUZZER_PIN, HIGH);
delay(300);
digitalWrite(BUZZER_PIN, LOW);
} else {
digitalWrite(RED_LED, HIGH);
digitalWrite(BUZZER_PIN, HIGH);
digitalWrite(RELAY_PIN, HIGH);
}
}
void updateDisplay() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Mountain Road Safety");
display.setCursor(0, 16);
display.print("Risk: ");
display.println(riskLevel);
display.print("Score: ");
display.println(riskScore);
display.print("Temp: ");
display.println(temperature);
display.print("Humidity: ");
display.println(humidity);
display.print("Distance: ");
display.println(distanceCm);
display.print("Fog: ");
display.println(fogDetected ? "YES" : "NO");
display.display();
}
void sendToThingSpeak() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
return;
}
HTTPClient http;
String url = "http://" + String(THINGSPEAK_HOST) + "/update?api_key=" +
THINGSPEAK_WRITE_KEY +
"&field1=" + String(temperature, 2) +
"&field2=" + String(humidity, 2) +
"&field3=" + String(gasValue) +
"&field4=" + String(lightValue) +
"&field5=" + String(rainValue) +
"&field6=" + String(distanceCm) +
"&field7=" + String(riskScore) +
"&field8=" + String(fogDetected ? 1 : 0);
http.begin(url);
int httpCode = http.GET();
if (httpCode > 0) {
Serial.print("ThingSpeak response: ");
Serial.println(httpCode);
} else {
Serial.print("ThingSpeak error: ");
Serial.println(httpCode);
}
http.end();
}
void sendToN8N() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
return;
}
HTTPClient http;
http.begin(N8N_WEBHOOK_URL);
http.addHeader("Content-Type", "application/json");
StaticJsonDocument<768> doc;
doc["device_id"] = "MOUNTAIN_ROAD_01";
doc["location"] = "Ghat Road Curve 7";
doc["risk_level"] = riskLevel;
doc["risk_score"] = riskScore;
doc["temperature"] = temperature;
doc["humidity"] = humidity;
doc["gas_value"] = gasValue;
doc["light_value"] = lightValue;
doc["rain_value"] = rainValue;
doc["distance_cm"] = distanceCm;
doc["fog_detected"] = fogDetected;
doc["vehicle_detected"] = vehicleDetected;
doc["vibration"] = vibration;
String payload;
serializeJson(doc, payload);
int httpCode = http.POST(payload);
if (httpCode > 0) {
Serial.print("n8n response: ");
Serial.println(httpCode);
} else {
Serial.print("n8n error: ");
Serial.println(httpCode);
}
http.end();
}
ThingSpeak Dashboard Setup
ThingSpeak channels store data in fields, and the Write API key is required to update a private channel.[mathworks][se.mathworks]
Create the channel
- Sign in at ThingSpeak.
- Go to Channels > My Channels > New Channel.
-
Enter:
- Name:
AI Mountain Road Safety - Field 1:
Temperature C - Field 2:
Humidity % - Field 3:
Gas / Smoke - Field 4:
Light Level - Field 5:
Rain Value - Field 6:
Obstacle Distance cm - Field 7:
AI Risk Score - Field 8:
Fog Detected
- Name:
- Enable Make Public if you want a public dashboard.
- Save the channel.
- Open API Keys and copy the Write API Key.
- Paste it into
THINGSPEAK_WRITE_KEYin the ESP32 code.
ThingSpeak accepts updates using HTTP GET/POST or MQTT, and its REST API supports writing one entry with multiple fields.[in.mathworks][in.mathworks]
Dashboard widgets
Add these widgets:
- Temperature: Field 1 line chart
- Humidity: Field 2 line chart
- Gas/smoke: Field 3 gauge or chart
- Visibility/fog: Field 8 status chart
- Risk score: Field 7 MATLAB visualization
- Distance: Field 6 line chart
Optional MATLAB risk visualization
readKey = 'YOUR_READ_API_KEY';
data = thingSpeakRead(YOUR_CHANNEL_ID, ...
'Fields', [1 2 7], ...
'NumPoints', 50, ...
'ReadKey', readKey);
temperature = data(:,1);
humidity = data(:,2);
risk = data(:,3);
plot(risk, 'r-', 'LineWidth', 2);
ylabel('AI Risk Score');
title('Mountain Road Risk Trend');
grid on;
ThingSpeak’s MATLAB Analysis app can read, process, and write channel data; it uses channel IDs and API keys for access.[uk.mathworks][in.mathworks]
n8n Workflow
Required credentials
- Telegram API credential: Bot token from
@BotFather. - Google Sheets OAuth2 credential: Google account authorization.
- OpenAI credential: For the AI Agent, if using OpenAI.
- Optional: Telegram Chat ID, obtained by messaging the bot and using n8n’s Telegram trigger or Telegram API.
n8n’s Telegram node supports sending audio, and its Google Sheets node supports appending rows to a selected spreadsheet and sheet.[docs.n8n][docs.n8n]
Workflow flow
ESP32 HTTP POST
│
▼
Webhook: /mountain-road-alert
│
▼
IF Risk Level is CAUTION / DANGER / EMERGENCY
│
├── Yes ──▶ Google Sheets: Append Row
│ │
│ ▼
│ AI Agent: Analyze Risk
│ │
│ ▼
│ Generate Voice MP3 / OGG
│ │
│ ▼
│ Telegram: Send Voice Alert
│ │
│ ▼
│ Telegram: Send Text Summary
│
└── No ──▶ End
n8n Workflow JSON
Import this JSON in n8n using Workflows > Import from File or paste it into the canvas.
{
"name": "AI Smart Mountain Road Safety System",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "mountain-road-alert",
"responseMode": "onReceived",
"options": {}
},
"id": "webhook-node",
"name": "ESP32 Alert Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [0, 300],
"webhookId": "mountain-road-alert"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"looseTypeValidation": false,
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.body.risk_level }}",
"rightValue": "SAFE",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "risk-check",
"name": "Is Alert Required?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [220, 300]
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "YOUR_GOOGLE_SHEET_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "RoadSafetyLog",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"Timestamp": "={{ $now.format('yyyy-MM-dd HH:mm:ss') }}",
"DeviceID": "={{ $json.body.device_id }}",
"Location": "={{ $json.body.location }}",
"RiskLevel": "={{ $json.body.risk_level }}",
"RiskScore": "={{ $json.body.risk_score }}",
"Temperature": "={{ $json.body.temperature }}",
"Humidity": "={{ $json.body.humidity }}",
"GasValue": "={{ $json.body.gas_value }}",
"LightValue": "={{ $json.body.light_value }}",
"RainValue": "={{ $json.body.rain_value }}",
"DistanceCm": "={{ $json.body.distance_cm }}",
"FogDetected": "={{ $json.body.fog_detected }}",
"VehicleDetected": "={{ $json.body.vehicle_detected }}",
"Vibration": "={{ $json.body.vibration }}"
}
},
"options": {}
},
"id": "google-sheet-log",
"name": "Log Alert to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [440, 180],
"credentials": {
"googleSheetsOAuth2Api": {
"id": "REPLACE_WITH_CREDENTIAL_ID",
"name": "Google Sheets account"
}
}
},
{
"parameters": {
"promptType": "define",
"text": "=You are an AI road-safety agent.\n\nAnalyze this mountain-road IoT alert and return JSON only with fields:\n- risk_level\n- recommended_action\n- driver_warning\n- authority_message\n\nSensor data:\nDevice: {{ $json.body.device_id }}\nLocation: {{ $json.body.location }}\nRisk level: {{ $json.body.risk_level }}\nRisk score: {{ $json.body.risk_score }}\nTemperature: {{ $json.body.temperature }} C\nHumidity: {{ $json.body.humidity }}%\nGas value: {{ $json.body.gas_value }}\nLight value: {{ $json.body.light_value }}\nRain value: {{ $json.body.rain_value }}\nObstacle distance: {{ $json.body.distance_cm }} cm\nFog detected: {{ $json.body.fog_detected }}\nVehicle detected: {{ $json.body.vehicle_detected }}\nVibration: {{ $json.body.vibration }}"
},
"id": "ai-agent",
"name": "AI Road Safety Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.7,
"position": [660, 180]
},
{
"parameters": {
"model": {
"__rl": true,
"value": "gpt-4o-mini",
"mode": "list"
},
"options": {}
},
"id": "openai-model",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"position": [620, 380],
"credentials": {
"openAiApi": {
"id": "REPLACE_WITH_CREDENTIAL_ID",
"name": "OpenAI account"
}
}
},
{
"parameters": {
"jsCode": "const raw = $json.output || $json.text || '';\nlet parsed;\ntry {\n parsed = JSON.parse(raw);\n} catch (e) {\n const match = raw.match(/\\{[\\s\\S]*\\}/);\n parsed = match ? JSON.parse(match[0]) : {\n risk_level: 'DANGER',\n recommended_action: 'Activate road warning system.',\n driver_warning: 'Caution: poor visibility or obstacle ahead.',\n authority_message: 'Mountain road safety alert detected.'\n };\n}\n\nconst voiceText =\n 'Attention. Mountain road safety alert. ' +\n parsed.driver_warning + ' ' +\n 'Recommended action: ' + parsed.recommended_action;\n\nreturn [{ json: { ...parsed, voice_text: voiceText } }];"
},
"id": "parse-ai",
"name": "Parse AI Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [880, 180]
},
{
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/audio/speech",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"model\": \"gpt-4o-mini-tts\",\n \"voice\": \"alloy\",\n \"input\": {{ JSON.stringify($json.voice_text) }},\n \"response_format\": \"mp3\"\n}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"id": "tts-node",
"name": "Generate AI Voice",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1100, 180],
"credentials": {
"openAiApi": {
"id": "REPLACE_WITH_CREDENTIAL_ID",
"name": "OpenAI account"
}
}
},
{
"parameters": {
"operation": "sendAudio",
"chatId": "YOUR_TELEGRAM_CHAT_ID",
"binaryFile": true,
"inputBinaryField": "data",
"additionalFields": {
"caption": "={{ $('Parse AI Response').item.json.driver_warning }}"
}
},
"id": "telegram-voice",
"name": "Telegram Voice Alert",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [1320, 180],
"credentials": {
"telegramApi": {
"id": "REPLACE_WITH_CREDENTIAL_ID",
"name": "Telegram Bot"
}
}
},
{
"parameters": {
"chatId": "YOUR_TELEGRAM_CHAT_ID",
"text": "=🚨 *Mountain Road Safety Alert*\n\n📍 Location: {{ $('ESP32 Alert Webhook').item.json.body.location }}\n⚠️ Risk: {{ $('Parse AI Response').item.json.risk_level }}\n🧠 AI Advice: {{ $('Parse AI Response').item.json.recommended_action }}\n🚗 Driver Warning: {{ $('Parse AI Response').item.json.driver_warning }}\n\n📊 Risk Score: {{ $('ESP32 Alert Webhook').item.json.body.risk_score }}/100\n🌫️ Fog: {{ $('ESP32 Alert Webhook').item.json.body.fog_detected }}\n🌧️ Rain Value: {{ $('ESP32 Alert Webhook').item.json.body.rain_value }}\n🚗 Distance: {{ $('ESP32 Alert Webhook').item.json.body.distance_cm }} cm",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "telegram-text",
"name": "Telegram Text Summary",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [1540, 180],
"credentials": {
"telegramApi": {
"id": "REPLACE_WITH_CREDENTIAL_ID",
"name": "Telegram Bot"
}
}
}
],
"connections": {
"ESP32 Alert Webhook": {
"main": [
[
{
"node": "Is Alert Required?",
"type": "main",
"index": 0
}
]
]
},
"Is Alert Required?": {
"main": [
[
{
"node": "Log Alert to Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"Log Alert to Google Sheets": {
"main": [
[
{
"node": "AI Road Safety Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Road Safety Agent": {
"main": [
[
{
"node": "Parse AI Response",
"type": "main",
"index": 0
}
]
]
},
"Parse AI Response": {
"main": [
[
{
"node": "Generate AI Voice",
"type": "main",
"index": 0
}
]
]
},
"Generate AI Voice": {
"main": [
[
{
"node": "Telegram Voice Alert",
"type": "main",
"index": 0
}
]
]
},
"Telegram Voice Alert": {
"main": [
[
{
"node": "Telegram Text Summary",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
Telegram Bot Setup
- Open Telegram and search for @BotFather.
- Send
/newbot. - Choose a display name, for example
Mountain Road Safety Bot. - Choose a username ending in
bot, such asmountain_road_safety_bot. - Copy the bot token.
- Add the token to n8n under Credentials > Telegram API.
- Message your bot once so Telegram can identify your chat.
- Get your chat ID using n8n’s Telegram Trigger or a Telegram API tool.
- Replace
YOUR_TELEGRAM_CHAT_IDin the workflow.
Telegram Bot API requests use the format https://api.telegram.org/bot<token>/METHOD_NAME. For voice messages, Telegram supports sendVoice; n8n’s Telegram node also provides a Send Audio operation that accepts a binary file.[docs.n8n][core.telegram]
Google Sheets Integration
Create a Google Sheet named Mountain Road Safety System.
Create a tab named RoadSafetyLog with these headers:
| Timestamp | DeviceID | Location | RiskLevel | RiskScore | Temperature | Humidity | GasValue | LightValue | RainValue | DistanceCm | FogDetected | VehicleDetected | Vibration | AIAction |
|---|
In n8n:
- Add the Google Sheets node.
- Select Resource:
Sheet Within Document. - Select Operation:
Append Row. - Choose your spreadsheet and
RoadSafetyLogsheet. - Map each incoming ESP32 field to the correct column.
- Connect your Google Sheets OAuth2 credential.
n8n identifies a spreadsheet either by list, URL, or spreadsheet ID, and its Append Row operation adds a new row at the end of the selected sheet.[docs.n8n]
AI Power Consumption Prediction Logic
The system predicts expected power use from sensor polling, Wi-Fi transmission, local warnings, and cloud/automation events.
Simple prediction formula
P_{predicted} = P_{base} + P_{wifi}N_w + P_{alert}N_a + P_{lamp}T_l
Where:
P_{base}= ESP32 and sensor baseline powerN_w= number of Wi-Fi uploads per hourP_{wifi}= energy per uploadN_a= number of alerts per hourP_{alert}= energy per alert/buzzer eventT_l= warning-lamp ON time in hoursP_{lamp}= lamp power
Example calculation
Assume:
- ESP32 average current: 120 mA
- Sensors average current: 80 mA
- Wi-Fi transmission current: 180 mA for 2 seconds
- Buzzer current: 30 mA
- 12 V warning lamp: 500 mA
- Uploads: 180 per hour at 20-second intervals
- Alerts: 5 per hour
- Lamp ON: 0.2 hours per hour during fog
Approximate daily energy:
E_{base} = 0.2A \times 5V \times 24h = 24Wh
E_{wifi} = 0.18A \times 5V \times \frac{2}{3600}h \times 180 = 0.09Wh
E_{lamp} = 12V \times 0.5A \times 0.2h = 1.2Wh
E_{total} \approx 25.3Wh/day
For a 12 V 7 Ah battery:
Capacity = 12V \times 7Ah = 84Wh
Estimated backup time:
\frac{84Wh}{25.3Wh/day} \approx 3.3 days
In a real deployment, add a solar panel, battery-management system, low-power sleep mode, and adaptive upload frequency.
Adaptive power-saving rule
| Condition | Upload Interval | Local Warning |
|---|---|---|
| Safe, daytime | 60 seconds | Green LED only |
| Fog detected | 15–20 seconds | Yellow/red warning |
| Emergency | 10 seconds | Lamp, buzzer, immediate n8n alert |
| Low battery | 120 seconds | Reduce Wi-Fi and lamp usage |
ThingSpeak’s free tier permits one update every 15 seconds, so use 15 seconds as the fastest normal publishing interval.[uk.mathworks]
Voice Notification Automation
The n8n workflow can convert the AI Agent’s warning into speech using an OpenAI text-to-speech API call, then send the resulting MP3 file through Telegram.
Telegram’s sendVoice method is intended for playable voice notes; n8n’s Telegram node can send an audio binary file using Send Audio.[docs.n8n][core.telegram]
Voice message template
Attention. Mountain road safety alert at Ghat Road Curve 7.
Fog and low visibility detected.
Reduce speed, use low-beam headlights, and maintain a safe distance.
Emergency warning lights are active.
Alert priority
| Event | Voice Priority | Telegram Message |
|---|---|---|
| Fog only | Medium | “Low visibility detected. Drive cautiously.” |
| Fog + vehicle | High | “Vehicle approaching low-visibility zone. Reduce speed.” |
| Smoke/gas | High | “Possible smoke or gas detected. Avoid the area.” |
| Vibration/landslide | Emergency | “Possible ground movement detected. Stop and evacuate the zone.” |
| Heavy rain + fog | Emergency | “Dangerous road condition. Avoid travel until conditions improve.” |
Testing Procedure
- Power the ESP32 and confirm it connects to Wi-Fi.
- Open Serial Monitor at 115200 baud.
- Verify DHT22 temperature and humidity readings.
- Cover the LDR to simulate night/fog.
- Place an object before the fog IR sensor.
- Move your hand in front of HC-SR04 and verify distance.
- Apply heat or gas near MQ-2 carefully and observe the ADC value.
- Trigger the vibration sensor.
- Confirm LED and buzzer behavior for SAFE, CAUTION, DANGER, and EMERGENCY.
- Check ThingSpeak fields update.
- Trigger n8n manually using this test request:
curl -X POST https://YOUR_N8N_DOMAIN/webhook/mountain-road-alert \
-H "Content-Type: application/json" \
-d '{
"device_id": "MOUNTAIN_ROAD_01",
"location": "Ghat Road Curve 7",
"risk_level": "EMERGENCY",
"risk_score": 92,
"temperature": 18.5,
"humidity": 94,
"gas_value": 820,
"light_value": 320,
"rain_value": 1200,
"distance_cm": 55,
"fog_detected": true,
"vehicle_detected": true,
"vibration": false
}'
- Confirm Google Sheets receives a new row.
- Confirm Telegram receives both text and voice alerts.
Deployment Guide
Hardware protection
- Use an IP65 or IP66 enclosure.
- Place the MQ-2 sensor where airflow is available but rain cannot directly enter.
- Mount ultrasonic and IR sensors away from direct sunlight.
- Use silicone-sealed cable glands.
- Add surge protection and fuse protection for 12 V lamps.
- Mount the enclosure at least 2–3 meters above road level.
- Use reflective warning signs near the installation.
Network reliability
- Prefer a 4G Wi-Fi router or LTE modem at remote mountain sites.
- Configure ESP32 reconnect logic.
- Store failed alerts locally in SPIFFS/LittleFS and retry later.
- Use a watchdog timer to recover from hangs.
- Keep n8n on a reliable cloud VPS or local server with internet access.
Calibration
| Sensor | Calibration Method |
|---|---|
| MQ-2 | Warm up 24–48 hours initially; record clean-air baseline and smoke threshold |
| LDR | Record values in daylight, dusk, fog, and night |
| IR fog sensor | Test with water mist or fog machine |
| HC-SR04 | Measure known distances and adjust offset |
| Rain sensor | Test dry, light spray, and heavy water conditions |
| Vibration sensor | Adjust sensitivity potentiometer |
Future Enhancements
- Add a camera module for AI-based vehicle, animal, rockfall, and accident detection.
- Use LoRaWAN for locations without mobile/Wi-Fi coverage.
- Add solar power with battery-state monitoring.
- Add variable-message signs showing “FOG AHEAD” or “SLOW DOWN.”
- Use GPS to send exact coordinates during emergencies.
- Add automatic gate/barrier control during landslide risk.
- Add a traffic-light-style system for blind curves.
- Use TinyML on ESP32 for offline fog and accident-risk classification.
- Add weather API integration for advance fog and rainfall prediction.
- Create a mobile app with live map, camera snapshots, and route-risk scoring.
- Add automatic emergency calls to highway police or local disaster-response teams.
Documentation Summary
This project combines edge sensing, local actuation, cloud monitoring, AI decision-making, and automated communication. The ESP32 provides immediate road warnings even before cloud processing, while n8n adds AI-based interpretation, Google Sheets record keeping, ThingSpeak visualization, and Telegram voice alerts. ThingSpeak is designed for IoT data collection and analysis through REST/MQTT and MATLAB tools, while n8n connects the ESP32 events to Telegram and Google Sheets automation.[docs.n8n][docs.n8n][mathworks]
