脉冲式智能水表的工作原理并不复杂:基表每流过一定体积的水,机械指针触发光电或磁敏开关,产生一个电脉冲。脉冲数量乘以脉冲当量(比如每个脉冲代表0.001立方米),就是累计用水量。iOS端要做的,是扫描并连接水表内置的BLE模块,订阅脉冲通知特征,收到数据后做抗干扰过滤,再换算成流量。这篇文章围绕Core Bluetooth框架,把整个链路中容易踩坑的环节逐一拆解。

一、Core Bluetooth基础与水表设备连接
Core Bluetooth是苹果官方提供的BLE(Bluetooth Low Energy)开发框架,核心角色分工很清晰:CBCentralManager负责扫描和连接外围设备,CBPeripheral代表被连接的水表。整个通信流程是:初始化中央管理器、扫描广播、发现设备、建立连接、发现服务、获取特征、订阅通知。
实际项目中,水表厂商通常会在广播包里携带自定义的厂商字段,包含表号、电池电量等信息。扫描时可以按Service UUID过滤,避免扫到一堆无关设备:
// 初始化中央管理器,队列传nil表示在主队列回调
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
// 状态就绪后按服务UUID扫描
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
if (central.state == CBManagerStatePoweredOn) {
NSArray *services = @[[CBUUID UUIDWithString:@"0000FFE0-0000-1000-8000-00805F9B34FB"]];
[central scanForPeripheralsWithServices:services options:nil];
}
}
// 发现目标设备后停止扫描再连接,减少信道冲突
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI {
[central stopScan];
self.waterMeter = peripheral;
peripheral.delegate = self;
[central connectPeripheral:peripheral options:nil];
}
连接成功后需要在didConnectPeripheral回调里调用discoverServices,再逐层发现特征。找到脉冲数据特征后,调用setNotifyValue:YES订阅通知。这一步非常关键,很多新手连不上数据,就是因为忘了订阅或者订阅的特征UUID不对。另外要注意,iOS不会缓存GATT数据库的变更,如果固件升级后服务结构变了,测试时最好先在蓝牙设置里忽略设备再重连。
二、脉冲数据解析与累计流量计算
不同厂商的脉冲上报格式差异较大,常见的有两类:一类是水表端只发脉冲事件,每来一个脉冲推一条通知,数据负载里带时间戳或序号;另一类是水表端自带计数器,周期性上报累计脉冲数。前者实时性好但丢一个通知就少计一个脉冲,后者更可靠,推荐优先采用周期上报加序号校验的方式。
假设厂商协议定义:特征FFE1每200毫秒上报一帧,帧格式为1字节帧头0xAA、4字节大端累计脉冲数、1字节校验和。解析代码如下:
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
if (error) return;
NSData *data = characteristic.value;
const uint8_t *bytes = data.bytes;
if (data.length != 6 || bytes[0] != 0xAA) return;
// 校验和验证,防止损坏帧进入统计
uint8_t sum = 0;
for (int i = 0; i < 5; i++) sum += bytes[i];
if (sum != bytes[5]) return;
uint32_t pulseCount = ((uint32_t)bytes[1] << 24) | ((uint32_t)bytes[2] << 16)
| ((uint32_t)bytes[3] << 8) | bytes[4];
// 处理32位计数器回绕
uint32_t delta = pulseCount - self.lastPulseCount;
self.lastPulseCount = pulseCount;
[self accumulateWithPulseDelta:delta];
}
累计流量的换算公式是:累计流量 = 累计脉冲数 × 脉冲当量。脉冲当量一定要从厂商资料中确认,常见值有1脉冲对应1升、10升或0.1立方米,算错一位小数结果就差十倍。瞬时流量可以按固定时间窗口内的脉冲数除以窗口时长得到,再乘以当量换算成升每分钟:
static const double kPulseEquivalent = 0.001; // 每脉冲0.001立方米,即1升
static const NSUInteger kWindowSeconds = 30; // 统计窗口30秒
- (void)accumulateWithPulseDelta:(uint32_t)delta {
self.totalPulses += delta;
self.totalVolume = self.totalPulses * kPulseEquivalent; // 立方米
}
- (double)instantFlowLPM {
return (double)self.pulsesInWindow / kWindowSeconds * 60.0
* kPulseEquivalent * 1000.0; // 换算为升/分钟
}
这里有个细节值得强调:不要用收到通知的系统时间来计算流量,BLE传输和iOS调度都有延迟,秒级抖动很正常。如果必须本地计时,用CACurrentMediaTime或mach_absolute_time这类单调时钟,绝不能用NSDate,后者会被用户手动改时间影响,改一次时间累计流量就可能算出负数。
三、抗干扰算法:过滤虚假脉冲
脉冲式水表最大的痛点是干扰。水泵启停、变频器运行、静电、接触抖动,都可能产生虚假脉冲,导致计量偏多。抗干扰要在两端配合做:水表固件端做硬件滤波和脉宽校验,iOS端做软件层面的合理性校验。下面介绍三种常用算法。
第一种是脉宽校验。真实的机械脉冲宽度通常在几十到几百毫秒,而干扰脉冲往往极窄或极宽。维护一个最近N个有效脉冲的宽度中位数,新脉冲宽度偏离中位数超过阈值就判定可疑。中位数比均值更抗极端值干扰,实现也简单:
- (BOOL)isPulseWidthValid:(NSTimeInterval)width {
if (width < 0.02 || width > 2.0) return NO; // 硬性边界
NSArray *sorted = [self.recentWidths sortedArrayUsingSelector:@selector(compare:)];
NSTimeInterval median = [sorted[sorted.count / 2] doubleValue];
NSTimeInterval ratio = width / median;
return (ratio > 0.3 && ratio < 3.0); // 允许3倍浮动
}
第二种是速率上限校验。根据水表的公称口径可以算出物理上不可能超过的最大流量,比如DN15水表最大流量约2.5立方米每小时,折算成脉冲频率后设一个上限。如果当前窗口的脉冲速率超过物理上限,说明出现了干扰或计数异常,此时应该丢弃增量并标记数据可疑,同时在界面上提示用户核查:
- (BOOL)isRatePlausible:(uint32_t)pulsesInWindow {
// DN15最大流量2.5m3/h,当量1L,即最大约2500脉冲/小时
double maxPulsesPerWindow = 2500.0 / 3600.0 * kWindowSeconds * 1.5; // 留50%余量
return pulsesInWindow <= maxPulsesPerWindow;
}
第三种是滑动窗口中位数滤波,适合水表端直接上报原始脉冲流的场景。用一个固定长度的窗口统计相邻脉冲间隔,取中位数作为基准间隔,连续多个间隔显著低于基准的脉冲群,大概率是接触抖动产生的毛刺,可以合并为一个有效脉冲。这三种方法可以叠加使用,实测能把干扰导致的计量误差从百分之几压到千分之一以下。
四、工程化细节:断线重连与数据持久化
真实使用场景里,用户不会一直停留在App页面,水表也可能因为电池省电策略主动断开。Core Bluetooth的连接恢复机制(State Restoration)能帮你在App被系统杀掉后恢复蓝牙任务:初始化CBCentralManager时传入CBCentralManagerOptionRestoreIdentifierKey,并在willRestoreState回调中恢复外围设备引用,重新订阅特征即可。
数据持久化方面,累计脉冲数每次变化后写入本地,可以存NSUserDefaults(简单场景)或SQLite(需要历史曲线时)。要注意写入频率控制,高频脉冲场景下每次变化都写磁盘会明显耗电,建议按分钟级批量落盘,并在App进入后台时强制写一次。此外建议保存水表端累计值和本地累计值两份数据,定期用水表端的值做校准,防止本地因丢通知导致的长期累积偏差。
最后提一点后台运行的限制:iOS后台模式下BLE保持连接是允许的,但需要在Capabilities中开启bluetooth-central后台模式,否则App退到后台几秒后连接就会被挂起。如果业务要求长时间后台采集,还需配合后台任务或考虑由水表端自行存储、App上线后拉取历史数据的设计,后者对电池和网络都更友好,也是目前商用方案的主流做法。
Core Bluetooth蓝牙智能水表脉冲计数修改时间:2026-09-13 02:54:38