1. 项目缘起从“找车位”到“智能车位”的实践停车难尤其是在大型商场、办公园区或者老旧小区几乎成了每个开车人的日常烦恼。我最近就遇到了一个挺有意思的需求一个朋友管理着一个有几十个车位的小型停车场他希望能实时知道哪些车位是空的方便引导访客也便于内部管理。传统的方案要么成本太高要么部署太麻烦。于是我琢磨着能不能用最普及的硬件和云服务低成本地搭一套车位状态监测与上报系统。核心思路很直接在每个车位上部署一个传感器来检测是否有车然后通过无线网络把状态数据发送到云端最后在手机或电脑上就能实时查看。硬件上ESP32几乎是这个场景的不二之选——它集成了Wi-Fi和蓝牙性能足够价格低廉社区生态极其丰富。云端服务我选择了Firebase特别是它的Firestore数据库和Realtime Database它们能提供实时的数据同步非常适合这种状态频繁更新的物联网应用。这个项目麻雀虽小五脏俱全。它涉及了嵌入式端的传感器数据采集、Wi-Fi连接与网络通信云端的数据库设计、安全规则配置以及应用端的数据展示。整个过程踩了不少坑也积累了一些非常实用的经验。下面我就把这个从零到一的完整实现过程包括硬件选型、代码逻辑、云端配置和那些容易出错的细节毫无保留地分享出来。2. 硬件选型与电路设计传感器是眼睛ESP32是大脑要实现车位状态检测第一步是选择合适的“眼睛”。市面上常见的方案有超声波测距、红外对射、地磁传感器和压力传感器等。我们需要根据成本、安装复杂度、环境抗干扰能力和功耗来综合选择。2.1 传感器方案对比与最终选择我对比了几种主流方案超声波传感器如HC-SR04通过发射和接收超声波来计算距离。如果车位上方有遮挡物如雨棚、管道安装比较方便。但它的探测区域是一个锥形容易受到旁边车辆或行人经过的干扰且需要定期清洁探头防止灰尘影响精度。在露天多尘的环境下稳定性是个挑战。红外对射传感器在车位入口两侧安装发射管和接收管车辆进入会阻断光束。这种方式检测非常准确几乎无误报。但安装需要精准对位布线稍显麻烦需要两边走线且镜头容易被泥土或雪覆盖。地磁传感器如HM01B0A检测车辆金属物体对地球磁场的扰动。这是目前智慧停车领域的主流方案因为它通常采用电池供电、无线传输安装时只需在路面挖一个小孔埋入施工简单寿命长。但它的成本相对较高且对安装位置的地磁场环境如地下有钢筋有一定要求算法需要校准。压力传感器/地感线圈需要埋设在路面下施工量大成本高一般用于道闸控制不适合我们这种轻量级改造项目。考虑到这是一个原型验证和中小型场景应用我最终选择了最经济、最快速上手的方案超声波传感器HC-SR04。它的优点在于价格极低十几元人民币、接口简单仅需4个GPIO口、代码库成熟。虽然存在上述干扰问题但我们可以通过软件算法进行滤波和优化在环境相对简单的室内停车场或车库完全可以达到可用状态。注意如果你追求更高的稳定性和免维护性并且预算允许强烈建议选择地磁传感器模块。许多模块已经集成了LoRa或NB-IoT无线方案直接通过基站上传数据完全无需本地Wi-Fi覆盖更适合大规模、分散的室外停车场。2.2 ESP32核心板与外围电路连接我们使用ESP32作为主控它负责读取传感器数据、处理逻辑、连接Wi-Fi并与Firebase通信。这里以最常见的ESP32 DevKit V1开发板为例。所需材料清单ESP32开发板 x1每个车位一个HC-SR04超声波传感器 x1杜邦线母对母若干5V电源如USB充电器和Micro-USB数据线 x1可选10kΩ电阻 x2用于电平转换后文会解释电路连接示意图 HC-SR04有4个引脚VCC、Trig触发、Echo回声、GND。VCC- 接ESP32的5V引脚。这里是一个关键点虽然HC-SR04的工作电压是5V但ESP32的绝大多数GPIO引脚只能耐受3.3V电压。直接将Echo脚输出5V信号接到ESP32的GPIO上长期可能损坏芯片。GND- 接ESP32的GND。Trig- 接ESP32的任何一个GPIO例如GPIO 4。这个引脚是ESP32输出信号给传感器3.3V输出足以触发5V的HC-SR04。Echo-不能直接接需要分压。将Echo脚通过一个1kΩ电阻接到ESP32的GPIO例如GPIO 5同时在该GPIO与GND之间连接一个2kΩ电阻形成一个分压电路将5V信号降至大约3.3V5V * (2k/(1k2k)) ≈ 3.3V。如果你有现成的“电平转换模块”使用它会更方便可靠。连接好后一个简单的车位检测节点就准备好了。实际部署时需要将传感器用支架固定在每个车位的正上方中央垂直向下照射并调整好高度确保能覆盖整个车位区域。3. 软件环境搭建与核心库的引入硬件连接好后我们需要在电脑上搭建编程环境并安装必要的库。对于ESP32开发最常见的是使用Arduino IDE或PlatformIO。这里我使用Arduino IDE因为它对新手更友好。3.1 Arduino IDE与ESP32开发板的配置安装Arduino IDE从官网下载并安装最新版。添加ESP32开发板支持打开Arduino IDE进入文件 - 首选项。在“附加开发板管理器网址”中填入以下网址如果已有其他用逗号分隔https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json点击“好”保存。然后进入工具 - 开发板 - 开发板管理器。搜索“esp32”找到由Espressif Systems发布的“ESP32”开发板包点击安装。这个过程可能需要一些时间因为要下载很多工具链。安装必要的库我们需要两个核心库用于操作Firebase的Firebase ESP32 Client和用于处理JSON的ArduinoJson。进入工具 - 管理库...分别搜索并安装Firebase ESP32 Client by Mobizt这是功能最全、维护最积极的Firebase ESP32库。ArduinoJson by Benoit Blanchon版本选择6.x.x即可。3.2 Firebase项目创建与关键信息获取接下来我们需要在Firebase云端创建项目并设置数据库。创建Firebase项目访问 Firebase 控制台 。点击“创建项目”输入项目名称如smart-parking-system按照向导完成创建。启用Firestore数据库在项目概览页点击左侧边栏的“Firestore Database”。点击“创建数据库”启动安全模式选择一个地理位置如asia-east1。在规则页面我们暂时将规则改为测试模式以便快速开发rules_version 2; service cloud.firestore { match /databases/{database}/documents { match /{document**} { allow read, write: if true; // 允许所有人读写仅用于测试 } } }重要警告这个规则非常不安全允许任何人读写你的数据库。仅在开发测试阶段使用并在部署前务必更新为安全的身份验证规则。获取项目配置信息点击项目概览页旁边的齿轮图标设置选择“项目设置”。滚动到“您的应用”部分如果你还没有添加应用可以点击“/”图标添加一个Web应用我们只需要它的配置信息并不真正部署网页。注册应用后你会看到一个包含firebaseConfig对象的代码片段。我们需要其中的以下信息apiKeyauthDomainprojectIdstorageBucketmessagingSenderIdappIddatabaseURL对于Firestore这个URL通常是https://project-id.firebaseio.com但Firestore库可能用projectId就够了具体看库文档把这些信息记录下来稍后要填入ESP32的代码中。4. ESP32端代码详解从测距到数据上传这是整个项目的核心逻辑部分。代码主要完成以下功能初始化Wi-Fi和Firebase、循环触发超声波测距、根据距离判断车位状态、将状态更新至Firestore。4.1 代码结构与全局变量定义首先我们引入必要的头文件并定义连接信息和全局变量。#include WiFi.h #include FirebaseESP32.h #include ArduinoJson.h // 1. 定义你的Wi-Fi凭证 #define WIFI_SSID 你的Wi-Fi名称 #define WIFI_PASSWORD 你的Wi-Fi密码 // 2. 定义Firebase项目配置从Firebase控制台获取 #define FIREBASE_HOST 你的项目ID.firebaseio.com // 或直接用Firestore库要求的格式 #define FIREBASE_API_KEY 你的Web API Key // 3. 定义Firebase数据路径 // 假设我们在Firestore中有一个parking_spots集合每个车位是一个文档文档ID为spot_01spot_02... #define FIREBASE_COLLECTION parking_spots #define SPOT_ID spot_01 // 当前ESP32对应的车位编号每个设备需不同 // 4. 定义超声波传感器引脚 const int trigPin 4; const int echoPin 5; // 5. 定义测距参数 const float SOUND_SPEED 0.0343; // 声速 (cm/μs) const float MAX_DETECTION_DISTANCE 300.0; // 最大有效检测距离 (cm) const float PARKING_SPOT_HEIGHT 200.0; // 传感器离地面的高度 (cm)需现场测量校准 const float OCCUPIED_THRESHOLD 50.0; // 判断有车的距离阈值 (cm)。如果测距值小于高度-阈值则认为有车。 // 6. 定义状态变量与时间间隔 bool lastOccupiedStatus false; unsigned long lastUpdateTime 0; const unsigned long UPDATE_INTERVAL 5000; // 上传数据到Firebase的间隔毫秒避免过于频繁 // 7. 声明Firebase数据对象 FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config;关键参数解释PARKING_SPOT_HEIGHT这是安装好后传感器超声波发射面到地面的垂直距离。必须用卷尺实际测量并填入这是计算车位是否有车的基准。OCCUPIED_THRESHOLD这是一个“缓冲区间”。假设车位高度是200cm阈值设为50cm。那么当测得的距离小于200 - 50 150cm时我们就认为有物体车进入了这个区域判定为“占用”。这个阈值可以过滤掉一些小的飘忽干扰比如飞过的鸟或落叶。UPDATE_INTERVAL这是为了平衡实时性和网络负载、Firebase免费配额而设置的。5秒一次更新对于停车场引导系统来说已经足够实时。切忌设置为几百毫秒否则很快会触发Firebase的读写限额。4.2setup()函数初始化一切在setup()函数中我们需要初始化串口、连接Wi-Fi、配置Firebase。void setup() { Serial.begin(115200); pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); // 连接Wi-Fi WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print(正在连接到Wi-Fi); while (WiFi.status() ! WL_CONNECTED) { Serial.print(.); delay(300); } Serial.println(); Serial.print(已连接IP地址: ); Serial.println(WiFi.localIP()); // 配置Firebase config.host FIREBASE_HOST; config.api_key FIREBASE_API_KEY; // 可选如果你启用了Firebase身份验证推荐生产环境使用在这里配置用户邮箱和密码 // auth.user.email user_email; // auth.user.password user_password; Firebase.begin(config, auth); Firebase.reconnectWiFi(true); // 设置Firebase读取超时和写入大小可选 fbdo.setBSSLBufferSize(4096, 1024); fbdo.setResponseSize(2048); Serial.println(Firebase初始化完成); }4.3 超声波测距与状态判断函数我们编写一个函数来获取当前距离并基于此判断车位状态。float getDistanceCM() { // 发送一个10μs的高电平脉冲触发Trig引脚 digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); // 读取Echo引脚高电平的持续时间单位微秒 long duration pulseIn(echoPin, HIGH, 30000); // 设置超时30ms对应大约5米距离 // 计算距离厘米 float distance duration * SOUND_SPEED / 2; if (distance 0 || distance MAX_DETECTION_DISTANCE) { Serial.println(测距无效或超范围); return -1.0; // 返回无效值 } return distance; } bool isOccupied(float currentDistance) { if (currentDistance 0) { // 测距无效时保持上一次状态避免误报 return lastOccupiedStatus; } // 如果当前距离小于安装高度 - 阈值则认为有车 bool occupied (currentDistance (PARKING_SPOT_HEIGHT - OCCUPIED_THRESHOLD)); return occupied; }pulseIn函数的坑pulseIn会阻塞程序直到收到回声或超时。上面的代码设置了30ms的超时对应约5米。如果传感器没接好或物体太远程序会在这里卡住30ms。在生产环境中可以考虑使用中断来非阻塞地测量脉冲宽度但对于我们这个简单应用阻塞30ms是可以接受的。4.4loop()函数主循环与数据上传主循环中我们周期性地测量距离、判断状态并在状态变化或定时到达时更新Firebase。void loop() { float distance getDistanceCM(); bool occupied isOccupied(distance); // 打印调试信息到串口 Serial.printf(距离: %.2f cm, 状态: %s\n, distance, occupied ? 占用 : 空闲); unsigned long currentTime millis(); // 状态发生变化或者到达定时上传间隔则更新Firebase if (occupied ! lastOccupiedStatus || (currentTime - lastUpdateTime) UPDATE_INTERVAL) { if (updateFirebaseStatus(occupied, distance)) { lastOccupiedStatus occupied; // 只有成功上传后才更新本地状态 lastUpdateTime currentTime; Serial.println(状态已更新至Firebase); } else { Serial.println(Firebase更新失败); } } delay(1000); // 主循环延迟1秒控制测距频率 }4.5 数据上传函数与Firestore交互这是将数据推送到云端的核心函数。我们使用Firestore的“更新文档”功能。bool updateFirebaseStatus(bool occupied, float distance) { // 如果Firebase没有就绪直接返回失败 if (!Firebase.ready()) { Serial.println(Firebase未就绪); return false; } // 创建要更新的数据对象使用ArduinoJson库 FirebaseJson content; // 我们更新文档中的is_occupied和last_distance字段同时用服务器时间戳作为last_update content.set(fields/is_occupied/booleanValue, occupied); content.set(fields/last_distance/doubleValue, distance); content.set(fields/last_update/timestampValue, server_timestamp); // 使用服务器时间 // 构建文档路径 String documentPath String(FIREBASE_COLLECTION) / String(SPOT_ID); // Firestore REST API的PATCH请求路径 String firestorePath /v1/projects/ String(config.project_id) /databases/(default)/documents/ documentPath ?updateMask.fieldPathsis_occupied,last_distance,last_update; // 发送PATCH请求更新文档 if (Firebase.patchDocument(fbdo, firestorePath.c_str(), content.raw())) { Serial.println(文档更新成功); return true; } else { Serial.printf(更新失败。错误: %s\n, fbdo.errorReason().c_str()); return false; } }关键点解析updateMask参数这是Firestore API的一个重要特性。它指定了本次更新只修改is_occupiedlast_distance和last_update这几个字段。这样即使文档里还有其他字段比如location也不会被意外清空。server_timestamp这是一个特殊的标记值。Firebase接收到这个值后会自动用服务器的时间戳替换它。这保证了所有设备的时间同步避免了因设备时钟不准导致的时间错乱。错误处理fbdo.errorReason()会返回详细的错误信息比如“权限拒绝”、“网络错误”等这对于调试至关重要。将以上所有代码段组合成一个完整的.ino文件用Arduino IDE上传到你的ESP32开发板。打开串口监视器波特率115200你应该能看到连接Wi-Fi、Firebase以及周期性打印距离和状态的信息。5. Firebase安全规则与数据结构优化当设备能成功上传数据后我们必须回过头来加固云端的安全并设计一个更合理的数据结构。5.1 设计健壮的Firestore数据结构之前我们直接更新了parking_spots/spot_01文档。一个更清晰的结构可能如下集合parking_lots存放停车场信息。文档lot_alpha一个具体的停车场。字段name(字符串),total_spots(数字)子集合spots(位于parking_lots/lot_alpha下)存放该停车场所有车位。文档A01一个车位。字段is_occupied(布尔值)last_distance(数字)last_update(时间戳)sensor_id(字符串对应ESP32的设备ID)location(地图包含row,column等信息)这样要获取“Alpha停车场”的所有车位状态只需要查询parking_lots/lot_alpha/spots这个子集合。数据层次清晰易于扩展。5.2 配置安全的Firestore规则测试期的“允许所有人读写”规则必须被替换。一个基础的、基于身份验证的安全规则如下rules_version 2; service cloud.firestore { match /databases/{database}/documents { // 允许已验证用户读取停车场列表和车位状态用于App展示 match /parking_lots/{lotId} { allow read: if request.auth ! null; allow write: if false; // 普通用户不能修改停车场信息 } match /parking_lots/{lotId}/spots/{spotId} { allow read: if request.auth ! null; // 只允许特定服务账号即我们的ESP32更新其对应的车位状态 // 这里假设我们在ESP32代码中使用了服务账号的私钥进行身份验证 allow write: if request.auth ! null request.auth.uid get(/databases/$(database)/documents/parking_lots/$(lotId)/spots/$(spotId)).data.sensor_id; } } }这个规则的意思是任何登录的用户都可以读取数据。只有sensor_id字段等于当前操作者UID用户ID的设备才能修改对应的车位文档。如何在ESP32上实现身份验证我们需要在Firebase控制台生成一个服务账号的私钥JSON文件。进入“项目设置” - “服务账号” - “生成新的私钥”。将下载的JSON文件中的private_key和client_email等信息通过一定方式如SD卡、编译进代码安全地配置到ESP32中。在ESP32代码的setup()中使用这些信息进行认证config.service_account.data.client_email 你的服务账号邮箱; config.service_account.data.private_key 你的私钥字符串;安全警告将私钥硬编码在固件中仍有泄露风险。对于高安全要求场景应考虑使用更安全的认证方式如IoT Core或定期轮换密钥。6. 数据展示端构建一个简单的状态看板数据上了云我们还需要一个界面来看。这里给出一个极其简单的Web示例使用Firebase JavaScript SDK实时监听车位状态变化。!DOCTYPE html html head title停车场状态看板/title script srchttps://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js/script script srchttps://www.gstatic.com/firebasejs/9.0.0/firebase-firestore-compat.js/script style .spot { display: inline-block; width: 60px; height: 60px; margin: 5px; text-align: center; line-height: 60px; border: 2px solid #333; } .free { background-color: lightgreen; } .occupied { background-color: salmon; } .unknown { background-color: lightgray; } /style /head body h1停车场实时状态/h1 div idparkingGrid/div script // 从你的Firebase控制台获取配置 const firebaseConfig { apiKey: YOUR_API_KEY, authDomain: YOUR_PROJECT_ID.firebaseapp.com, projectId: YOUR_PROJECT_ID, storageBucket: YOUR_PROJECT_ID.appspot.com, messagingSenderId: YOUR_SENDER_ID, appId: YOUR_APP_ID }; // 初始化Firebase firebase.initializeApp(firebaseConfig); const db firebase.firestore(); // 假设我们监听 parking_lots/lot_alpha/spots 这个子集合 const spotsRef db.collection(parking_lots).doc(lot_alpha).collection(spots); // 实时监听所有车位文档的变化 spotsRef.onSnapshot((snapshot) { const grid document.getElementById(parkingGrid); grid.innerHTML ; // 清空现有显示 snapshot.forEach((doc) { const data doc.data(); const spotId doc.id; const isOccupied data.is_occupied; const spotDiv document.createElement(div); spotDiv.className spot ${isOccupied ? occupied : free}; spotDiv.id spot-${spotId}; spotDiv.textContent ${spotId} (${isOccupied ? 占 : 空}); grid.appendChild(spotDiv); }); }, (error) { console.error(监听失败, error); }); /script /body /html将这个HTML文件部署到任何静态托管服务如Firebase Hosting, GitHub Pages等或者直接在本地用浏览器打开需配置CORS较麻烦。打开页面你就能看到一个能实时反映每个车位占用状态的简单看板了。7. 部署实战与避坑指南将原型转化为稳定可用的部署会遇到很多在开发板上测试时没暴露的问题。7.1 电源与信号稳定性处理电源干扰超声波传感器在工作时瞬间电流较大可能引起ESP32电源波动导致重启。务必在ESP32的5V和GND引脚之间并联一个100μF以上的电解电容用于稳压滤波。信号抗干扰软件滤波不要单次测距就判定状态。我在代码中实现了一个“滑动窗口滤波”连续采样10次距离去掉最大最小值取中间值的平均再用这个平均值去判断。这能有效滤除偶然的跳动。const int SAMPLE_SIZE 10; float distanceSamples[SAMPLE_SIZE]; int sampleIndex 0; float getFilteredDistance() { distanceSamples[sampleIndex] getRawDistanceCM(); // 假设这是原始的测距函数 sampleIndex (sampleIndex 1) % SAMPLE_SIZE; // 排序并取中间值的平均... (代码略) return filteredValue; }状态去抖避免因短暂遮挡如人走过导致状态频繁切换。可以设置一个“状态保持时间”例如只有连续3次检测到“占用”才将状态改为占用只有连续3次检测到“空闲”才改为空闲。7.2 Wi-Fi连接与断线重连的鲁棒性车库或室外的Wi-Fi信号可能不稳定。ESP32的Wi-Fi连接必须非常健壮。在loop()开头检查连接状态if (WiFi.status() ! WL_CONNECTED) { Serial.println(Wi-Fi断开尝试重连...); WiFi.reconnect(); // 等待一段时间再继续避免快速重试 delay(10000); return; // 本次loop跳过 }使用更强大的Wi-Fi管理库如WiFiManager库它可以让设备在无法连接预设网络时启动一个配置热点允许用户用手机配网这在现场部署时非常有用。Firebase重连Firebase ESP32库的Firebase.reconnectWiFi(true);已经帮我们处理了一部分。但网络长时间中断后可能需要重新调用Firebase.begin()。7.3 功耗考量与远程维护如果采用电池供电功耗就是生命线。深度睡眠模式ESP32支持深度睡眠电流可降至10μA级别。我们可以让设备每10秒唤醒一次测量、上报然后继续睡眠。这需要连接GPIO16 (XPD_DCDC)到RST引脚来实现定时唤醒。// 在loop()末尾上传数据后 esp_sleep_enable_timer_wakeup(10 * 1000000); // 休眠10秒单位微秒 Serial.println(进入深度睡眠); esp_deep_sleep_start();远程固件升级OTA当部署了数十上百个节点后逐个手动更新固件是噩梦。务必在代码中实现OTA功能。Arduino IDE配合BasicOTA示例库可以比较容易地实现。你需要为设备提供稳定的Wi-Fi连接并通过一个服务器来管理OTA更新。7.4 现场安装校准与调试这是最容易出问题的环节。高度校准PARKING_SPOT_HEIGHT必须现场用卷尺精确测量。安装支架要牢固避免因震动导致高度变化。阈值调试OCCUPIED_THRESHOLD需要现场调试。开一辆车进去观察串口输出的距离值。确保车辆停稳后测距值稳定地小于高度-阈值。同时让人在车位旁走动确保不会误触发。环境测试在不同天气晴、雨、不同时间白天、夜晚、有无人车经过时长时间观察设备状态是否稳定。超声波在极端温度下声速会变但我们的应用场景温差不大影响可忽略。标记与记录为每个ESP32设备贴上标签记录其对应的SPOT_ID、MAC地址、安装位置。在Firestore的文档里也可以增加这些元数据字段方便后期维护。通过以上这些步骤一个低成本、可扩展的智能车位检测系统就真正搭建起来了。从硬件焊接、代码调试到云端配置、安全加固最后到现场部署和优化每一个环节都有需要注意的细节。这个项目不仅解决了具体的停车问题更是一个完整的物联网技术栈的实践涵盖了嵌入式、无线通信、云服务和前端展示对于想深入IoT领域的开发者来说是一个非常好的练手项目。