智能井盖是智慧城市建设中一个很典型的物联网落地场景。市政部门最担心的情况之一,就是井盖被过往车辆碾压后发生倾斜,或者被人为撬动、盗走。传统的人工巡检覆盖面有限,而给井盖装上内置倾角传感器的BLE模组后,巡检人员或管理平台就能通过手机实时获知每个井盖的姿态状态。本文将以iOS端的Core Bluetooth框架为核心,完整实现从扫描连接、倾角数据解析、异常倾倒报警到姿态恢复通知的一整套流程。

一、智能井盖倾角传感器的通信协议设计
在动手写iOS代码之前,必须先弄清楚传感器端的BLE协议。市面上大多数倾角传感器模组(例如基于加速度计芯片加nRF52或CC2541的方案)都会自定义一个GATT服务。典型的做法是:设备广播包中携带厂商自定义字段,里面包含一个字节的当前状态(0x00表示正常水平,0x01表示倾斜,0x02表示被移动),以及两个字节的角度原始值。
本文假设的协议如下:传感器对外提供一个主服务,UUID为FFA0,其中包含三个特征——FFA1是倾角数据特征,支持Notify通知;FFA2是命令特征,App可以往里面写阈值配置;FFA3是设备信息特征,只读。倾角数据包共8个字节:第0字节为状态码,第1到2字节为X轴角度(有符号整数,单位0.1度),第3到4字节为Y轴角度,第5到6字节为Z轴角度,第7字节为校验和。
这种协议设计的优点是数据包短,BLE单次通知即可传输完成,功耗低。传感器端通常会设置一个机械开关或加速度阈值判断,当检测到倾角超过15度并持续5秒以上时,才会把状态置为倾斜并提升广播频率,方便附近的iOS设备快速发现异常。
二、Core Bluetooth扫描与连接实现
iOS端使用Core Bluetooth框架需要引入CoreBluetooth头文件,并让控制器遵循CBCentralManagerDelegate和CBPeripheralDelegate两个协议。整个流程是:创建中心管理器、等待蓝牙状态就绪、按服务UUID过滤扫描、发现设备后建立连接、连接成功后扫描服务和特征,最后订阅倾角数据特征的通知。
#import <CoreBluetooth/CoreBluetooth.h>
@interface WellCoverViewController () <CBCentralManagerDelegate, CBPeripheralDelegate>
@property (strong, nonatomic) CBCentralManager *centralManager;
@property (strong, nonatomic) CBPeripheral *currentPeripheral;
@property (strong, nonatomic) CBCharacteristic *angleCharacteristic;
@end
@implementation WellCoverViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建中心管理器,队列传nil表示在主队列回调
self.centralManager = [[CBCentralManager alloc]
initWithDelegate:self queue:nil];
}
// 蓝牙状态就绪后开始扫描,只扫描包含FFA0服务的设备
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
if (central.state == CBManagerStatePoweredOn) {
NSArray *services = @[[CBUUID UUIDWithString:@"FFA0"]];
NSDictionary *options = @{CBCentralManagerScanOptionAllowDuplicatesKey:@NO};
[self.centralManager scanForPeripheralsWithServices:services
options:options];
}
}
// 发现目标设备,广播包中可直接解析状态码
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *,id> *)advertisementData
RSSI:(NSNumber *)RSSI {
self.currentPeripheral = peripheral;
self.currentPeripheral.delegate = self;
[self.centralManager stopScan];
[self.centralManager connectPeripheral:peripheral options:nil];
}
// 连接成功后扫描主服务
- (void)centralManager:(CBCentralManager *)central
didConnectPeripheral:(CBPeripheral *)peripheral {
[peripheral discoverServices:@[[CBUUID UUIDWithString:@"FFA0"]]];
}
@end
连接成功后在peripheral:didDiscoverServices:回调中继续发现特征,找到FFA1后调用setNotifyValue:YES订阅通知,同时读取一次FFA3获取设备信息。需要注意的是,扫描时传入服务UUID做过滤非常重要,否则手机周围的耳机、手环等BLE设备会大量进入发现回调,既浪费电量又影响匹配效率。
三、倾角数据解析与异常倾倒报警
传感器每次姿态变化都会通过Notify推送一帧数据。收到回调后,第一件事是校验:将前7个字节累加取低8位,与第8字节校验和比对,不一致则丢弃该帧。校验通过后,按小端序解析出三个轴的角度值。判断异常的逻辑很简单——Z轴倾角(即井盖平面相对水平面的夹角)超过设定阈值,比如15度,且状态码为倾斜,即触发报警流程。
// 收到倾角数据通知
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
if (error || ![characteristic.UUID isEqual:
[CBUUID UUIDWithString:@"FFA1"]]) {
return;
}
NSData *data = characteristic.value;
if (data.length != 8) return;
const uint8_t *bytes = data.bytes;
// 校验和验证:前7字节累加的低8位应等于第8字节
uint8_t sum = 0;
for (int i = 0; i < 7; i++) sum += bytes[i];
if (sum != bytes[7]) return;
uint8_t status = bytes[0];
// 小端序解析有符号角度,单位0.1度
int16_t xAngle = (int16_t)(bytes[1] | (bytes[2] << 8));
int16_t zAngle = (int16_t)(bytes[5] | (bytes[6] << 8));
CGFloat tilt = zAngle / 10.0;
if (status == 0x01 && fabs(tilt) >= 15.0) {
[self triggerTiltAlarmWithAngle:tilt];
} else if (status == 0x00) {
[self markCoverNormal];
}
}
// 触发本地报警:震动加本地通知
- (void)triggerTiltAlarmWithAngle:(CGFloat)angle {
UNMutableNotificationContent *content =
[[UNMutableNotificationContent alloc] init];
content.title = @"井盖倾斜报警";
content.body = [NSString stringWithFormat:
@"检测到井盖倾斜 %.1f 度,请尽快处理", angle];
content.sound = [UNNotificationSound defaultSound];
UNNotificationRequest *request =
[UNNotificationRequest requestWithIdentifier:@"tiltAlarm"
content:content trigger:nil];
[[UNNotificationCenter currentNotificationCenter]
addNotificationRequest:request withCompletionHandler:nil];
}
报警方式上建议做分层:App在前台时直接弹窗加声音提醒;在后台或锁屏状态下依赖本地通知,前提是必须在Xcode的Capabilities中开启Background Modes并勾选Uses Bluetooth LE accessories,否则App退到后台几秒后蓝牙回调就会被系统挂起,倾倒报警就收不到了。另外要注意iOS对本地通知有频率限制,短时间内重复的倾斜事件应当做去重和节流,比如同一设备30秒内只推一条报警。
四、姿态恢复判断与断线重连
姿态恢复和倾倒报警同样重要。运维人员把倾斜的井盖复位后,传感器会推送一帧状态码为0x00的数据,App收到后应当把界面上该井盖的红色告警状态改为绿色正常,并记录恢复时间形成完整的告警闭环。为了避免传感器抖动导致的误判,建议在App层再做一个简单的确认逻辑:连续收到3帧正常状态数据才确认恢复,帧间隔通常是传感器端500毫秒一次的心跳。
// 姿态恢复确认:连续3帧正常才标记恢复
- (void)markCoverNormal {
static NSInteger normalCount = 0;
normalCount++;
if (normalCount >= 3) {
normalCount = 0;
dispatch_async(dispatch_get_main_queue(), ^{
self.statusLabel.text = @"姿态正常";
self.statusLabel.textColor = [UIColor systemGreenColor];
});
// 上报恢复事件到服务器,形成告警闭环
[self reportRecoveryEvent];
}
}
// 断开后自动重连
- (void)centralManager:(CBCentralManager *)central
didDisconnectPeripheral:(CBPeripheral *)peripheral
error:(NSError *)error {
self.currentPeripheral = nil;
// 延迟2秒重试连接,避免立即重连失败风暴
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
[self.centralManager retrievePeripheralsWithIdentifiers:
@[[self savedIdentifier]]];
});
}
重连策略上有个实用技巧:首次连接成功后把peripheral.identifier存到UserDefaults,之后断线时优先用retrievePeripheralsWithIdentifiers:直接取回外设对象再连接,比重新扫描快得多。如果设备已经超出蓝牙范围,则退回到扫描广播的流程。对于批量管理大量井盖的场景,还可以结合iBeacon的区域监听能力,在巡检人员靠近某个井盖区域时再建立连接,进一步降低功耗。
五、实测中的常见坑与优化建议
第一个坑是后台扫描限制。iOS后台模式下扫描到的广播事件会被系统降频去重,若依赖广播包中的状态码做报警,可能会延迟较久,所以核心报警一定要走连接后的Notify通道而不是广播解析。第二个坑是连接参数,部分低成本传感器模组连接间隔较长,导致数据推送有明显延迟,可以在连接成功后通过写命令特征调整传感器的上报间隔,或者直接要求固件端在倾斜事件发生时临时缩短连接间隔。
第三个坑是角度精度。加速度计方案在井盖受到持续震动时读数会抖动,软件侧除了去重节流,还可以做滑动平均滤波,取最近5帧的平均角度再与阈值比较,能有效减少车辆驶过引起的误报。最后,如果App需要长期在后台监听,建议向用户说明并引导开启常驻通知,同时做好电量监控,因为蓝牙持续连接对iPhone续航的影响是不可忽视的,合理的设计是在巡检工作时段才保持连接,非工作时段依靠传感器端直接上报服务器的通道兜底。
Core Bluetooth智能井盖倾角传感器修改时间:2026-09-03 18:09:20