ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32+舵机+OLED:打造可远程控制的隐藏式挪车电话牌

ESP32+舵机+OLED:打造可远程控制的隐藏式挪车电话牌 很多人停车都会在挡风玻璃下放一块挪车电话牌上面直接印着手机号。结果就是路过的人随手拍、推销员挨个记、别有用心的人顺着号码加你微信骚扰电话接个不停。我做这个“翻盖式挪车电话隐藏牌”的出发点很简单——平时把号码彻底藏起来需要的时候再让它露出来而且一定要保证交警或对方车主在合理的场景下能快速联系到我。项目最核心的部分就是“写程序”单片机端的整套控制逻辑加上手机端的远程开关两者配合才让这块牌子真正“活”起来。这个项目其实不复杂但涉及状态管理、舵机控制、OLED显示、传感器触发、蓝牙通信好几个模块。如果你玩过Arduino或者ESP32跟着本文走完基本上可以自己复刻一套就算你只是对这类小工具有兴趣也能从中看到“程序”是怎么把一堆零散硬件串成一个完整功能的。下面我就把这个项目的完整设计思路、程序怎么写、调试中踩过的坑一次性讲清楚。1. 项目要解决什么为什么选择“翻盖式程序控制”1.1 传统挪车牌的隐私痛点传统挪车电话牌最大的问题就是“信息裸奔”。你把电话号码往那一摆不仅车主之间能看见所有路过的人都能看见。我身边真实发生过两件事一位同事的车牌号被抄走一个月内接到七八个推销电话另一位朋友更夸张被人用手机号搜到了微信和支付宝天天收到好友申请。这种骚扰已经不只是“烦”而是隐私安全问题了。有人会说那用二维码挪车不就行了吗扫码后通过平台匿名联系车主双方都不用暴露号码。这个思路方向是对的但实际使用有不少门槛对方得会扫码、得有微信、还得愿意走这个流程。上了年纪的人或者着急赶时间的人经常直接就放弃了。而且一旦车主不在服务区、手机没电或者没开通知二维码挪车就变成了“联系不上”。所以我在想有没有一种方案能同时满足三个条件平时号码看得见吗不能完全藏住真需要挪车时对方能联系到我吗必须能而且最好不要依赖对方装任何软件或走复杂流程。答案其实很朴素做一个物理翻盖平时合上把号码盖住需要时翻起来让号码直接露出来。谁都能看谁都不用装App零学习成本。1.2 为什么选择“翻盖式程序控制”既然是“翻盖”最简单的做法是装一个手动的合页车主手动翻。但这个方案有个致命问题人不在车边盖子是合着的交警到了也看不到号码等于白装。所以必须让翻盖“可远程控制”这就引出了“程序控制”的必要性。程序要解决的核心问题有几层默认状态必须合盖。上电后盖子处于关闭位置号码不外露。提供多种开盖途径。物理按键、震动传感器比如敲车窗三下、手机蓝牙/小程序远程开盖总有一种方式能让人联系到你。自动延时合盖。开盖后过一段时间自动关上防止人走开忘了合导致隐私又暴露。状态可查。小程序里能看到当前盖子是开还是合也能设置合盖延时时间。有了程序就不再是“一块带盖的牌子”而是一个有状态、能感知、可远程控制的智能设备。这也是整个项目最有意思的地方硬件只提供一个执行机构真正的“智商”全在程序里。2. 硬件选型与整体架构2.1 主控选型——为什么是ESP32刚开始我考虑过用51单片机毕竟最便宜几块钱就能买到写个流水灯、控制个舵机完全够用。但后来一规划功能发现51的短板很明显没有蓝牙、没有WiFi、内存小想跟小程序做远程控制基本要从零开始做协议栈太痛苦了。换到ESP32之后问题一下简单了。ESP32自带蓝牙BLE和WiFi可以直接跟微信小程序通信运行频率240MHz内存520KB跑Arduino框架非常轻松价格大概十几块钱比STM32便宜还省了外接蓝牙模块的钱。最关键的是社区生态极其完善ESP32Servo、Adafruit SSD1306这些库都是现成的拿来即用。整个项目的主控板我用的是ESP32 DevKitC V4如果你手里有NodeMCU-32S或者其他ESP32开发板也完全没问题引脚分配稍微调整一下就行。2.2 执行机构、显示与传感器翻盖的动作我用的是SG90舵机。有人问为什么不用步进电机或者直流电机因为翻盖这个动作幅度小、需要定位到固定角度舵机自带角度闭环写个write(150)它就到了150度不需要编码器反馈简单可靠。SG90便宜9克级别扭矩1.8kg/cm拖动一个亚克力翻板绰绰有余。显示部分用的是0.96寸OLEDI2C接口四根线VCC、GND、SCL、SDA接上就能用。OLED的好处是自发光、对比度高阳光下也能看清。电话号码平时不显示开盖后才显示这就实现了“号码隐藏”的又一重保障。传感器方面我用了两类震动传感器SW-420贴在车窗附近或车架上用来检测敲击。光敏电阻模块用于判断白天黑夜夜间可以调整触发灵敏度或直接禁用震动开盖减少误触发。按键也得留一个我加了一个轻触按键装在翻盖内侧接的是GPIO0既能手动强制开盖也能在程序里做“长按合盖”处理。完整的引脚连接表如下方便你对照接线模块ESP32引脚说明SG90舵机信号线GPIO13PWM输出控制角度OLED SCLGPIO22I2C时钟OLED SDAGPIO21I2C数据震动传感器DOGPIO17数字输出触发为HIGH光敏模块DOGPIO16数字输出夜晚为HIGH按键GPIO0默认上拉按下为LOW电源模块输出5V/VIN给ESP32和舵机供电2.3 供电与低功耗设计供电是这个项目里最容易翻车的地方。SG90舵机看起来小但堵转瞬间电流能到500mA以上如果直接拿开发板的USB口供电舵机一转电压就被拉低ESP32直接重启表现就是翻盖翻到一半突然断了。我在车上用的是一个DC-DC降压模块输入接车载12V输出5V/3A专门给舵机喂电。实验台上则用带电流保护的5V电源适配器替代。待机功耗也要考虑ESP32开着WiFi待机电流能到100mA对车载电瓶来说不算大但长期耗着也不舒服。我的做法是不用蓝牙和WiFi的时候把外设都关掉主控进入深度睡眠保留震动传感器通过GPIO唤醒。实测整机待机电流可以压到30mA左右对一个停车常驻设备来说可以接受。如果担心车辆电瓶亏电还可以在电源输入端并联一个5V超级电容模块车子断电后还能撑几秒钟让舵机回复到合盖位置。这一点在常见问题里我再展开讲。3. 核心控制程序设计与实现3.1 状态机才是这个程序的骨架很多人一上来就写死逻辑检测到震动就开盖延时关了。看起来简单但真跑起来会发现各种Bug——比如开盖过程中又检测到一次震动程序到底该响应谁比如开盖后手动合盖程序如何知道当前状态这些问题归结起来就是缺一个“状态机”。我用了枚举定义六个状态enum LidState { SLEEP, // 合盖待机 DETECT, // 触发判定中 OPENING, // 翻盖过程中 SHOW, // 翻盖完成显示号码 CLOSING, // 合盖过程中 FORCED_OPEN // 强制开盖 };状态流转的规则是SLEEP 状态检测到震动信号进入 DETECT检测到按键按下直接进 OPENING。DETECT 状态连续满足触发条件进入 OPENING触发条件不满足且超时回到 SLEEP。OPENING 状态舵机转到位后进入 SHOW同时刷新 OLED 显示号码。SHOW 状态到达延时时间进入 CLOSING或者收到小程序/按键的合盖指令也进 CLOSING。CLOSING 状态舵机转回合盖位清空 OLED回到 SLEEP。FORCED_OPEN 状态不管当前处于什么状态只要收到小程序的“强制开盖”立即切到这个状态并保持开盖直到收到恢复指令。状态机的好处是每个状态只关心“我进来了要干什么”和“我满足什么条件可以出去”逻辑非常清晰。我后面很多调试问题都是靠把状态打印出来定位的——串口输出当前状态值就能看出程序卡在哪一步。3.2 舵机PWM控制与翻盖动作SG90舵机的控制原理不复杂给一个50Hz的PWM信号通过脉冲宽度控制角度。0.5ms对应0度2.5ms对应180度常规1.5ms是90度。市面上很多SG90实际上是“1ms到2ms”映射0到180度不同批次有差异所以最保险的做法是实际测试确定角度值。我用的是150度作为开盖角度这个位置刚好让翻板竖起号码正对车窗外又不至于顶到挡风玻璃。ESP32的Arduino库里有现成的ESP32Servo库用起来很省心#include ESP32Servo.h Servo lidServo; #define SERVO_PIN 13 #define LID_CLOSED_ANGLE 0 #define LID_OPENED_ANGLE 150 void setup() { lidServo.attach(SERVO_PIN, 500, 2500); // 500us~2500us对应0~180度 lidServo.write(LID_CLOSED_ANGLE); } void openLid() { // 分步转动避免猛甩 for (int angle LID_CLOSED_ANGLE; angle LID_OPENED_ANGLE; angle 2) { lidServo.write(angle); delay(15); } } void closeLid() { for (int angle LID_OPENED_ANGLE; angle LID_CLOSED_ANGLE; angle - 2) { lidServo.write(angle); delay(15); } }注意两个细节一是写循环里每次加2度并延时15ms实际转动时间大概1秒出头。这样做是防止舵机猛地甩过去机械结构容易松盖子弹起来还可能碰到玻璃。二是ESP32Servo库的attach函数里我显式传了500和2500两个脉宽参数不传的话默认是544和2400虽然也能用但显式传参能让角度更准确。3.3 OLED显示与电话号码隐藏逻辑屏幕在开盖状态下显示电话号码合盖后必须清屏。这里的“必须”不只是界面美观问题而是隐私底线OLED如果不清屏合盖后号码仍然透光可见尤其是夜间从车外能清楚看到字。所以我在程序里把清屏和舵机合盖动作绑定在一起舵机开始合盖前先清屏而不是等合盖完成后才清。显示部分用的Adafruit SSD1306库代码很直接#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h #define OLED_ADDR 0x3C Adafruit_SSD1306 display(128, 64, Wire, -1); void showPhoneNumber(const char* phone) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(22, 8); display.println(F(Please Call)); display.setTextSize(2); display.setCursor(8, 30); display.println(phone); display.display(); } void hidePhoneNumber() { display.clearDisplay(); display.display(); }电话号码存成一个常量字符串比如“13800138000”。这里有个小问题如果用的是中文提示比如“请拨打”OLED默认没有中文字库需要额外做取模或刷字库比较麻烦。我用英文“Please Call”任何年龄段的人都能看懂图标式的提示也省去了中文字库的折腾。OLED的I2C地址一般是0x3C但也有少数屏是0x3D。如果屏幕不亮先用I2C扫描程序确认一下地址别一上来就改线。3.4 触发逻辑与防误触发处理震动传感器是最容易误触发的。停在路边旁边大卡车经过地面震动一下翻盖就自动开了这显然不合适。我设计的触发逻辑不是“震动一次就开盖”而是“在一段时间内连续震动多次才开盖”。具体思路是500ms内记录震动脉冲累计达到3次才认为是一次有效触发。这个设计对应到使用场景就是别人需要联系你时敲车窗三下而旁边车辆经过产生的偶发震动不够3次不会触发。如果不希望敲击次数那么死板也可以在代码里把阈值改成参数比如2次或4次根据自己的停车环境调节。代码实现如下#define VIB_PIN 17 #define REQUIRED_VIBRATIONS 3 #define VIB_WINDOW_MS 500 unsigned long lastVibMs 0; int vibCount 0; bool checkVibrationTrigger() { if (digitalRead(VIB_PIN) HIGH) { unsigned long now millis(); if (now - lastVibMs 80) { // 简单去抖 lastVibMs now; vibCount; } } if (vibCount REQUIRED_VIBRATIONS) { vibCount 0; return true; } if (millis() - lastVibMs VIB_WINDOW_MS) { vibCount 0; // 窗口超时清零 } return false; }注意那个80ms的去抖时间实测SG90舵机转动时整个壳体会振动如果不加这个最小间隔过滤舵机自己的振动就可能把震动传感器触发成“连续敲击”最后变成开盖后反复重启的闹剧。这个坑我实际踩过调试时把程序打印全打开才发现是舵机在“自触发”。光敏模块则用来做昼夜逻辑。我设定夜晚时震动触发功能关闭只保留按键和蓝牙小程序开盖通道。原因很简单——晚上正常临停很少有人会去敲别人车窗真敲了大概率也是异常情况与其自动翻盖暴露号码不如让车主保持警惕。4. 微信小程序远程控制端实现4.1 蓝牙服务与通信协议设计ESP32做蓝牙BLE服务端小程序做客户端这是很典型的“手机外设”架构。ESP32端我定义了一个Service和三个Characteristic分别对应开盖、合盖、状态上报。为了简化我把指令合并成一个Characteristic写数据进去设备返回状态。官方建议的Service UUID一般用128位自定义UUID但为了演示方便我用了16位UUID实际使用中你可以用UUID生成器生成自己的128位值。UUID类型读写属性说明180CService-自定义翻盖服务2A56Characteristic: Command写写入控制指令2A57Characteristic: State读/通知读取当前状态指令协议定义非常简单指令值含义0x01开盖0x02合盖0x03查询状态0x04强制开盖并保持ESP32端用Arduino的BLEDevice库写得比较冗长我这里只贴核心的处理回调#include BLEDevice.h #include BLEUtils.h #include BLEServer.h #define SERVICE_UUID 180C #define CHAR_CMD_UUID 2A56 #define CHAR_STATE_UUID 2A57 BLECharacteristic *pStateChar; class CmdCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pChar) { std::string value pChar-getValue(); if (value.length() 0) return; switch (value[0]) { case 0x01: openLid(); break; case 0x02: closeLid(); break; case 0x03: pStateChar-setValue((uint8_t*)currentState, 1); pStateChar-notify(); break; case 0x04: forceOpenLid(); break; } } };4.2 小程序连接ESP32的关键代码微信小程序这边的BLE开发有几个必须注意的点必须在真机上调试开发者工具的模拟器不支持蓝牙。需要获取蓝牙权限iOS和Android都会弹出授权框拒绝授权后面全是白瞎。发现设备后要先匹配设备名比如“CarLid”避免误连其他蓝牙设备。小程序端连接流程的核心代码openBluetooth() { wx.openBluetoothAdapter({ success: () { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, success: () console.log(扫描中) }); }, fail: (err) console.error(蓝牙初始化失败, err) }); } onBluetoothDeviceFound(device) { if (device.name CarLid) { wx.stopBluetoothDevicesDiscovery(); wx.createBLEConnection({ deviceId: device.deviceId, success: () { wx.getBLEDeviceServices({ deviceId: device.deviceId, success: (res) { const service res.services.find(s s.uuid.includes(180c)); wx.getBLEDeviceCharacteristics({ deviceId: device.deviceId, serviceId: service.uuid, success: (res) { this.cmdChar res.characteristics.find(c c.uuid.includes(2a56)); } }); } }); } }); } } sendCommand(hex) { const buf new ArrayBuffer(1); const view new DataView(buf); view.setUint8(0, hex); wx.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.cmdChar.uuid, value: buf, success: () console.log(指令已发送) }); }注意Swift等原生开发里UUID的匹配要统一大小写小程序里经常是“180C”全大写而设备端返回的可能是“180c”全小写直接includes判断写死了字母大小写容易漏。我代码里用了includes来处理就不会被大小写卡住了。4.3 场景补充扫码与挪车通知既然做了小程序还能再拓展一个场景在翻盖外侧贴一个二维码别人扫码后打开小程序点击“通知挪车”小程序通过云函数给你的微信推送一条提醒并自动把翻盖打开显示电话号码。这样对方不需要打电话你也避免了号码直接暴露给对方隐私保护更彻底。其实这一步涉及小程序云开发、模板消息推送等属于后端范畴了。我没有详细展开如果你只是做硬件端用前面说的BLE按键触发和震动触发已经完全够用。5. 常见问题与排查实战5.1 舵机抖动、卡死与角度不准舵机问题出现频率最高尤其是“翻盖翻到一半不动了”。这个问题八成是供电问题SG90在堵转时电流很大USB供电或者小电流模块会直接掉电压主控跟着重启。排查方法用万用表量舵机电源引脚转动瞬间电压低于4.5V就是供电不足换带载能力更强的电源模块。还有一个常见情况舵机角度写150度实际翻过头或者盖不严。这不一定是程序问题而是SG90的舵机臂和翻盖机械结构之间有间隙每次定位误差会被放大。解决办法是留机械余量比如程序里开盖角度设为145度合盖角度设为5度只靠舵机本身的限位来定位长期使用也不会因为机械磨损导致位置漂移。5.2 OLED不显示或花屏OLED不亮先检查接线和I2C地址。SSD1306的I2C地址大部分是0x3C但也有一部分是0x3D。Adafruit库里默认是0x3D不改成0x3C的话就什么都不显示。花屏则多数是供电不足或者SCL/SDA线太长尤其当你把OLED和舵机共用一路5V电源时舵机一转OLED供电波动屏幕上就会出现杂点。建议OLED单独吃3.3V或者至少加一个100uF电容做滤波。5.3 传感器误触发与反应不灵敏震动传感器灵敏度是个矛盾体太灵敏旁边过个车都触发太迟钝敲窗三下没反应。我的经验是先把传感器安装位置固定好再做软件滤波。模块上的蓝色电位器可以调节比较器阈值别把灵敏度拧到头留一点余量。然后用代码做“连续震动N次”判定比单纯调阈值可靠得多。5.4 蓝牙连接不稳定小程序经常出现“设备已连接但发送指令无响应”原因一般是服务发现没走完就发指令了。解决办法是在getBLEDeviceCharacteristics的success回调里再发指令而不是在createBLEConnection成功后立刻发。另一个坑是断线重连Android上如果蓝牙权限被系统回收设备会出现假连接必须在onBLEConnectionStateChange里重新走完整连接流程。5.5 综合排查速查表现象可能原因解决办法翻盖不动舵机信号线接错或电源不足检查GPIO13接线换3A电源翻盖反复重启舵机堵转拉低电压给舵机独立供电加电容OLED无显示I2C地址不对或供电干扰改0x3C加滤波电容震动误触发灵敏度太高或舵机自振调电位器加80ms去抖小程序连不上权限未开或服务未发现授权蓝牙等待服务发现完成合盖后号码仍可见清屏指令未执行确认hidePhoneNumber被调用夜间自动开盖光敏逻辑未生效检查GPIO16光敏模块输出电平还有一些心得想分享出来。壳体的机械结构如果条件允许最好用3D打印做一个翻盖支架舵机臂和翻板之间用螺丝固定别用热熔胶凑合。热熔胶在夏天车里温度能到六七十度直接软化翻盖会掉下来。我之前就是图省事结果一星期就重做了。另外程序里我给所有状态都加了串口打印调试时连接电脑打开串口监视器能看到每个状态切换的日志排查效率翻倍。量产之后再把调试打印关掉反正ESP32的Serial打印会消耗一点CPU时间但放在这种项目里影响不大。还有一个值得考虑的细节由于车辆熄火后点烟器可能断电如果你的电源是从点烟器取的建议在ESP32的EN引脚加一个延时断电控制让系统在断电瞬间强制合盖。或者直接采用带备用电池的方案否则下次通电时翻盖停在开盖位置号码就一直在外面隐私保护就失效了。我目前用的方案是5V超级电容模块断电后还能坚持几秒足够完成合盖动作。这个细节一开始没注意后来有一次在楼下停了一夜第二天发现盖子开着号码暴露了一整晚才知道这件事有多重要。如果你也想做这个项目我的建议是分两步走第一步先跑通纯硬件逻辑震动触发开盖、延时合盖第二步再加入小程序蓝牙控制。先把基础状态机调稳再加上远程控制问题会好定位得多。小程序和ESP32的BLE调试比较费时间最好从一开始就把串口日志和微信开发者工具的调试信息都打开两边对照着看一次就能定位问题。
返回列表