Community project
Manhole Hazard Monitoring System
Generated with AIThis project builds a remote monitoring system for manhole safety using an ESP32 microcontroller to detect hazardous gases, water ingress, unauthorized access, and rodent activity. The system combines a Winsen ZCE04B four-gas sensor, float switch, reed switch, infrared beam pair, and piezo buzzer to monitor conditions locally while transmitting alerts over LoRaWAN via an SX1262 radio module.
The guide provides a complete parts list, wiring diagram showing all sensor and radio connections, step-by-step assembly instructions for weatherproofing the electronics enclosure, and Arduino firmware with LoRaWAN OTAA join logic. Builders will learn how to configure network credentials, parse UART gas data, debounce digital inputs, trigger local alarms, and send status packets to a LoRaWAN network for remote monitoring.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| Winsen ZCE04B four-gas UART moduleCO / H2S / O2 / CH4 (LEL) | 1 | A four-sensor module that measures poisonous hydrogen sulfide and carbon monoxide, oxygen level, and combustible methane gas. |
| Float SwitchIP68, normally closed | 1 | Float switch (liquid level switch): a buoyant float carries a magnet that actuates a hermetically sealed reed contact as the liquid rises or falls past the mounting point. Two-wire SPST dry contact (normally-open or normally-closed) with no power supply of its own -- wired like a push-button on a GPIO with INPUT_PULLUP, where open/closed indicates the level is above/below the float. Common for pump on/off control, tank full/empty detection, and sump alarms. |
| Reed SwitchIP67, normally closed | 1 | Glass-encapsulated magnetic reed switch -- contacts close in the presence of a magnetic field. Standard door/window sensor for alarms. Wire as a digital input with internal pull-up; debounce in firmware. |
| 3.3 V infrared break-beam pair25 cm through-beam | 1 | A matched infrared emitter and receiver that counts an animal when it crosses the narrow light beam. |
| 3.3 V active piezo buzzer module3.3 V active | 1 | A small self-driven sounder that gives a local warning when the cover is disturbed or a hazard is detected. |
| SX1262 LoRaWAN radio moduleSX1262, regional frequency variant | 1 | A 3.3 V radio module that sends the manhole status to a LoRaWAN gateway over long range. |
Assembly
8 stepsKeep the electronics dry
Put the ESP32, LoRaWAN radio, buzzer, and gas-monitor circuit board in a weatherproof enclosure above the highest possible water line. Use cable glands for every cable so water cannot run down a wire into the box.
- Tip: Keep the radio antenna outside the metal cover and enclosure, or use a sealed antenna lead to a mounting point with a clear path toward the gateway.
- ⚠ Do not place ordinary electronics or a non-certified radio in a potentially explosive gas space; use certified equipment and installation practices where required.
Wire the gas monitor
With all power unplugged, join the gas monitor VCC to the ESP32 3V3 pin (power), GND to an ESP32 GND pin (ground), TX to GPIO16 (gas-reading data), and RX to GPIO17 (gas-monitor command data).
- Tip: Use the actual ZCE04B variant confirmed to work at 3.3 V UART levels; do not connect a 5 V serial output directly to the ESP32.
- ⚠ A gas monitor is a safety device: calibrate and maintain it exactly as its manufacturer specifies, and never use this DIY node as the only protection for people entering a manhole.
Install the water switch
Mount the IP68 float switch at the water height that should create an alarm. Connect its GND wire to ESP32 GND (ground) and its SIGNAL wire to GPIO26 (water-warning signal). Set the float orientation so its normally-closed contacts are closed while the water is below the alarm level.
- Tip: The code treats an open contact or broken wire as a water alarm, which is safer than silently ignoring a damaged cable.
- ⚠ Make sure the float can move freely; trapped debris can stop it from reporting rising water.
Fit the cover tamper switch
Fix the sealed reed switch to the fixed rim and its magnet to the underside of the cover so the magnet holds the switch closed when the cover is seated. Connect PIN1 to GPIO27 (cover-movement signal) and PIN2 to ESP32 GND (ground).
- Tip: Test the spacing before sealing it: lifting the cover or moving the magnet away should create the tamper alarm.
- ⚠ Keep the switch and magnet clear of pinch points so a moving cover cannot cut their cable.
Place the rodent light beam
Mount the infrared transmitter and receiver facing each other across a narrow route where an animal must pass. Connect VCC to 3V3 (power), GND to GND (ground), and OUT to GPIO32 (rodent-crossing signal).
- Tip: Use a short protected path because this low-voltage beam is intended for about 20–25 cm at 3.3 V. Aim it before permanently fastening it.
- ⚠ This small beam pair is not waterproof; install it behind a clear protected window or replace it with an equivalently wired IP-rated beam sensor.
Wire the LoRaWAN radio
Connect the SX1262 VCC to ESP32 3V3 (power), GND to ESP32 GND (ground), SCK to GPIO18 (clock), MISO to GPIO19 (radio data), MOSI to GPIO23 (radio data), NSS to GPIO4 (radio select), DIO1 to GPIO33 (radio-ready signal), NRST to GPIO13 (radio reset), and BUSY to GPIO14 (radio-busy signal). Attach the matching Singapore AS923 antenna before powering the radio.
- Tip: One gateway can serve many manholes when radio coverage reaches them; add gateways or outdoor antennas for weak underground coverage rather than expecting one manhole node to relay another node's message.
- ⚠ Do not transmit without the correct antenna attached — transmitting with no antenna can damage the radio. Use only a radio and antenna approved for the Singapore 920–925 MHz band.
Wire the local warning sounder
Connect the buzzer VCC to 3V3 (power), GND to GND (ground), and SIG to GPIO25 (warning-sound control).
- Tip: The buzzer sounds for water, cover, or gas alarms even when the LoRaWAN gateway cannot be reached.
- ⚠ Use only a 3.3 V active buzzer module here; a higher-voltage sounder can damage the ESP32 output or fail to sound.
Add the network identity and test safely
Before deployment, enter this node's four OTAA credentials from your LoRaWAN network in the firmware. Plug the ESP32 into USB at a safe work area, check that the wiring matches the diagram, and test each switch by hand. After it behaves correctly, close the enclosure and secure all cables.
- Tip: The device sends one compact LoRaWAN report every five minutes, with water, cover, and gas alarm flags plus the rodent-crossing count.
- ⚠ Never enter a manhole to test this device. Poisonous gases and low oxygen can seriously injure or kill; use trained personnel and approved confined-space procedures.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 3V3 | gas_monitor VCC | power |
| GND | gas_monitor GND | ground |
| GPIO 16 | gas_monitor TX | uart |
| GPIO 17 | gas_monitor RX | uart |
| GND | float_switch GND | ground |
| GND | cover_reed PIN2 | ground |
| 3V3 | rodent_beam VCC | power |
| GND | rodent_beam GND | ground |
| GPIO 32 | rodent_beam OUT | digital |
| 3V3 | buzzer VCC | power |
| GND | buzzer GND | ground |
| GPIO 25 | buzzer SIG | digital |
| GPIO 26 | float_switch SIGNAL | data |
| GPIO 27 | cover_reed PIN1 | data |
| 3V3 | lorawan_radio VCC | power |
| GND | lorawan_radio GND | ground |
| GPIO 18 | lorawan_radio SCK | spi |
| GPIO 19 | lorawan_radio MISO | spi |
| GPIO 23 | lorawan_radio MOSI | spi |
| GPIO 4 | lorawan_radio NSS | spi |
| GPIO 33 | lorawan_radio DIO1 | digital |
| GPIO 13 | lorawan_radio NRST | digital |
| GPIO 14 | lorawan_radio BUSY | digital |
Firmware
ESP32#include <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>
// Enter the OTAA credentials issued for this individual node by your LoRaWAN server.
// Each value is hexadecimal text without spaces. Do not reuse device credentials.
// Forward declarations
void inspectGasLine(const String &line);
void readGasMonitor();
void updateInputs();
void sendStatus();
// Replace these zero placeholders with the hexadecimal OTAA values assigned to this node.
// RadioLib expects EUIs as 64-bit values and keys as 16 raw bytes.
const uint64_t JOIN_EUI = 0x0000000000000000ULL;
const uint64_t DEV_EUI = 0x0000000000000000ULL;
const uint8_t NWK_KEY[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t APP_KEY[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
constexpr int GAS_RX_PIN = 16;
constexpr int GAS_TX_PIN = 17;
constexpr int WATER_PIN = 26;
constexpr int COVER_PIN = 27;
constexpr int RODENT_PIN = 32;
constexpr int BUZZER_PIN = 25;
constexpr int LORA_SCK_PIN = 18;
constexpr int LORA_MISO_PIN = 19;
constexpr int LORA_MOSI_PIN = 23;
constexpr int LORA_NSS_PIN = 4;
constexpr int LORA_DIO1_PIN = 33;
constexpr int LORA_RESET_PIN = 13;
constexpr int LORA_BUSY_PIN = 14;
constexpr uint8_t LORAWAN_PORT = 10;
constexpr unsigned long REPORT_INTERVAL_MS = 300000UL;
constexpr unsigned long DEBOUNCE_MS = 60;
HardwareSerial gasSerial(2);
SX1262 radio = new Module(LORA_NSS_PIN, LORA_DIO1_PIN, LORA_RESET_PIN, LORA_BUSY_PIN);
LoRaWANNode node(&radio, &AS923);
bool waterAlarm = false;
bool coverAlarm = false;
bool gasAlarm = false;
bool lorawanJoined = false;
uint32_t rodentCount = 0;
int lastRodentRaw = HIGH;
unsigned long lastRodentEdgeMs = 0;
unsigned long lastReportMs = 0;
String gasLine;
void inspectGasLine(const String &line) {
String upper = line;
upper.toUpperCase();
// The gas monitor itself must have its alarm thresholds configured by qualified staff.
gasAlarm = upper.indexOf("ALARM") >= 0 || upper.indexOf("FAULT") >= 0;
}
void readGasMonitor() {
while (gasSerial.available()) {
char c = static_cast<char>(gasSerial.read());
if (c == '\n' || c == '\r') {
if (gasLine.length() > 0) {
inspectGasLine(gasLine);
gasLine = "";
}
} else if (gasLine.length() < 120) {
gasLine += c;
}
}
}
void updateInputs() {
// Normally-closed circuits: an open contact or cut cable becomes an alarm.
waterAlarm = digitalRead(WATER_PIN) == HIGH;
coverAlarm = digitalRead(COVER_PIN) == HIGH;
int rodentRaw = digitalRead(RODENT_PIN);
if (lastRodentRaw == HIGH && rodentRaw == LOW && millis() - lastRodentEdgeMs > DEBOUNCE_MS) {
++rodentCount;
lastRodentEdgeMs = millis();
}
lastRodentRaw = rodentRaw;
digitalWrite(BUZZER_PIN, (waterAlarm || coverAlarm || gasAlarm) ? HIGH : LOW);
}
void sendStatus() {
// Compact LoRaWAN payload: flags, 16-bit rodent count, and joined-state marker.
uint8_t payload[4];
payload[0] = (waterAlarm ? 0x01 : 0x00) |
(coverAlarm ? 0x02 : 0x00) |
(gasAlarm ? 0x04 : 0x00);
payload[1] = static_cast<uint8_t>((rodentCount >> 8) & 0xFF);
payload[2] = static_cast<uint8_t>(rodentCount & 0xFF);
payload[3] = 1;
if (!lorawanJoined) {
Serial.println("LoRaWAN not joined; status retained for next report.");
return;
}
// sendReceive also listens during LoRaWAN receive windows for network commands.
uint8_t downlink[32];
size_t downlinkLength = 0;
int state = node.sendReceive(payload, sizeof(payload), LORAWAN_PORT,
downlink, &downlinkLength);
if (state >= RADIOLIB_ERR_NONE) {
Serial.println("LoRaWAN status sent.");
} else {
Serial.printf("LoRaWAN uplink failed: %d\n", state);
}
}
void setup() {
Serial.begin(115200);
gasSerial.begin(9600, SERIAL_8N1, GAS_RX_PIN, GAS_TX_PIN);
pinMode(WATER_PIN, INPUT_PULLUP);
pinMode(COVER_PIN, INPUT_PULLUP);
pinMode(RODENT_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
SPI.begin(LORA_SCK_PIN, LORA_MISO_PIN, LORA_MOSI_PIN, LORA_NSS_PIN);
int state = radio.begin();
if (state != RADIOLIB_ERR_NONE) {
Serial.printf("SX1262 startup failed: %d\n", state);
return;
}
node.beginOTAA(JOIN_EUI, DEV_EUI, NWK_KEY, APP_KEY);
state = node.activateOTAA();
lorawanJoined = (state == RADIOLIB_LORAWAN_NEW_SESSION || state == RADIOLIB_LORAWAN_SESSION_RESTORED);
if (lorawanJoined) {
Serial.println("Joined Singapore AS923 LoRaWAN network.");
} else {
Serial.printf("LoRaWAN join failed: %d\n", state);
}
sendStatus();
}
void loop() {
readGasMonitor();
updateInputs();
if (millis() - lastReportMs >= REPORT_INTERVAL_MS) {
lastReportMs = millis();
sendStatus();
}
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.