Contents — 26 sections
Project Overview
Senses whether every individual bay is occupied and guides drivers straight to a free one — cutting the circling that wastes fuel, time and tempers, and clogs streets.
A surprising share of urban traffic is not going anywhere — it is drivers circling, looking for parking. Studies in busy districts have found that a third or more of moving cars at peak times are just hunting for a space, burning fuel, wasting time, and adding congestion and emissions to already-crowded streets. The reason is simple: a driver cannot see which bays are free until they are next to them. This project fixes that with a bay-level occupancy grid — a sensor in (or over) every parking bay that knows whether it is occupied — feeding real-time availability that guides drivers straight to a free spot instead of circling.
Each bay gets an occupancy sensor — a magnetometer that detects the large steel mass of a car, an ultrasonic/IR sensor that senses a vehicle above/in front of it, or a similar detector — that reliably reports occupied or free. These per-bay states aggregate into a live map of availability, which drives guidance: dynamic signs at entrances and junctions showing how many spaces are free on each level or zone and pointing toward them, and app/map data so a driver knows before they even arrive. The effect is to replace blind circling with direct routing — the driver goes to where the space actually is.
Beyond guidance, bay-level data enables enforcement and analytics (over-stays, occupancy patterns, pricing), and, networked, a city-wide parking picture. Sensors are typically battery-powered and wireless (LoRa) because running power and cable to every bay is impractical, so long battery life and reliable low-power communication matter. The design is honest that per-bay accuracy depends on the sensor type and siting (a magnetometer must reject neighbouring cars; an overhead sensor needs a clear view), that a whole grid is an infrastructure deployment, and that guidance must degrade gracefully if some sensors drop out. But as a bay-level occupancy grid that turns "somewhere in here is a space" into "that space, over there", it directly attacks one of the most wasteful and frustrating problems in urban mobility — the hunt for parking.
What this project does
- Senses occupancy of each individual parking bay
- Aggregates per-bay states into live availability
- Guides drivers to free spots via dynamic signs and app/map data
- Cuts the circling that wastes fuel/time and adds congestion
- Enables enforcement/analytics (over-stays, patterns, pricing)
- Runs on battery + wireless (LoRa) per bay for practical deployment
- Degrades gracefully if some sensors drop out
Real-World Applications
| Setting | How it is used |
|---|---|
| On-street parking guidance | Guiding drivers to free curbside bays to cut circling and congestion. |
| Car parks / garages | Per-bay availability and guidance to free spaces by level/zone. |
| Smart-city mobility | City-wide parking data for guidance, enforcement and policy. |
| Campus / commercial lots | Availability signs and app guidance for staff/visitor parking. |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- Bay-level occupancy sensing
- Live availability aggregation
- Dynamic guidance (signs + app)
- Over-stay/analytics/pricing support
- Battery + LoRa per-bay design
- City-wide networked picture
- Honest about sensor accuracy/siting
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Intermediate |
| Estimated completion time | 12–18 hours |
| Indicative build cost | ₹600 – ₹1,000 per bay (+ gateway/signs) |
| Primary discipline | Smart City |
| Reference platform | ESP32 DevKit V1 (ESP-WROOM-32) |
Skills you should have (or will pick up)
- Bay occupancy sensing (magnetometer/ultrasonic/IR)
- Reliable occupied/free detection and neighbour rejection
- Availability aggregation and guidance
- Battery + LoRa low-power design
- Graceful-degradation and analytics
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 |
| MPU-9250 9-axis IMU Magnetometer needs a figure-of-eight hard/soft-iron calibration. | Gyro + accel + AK8963 magnetometer, 16-bit, on-chip sensor fusion DMP | 1 | ₹620 |
| JSN-SR04T waterproof ultrasonic sensor The 25 cm blind zone matters — mount it above the maximum expected water level. | 25–450 cm, ±1 cm, IP67 sealed transducer, 45° beam | 1 | ₹450 |
| TCRT5000 IR reflectance sensor module Ambient sunlight saturates it — shroud the sensor on outdoor robots. | 1–8 mm optimum sensing gap, 950 nm emitter, analogue + digital out | 1 | ₹45 |
| 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 |
| Bay occupancy sensor Magnetometer detects the car's steel; overhead senses the vehicle | Magnetometer (in-ground) or overhead ultrasonic/IR per bay | 20 | ₹8,000 |
| LoRa gateway | Site/city gateway aggregating bay sensors | 1 | ₹2,500 |
| Dynamic guidance signs | Entrance/junction signs showing free spaces + direction | 1 | ₹3,000 |
| Battery + weatherproof housing Battery life is critical at grid scale | Long-life battery and rugged in-ground/overhead housing per bay | 20 | ₹6,000 |
Estimated total: ₹23,240, 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 |
| MPU-9250 9-axis IMU | Gyro + accel + AK8963 magnetometer, 16-bit, on-chip sensor fusion DMP | 2.4–3.6 V | I²C / SPI | Datasheet |
| JSN-SR04T waterproof ultrasonic sensor | 25–450 cm, ±1 cm, IP67 sealed transducer, 45° beam | 5 V | Trigger/Echo or UART | Datasheet |
| TCRT5000 IR reflectance sensor module | 1–8 mm optimum sensing gap, 950 nm emitter, analogue + digital out | 3.3–5 V | Analogue + digital | 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. |
| MPU-9250 9-axis IMU | 2.4–3.6 V | 4.5 | Magnetometer needs a figure-of-eight hard/soft-iron calibration. |
| JSN-SR04T waterproof ultrasonic sensor | 5 V | 30 | The 25 cm blind zone matters — mount it above the maximum expected water level. |
| TCRT5000 IR reflectance sensor module | 3.3–5 V | 20 | Ambient sunlight saturates it — shroud the sensor on outdoor robots. |
| 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 2474.5 mA. With a 1.5× design margin the supply should deliver at least 3800 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 |
| MPU6050_light / Adafruit MPU6050 1.3.x | IMU register access, calibration and complementary-filter angles. | Library Manager → "MPU6050_light" by rfetick |
| LoRa (sandeepmistry) 0.8.0 | SX127x radio configuration, packet TX/RX and callbacks. | Library Manager → "LoRa" by Sandeep Mistry |
| 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 |
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 |
|---|---|---|---|
| Magnetometer | SDA/SCL | GPIO 21/22 | Vehicle steel mass (I²C) |
| Ultrasonic/IR (alt) | TRIG/ECHO/OUT | GPIO 26/25/27 | Vehicle presence |
| LoRa | SPI | GPIO 18/19/23/5 | Bay state to gateway |
| Solar + TP4056 | OUT | 3V3 reg | Charged supply |
| Status LED | IN | GPIO 2 | Occupied/free |
| Battery sense | ADC | GPIO 34 | Supervision |
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
- Choose the sensor for the bay type: an in-ground magnetometer detects the car's steel mass (good on-street); an overhead ultrasonic/IR senses the vehicle (good in garages).
- A magnetometer must be sited/calibrated to reject the magnetic influence of cars in neighbouring bays; an overhead sensor needs a clear view of the bay.
- Battery + LoRa per bay — running power/cable to every bay is impractical; optimise for long battery life (deep sleep, report on change).
- Aggregate bay states at a gateway; drive guidance signs and app/map data from the live availability.
- Supervise battery so a dead bay sensor is flagged; degrade guidance gracefully if sensors drop out.
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
The problem the grid solves is a pure information problem: spaces exist, but drivers cannot see which ones are free until they are right next to them, so they circle, and that circling is a large, wasteful, and entirely avoidable component of urban traffic. Making per-bay availability visible — knowing not just "the lot is 80% full" but "bay 47 on level 2 is free, this way" — converts a blind search into a direct route. That is the whole value proposition, and it is why the system must sense at the granularity of the individual bay, not just count cars in and out.
Reliable bay-level occupancy sensing is the technical heart, and the sensor choice depends on the setting. A magnetometer buried in or beside the bay detects the large ferrous mass of a parked car by the distortion it causes in the Earth's magnetic field — cheap, robust, and well-suited to on-street bays — but it must be sited and calibrated to reject the influence of cars in adjacent bays, or it will report neighbours as its own occupancy. An ultrasonic or IR sensor mounted overhead (common in garages) detects the vehicle directly by ranging to it, needing a clear view of the bay. Whatever the sensor, the requirement is a dependable occupied/free state per bay, robust to the edge cases (a person standing in the bay, a motorcycle, a car parked askew).
Those per-bay states aggregate into live availability that drives guidance — and guidance is where the benefit is realised. Dynamic signs at entrances and decision points show how many spaces are free in each zone/level and point toward them; app and map integration lets a driver know availability before they arrive and routes them to it. The tighter and timelier this loop — sense, aggregate, guide — the more circling it eliminates. Bay-level data also unlocks secondary value: enforcement (detecting over-stays), analytics (occupancy patterns to inform capacity and dynamic pricing), and a city-wide parking picture for policy.
The deployment realities shape the engineering and the honesty. A parking grid is infrastructure at scale — potentially thousands of bays — and running power and cable to every one is impractical, so sensors are battery-powered and wireless (LoRa is well-matched: long range, low power, low data rate), which makes long battery life (deep sleep, report only on state change) and reliable low-power communication first-order design concerns, along with supervision so a dead sensor is noticed. And the system must degrade gracefully: if some sensors drop out, guidance should fall back sensibly (e.g. to zone-level counts) rather than mislead. The design is candid that per-bay accuracy depends on sensor type and careful siting, and that a full grid is a real deployment, not a bench demo. But the core idea is powerful and proven: sense every bay, aggregate the truth, and guide drivers straight to a free space — turning one of the most wasteful, frustrating, congestion-causing behaviours in a city into a solved information problem.
The maths behind it
Occupancy from a magnetometer
A parked car distorts the local magnetic field:
|B − B_baseline| > threshold → occupied
Baseline learned per bay (empty). Reject neighbour bays by
siting/threshold so an adjacent car does not trigger it.
Availability aggregation
free(zone) = Σ bays in zone with state == free
Guidance = show free(zone) and direction to the nearest
zone with free > 0. Timely sense→aggregate→guide loop
minimises circling.
Battery life (report-on-change)
life ≈ capacity / ( I_sleep + f_change·E_tx )
Reporting only on occupancy CHANGE (not periodically) and
deep-sleeping between makes battery life years, viable at
grid scale.
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.
Deploy and calibrate bay sensors
Install the chosen sensor per bay (in-ground magnetometer or overhead ultrasonic/IR), learn each bay's empty baseline, and calibrate to reliably detect occupancy while rejecting neighbouring bays.
Battery-power each with deep sleep and report-on-change; supervise the battery.
Aggregate and guide
Aggregate bay states at a LoRa gateway into live availability, and drive dynamic guidance signs and app/map data toward free spaces, degrading gracefully if sensors drop out.
Add analytics
Use bay-level data for over-stay enforcement, occupancy analytics and (optionally) dynamic pricing.
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.
Detect occupancy and report on change
Learn the empty baseline, detect occupied/free from the sensor with hysteresis, and report only on a state change to save battery.
cppbay-occupancy.inofloat baseline=0; bool occupied=false; #define OCC_THRESH 30.0f // magnetometer delta for a car (uT) #define HYST 8.0f // Learn the empty-bay baseline (call when known empty). void learnBaseline(float field){ baseline = field; } // Detect occupancy with hysteresis; return true if state CHANGED. bool updateOccupancy(float field){ float delta = fabsf(field - baseline); bool now = occupied ? (delta > OCC_THRESH - HYST) // stay occupied until well below : (delta > OCC_THRESH + HYST); // become occupied only well above if (now != occupied){ occupied = now; return true; } // changed return false; } void onWake(float field){ if (updateOccupancy(field)) reportBayState(occupied); // report only on change (save battery) // else stay silent and sleep }void learnBaseline(float field)Each bay learns its own empty-field baseline, so occupancy is a change from that specific bay's normal — accommodating local variation.bool now = occupied ? (delta > OCC_THRESH - HYST)Hysteresis around the threshold prevents the bay flickering between occupied and free at the boundary.if (now != occupied){ occupied = now; return true; }The function reports only when the state actually changes, which is what makes report-on-change (and long battery life) possible.if (updateOccupancy(field)) reportBayState(occupied);A LoRa message is sent only on a change, so a bay that sits occupied or free for hours transmits nothing — key to years of battery life at grid scale.Aggregate, guide and supervise
At the gateway, aggregate bay states into zone availability, drive signs/app guidance to free spaces, supervise battery/health, and degrade gracefully if sensors drop out.
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.
/* ═══════════════════════════════════════════════════════════════
Smart Parking Bay Sensor — ESP32 + magnetometer, LoRa, battery
Detects bay occupancy (report-on-change for battery life), which
aggregates into live availability and guidance. Battery + LoRa per
bay; supervised; graceful degradation.
══════════════════════════════════════════════════════════════════ */
#include <Wire.h>
#include <MPU9250.h> // magnetometer
#include <LoRa.h>
#include <SPI.h>
#include <Preferences.h>
#include <math.h>
#define OCC_THRESH 30.0f
#define HYST 8.0f
#define HEARTBEAT_S 3600 // 1 h heartbeat (supervision)
#define BAY_ID 47
MPU9250 imu(Wire, 0x68);
Preferences prefs;
RTC_DATA_ATTR float baseline = 0;
RTC_DATA_ATTR bool occupied = false;
RTC_DATA_ATTR uint32_t sinceBeat = 0;
float fieldMagnitude(){
imu.readSensor();
float x=imu.getMagX_uT(), y=imu.getMagY_uT(), z=imu.getMagZ_uT();
return sqrtf(x*x+y*y+z*z);
}
void report(bool occ, bool beat){
LoRa.beginPacket();
LoRa.printf("{\"bay\":%d,\"occupied\":%s,\"vbat\":%.2f,\"hb\":%d}",
BAY_ID, occ?"true":"false", readBattery(), beat?1:0);
LoRa.endPacket();
}
void setup(){
Serial.begin(115200);
Wire.begin(21,22); imu.begin();
prefs.begin("bay",true);
if (baseline==0) baseline = prefs.getFloat("base", fieldMagnitude());
prefs.end();
SPI.begin(); LoRa.setPins(5,14,2); LoRa.begin(433E6); LoRa.setSpreadingFactor(10);
float field = fieldMagnitude();
float delta = fabsf(field - baseline);
bool now = occupied ? (delta > OCC_THRESH-HYST) : (delta > OCC_THRESH+HYST);
bool changed = (now != occupied);
occupied = now;
sinceBeat += 60; // approx per wake (sleep 60s)
bool beat = sinceBeat >= HEARTBEAT_S;
if (changed || beat){ // report on change OR heartbeat
report(occupied, beat && !changed);
if (beat) sinceBeat = 0;
}
esp_sleep_enable_timer_wakeup(60ULL*1000000ULL); // check each minute
esp_deep_sleep_start();
}
void loop(){}
Configuration & Calibration
Configuration steps
- Choose and site the sensor per bay; learn the empty baseline and set the occupancy threshold/hysteresis (reject neighbours).
- Configure report-on-change plus a heartbeat interval, and battery supervision.
- Configure LoRa aggregation, availability zones, and guidance signs/app.
- Set graceful-degradation behaviour and analytics.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
Occupancy detection
Calibrate the baseline and threshold so a parked car reads occupied and an empty bay free, and a neighbouring car does not trigger it.
Battery/report
Verify report-on-change and heartbeat, and measure battery life at the expected change rate.
Aggregation/guidance
Confirm the sense→aggregate→guide loop is timely and signs/app reflect availability quickly.
Network Architecture & Connectivity
Communication protocol
Bays report occupancy on change (plus heartbeat); the gateway aggregates live availability that drives signs and app guidance, and feeds analytics/enforcement.
| Topic / endpoint | Direction | Payload |
|---|---|---|
parking/bay/<id>/state | bay → gateway | occupied/free, battery, heartbeat |
parking/zone/<id>/free | gateway → signs/app | free count + direction |
parking/bay/<id>/health | bay → ops | battery/supervision |
Message contract between the device and the broker.
Cloud platform configuration
A parking platform aggregates bay states into live availability, drives guidance signs and app/map data, and provides analytics (occupancy, over-stays) and city-wide visibility.
Dashboard setup
A parking map by bay/zone availability, guidance status, sensor health, and occupancy analytics.
Mobile app integration
App guidance to free spaces before arrival; availability by zone.
Security considerations
- Authenticate bay reports so availability cannot be spoofed.
- Supervise battery/health; degrade guidance gracefully.
- Protect any personal/enforcement data appropriately.
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 |
|---|---|
| Park/remove a car | Bay reports occupied/free on change; guidance updates |
| Car in a neighbouring bay | This bay unaffected (neighbour rejection) |
| Bay empty/occupied for hours | No transmissions except heartbeat — battery saved |
| Disable a sensor | Missing heartbeat flags it; guidance degrades gracefully |
| Fill a zone | Signs show zero free; guide to the next zone |
| Battery run-down | Low battery reported (supervision) |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
Dynamic signs and the app show free spaces by zone and directions; the dashboard shows per-bay occupancy, availability and analytics.
{
"bay": 47,
"occupied": false,
"vbat": 3.78,
"hb": 0
}
Bay 47 reporting free — aggregated with its neighbours into a zone count that a sign and the app use to guide a driver straight to it, instead of circling to find it.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Report on change plus a slow heartbeat; deep-sleep between checks for years of battery life.
- Keep the sense→aggregate→guide loop timely so guidance reflects reality.
- Reject neighbour influence in detection.
- Degrade gracefully (zone counts) if some bays drop out.
- 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
- Site sensors safely (in-ground/overhead) without creating hazards; follow road/works safety for installation.
- Per-bay accuracy depends on sensor type and siting — calibrate to reject neighbours/obstructions.
- Degrade guidance gracefully rather than mislead if sensors fail.
- Protect any enforcement/personal data per policy/law.
- 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
- Act on low-battery/dead-sensor supervision; replace batteries at grid scale on a plan.
- Recalibrate baselines/thresholds as needed.
- Verify guidance signs/app reflect availability.
- Review analytics and sensor health.
- 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 reservation/booking of bays via the app.
- Add dynamic pricing from occupancy analytics.
- Fuse with navigation for turn-by-turn to a free bay.
- Add EV-bay and accessible-bay specific detection.
- 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.
- Smart parking systemsReference
- Cruising for parking (traffic impact)Reference
- Magnetometer vehicle detectionReference
- LoRa low-power wide-area networkingReference
- Parking guidance systemsReference