Contents — 26 sections
Project Overview
Runs streetlights dusk-to-dawn and dims them when no one is around — cutting a municipality's biggest controllable electricity bill while keeping streets safe.
Street lighting is often a municipality's single largest electricity expense, and much of that energy is wasted: lights burning at full brightness on empty streets at 3 a.m., lights left on after dawn or switched on before dusk because a timer drifted, and lights that fail unnoticed until someone complains. This project builds a smarter streetlight controller that attacks all of that: it switches precisely dusk-to-dawn from actual light levels, dims when no one is around and brightens when they are, and reports faults — cutting the bill substantially while keeping the street appropriately lit.
The two big savings are accurate dusk-to-dawn switching and adaptive dimming. A light sensor switches the lamp on at real dusk and off at real dawn (not a fixed clock that is wrong half the year and drifts), eliminating the hours of daylight burning that fixed timers waste. Then, during the night, a motion sensor (PIR or radar) lets the light run dimmed when the street is empty and ramp to full brightness when a person or vehicle approaches — so the street is brightly lit exactly when someone is there to use it, and sips power the rest of the time. Because a dimmed LED uses proportionally less energy, and streets are empty most of the night, the saving is large.
Beyond energy, per-light intelligence enables fault reporting — a light that draws no current when it should be on has failed, and the controller can report it (with its location) so maintenance is proactive instead of complaint-driven — and, networked, a city-wide picture of energy, brightness and faults. It measures its own consumption to prove the savings. The design keeps safety first: dimming respects minimum lighting levels for the road type so streets are never unsafely dark, and it fails to a safe (on) state. It is honest that lighting levels are governed by standards and that dimming policy is a municipal decision, and that a real deployment integrates with the lighting infrastructure properly. But as a dusk-to-dawn, adaptively-dimming, fault-reporting controller, it turns a large, wasteful, static electricity bill into a managed one — saving money while keeping the light where and when it is needed.
What this project does
- Switches lights precisely dusk-to-dawn from actual light levels
- Dims when the street is empty and brightens on motion
- Cuts energy while keeping streets appropriately lit
- Reports faults (a light drawing no current when on) with location
- Measures consumption to prove the savings
- Respects minimum lighting levels and fails safe (on)
- Networks for a city-wide energy/brightness/fault picture
Real-World Applications
| Setting | How it is used |
|---|---|
| Municipal street lighting | Cutting the biggest controllable power bill with dusk-to-dawn + adaptive dimming. |
| Campus / industrial estates | Adaptive lighting of roads and car parks with fault reporting. |
| Highways / rural roads | Motion-brightening lighting where traffic is sparse most of the night. |
| Smart-city lighting networks | Central control, energy and fault visibility across a city. |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- Accurate dusk-to-dawn (no drifting timer waste)
- Adaptive motion-based dimming
- Large energy saving (dimmed LED + empty streets)
- Proactive fault reporting
- Consumption measurement / savings proof
- Safety: minimum levels, fail-safe on
- City-wide networked control
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Intermediate |
| Estimated completion time | 10–16 hours |
| Indicative build cost | ₹2,500 – ₹4,500 per light |
| Primary discipline | Energy |
| Reference platform | ESP32 DevKit V1 (ESP-WROOM-32) |
Skills you should have (or will pick up)
- Light-level (dusk/dawn) sensing and hysteresis
- Motion sensing and adaptive dimming (PWM/0-10V)
- Current sensing for fault detection
- Minimum-level/fail-safe design
- Networked reporting and savings measurement
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 |
| LDR (GL5528 photoresistor) + 10 kΩ divider Cheap and rugged, but non-linear and unit-to-unit variation is large. | 10–20 kΩ at 10 lx, 1 MΩ dark, peak response 540 nm | 1 | ₹15 |
| HC-SR501 PIR motion sensor Give it 60 s to settle after power-up or it fires false triggers. | 3–7 m range, 110° cone, 0.3–200 s adjustable hold, 5 µA idle | 1 | ₹80 |
| ACS712 hall-effect current sensor (20 A) Zero-offset drifts with temperature — re-zero at boot with no load. | ±20 A, 100 mV/A, 80 kHz bandwidth, 1.2 mΩ internal resistance, 2.1 kV isolation | 1 | ₹180 |
| 5 V single-channel opto-isolated relay module Cut the JD-VCC jumper and feed the coil separately for true isolation. | SPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input | 1 | ₹90 |
| 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 |
| 5 V 3 A regulated SMPS adapter Measure the real output — many "3 A" adapters sag below 4.7 V at 2 A. | 100–240 VAC in, 5 V ±5 % out, 3 A, short-circuit and over-voltage protection | 1 | ₹350 |
| Dimmable LED driver interface Match to the luminaire's dimming standard | 0-10 V / PWM / DALI dimming interface to the luminaire driver | 1 | ₹600 |
| Motion sensor (PIR/radar) | Radar preferred outdoors for range/reliability | 1 | ₹400 |
| Current sensor | To confirm the light is drawing (fault detection) | 1 | ₹200 |
| Pole enclosure + LoRa | Outdoor enclosure and LoRa for city networking | 1 | ₹900 |
Estimated total: ₹3,995, 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 |
| LDR (GL5528 photoresistor) + 10 kΩ divider | 10–20 kΩ at 10 lx, 1 MΩ dark, peak response 540 nm | any (passive) | Analogue divider | Datasheet |
| HC-SR501 PIR motion sensor | 3–7 m range, 110° cone, 0.3–200 s adjustable hold, 5 µA idle | 4.5–20 V | Digital high on motion | Datasheet |
| ACS712 hall-effect current sensor (20 A) | ±20 A, 100 mV/A, 80 kHz bandwidth, 1.2 mΩ internal resistance, 2.1 kV isolation | 5 V | Analogue | Datasheet |
| 5 V single-channel opto-isolated relay module | SPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input | 5 V coil | Digital (active-low) | 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 |
| 5 V 3 A regulated SMPS adapter | 100–240 VAC in, 5 V ±5 % out, 3 A, short-circuit and over-voltage protection | 5 V | DC barrel / USB | 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. |
| LDR (GL5528 photoresistor) + 10 kΩ divider | any (passive) | 0.3 | Cheap and rugged, but non-linear and unit-to-unit variation is large. |
| HC-SR501 PIR motion sensor | 4.5–20 V | 0.05 | Give it 60 s to settle after power-up or it fires false triggers. |
| ACS712 hall-effect current sensor (20 A) | 5 V | 10 | Zero-offset drifts with temperature — re-zero at boot with no load. |
| 5 V single-channel opto-isolated relay module | 5 V coil | 70 | Cut the JD-VCC jumper and feed the coil separately for true isolation. |
| 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. |
| 5 V 3 A regulated SMPS adapter | 5 V | 3000 | Measure the real output — many "3 A" adapters sag below 4.7 V at 2 A. |
Summed typical draw is 3380.35 mA. With a 1.5× design margin the supply should deliver at least 5100 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" |
| LoRa (sandeepmistry) 0.8.0 | SX127x radio configuration, packet TX/RX and callbacks. | Library Manager → "LoRa" by Sandeep Mistry |
| NTPClient / configTime bundled | Wall-clock time from an NTP server for timestamping. | Bundled (`configTime()` on ESP32) |
| ArduinoJson 7.x | Zero-allocation JSON serialisation and parsing. | Library Manager → "ArduinoJson" by Benoit Blanchon |
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 |
|---|---|---|---|
| Light sensor (LDR) | AOUT | GPIO 34 | Dusk/dawn level |
| Motion (PIR/radar) | OUT | GPIO 27 | Presence |
| Current sensor | AOUT | GPIO 35 | Lamp current (fault) |
| Dimming (0-10V/PWM) | ctrl | GPIO 25 | Brightness to driver |
| Relay/contactor | IN | GPIO 26 | Lamp on/off |
| LoRa | SPI | GPIO 18/19/23/5 | City network |
| OLED | SDA/SCL | GPIO 21/22 | Status |
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
- Switch the lamp on/off from real light levels (LDR) with hysteresis so it does not flicker at dusk/dawn.
- Drive the luminaire's dimming input (0-10 V/PWM/DALI) to set brightness; match the luminaire's dimming standard.
- Sense lamp current to confirm the light is actually drawing when commanded on — the basis of fault detection.
- Respect minimum lighting levels for the road and fail to a safe (on) state if sensing fails.
- Use an outdoor pole enclosure and LoRa for city-wide networking; integrate with the lighting infrastructure properly.
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
Street lighting is a large, mostly static and wasteful load, and the optimiser's value comes from making it responsive in two dimensions — when it is on and how bright it is — while never compromising the safety the lighting exists to provide. The two biggest wastes are lights burning during daylight (from timers that drift or are mis-set) and lights burning at full brightness on empty streets. Fixing both, per light and city-wide, converts a fixed bill into a managed one, and the savings are large because the waste is large.
The first win is accurate dusk-to-dawn switching from real light levels. A light sensor turns the lamp on when it genuinely gets dark and off when it genuinely gets light, with hysteresis so passing clouds do not make it flicker at the threshold. This beats a fixed timer, which is right only twice a year and drifts, wasting hours of daylight burning across a season — hours multiplied by thousands of lights. Getting the on/off boundary right, automatically and locally, is a substantial saving before any dimming.
The second, larger win is adaptive dimming. A dimmed LED consumes proportionally less power, and a street is empty for most of the night — so running the light at a reduced (but safe) level when no one is present, and ramping it to full brightness when a motion sensor detects a person or vehicle approaching, delivers bright light exactly when it is needed and low power the rest of the time. The critical constraint, and the safety principle, is that dimming must respect the minimum lighting level appropriate to the road type: the street is never made unsafely dark, only reduced within safe limits, and the system fails to a safe (on/full) state if a sensor fails. Dimming policy — how low, and on what roads — is a municipal decision within lighting standards, which the design honours rather than overrides.
Per-light intelligence unlocks two further benefits. Fault reporting: by sensing whether the lamp actually draws current when commanded on, the controller knows if a light has failed and can report it with its location, so maintenance becomes proactive (fix the dark light before residents complain) instead of reactive — a major operational saving on top of the energy one. And measured savings: by metering its own consumption, the system proves the reduction, which is what justifies the investment to a municipality and lets it verify each policy. Networked over LoRa, all of this scales to a city-wide view — energy, brightness, and faults across every light, centrally controllable. The design keeps safety and honesty central — minimum levels respected, fail-safe on, standards and municipal policy honoured, proper integration with the lighting infrastructure — but within that, it does exactly what a city needs: it keeps the streets appropriately lit while cutting one of the largest, most wasteful bills a municipality carries.
The maths behind it
Dusk-to-dawn with hysteresis
From the light sensor level L:
turn ON when L < L_dark (dusk)
turn OFF when L > L_light (dawn), with L_light > L_dark
Hysteresis prevents flicker from clouds at the threshold.
Real-level switching avoids daylight burning of drifting timers.
Adaptive dimming energy saving
Brightness B: full when motion, else dim (≥ B_min):
power ∝ B (LED)
night energy ≈ Σ (B_full·t_occupied + B_dim·t_empty)
Streets empty most of the night → large saving, while
B_dim ≥ minimum safe level keeps the street safe.
Fault detection
Commanded ON but lamp draws no current → failed:
fault if (commanded_on AND I_lamp < I_min)
report fault with light location → proactive maintenance.
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.
Set up switching, dimming and current sensing
Connect the light sensor (with hysteresis) for dusk-to-dawn, the motion sensor for presence, the dimming interface to the luminaire driver, and a current sensor for fault detection.
Configure the minimum safe brightness for the road type and fail-safe (on) behaviour.
Set up energy measurement and networking
Measure consumption to prove savings, and add LoRa for city-wide reporting/control.
Set up fault reporting
Report a located fault when a commanded-on light draws no current, for proactive maintenance.
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.
Control on/off and adaptive brightness
Switch dusk-to-dawn with hysteresis, set brightness to full on motion or the safe dim level when empty, and detect faults from current.
cppstreetlight.ino#define L_DARK 800 // ADC: below = dark (dusk) #define L_LIGHT 1200 // ADC: above = light (dawn); hysteresis gap #define B_MIN 40 // % minimum safe brightness #define B_FULL 100 // % #define I_MIN 0.05f // A: below when on = fault #define MOTION_HOLD_MS 30000 bool lampOn=false; uint32_t lastMotion=0; int decide(int light, bool motion, uint32_t now, bool &on){ // dusk-to-dawn with hysteresis if (light < L_DARK) on = true; else if (light > L_LIGHT) on = false; // else keep previous (hysteresis) else on = lampOn; if (!on) return 0; // day: off if (motion) lastMotion = now; bool occupied = (now - lastMotion) < MOTION_HOLD_MS; return occupied ? B_FULL : B_MIN; // never below safe minimum } const char* checkFault(bool on, float iLamp){ if (on && iLamp < I_MIN) return "lamp failed (no current)"; return nullptr; }if (light < L_DARK) on = true;The lamp switches on at real darkness and off at real daylight, with a hysteresis gap so clouds do not cause flicker — no wasted daylight burning.return occupied ? B_FULL : B_MINBrightness is full when the street is occupied (recent motion) and the safe minimum when empty — bright when needed, low power otherwise, never below the safe level.bool occupied = (now - lastMotion) < MOTION_HOLD_MSA hold time keeps the light bright for a while after motion, so it does not drop while someone is still passing.if (on && iLamp < I_MIN) return "lamp failed (no current)"A commanded-on lamp drawing no current has failed, enabling proactive, located fault reporting.Drive brightness, measure and report
Set the dimming output and lamp relay per the decision, measure consumption to prove savings, report energy/brightness/faults over LoRa, and fail safe (on) if sensing fails.
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.
/* ═══════════════════════════════════════════════════════════════
Streetlight Energy Optimizer — ESP32
Dusk-to-dawn from real light levels, adaptive motion dimming (never
below the safe minimum), fault reporting from lamp current, and
measured savings. Fails safe (full on). Networked over LoRa.
══════════════════════════════════════════════════════════════════ */
#include <WiFi.h>
#include <PubSubClient.h>
#include <LoRa.h>
#include <SPI.h>
#define PIN_LDR 34
#define PIN_MOTION 27
#define PIN_ILAMP 35
#define PIN_DIM 25 // 0-10V/PWM to driver
#define PIN_RELAY 26
#define L_DARK 800
#define L_LIGHT 1200
#define B_MIN 40
#define B_FULL 100
#define I_MIN 0.05f
#define MOTION_HOLD_MS 30000
WiFiClient net; PubSubClient mqtt(net);
bool lampOn=false; uint32_t lastMotion=0; double energyWh=0; uint32_t lastMs=0;
const char *LIGHT_ID = "SL-4412";
void setBrightness(int pct){ ledcWrite(0, pct*1023/100); }
void setup(){
Serial.begin(115200);
pinMode(PIN_MOTION, INPUT); pinMode(PIN_RELAY, OUTPUT);
ledcSetup(0, 1000, 10); ledcAttachPin(PIN_DIM, 0);
SPI.begin(); LoRa.setPins(5,14,2); LoRa.begin(433E6);
WiFi.begin(WIFI_SSID,WIFI_PASS); mqtt.setServer(MQTT_HOST,1883);
lastMs=millis();
}
void loop(){
if(!mqtt.connected()&&WiFi.status()==WL_CONNECTED) mqtt.connect("sl-1");
mqtt.loop();
uint32_t now=millis(); float dtHr=(now-lastMs)/3600000.0f; lastMs=now;
int light = analogRead(PIN_LDR);
bool motion = digitalRead(PIN_MOTION)==HIGH;
float iLamp = analogRead(PIN_ILAMP)/4095.0f * I_FS; // A
bool sensorsOk = true; // (validate reads)
// dusk-to-dawn with hysteresis
if (light < L_DARK) lampOn = true;
else if (light > L_LIGHT) lampOn = false;
int brightness = 0;
if (lampOn || !sensorsOk){ // fail-safe: on
if (motion) lastMotion = now;
bool occupied = (now - lastMotion) < MOTION_HOLD_MS;
brightness = (!sensorsOk) ? B_FULL : (occupied ? B_FULL : B_MIN);
}
digitalWrite(PIN_RELAY, brightness>0 ? HIGH : LOW);
setBrightness(brightness);
// energy + fault
float watts = (brightness/100.0f) * LAMP_W;
energyWh += watts * dtHr;
const char* fault = (brightness>0 && iLamp < I_MIN) ? "lamp failed" : nullptr;
char m[220];
snprintf(m,sizeof m,
"{\"id\":\"%s\",\"on\":%s,\"bright\":%d,\"kWh\":%.3f,"
"\"fault\":\"%s\"}",
LIGHT_ID, lampOn?"true":"false", brightness, energyWh/1000.0,
fault?fault:"none");
LoRa.beginPacket(); LoRa.print(m); LoRa.endPacket(); // city network
if (fault) mqtt.publish("streetlight/fault", m);
delay(1000);
}
Configuration & Calibration
Configuration steps
- Set the dusk/dawn light thresholds (with hysteresis), the minimum safe brightness, and the motion hold time.
- Configure the dimming interface (0-10V/PWM/DALI), current-fault threshold and lamp wattage.
- Set fail-safe (on) behaviour and the road-type minimum level per lighting policy/standards.
- Configure LoRa networking, energy measurement and fault reporting.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
Dusk/dawn
Set the light thresholds so switching matches real dusk/dawn with adequate hysteresis (no cloud flicker).
Dimming/minimum
Verify the dim level meets the road's minimum lighting standard and the motion-brighten works with adequate hold time.
Fault/energy
Calibrate current sensing for fault detection and consumption for savings measurement.
Network Architecture & Connectivity
Communication protocol
Each light reports state, brightness, energy and faults; the city management can adjust policy (dim levels, schedules) and see energy/faults across the network.
| Topic / endpoint | Direction | Payload |
|---|---|---|
streetlight/<id>/status | light → mgmt | on, brightness, energy |
streetlight/<id>/fault | light → maintenance | failed lamp (located) |
streetlight/policy | mgmt → lights | dim levels, schedules |
Message contract between the device and the broker.
Cloud platform configuration
A lighting-management system maps every light's state, energy and faults, quantifies savings, and pushes dimming/schedule policy — a city-wide managed lighting network.
Dashboard setup
A city map of lights by state/brightness, energy and savings, and a fault list for proactive maintenance.
Mobile app integration
Fault alerts (failed lamps) and energy/savings summaries.
Security considerations
- Keep minimum-level and fail-safe behaviour local and safe, independent of the network.
- Authenticate policy pushes so only the municipality can change dimming/schedules.
- Alert on light silence — a non-reporting light may be failed.
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 |
|---|---|
| Cover/uncover the light sensor | Switches on at dark, off at light, with hysteresis |
| Trigger motion at night | Brightens to full; dims after the hold time when empty |
| Empty street at night | Runs at the safe minimum brightness (energy saved) |
| Disable the lamp (no current) | Fault reported with the light's ID |
| Simulate a sensor fault | Fails safe to full brightness |
| Measure over a night | Consumption/savings quantified |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
The city dashboard shows each light's on/brightness state, energy and faults on a map; savings are quantified against always-full-brightness.
{
"id": "SL-4412",
"on": true,
"bright": 40,
"kWh": 0.183,
"fault": "none"
}
A light running at 40% (safe minimum) on an empty street at night — brightening to full when someone approaches — with its energy measured; a failed lamp would appear as a located fault for proactive repair.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Simple local control loop; dusk-to-dawn and dimming need modest rates.
- Keep minimum-level/fail-safe local and safe.
- Report state/energy/faults over LoRa on a slow cadence, faults immediately.
- Measure consumption to prove savings.
- 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
- Dimming must respect the road's minimum lighting level — never make a street unsafely dark; fail to safe (full on).
- Lighting levels are standards-governed and dimming policy is a municipal decision; honour both.
- Integrate with the lighting infrastructure and mains safely (qualified work for power).
- Fault-report failed lamps for maintenance; do not leave dark spots.
- Mains voltage kills. Anything on the load side of the relay is at 230 V. Do not work on a powered circuit, and never leave exposed mains wiring on a bench where someone could touch it.
- Keep at least 6 mm of creepage between the mains and low-voltage sides of any board you make, and never route mains tracks under the microcontroller.
- Have a qualified electrician do the final installation into a consumer unit or wall fitting. In most jurisdictions this is a legal requirement, not a suggestion.
- Fit an RCD/RCBO upstream and fuse the load appropriately for its rating.
- 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
- Verify dusk/dawn thresholds and minimum-level compliance seasonally.
- Act on fault reports; the point is proactive maintenance.
- Check motion-sensor range/coverage and dimming operation.
- Review measured savings and adjust policy.
- Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
- 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 group/predictive brightening (light the path ahead of a moving vehicle).
- Add per-light scheduling and astronomical-clock backup.
- Add power-quality/energy analytics across the network.
- Integrate with traffic/occupancy data for smarter policy.
- 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.
- Street lighting and energy efficiencyReference
- Adaptive/dimming street lightingReference
- Road lighting standards (minimum levels)Reference
- LED dimming (0-10V/DALI/PWM)Reference
- Photocell dusk-to-dawn controlReference