Siddhant Kumar
Project 074 · Energy

Streetlight Energy Optimizer.

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.

Intermediate 10–16 hours 30 min read LightingEnergyAutomation
Jump to source Bill of materials
Streetlight Energy Optimizer — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
10–16 hours
Indicative cost
₹2,500 – ₹4,500 per light
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Energy
Last updated
28 July 2026
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.

An LED street light on a pole
Adaptive streetlights cut a city's biggest controllable bill while keeping streets safely lit. Photograph sourced from Wikimedia Commons — LED street light.jpg. Reused under the licence stated on that page; please check it before republishing.

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

SettingHow it is used
Municipal street lightingCutting the biggest controllable power bill with dusk-to-dawn + adaptive dimming.
Campus / industrial estatesAdaptive lighting of roads and car parks with fault reporting.
Highways / rural roadsMotion-brightening lighting where traffic is sparse most of the night.
Smart-city lighting networksCentral 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

AttributeValue
Difficulty levelIntermediate
Estimated completion time10–16 hours
Indicative build cost₹2,500 – ₹4,500 per light
Primary disciplineEnergy
Reference platformESP32 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.

ComponentKey specificationQtyApprox. 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 DAC1₹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 nm1₹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 idle1₹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 isolation1₹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 input1₹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²C1₹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–SF121₹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 protection1₹350
Dimmable LED driver interface
Match to the luminaire's dimming standard
0-10 V / PWM / DALI dimming interface to the luminaire driver1₹600
Motion sensor (PIR/radar)Radar preferred outdoors for range/reliability1₹400
Current sensorTo confirm the light is drawing (fault detection)1₹200
Pole enclosure + LoRaOutdoor enclosure and LoRa for city networking1₹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

PartSpecificationSupplyInterfaceReference
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 DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
LDR (GL5528 photoresistor) + 10 kΩ divider10–20 kΩ at 10 lx, 1 MΩ dark, peak response 540 nmany (passive)Analogue dividerDatasheet
HC-SR501 PIR motion sensor3–7 m range, 110° cone, 0.3–200 s adjustable hold, 5 µA idle4.5–20 VDigital high on motionDatasheet
ACS712 hall-effect current sensor (20 A)±20 A, 100 mV/A, 80 kHz bandwidth, 1.2 mΩ internal resistance, 2.1 kV isolation5 VAnalogueDatasheet
5 V single-channel opto-isolated relay moduleSPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input5 V coilDigital (active-low)Datasheet
0.96″ SSD1306 OLED display128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C3.3–5 VI²C (0x3C)Datasheet
SX1278 LoRa 433 MHz module (Ra-02)−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF123.3 VSPIDatasheet
5 V 3 A regulated SMPS adapter100–240 VAC in, 5 V ±5 % out, 3 A, short-circuit and over-voltage protection5 VDC barrel / USBDatasheet

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.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
LDR (GL5528 photoresistor) + 10 kΩ dividerany (passive)0.3Cheap and rugged, but non-linear and unit-to-unit variation is large.
HC-SR501 PIR motion sensor4.5–20 V0.05Give it 60 s to settle after power-up or it fires false triggers.
ACS712 hall-effect current sensor (20 A)5 V10Zero-offset drifts with temperature — re-zero at boot with no load.
5 V single-channel opto-isolated relay module5 V coil70Cut the JD-VCC jumper and feed the coil separately for true isolation.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.
SX1278 LoRa 433 MHz module (Ra-02)3.3 V120Never power the radio without an antenna — the PA will destroy itself.
5 V 3 A regulated SMPS adapter5 V3000Measure 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.json under 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 dialout group: sudo usermod -aG dialout $USER and 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

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
LoRa (sandeepmistry) 0.8.0SX127x radio configuration, packet TX/RX and callbacks.Library Manager → "LoRa" by Sandeep Mistry
NTPClient / configTime bundledWall-clock time from an NTP server for timestamping.Bundled (`configTime()` on ESP32)
ArduinoJson 7.xZero-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.

Streetlight Energy Optimizer — system block diagramFunctional block diagram of the Streetlight Energy Optimizer system. SenseLight leveldusk/dawnMotionpresenceCurrentfaultDecideESP32on + brightnessMin levelfail-safe onActuateDimbrightnessSwitchon/offReportEnergy/savingsmeasuredFaultlocatedrightrightnone
Streetlight Energy Optimizer — system block diagram

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.

Streetlight Energy Optimizer — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBLight sensor (LDR)GPIO 34Dusk/dawn levelMotion (PIR/radar)GPIO 27PresenceCurrent sensorGPIO 35Lamp current (fault)Dimming (0-10V/PWM)GPIO 25Brightness to driverRelay/contactorGPIO 26Lamp on/offLoRaGPIO 18/19/23/5City networkOLEDGPIO 21/22Status
Streetlight Energy Optimizer — wiring schematic
PeripheralPeripheral pinController pinSignal
Light sensor (LDR)AOUTGPIO 34Dusk/dawn level
Motion (PIR/radar)OUTGPIO 27Presence
Current sensorAOUTGPIO 35Lamp current (fault)
Dimming (0-10V/PWM)ctrlGPIO 25Brightness to driver
Relay/contactorINGPIO 26Lamp on/off
LoRaSPIGPIO 18/19/23/5City network
OLEDSDA/SCLGPIO 21/22Status

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.
An ESP32 development board with the ESP-WROOM-32 module and USB connector
ESP32 module switching dusk-to-dawn and dimming to a safe minimum when the street is empty. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

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.

