Node-MCU esp8266 Code:
/*************************************************************
NodeMCU (ESP8266) Ultrasonic -> Netlify POST (Push-Based)
Baud: 9600
Steps:
1) Connect to Wi-Fi Park-Atlas / Spots1000
2) Measure distance using HC-SR04 (TRIG = D5, ECHO = D6)
3) Every ~1 second, POST JSON to Netlify with:
{ "parkingSpotID": <ID>,
"inches": <measured>,
"cm": <converted>,
"carPresent": <true/false> }
4) Aggregator polls that function => sees "ONLINE" if data is recent,
"OFFLINE" if not updated recently.
If the NodeMCU is unplugged => aggregator sees no new data => "OFFLINE."
If replugged => aggregator sees new data => "ONLINE."
*************************************************************/
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <ESP8266HTTPClient.h>
// ---------- Wi-Fi Credentials -------------
const char* WIFI_SSID = "Park-Atlas"; // Replace with your SSID
const char* WIFI_PASSWORD = "Spots1000"; // Replace with your Password
// ---------- Netlify Function Endpoint -------------
// Example: "https://parkatlas.netlify.app/.netlify/functions/parkingData"
// Must be EXACT domain that yields 200 (no redirect).
const char* NETLIFY_ENDPOINT = "https://parkatlas.netlify.app/.netlify/functions/parkingData";
// ---------- Spot ID -------------
// For each NodeMCU, set a unique ID from 1..10
int PARKING_SPOT_ID = 1; // e.g. Spot #1
// ---------- Ultrasonic Pins + Threshold -------------
#define TRIG_PIN D5 // GPIO14
#define ECHO_PIN D6 // GPIO12
static const float CAR_THRESHOLD_INCHES = 30.0; // <30in => car present
/*************************************************************
measureUltrasonicInches():
- 10µs pulse on TRIG_PIN
- read echo on ECHO_PIN
- returns distance in inches, or -1 if no echo
*************************************************************/
float measureUltrasonicInches() {
pinMode(TRIG_PIN, OUTPUT);
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
pinMode(ECHO_PIN, INPUT);
unsigned long dur = pulseIn(ECHO_PIN, HIGH, 30000UL); // 30ms => ~5m
if (dur == 0) {
return -1.0;
}
float inches = dur / 148.0;
return inches;
}
/*************************************************************
postToNetlify():
- WiFiClientSecure (insecure) + HTTPClient
- POST JSON to the function
*************************************************************/
void postToNetlify(float inches, float cm, bool carPresent) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Wi-Fi not connected; skipping POST.");
return;
}
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, NETLIFY_ENDPOINT);
http.addHeader("Content-Type", "application/json");
String json = "{";
json += "\"parkingSpotID\":" + String(PARKING_SPOT_ID) + ",";
json += "\"inches\":" + String(inches, 2) + ",";
json += "\"cm\":" + String(cm, 2) + ",";
json += "\"carPresent\":" + String(carPresent ? "true" : "false");
json += "}";
int code = http.POST(json);
Serial.print("[Spot #");
Serial.print(PARKING_SPOT_ID);
Serial.print("] POST code: ");
Serial.println(code);
http.end();
}
void setup() {
Serial.begin(9600);
delay(100);
Serial.println("\nNodeMCU Spot #" + String(PARKING_SPOT_ID) + " Booting...");
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi: ");
Serial.println(WIFI_SSID);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi connected!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
}
void loop() {
float inches = measureUltrasonicInches();
float cm = (inches < 0) ? -1.0 : (inches * 2.54);
bool carPresent = (inches > 0 && inches < CAR_THRESHOLD_INCHES);
postToNetlify(inches, cm, carPresent);
delay(1000); // post every ~1 second => aggregator sees near-instant "ONLINE"
}
parkatlas.org html code: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Park Atlas Spot Monitor </title>
<!--
Using Google Maps API key: AIzaSyBnOuhFivmmXmCbKT6H9pRU8pnt3rH_ZdY
aggregator fetches from Netlify function => sees who posted recently => "ONLINE"
If a NodeMCU is unplugged => no new posts => aggregator marks "OFFLINE" after threshold
-->
<script async
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBnOuhFivmmXmCbKT6H9pRU8pnt3rH_ZdY&callback=initMap">
</script>
<style>
html, body {
margin:0; padding:0; width:100%; height:100%;
font-family:"Segoe UI",Arial,sans-serif; background:#f4f4f4;
}
header {
background:#34495e; color:#fff; text-align:center; padding:15px;
}
h1 { margin:0; font-size:1.8em; }
#map {
width:100%; height:60vh;
}
#spotList {
max-width:1200px; margin:20px auto; padding:0 20px;
}
.spotItem {
background:#fff; margin:10px 0; padding:10px;
border-radius:6px; box-shadow:0 2px 5px rgba(0,0,0,0.1);
}
.spotHeader {
display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;
}
.spotHeader h2 {
margin:0; font-size:1.2em; color:#333;
}
.statusTag {
padding:4px 8px; border-radius:4px; font-weight:bold; color:#fff;
}
.tag-online { background:#27ae60; }
.tag-offline{ background:#c0392b; }
.carTag {
margin-left:10px; font-weight:bold;
}
.car-open {
color:#2ecc71;
}
.car-closed {
color:#e67e22;
}
button {
padding:6px 12px; margin-right:6px; border:none; border-radius:4px;
cursor:pointer; color:#fff; background:#2980b9;
}
button:hover { background:#3498db; }
footer {
text-align:center; padding:10px; color:#777; font-size:0.9em;
}
</style>
</head>
<body>
<header>
<h1>Park Atlas Spot Monitor</h1>
</header>
<div id="map"></div>
<div id="spotList"></div>
<footer>
© Park Atlas 2025
</footer>
<script>
/*
aggregator: polls netlify function every 1s
if data for spot #X is <5s old => "ONLINE," else "OFFLINE"
shows distance, car presence, color-coded marker
*/
// up to 10 spots with lat/lng
const spots = [
{ id:1, lat:40.9072741, lng:-73.9050011 },
{ id:2, lat:40.9073253, lng:-73.9049921 },
{ id:3, lat:40.9073739, lng:-73.9049663 },
{ id:4, lat:40.9074226, lng:-73.9049455 },
{ id:5, lat:40.9074702, lng:-73.9049180 },
{ id:6, lat:40.9075214, lng:-73.9048962 },
{ id:7, lat:40.9075693, lng:-73.9048650 },
{ id:8, lat:40.9076202, lng:-73.9048516 },
{ id:9, lat:40.9076686, lng:-73.9048358 },
{ id:10, lat:40.9078088, lng:-73.9048164 }
];
// netlify function domain must match NodeMCU's
const NETLIFY_URL = "https://parkatlas.netlify.app/.netlify/functions/parkingData";
let map;
let markers = {};
let infoWindow;
let directionsService;
let directionsRenderer;
let userPosition = null;
const REFRESH_INTERVAL_MS = 1000; // aggregator fetches every 1s
const OFFLINE_THRESHOLD_MS = 5000; // if data is older than 5s => "OFFLINE"
function initMap() {
const defaultCenter = { lat:40.9072741, lng:-73.9050011 };
map = new google.maps.Map(document.getElementById("map"), {
center: defaultCenter,
zoom: 15
});
infoWindow = new google.maps.InfoWindow();
directionsService = new google.maps.DirectionsService();
directionsRenderer = new google.maps.DirectionsRenderer({ map: map });
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
pos => {
userPosition = { lat:pos.coords.latitude, lng:pos.coords.longitude };
map.setCenter(userPosition);
},
err => {
console.warn("Geolocation not permitted:", err);
}
);
}
loadSpots();
setInterval(loadSpots, REFRESH_INTERVAL_MS);
}
function loadSpots() {
fetch(NETLIFY_URL)
.then(r => r.json())
.then(allData => {
renderSpots(allData);
})
.catch(err => {
console.error("Error fetching data from Netlify:", err);
});
}
function renderSpots(allData) {
const now = Date.now();
const spotListEl = document.getElementById("spotList");
spotListEl.innerHTML = "";
spots.forEach(spot => {
if (!markers[spot.id]) {
markers[spot.id] = new google.maps.Marker({
position: { lat: spot.lat, lng: spot.lng },
map: map,
title: `Spot #${spot.id}`,
icon: getMarkerIcon(false, false)
});
}
let spotDiv = document.createElement("div");
spotDiv.className = "spotItem";
let headerDiv = document.createElement("div");
headerDiv.className = "spotHeader";
let titleH2 = document.createElement("h2");
titleH2.textContent = `Spot #${spot.id}`;
headerDiv.appendChild(titleH2);
let statusSpan = document.createElement("span");
statusSpan.id = `status-${spot.id}`;
statusSpan.textContent = "OFFLINE";
statusSpan.className = "statusTag tag-offline";
headerDiv.appendChild(statusSpan);
spotDiv.appendChild(headerDiv);
let carLine = document.createElement("div");
carLine.innerHTML = `Car: <span id="car-${spot.id}">N/A</span>`;
spotDiv.appendChild(carLine);
let distLine = document.createElement("div");
distLine.id = `dist-${spot.id}`;
distLine.textContent = "Distance: -- in";
spotDiv.appendChild(distLine);
let btnContainer = document.createElement("div");
let btnShow = document.createElement("button");
btnShow.textContent = "Show on Map";
btnShow.onclick = () => showOnMap(spot.id);
let btnDir = document.createElement("button");
btnDir.textContent = "Get Directions";
btnDir.onclick = () => getDirections(spot.id);
btnContainer.appendChild(btnShow);
btnContainer.appendChild(btnDir);
spotDiv.appendChild(btnContainer);
spotListEl.appendChild(spotDiv);
// check if netlify data has this ID
let data = allData[String(spot.id)];
if (data) {
let age = now - (data.updated||0);
if (age < OFFLINE_THRESHOLD_MS) {
// "ONLINE"
statusSpan.textContent = "ONLINE";
statusSpan.className = "statusTag tag-online";
// Car presence
let carSpan = document.getElementById(`car-${spot.id}`);
if (data.carPresent) {
carSpan.textContent = "CLOSED (car present)";
carSpan.className = "carTag car-closed";
markers[spot.id].setIcon(getMarkerIcon(true,true));
} else {
carSpan.textContent = "OPEN";
carSpan.className = "carTag car-open";
markers[spot.id].setIcon(getMarkerIcon(true,false));
}
// Distance
let distEl = document.getElementById(`dist-${spot.id}`);
if (typeof data.inches==="number") {
distEl.textContent=`Distance: ${data.inches.toFixed(2)} in (~${(data.inches*2.54).toFixed(2)} cm)`;
}
// marker click => InfoWindow
markers[spot.id].addListener("click", () => {
let html=`
<h3>Spot #${spot.id}</h3>
<p>Status: ONLINE<br/>
Car Present: ${data.carPresent?"Yes":"No"}<br/>
`;
if (typeof data.inches==="number") {
html+=`Distance: ${data.inches.toFixed(2)} in (~${(data.inches*2.54).toFixed(2)} cm)<br/>`;
}
html+=`</p>
<button onclick="getDirections(${spot.id})">Get Directions</button>`;
infoWindow.setContent(html);
infoWindow.open(map, markers[spot.id]);
});
} else {
// data older than 5s => OFFLINE
statusSpan.textContent="OFFLINE";
statusSpan.className="statusTag tag-offline";
document.getElementById(`car-${spot.id}`).textContent="N/A";
document.getElementById(`dist-${spot.id}`).textContent="Distance: -- in";
markers[spot.id].setIcon(getMarkerIcon(false,false));
}
} else {
// no data => OFFLINE
statusSpan.textContent="OFFLINE";
statusSpan.className="statusTag tag-offline";
document.getElementById(`car-${spot.id}`).textContent="N/A";
document.getElementById(`dist-${spot.id}`).textContent="Distance: -- in";
markers[spot.id].setIcon(getMarkerIcon(false,false));
}
});
}
function getMarkerIcon(online, carPresent) {
if(!online) {
return "http://maps.google.com/mapfiles/ms/icons/grey.png";
} else {
return carPresent
? "http://maps.google.com/mapfiles/ms/icons/orange.png"
: "http://maps.google.com/mapfiles/ms/icons/green.png";
}
}
function showOnMap(spotID) {
let marker=markers[spotID];
if(!marker)return;
map.setCenter(marker.getPosition());
map.setZoom(17);
google.maps.event.trigger(marker,"click");
}
function getDirections(spotID) {
let spot=spots.find(s=>s.id===spotID);
if(!spot)return;
if(!userPosition) {
alert("Cannot get directions: user location not available!");
return;
}
directionsService.route({
origin:new google.maps.LatLng(userPosition.lat,userPosition.lng),
destination:new google.maps.LatLng(spot.lat,spot.lng),
travelMode:google.maps.TravelMode.DRIVING
},(result,status)=>{
if(status==="OK") {
directionsRenderer.setDirections(result);
} else {
alert("Directions request failed: "+status);
}
});
}
</script>
</body>
</html>
parkatlas.org js code: parkingData.js
/*************************************************************
parkingData.js (Netlify Serverless Function)
ephemeral store for 10 spots
NodeMCU => POST => { parkingSpotID, inches, cm, carPresent }
aggregator => GET => sees all spots
*************************************************************/
let spotData = {};
exports.handler = async (event, context) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
};
if (event.httpMethod === "OPTIONS") {
return { statusCode: 200, headers, body: "OK" };
}
if (event.httpMethod === "POST") {
try {
const body = JSON.parse(event.body);
const sid = body.parkingSpotID ? String(body.parkingSpotID) : "unknown";
spotData[sid] = {
inches: body.inches || -1,
cm: body.cm || -1,
carPresent: body.carPresent || false,
updated: Date.now()
};
return {
statusCode: 200,
headers,
body: JSON.stringify({ success: true })
};
} catch (err) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Invalid JSON" })
};
}
}
if (event.httpMethod === "GET") {
return {
statusCode: 200,
headers,
body: JSON.stringify(spotData)
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" })
};
};