Contents — 26 sections
Project Overview
Measures sound the way regulations and human hearing do — A-weighted, time-averaged levels — to build an honest city-wide noise map without recording anyone.
Noise is a genuine pollutant — sustained exposure raises stress, disrupts sleep, and is linked to cardiovascular harm — yet it is one of the least-mapped. Complaints are subjective and sporadic, and official noise surveys are expensive and rare, so a city rarely knows which streets are actually too loud, when, and by how much. This project builds the sensing node for a distributed noise map: a device that measures sound levels the way noise regulations and the human ear do, reports them continuously, and does so without recording audio, so it maps loudness without surveilling conversations.
Measuring sound "properly" means two things that a naive peak-detector gets wrong. First, human hearing is not equally sensitive across frequencies — we hear mid frequencies far better than very low or very high ones — so meters apply A-weighting, a filter that discounts the frequencies our ears discount, giving dB(A), the unit used in virtually every noise regulation. Second, a single instant tells you nothing about a fluctuating quantity like traffic noise, so meters report time-averaged and statistical measures: the equivalent continuous level Leq (the steady level carrying the same energy as the fluctuating real sound over a period) and percentile levels like L90 (the background) and L10 (the intrusive peaks). This node computes those, because they are what make a reading comparable to a limit and to other nodes.
Privacy is a design constraint, not an afterthought. The node computes sound-pressure levels on-device from short analysis windows and transmits only the numbers — Leq, L10, L90, Lmax per interval — never audio, so it can sit in public space without recording what people say. It runs on mains or solar, reports over Wi-Fi or LoRa, and feeds a heat map that shows how loudness varies by street and by hour, revealing the difference between a road that roars at rush hour and a nightlife strip that peaks at midnight. It is honest about being a class-of-instrument below a calibrated sound-level meter — its microphone is not laboratory-grade — but with a reference calibration it produces maps a city can actually use to target the streets that most need quieting.
What this project does
- Measures A-weighted sound-pressure level (dB(A)) the way regulations define it
- Computes time-averaged Leq and statistical Lmax, L10 and L90 per interval
- Processes audio on-device and transmits only levels — never recordings
- Builds a time-and-place noise map from many nodes
- Runs on mains or solar and reports over Wi-Fi or LoRa
- Calibrates against a reference sound-level meter for comparable readings
- Flags when levels exceed day/night regulatory limits
Real-World Applications
| Setting | How it is used |
|---|---|
| City noise mapping and planning | Revealing which streets exceed limits and when, so authorities target enforcement, traffic calming or barriers where they matter. |
| Nightlife / entertainment districts | Monitoring venue and street noise against night limits to balance a lively economy with residents' sleep. |
| Construction and industry compliance | Continuous boundary-noise monitoring against consent limits, with an evidence trail and exceedance alerts. |
| Community and school environments | Quantifying noise exposure around homes, hospitals and schools to support quiet-zone advocacy. |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- A-weighting and Leq — the units and averages noise rules actually use
- Statistical levels (L10/L90) that separate peaks from background
- Privacy by design: levels transmitted, audio never leaves the device
- Diurnal noise mapping (rush-hour road vs midnight nightlife)
- Reference calibration for comparability across the network
- Day/night limit flagging
- Cheap enough to deploy as a dense map, not a single meter
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Intermediate |
| Estimated completion time | 12–18 hours |
| Indicative build cost | ₹2,600 – ₹3,800 |
| Primary discipline | Environment |
| Reference platform | ESP32 DevKit V1 (ESP-WROOM-32) |
Skills you should have (or will pick up)
- Reading an I²S MEMS microphone and computing RMS sound levels
- Implementing A-weighting and time-averaged Leq / percentile levels
- Calibrating a level meter against a reference dB(A) source
- Designing for privacy (on-device processing, no audio transmission)
- Wi-Fi/LoRa reporting and heat-map data feeds
Bill of Materials
Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.
| Component | Key specification | Qty | Approx. cost |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC | 1 | ₹450 |
| INMP441 I²S MEMS microphone Digital output means no analogue noise pickup — far better than an MAX9814 for keyword spotting. | 61 dB SNR, −26 dBFS sensitivity, 60 Hz–15 kHz, 24-bit I²S output | 1 | ₹220 |
| 0.96″ SSD1306 OLED display Static images burn in — invert or scroll the screen periodically. | 128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C | 1 | ₹250 |
| SX1278 LoRa 433 MHz module (Ra-02) Never power the radio without an antenna — the PA will destroy itself. | −148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF12 | 1 | ₹480 |
| 20 W 12 V polycrystalline solar panel Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs. | Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame | 1 | ₹1,200 |
| TP4056 Li-ion charger + DW01 protection Buy the version *with* protection ICs — the bare charger will over-discharge your cell. | 1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection | 1 | ₹45 |
| 18650 Li-ion cell 3400 mAh + holder Never charge below 0 °C; always use a protected cell or a BMS. | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 1 | ₹450 |
| Weatherproof mic windshield Wind noise ruins outdoor SPL without a windscreen | Foam/mesh windscreen + rain hood over the MEMS port, IP-rated housing for electronics | 1 | ₹250 |
| Reference calibration access Not hardware — essential for comparable dB(A) | A calibrated sound-level meter or acoustic calibrator to set the node's offset | 1 | — |
| Pole/wall mount | Positions the mic at a standard height, clear of reflecting surfaces | 1 | ₹200 |
Estimated total: ₹3,545, excluding tools, shipping and consumables.
Tools and consumables
- Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
- Digital multimeter — continuity, DC volts and current ranges
- Wire strippers, flush cutters and a small set of precision screwdrivers
- Heat-shrink tubing and a heat gun (or a lighter, carefully)
- A laptop with a USB port and the toolchain listed above
Hardware Specifications
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC | 3.3 V logic / 5 V USB | UART, SPI, I²C, I²S, CAN, PWM | Datasheet |
| INMP441 I²S MEMS microphone | 61 dB SNR, −26 dBFS sensitivity, 60 Hz–15 kHz, 24-bit I²S output | 1.8–3.3 V | I²S | Datasheet |
| 0.96″ SSD1306 OLED display | 128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C | 3.3–5 V | I²C (0x3C) | Datasheet |
| SX1278 LoRa 433 MHz module (Ra-02) | −148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF12 | 3.3 V | SPI | Datasheet |
| 20 W 12 V polycrystalline solar panel | Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame | 12 V nominal | MC4 / screw terminals | Datasheet |
| TP4056 Li-ion charger + DW01 protection | 1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection | 4.5–5.5 V in | micro-USB / pads | Datasheet |
| 18650 Li-ion cell 3400 mAh + holder | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 3.0–4.2 V | Holder / spot-welded tabs | Datasheet |
Consolidated electrical and interface specifications for every active part in the build.
Power Budget & Supply Sizing
Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | 3.3 V logic / 5 V USB | 160 | Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. |
| INMP441 I²S MEMS microphone | 1.8–3.3 V | 1.4 | Digital output means no analogue noise pickup — far better than an MAX9814 for keyword spotting. |
| 0.96″ SSD1306 OLED display | 3.3–5 V | 20 | Static images burn in — invert or scroll the screen periodically. |
| SX1278 LoRa 433 MHz module (Ra-02) | 3.3 V | 120 | Never power the radio without an antenna — the PA will destroy itself. |
| 20 W 12 V polycrystalline solar panel | 12 V nominal | 1140 | Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs. |
| TP4056 Li-ion charger + DW01 protection | 4.5–5.5 V in | 1000 | Buy the version *with* protection ICs — the bare charger will over-discharge your cell. |
Summed typical draw is 2441.4 mA. With a 1.5× design margin the supply should deliver at least 3700 mA continuously at the stated rail voltage.
Software Requirements & Development Environment
Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.
- Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
- Add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonunder File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager. - Set the correct port under Tools → Port. On Linux add yourself to the
dialoutgroup:sudo usermod -aG dialout $USERand log out and back in. - Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
- Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.
Required libraries
| Library | Why it is needed | Install |
|---|---|---|
| WiFi (ESP32 core) bundled | Station/AP connection management for the ESP32. | Bundled with the ESP32 Arduino core |
| PubSubClient 2.8 | Lightweight MQTT 3.1.1 client for constrained devices. | Library Manager → "PubSubClient" by Nick O'Leary |
| Adafruit SSD1306 + GFX 2.5.x | Framebuffer and text/graphics primitives for the OLED. | Library Manager → "Adafruit SSD1306" |
| ArduinoJson 7.x | Zero-allocation JSON serialisation and parsing. | Library Manager → "ArduinoJson" by Benoit Blanchon |
| Preferences (NVS) bundled | Wear-levelled key/value storage in ESP32 flash for settings. | Bundled with the ESP32 core |
| NTPClient / configTime bundled | Wall-clock time from an NTP server for timestamping. | Bundled (`configTime()` on ESP32) |
Block Diagram
The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.
Circuit Diagram & Wiring
Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| INMP441 mic | SD/WS/SCK | GPIO 33/25/32 | I²S digital audio |
| INMP441 mic | L/R | GND | Channel select |
| OLED (opt) | SDA/SCL | GPIO 21/22 | Local dB(A) display |
| LoRa/Wi-Fi | bus | SPI / on-chip | Level telemetry (no audio) |
| TP4056 | OUT | VIN / 3V3 reg | Solar/mains supply |
| Solar/mains | +/– | TP4056 IN | Charge source |
Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.
Wiring explanation
- Wire the INMP441 as an I²S input (SD, WS, SCK) and tie its L/R pin to select a single channel; the ESP32 I²S peripheral streams samples with no CPU polling.
- Mount the microphone port through a foam windscreen and rain hood — outdoor wind directly on the MEMS port produces large false low-frequency levels.
- Position the mic at a standard measurement height on a pole or wall, away from large reflecting surfaces that would inflate levels.
- Keep the mic away from the node's own switching supplies and any fan; you are measuring the environment, not the enclosure.
- If solar, ensure the panel/charger noise does not couple into the mic supply.
System Architecture
Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.
Working Principle
To be a noise measurement rather than a raw amplitude, the node must mimic the ear and the regulation. The ear's frequency response is uneven — most sensitive around 1–4 kHz, much less so at very low and very high frequencies — and A-weighting is the standard filter that shapes the measured spectrum to match, producing dB(A). A truck's low rumble and a whistle of the same physical energy are not equally loud to us, and dB(A) encodes that. Every noise limit in the world is written in dB(A), so applying A-weighting is what makes the node's number comparable to a limit at all.
Because environmental noise fluctuates constantly, a single instantaneous level is nearly meaningless; the meaningful quantities are time-integrated. The equivalent continuous level, Leq, is the steady dB(A) that would deliver the same total sound energy as the actual fluctuating sound over the measurement period — the honest "average loudness" of a noisy interval. Alongside it, statistical percentile levels describe the shape of the noise: L90 (the level exceeded 90% of the time) is effectively the background or residual level, while L10 (exceeded only 10% of the time) captures the intrusive peaks like passing vehicles. A street with a low L90 but a high L10 is quiet with sharp intrusions; one with a high L90 is relentlessly loud. These distinctions are exactly what a noise map needs to be actionable.
The computation is a pipeline the ESP32 can run in real time without ever storing audio. Samples stream from the I²S microphone; each short analysis window is A-weighted (a small digital filter) and reduced to a mean-square energy; those energies accumulate over the reporting interval to form Leq, while a running histogram of short-window levels yields the percentiles and the maximum. At the end of each interval the node emits a handful of numbers and discards the audio. Nothing that could reconstruct speech ever leaves the device — the privacy property is structural, a consequence of computing statistics and throwing the samples away, not a policy layered on top.
Finally, comparability requires calibration. A MEMS microphone has a sensitivity and the whole chain has a gain, so the raw RMS must be mapped to true dB(A) by comparing the node against a reference sound-level meter or acoustic calibrator and storing the offset. With that offset the node produces readings that line up with a proper meter and with its neighbours, turning a field of cheap nodes into a coherent map. The node is candid that it is a class below laboratory metrology — the microphone is not certified — but calibrated and A-weighted, it is more than good enough to show which streets are too loud, and when, which is the entire point of a noise map.
The maths behind it
Sound-pressure level from RMS
For an analysis window, from A-weighted samples x[n]:
rms = sqrt( (1/N) Σ x[n]^2 )
SPL_dBA = 20·log10(rms) + CAL_OFFSET
CAL_OFFSET (from reference calibration) maps the mic/ADC
chain to true dB(A). A-weighting is applied to x[n] before
the RMS so the level matches the ear and the regulation.
Equivalent continuous level (Leq)
Leq is the energy-average over the interval T:
Leq = 10·log10( (1/M) Σ 10^(L_i/10) )
where L_i are the short-window SPL_dBA values (M of them
in T). Averaging in the ENERGY domain (10^(L/10)), not the
dB domain, is essential — a few loud windows dominate,
just as they dominate real exposure.
Percentile levels (L10, L90)
Build a histogram of the short-window L_i over the interval.
L90 = level exceeded 90% of the time → background/residual
L10 = level exceeded 10% of the time → intrusive peaks
Lmax = maximum short-window level
High L10 with low L90 = quiet with sharp intrusions;
high L90 = persistently loud. Both matter for a map.
Program Flowchart
The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.
Assembly Instructions
Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.
Mount the microphone correctly
Fit the INMP441 behind a foam windscreen and rain hood, port facing the environment, at a standard measurement height on a pole or wall and clear of large reflecting surfaces.
Keep the mic away from the node's own supplies and any moving parts so it measures the street, not the box.
Wire and stream the I²S mic
Connect SD/WS/SCK to the I²S pins and select a single channel. Configure the I²S peripheral to stream continuously so the CPU only processes buffers, never polls.
Set up display, power and link
Optionally add an OLED to show the live dB(A) locally. Power from mains or solar, and configure Wi-Fi or LoRa for level-only telemetry.
Step-by-Step Implementation Guide
Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.
Build the level pipeline
For each I²S buffer, apply the A-weighting filter, compute the window RMS and SPL, accumulate the energy for Leq, and update a level histogram for the percentiles and maximum.
cppspl-pipeline.ino// Accumulators for one reporting interval. double energySum = 0; // Σ 10^(L/10) for Leq uint32_t windows = 0; uint16_t hist[140] = {0}; // 1 dB bins, 0..139 dB(A) float lmax = 0; float CAL_OFFSET; // from reference calibration // A-weighting as a cascade of biquads (coeffs precomputed for fs). float aWeight(float x) { /* IIR biquad cascade */ return applyBiquads(x); } // Process one window of N A-weighted samples → SPL, and accumulate. void processWindow(const int32_t *buf, int N) { double ss = 0; for (int i = 0; i < N; i++) { float x = aWeight((float)(buf[i] >> 8)); // 24-bit sample, A-weighted ss += (double)x * x; } float rms = sqrt(ss / N); float spl = 20.0f * log10f(rms + 1e-9f) + CAL_OFFSET; energySum += pow(10.0, spl / 10.0); // energy domain for Leq windows++; int bin = constrain((int)lroundf(spl), 0, 139); hist[bin]++; if (spl > lmax) lmax = spl; } // At interval end: derive Leq and percentiles from the accumulators. void intervalStats(float &leq, float &l10, float &l90) { leq = 10.0f * log10f(energySum / windows); uint32_t c = 0, t = windows; l90 = l10 = 0; for (int b = 139; b >= 0; b--) { // high → low c += hist[b]; if (!l10 && c >= t * 0.10f) l10 = b; // exceeded 10% of time if (!l90 && c >= t * 0.90f) { l90 = b; break; } // exceeded 90% } }float aWeight(float x)Applies the standard A-weighting filter to each sample so the level reflects how loud humans actually perceive that frequency content — the step that turns raw amplitude into dB(A).energySum += pow(10.0, spl / 10.0)Leq is accumulated in the energy domain, not by averaging decibels, so a few loud windows dominate the interval average exactly as they dominate real exposure.hist[bin]++A running 1 dB histogram of window levels lets the percentile levels be read out at the end without storing the whole time series.if (!l10 && c >= t * 0.10f)Walking the histogram from loud to quiet finds the level exceeded 10% of the time (the intrusive peaks) and 90% of the time (the background) — L10 and L90.float CAL_OFFSETThe single calibration constant that maps this microphone-and-ADC chain to true dB(A), set once against a reference so the node agrees with a real meter and with its neighbours.Report levels and flag exceedances
At the end of each interval, transmit Leq, L10, L90 and Lmax (numbers only), compare Leq to the applicable day/night limit, flag exceedances, and reset the accumulators.
Complete Source Code
The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.
/* ═══════════════════════════════════════════════════════════════
Noise Pollution Mapper — ESP32 + INMP441 I2S mic
Measures A-weighted SPL, computes Leq / L10 / L90 / Lmax per
interval entirely on-device, and transmits ONLY the levels — never
audio. Feeds a city noise heat map. Calibrated to a reference dB(A).
══════════════════════════════════════════════════════════════════ */
#include <WiFi.h>
#include <PubSubClient.h>
#include <driver/i2s.h>
#include <Preferences.h>
#include <math.h>
#define I2S_SD 33
#define I2S_WS 25
#define I2S_SCK 32
#define FS 16000 // sample rate
#define WIN 1600 // 100 ms windows
#define INTERVAL_MS 60000UL // 1-minute reporting interval
Preferences prefs;
WiFiClient net;
PubSubClient mqtt(net);
float CAL_OFFSET;
double energySum = 0; uint32_t windows = 0;
uint16_t hist[140]; float lmax = 0;
uint32_t intervalStart = 0;
/* A-weighting IIR (biquad coeffs precomputed for FS = 16 kHz). */
float applyBiquads(float x) {
static float z1a=0,z2a=0,z1b=0,z2b=0;
// Two biquad sections approximating the A-weighting curve.
const float b0a=0.255f,b1a=-0.510f,b2a=0.255f,a1a=-0.734f,a2a=0.181f;
float ya = b0a*x + z1a; z1a = b1a*x - a1a*ya + z2a; z2a = b2a*x - a2a*ya;
const float b0b=1.0f,b1b=-2.0f,b2b=1.0f,a1b=-1.889f,a2b=0.895f;
float yb = b0b*ya + z1b; z1b = b1b*ya - a1b*yb + z2b; z2b = b2b*ya - a2b*yb;
return yb;
}
void i2sInit() {
i2s_config_t cfg = {
.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_RX), .sample_rate=FS,
.bits_per_sample=I2S_BITS_PER_SAMPLE_32BIT,
.channel_format=I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format=I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags=0, .dma_buf_count=8, .dma_buf_len=WIN/4 };
i2s_pin_config_t pins={ I2S_SCK, I2S_WS, I2S_PIN_NO_CHANGE, I2S_SD };
i2s_driver_install(I2S_NUM_0,&cfg,0,NULL);
i2s_set_pin(I2S_NUM_0,&pins);
}
void processWindow(const int32_t *buf,int N){
double ss=0;
for(int i=0;i<N;i++){ float x=applyBiquads((float)(buf[i]>>8)); ss+=(double)x*x; }
float spl=20.0f*log10f(sqrtf(ss/N)+1e-9f)+CAL_OFFSET;
energySum+=pow(10.0,spl/10.0); windows++;
int bin=constrain((int)lroundf(spl),0,139); hist[bin]++;
if(spl>lmax) lmax=spl;
}
void reportInterval(){
if(!windows) return;
float leq=10.0f*log10f(energySum/windows);
uint32_t c=0,t=windows; float l10=0,l90=0;
for(int b=139;b>=0;b--){ c+=hist[b];
if(!l10 && c>=t*0.10f) l10=b;
if(!l90 && c>=t*0.90f){ l90=b; break; } }
bool night = isNight(); // stricter limits at night
float limit = night?45.0f:55.0f; // example dB(A) limits
bool exceed = leq>limit;
char b[200];
snprintf(b,sizeof b,
"{\"node\":1,\"leq\":%.1f,\"l10\":%.0f,\"l90\":%.0f,"
"\"lmax\":%.1f,\"limit\":%.0f,\"exceed\":%d}",
leq,l10,l90,lmax,limit,exceed?1:0);
mqtt.publish("noise/node/1/levels", b); // NUMBERS ONLY — no audio
energySum=0; windows=0; lmax=0;
for(int i=0;i<140;i++) hist[i]=0; // reset for next interval
}
void setup(){
Serial.begin(115200);
prefs.begin("noise",true);
CAL_OFFSET=prefs.getFloat("cal",90.0f); // from reference calibration
prefs.end();
i2sInit();
WiFi.begin(WIFI_SSID,WIFI_PASS);
mqtt.setServer(MQTT_HOST,1883);
intervalStart=millis();
}
void loop(){
if(!mqtt.connected()&&WiFi.status()==WL_CONNECTED) mqtt.connect("noise-1");
mqtt.loop();
static int32_t buf[WIN];
size_t br;
i2s_read(I2S_NUM_0,buf,sizeof buf,&br,portMAX_DELAY);
processWindow(buf,br/4); // one ~100 ms window
if(millis()-intervalStart>=INTERVAL_MS){
reportInterval();
intervalStart=millis();
}
}
Configuration & Calibration
Configuration steps
- Set CAL_OFFSET from a reference calibration so the node reports true dB(A).
- Set the day/night dB(A) limits and the day/night boundary times for your jurisdiction.
- Choose the reporting interval (1 min is common; 15 min Leq is also standard for surveys).
- Select Wi-Fi/MQTT or LoRa, and confirm the payload carries levels only, never audio.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
Reference offset
Place the node beside a calibrated sound-level meter (or use an acoustic calibrator) across a range of levels and set CAL_OFFSET so the node's dB(A) matches the reference.
A-weighting check
Verify with tones that the node discounts very low and very high frequencies relative to mid frequencies, confirming the A-weighting filter is working.
Wind/self-noise floor
In a quiet space confirm the node's noise floor is well below the levels you care about, and that the windscreen suppresses wind-induced false levels outdoors.
Network Architecture & Connectivity
Communication protocol
Each node publishes Leq/L10/L90/Lmax and an exceedance flag per interval. The payload is numeric only — audio never leaves the device — so a dense public network maps loudness without any surveillance capability.
| Topic / endpoint | Direction | Payload |
|---|---|---|
noise/node/1/levels | node → broker | Leq, L10, L90, Lmax, limit, exceed |
noise/node/1/status | node → broker | battery, RSSI, calibration date |
noise/node/1/config | broker → node | limits, interval, calibration offset |
Message contract between the device and the broker.
Cloud platform configuration
A broker feeds a heat map that renders Leq by location and hour, so a city sees which streets breach limits and when — rush-hour roads versus late-night nightlife strips emerge clearly.
Dashboard setup
A time-sliderable heat map plus per-node daily curves of Leq/L10/L90, with exceedance shading against the day/night limits.
Mobile app integration
Alerts when a monitored location exceeds its night or day limit, useful for residents and for compliance monitoring.
Security considerations
- Sign each node's levels so the public map cannot be spoofed with fake quiet/loud readings.
- Guarantee and document that no audio is transmitted or stored — the privacy property is the network's social licence.
- Authenticate configuration/calibration pushes to nodes.
Testing Procedure & Expected Output
Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.
| Test | What you should see |
|---|---|
| Play a 1 kHz tone at a known level | Node dB(A) matches the reference meter after calibration |
| Play equal-energy low and high tones | A-weighting discounts them relative to mid frequencies |
| Create brief loud events amid quiet | Lmax and L10 rise; L90 (background) stays low |
| Run a steady loud source | L90 rises toward Leq — persistently loud signature |
| Inspect the transmitted payload | Only levels present; no audio field anywhere |
| Compare day vs night intervals | Correct (stricter) night limit applied to exceedance flag |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
The heat map colours each node by Leq for the selected hour; a node view shows Leq/L10/L90/Lmax over the day and marks limit exceedances.
{
"node": 1,
"leq": 63.4,
"l10": 68,
"l90": 54,
"lmax": 81.2,
"limit": 55,
"exceed": 1
}
Here the interval Leq of 63.4 dB(A) exceeds the 55 dB(A) day limit, with an L90 of 54 (persistently busy) and an L10 of 68 (frequent traffic peaks) — a genuinely loud street, not one quiet stretch spoiled by a single event.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Stream audio via I²S DMA and process per buffer; the ESP32 handles A-weighting and RMS in real time without storing audio.
- Use a running histogram for percentiles instead of buffering the time series — constant memory per interval.
- Report compact numeric packets once per interval; the heavy lifting is already done on-device.
- On solar, the mic and CPU are the main draw; a modest panel covers continuous operation.
- Replace every
delay()with amillis()comparison — blocking delays are the single most common cause of dropped readings. - Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
- Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
- Use
uint8_t/uint16_twhere the range allows; on an 8-bit AVR a 32-bit add costs four times as much. - Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
- Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
- For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
Safety Precautions
- Publish and honour the privacy design: levels only, no audio recorded or transmitted — this is the node's social licence to sit in public space.
- Present readings as indicative unless certified hardware is used; a MEMS node is below Class-1/2 metrology.
- Mount nodes safely at height, clear of traffic and tampering.
- Keep the lithium battery and charger sheltered from weather.
- Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 °C, and never leave a charging pack unattended on a wooden desk.
- Never power an RF module without its antenna fitted — the reflected power destroys the output stage. Check your local licence-free band and duty-cycle limits before transmitting.
- Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
- Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
- Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.
Maintenance
- Re-verify the reference calibration periodically; microphone sensitivity can drift.
- Inspect and replace the windscreen as it degrades; a perished windscreen lets wind noise back in.
- Check the mic port for dust/insects blocking the acoustic path.
- Confirm day/night limits and clock sync remain correct so exceedance flags stay meaningful.
- Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
- Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life — replace it.
- Wash the panel every few weeks in dusty conditions; a visible dust film costs 15–25 % of the harvest.
- Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
- Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
- Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.
Future Improvements & Upgrades
A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.
- Add coarse spectral bands (octave levels) for source classification without recording audio.
- Run an on-device classifier to tag noise type (traffic, aircraft, music) from spectral features, still transmitting no audio.
- Fuse many nodes with a propagation model for a continuous city noise surface between sensors.
- Correlate exceedances with traffic and event calendars to attribute and target interventions.
- Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
- Add over-the-air firmware updates so you never have to physically reach a deployed node again.
- Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
- Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
- Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
- Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.
Frequently Asked Questions
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.