Streetlight Energy Optimizer — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · HC-SR501 PIR motion sensor · ACS712hall-effect current sensor (20 A)Driver layerwifi · pubsub · ssd1306 · lorolibApplication logicsampling loop · filtering · thresholds · state machineTransport layerLoRa → city lighting management · TLS · retry and backoffPresentation layerdashboard · mobile notifications · historical charts
Streetlight Energy Optimizer — architecture stack

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

plainDusk-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

plainAdaptive 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

plainFault 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.

Streetlight Energy Optimizer — firmware flowchartControl flow through the main program loop. Read light level, motion,currentDark (dusk-to-dawn)?Lamp onLamp off (day)Lamp onLamp off (day)Motion present?Full brightnessDim to minimum levelFull brightnessDim to minimum levelOn but no current? → faultReport fault (located)Report energyReport fault (located)Report energy
Streetlight Energy Optimizer — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

  1. 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.

  2. Set up energy measurement and networking

    Measure consumption to prove savings, and add LoRa for city-wide reporting/control.

  3. 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.

  1. 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.
  2. 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.

cppstreetlight-optimizer.ino
/* ═══════════════════════════════════════════════════════════════
   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);
}
if (light < L_DARK) lampOn = true; else if (light > L_LIGHT) lampOn = false;Real-light-level dusk-to-dawn with a hysteresis gap avoids the daylight burning and flicker of a drifting timer.
if (lampOn || !sensorsOk){ // fail-safe: onIf sensing fails, the light stays on at full — a controller fault must never leave a street dark.
brightness = (!sensorsOk) ? B_FULL : (occupied ? B_FULL : B_MIN)Brightness is full when occupied (or on fault) and the safe minimum when empty — the adaptive dimming that saves energy without unsafe darkness.
energyWh += watts * dtHrThe controller meters its own consumption so the savings from dusk-to-dawn and dimming are measured and provable.
const char* fault = (brightness>0 && iLamp < I_MIN)A commanded-on light drawing no current is a failed lamp, reported (with its ID) for proactive maintenance.

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.

  1. Dusk/dawn

    Set the light thresholds so switching matches real dusk/dawn with adequate hysteresis (no cloud flicker).

  2. Dimming/minimum

    Verify the dim level meets the road's minimum lighting standard and the motion-brighten works with adequate hold time.

  3. Fault/energy

    Calibrate current sensing for fault detection and consumption for savings measurement.

Network Architecture & Connectivity

Streetlight Energy Optimizer — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsStreetlightESP32Other lightscity gridLoRaCity gatewayto managementMQTTLighting managementenergy/brightness/faultsDashboardmap + energyMaintenancefaults
Streetlight Energy Optimizer — network topology

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 / endpointDirectionPayload
streetlight/<id>/statuslight → mgmton, brightness, energy
streetlight/<id>/faultlight → maintenancefailed lamp (located)
streetlight/policymgmt → lightsdim 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.

TestWhat you should see
Cover/uncover the light sensorSwitches on at dark, off at light, with hysteresis
Trigger motion at nightBrightens to full; dims after the hold time when empty
Empty street at nightRuns at the safe minimum brightness (energy saved)
Disable the lamp (no current)Fault reported with the light's ID
Simulate a sensor faultFails safe to full brightness
Measure over a nightConsumption/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.

jsonstreetlight.json
{
  "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.

A city skyline at night
Networked, it gives a city-wide view of energy, brightness and faults for proactive management. Photograph sourced from Wikimedia Commons — Smart city.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Light flickers at dusk/dawn

Likely cause. No hysteresis

Fix. Add a hysteresis gap between on and off thresholds

Street too dark when dimmed

Likely cause. Dim level below the safe minimum

Fix. Raise the minimum brightness to the road standard; dimming must stay safe

Doesn't brighten in time

Likely cause. Motion range/hold too short

Fix. Use a longer-range sensor (radar); increase the hold time

Faults not detected

Likely cause. No current sensing

Fix. Sense lamp current; report when on but drawing nothing

Dark street on controller fault

Likely cause. Not failing safe

Fix. Fail to full brightness on any sensor/controller fault

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

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 a millis() 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_t where 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

Where do the savings come from?

Two places: accurate dusk-to-dawn switching (no daylight burning from drifting timers) and adaptive dimming (a dimmed LED uses proportionally less power, and streets are empty most of the night). Together they cut a large, wasteful bill substantially.

Is it safe to dim the streets?

Yes, within limits. Dimming never goes below the road type's minimum safe lighting level, the light brightens to full when someone is present, and the system fails to full on if a sensor fails. Safety and standards come first.

How does fault reporting work?

By sensing whether the lamp actually draws current when commanded on. A commanded-on light drawing nothing has failed, and it is reported with its location so maintenance is proactive instead of waiting for complaints.

Why not just use a timer?

A fixed timer is right only twice a year and drifts, wasting hours of daylight burning across thousands of lights. Switching from real light levels eliminates that waste automatically.

How is this managed across a city?

Each light networks over LoRa to a lighting-management system that maps state, energy and faults and pushes dimming/schedule policy — a centrally managed, measurable lighting network.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. Street lighting and energy efficiencyReference
  2. Adaptive/dimming street lightingReference
  3. Road lighting standards (minimum levels)Reference
  4. LED dimming (0-10V/DALI/PWM)Reference
  5. Photocell dusk-to-dawn controlReference