导读:本期聚焦于本地能跑创作的《如何用Core Bluetooth实现iOS蓝牙智能风扇?风速调节、摇头控制、定时关机与温感调速》,敬请观看详情。为什么外设连接成功后,写入风速指令却偶尔丢失?这是iOS Core Bluetooth开发中典型的特征配置问题。本文以蓝牙智能风扇为例,梳理基于Core Bluetooth的完整控制链路:从CBCentralManager扫描外设、CBPeripheralDelegate维护服务与特征,到构建风速调节、摇头控制、定时关机三条写入通道。风扇通常自定义FFE0服务,风速和摇头特征使用WriteWithResponse保证指令可靠送达;定时特征适合使用WriteWithoutResponse降低交互延迟。温感调速则依赖温度特征的通知订阅,收到温度数据后在本地按阈值换算出目标风速并回写。整个过程需要注意特征属性判断、写类型选择、定时器与蓝牙状态联动,以及iOS后台蓝牙限制。掌握这些细节后,可快速复用到其他BLE家电控制场景。

在iOS平台接入一个BLE智能风扇,核心并不只是调用scanForPeripherals,而是要把连接、特征读写、通知订阅和本地控制策略串成一条稳定链路。假定风扇端已经暴露了自定义服务,主服务UUID为0000FFE0-0000-1000-8000-00805F9B34FB,风速、摇头、定时和温度特征分别使用FFE1到FFE4。下面会以这套协议为例,拆解从扫描到温感调速的完整实现。

如何用Core Bluetooth实现iOS蓝牙智能风扇?风速调节、摇头控制、定时关机与温感调速

一、外设扫描、连接与特征发现

Core Bluetooth的中心设备模式把手机作为CBCentralManager的角色。创建实例后,必须先在centralManagerDidUpdateState里判断蓝牙状态,只有在.poweredOn时才能发起扫描。扫描时建议直接指定主服务UUID,而不是扫描全部外设。全量扫描虽然省事,但会引入大量无关设备,后续还要根据广播数据做二次筛选,反而增加逻辑复杂度。

连接成功后,很多问题都源自忘记设置peripheral.delegate。这一步没做,后续的didDiscoverServices和didDiscoverCharacteristicsFor不会触发。发现服务后,再按UUID列表发现特征。温度特征需要主动订阅通知,setNotifyValue(true, for: char)会触发系统向设备的CCCD写入通知使能,这属于标准BLE流程,但对iOS开发者来说是透明的。

import CoreBluetooth

class FanController: NSObject {
    private var centralManager: CBCentralManager!
    private var targetPeripheral: CBPeripheral?
    private var windSpeedChar: CBCharacteristic?
    private var swingChar: CBCharacteristic?
    private var timerChar: CBCharacteristic?
    private var temperatureChar: CBCharacteristic?

    private let serviceUUID = CBUUID(string: "0000FFE0-0000-1000-8000-00805F9B34FB")
    private let windSpeedUUID = CBUUID(string: "0000FFE1-0000-1000-8000-00805F9B34FB")
    private let swingUUID = CBUUID(string: "0000FFE2-0000-1000-8000-00805F9B34FB")
    private let timerUUID = CBUUID(string: "0000FFE3-0000-1000-8000-00805F9B34FB")
    private let temperatureUUID = CBUUID(string: "0000FFE4-0000-1000-8000-00805F9B34FB")

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: .main)
    }
}

extension FanController: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            central.scanForPeripherals(withServices: [serviceUUID], options: nil)
        }
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        targetPeripheral = peripheral
        central.stopScan()
        central.connect(peripheral, options: nil)
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        peripheral.delegate = self
        peripheral.discoverServices([serviceUUID])
    }
}

extension FanController: CBPeripheralDelegate {
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let service = peripheral.services?.first(where: { $0.uuid == serviceUUID }) else { return }
        peripheral.discoverCharacteristics([windSpeedUUID, swingUUID, timerUUID, temperatureUUID], for: service)
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        for char in service.characteristics ?? [] {
            switch char.uuid {
            case windSpeedUUID:
                windSpeedChar = char
            case swingUUID:
                swingChar = char
            case timerUUID:
                timerChar = char
            case temperatureUUID:
                temperatureChar = char
                peripheral.setNotifyValue(true, for: char)
            default:
                break
            }
        }
    }
}

上面的代码将特征引用保存在属性里,后续所有操作都基于这些引用,不需要每次都去遍历service.characteristics。实际项目中,还应该处理连接失败、断开重连以及特征发现不完整的情况。可以给每个特征增加标记位,当全部关键特征都发现后再通知UI进入可操作状态。

二、风速调节与摇头控制的写入设计

风速和摇头通常各用一字节指令表示。风速可以约定为0x00静音、0x01低档、0x02中档、0x03高档,摇头用0x00关闭、0x01开启。这类协议需要和固件端提前对齐,字节序一般遵循低字节在前,单字节值不存在大小端问题。如果风速等级包含保留位或校验位,则需要按协议拼出完整帧,而不是简单写入等级值。

写入类型的选择直接影响交互体验。CBCharacteristicWriteType有两种:.withResponse会等待外设的ATT确认,适合关键指令,但单次耗时稍长;.withoutResponse不等待确认,速度快,适合高频指令。实际开发中应该先检查特征支持哪些properties,再决定写入类型。如果特征只支持.writeWithoutResponse,却强制使用.withResponse,系统会直接抛错。

func setWindSpeed(level: UInt8) {
    guard let char = windSpeedChar, let peripheral = targetPeripheral else { return }
    var value = level
    let data = Data(bytes: &value, count: 1)
    let type: CBCharacteristicWriteType = char.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
    peripheral.writeValue(data, for: char, type: type)
}

func setSwing(enabled: Bool) {
    guard let char = swingChar, let peripheral = targetPeripheral else { return }
    var value: UInt8 = enabled ? 0x01 : 0x00
    let data = Data(bytes: &value, count: 1)
    peripheral.writeValue(data, for: char, type: .withResponse)
}

摇头控制建议始终走.withResponse,因为用户点击按钮后需要明确知道是否成功。风速调节如果涉及滑块连续拖动,走.withoutResponse会流畅很多,但要注意蓝牙底层一次连接间隔能承载的写入次数有限,连续高频写入会被排队,甚至导致设备响应滞后。若产品需求中包含滑块调速,最好在滑块停止滑动后再发送最终挡位,而不是每变化一个像素就写一次。

发送失败或者超时是常见的坑。iOS没有为每次writeValue单独提供准确超时回调,didWriteValueFor的error只能反映底层链路错误。对关键指令,可以在业务层增加确认计时器,若限定时间内没有从设备状态通知中读到对应变化,就提示用户重试。

三、定时关机与本地倒计时方案

定时关机的实现有两种思路。第一种是设备端定时,App只是把用户设定的分钟数写入到定时特征里,由风扇固件自行倒计时关机。第二种是App本地定时,时间到了再向设备发送关机指令。能走设备端方案时优先选择设备端,因为App进入后台后本地Timer可能被系统挂起,导致倒计时不准,而外设时钟不受iOS后台状态影响。

定时特征可以约定为1字节,表示分钟数,范围0-180,写入0x00表示取消当前定时。写入类型建议使用.withResponse,这样可以确保定时指令到达设备。如果设备返回错误或者写入失败,App侧要及时恢复UI状态,避免出现界面显示已开启定时但风扇并未执行的情况。

func scheduleShutdown(minutes: UInt8) {
    guard let char = timerChar, let peripheral = targetPeripheral else { return }
    var value = minutes
    let data = Data(bytes: &value, count: 1)
    peripheral.writeValue(data, for: char, type: .withResponse)
}

如果外设没有定时特征,只能由App本地定时,那就要考虑后台限制。比较稳妥的做法是用UNUserNotificationCenter注册一个本地通知,在到达预设时间时提醒用户已经执行或即将执行关机。真正按时发送蓝牙指令,则需要依赖BGTaskScheduler或蓝牙后台模式。iOS对后台蓝牙连接有严格约束,App被杀死后无法继续维持连接,所以完全依赖本地的定时关机在用户体验上不可靠。

四、温感调速的数据链路与调优

温感调速依赖温度上报,而温度上报通常通过通知特征传递。打开通知后,外设可能每30秒或温度变化超过阈值时推送一次数据。iOS端在didUpdateValueFor characteristic中拿到Data后,需要按协议解析温度。很多BLE温度传感器使用2字节无符号整数,单位是0.1摄氏度,例如253表示25.3度;也有的设备直接发送1字节整摄氏度。解析错误会导致调速完全不准确。

拿到温度后,可以根据本地阈值计算目标风速。简单规则可以设为低于26度用1档,26到30度用2档,超过30度用3档。直接这样判断会出现临界点抖动问题。比如温度在30度附近来回波动,风速会频繁切换,电机噪声和蓝牙写入压力都会增大。解决方法是增加滞回区间,例如升温到30.5度才升到3档,降温到28.5度才降回2档。

private var lastLevel: UInt8 = 0

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    guard characteristic.uuid == temperatureUUID,
          let data = characteristic.value,
          data.count >= 2 else { return }
    let bytes = [UInt8](data)
    let rawValue = (UInt16(bytes[1]) << 8) | UInt16(bytes[0])
    let temperature = Double(rawValue) / 10.0
    autoAdjustSpeed(for: temperature)
}

private func autoAdjustSpeed(for temp: Double) {
    let level: UInt8
    switch temp {
    case ..<26.0:
        level = 1
    case 26.0..<30.0:
        level = 2
    default:
        level = 3
    }
    if lastLevel != level {
        lastLevel = level
        setWindSpeed(level: level)
    }
}

上述代码先按2字节小端格式解析温度数据,再判断是否真正需要切换风速。保存lastLevel可以避免同样的指令被重复写入。若想降低温度波动造成的影响,还可把滞回逻辑放到autoAdjustSpeed内部,用两个不同的阈值边界来做升档和降档。

温感调速模式还需要和手动模式做好隔离。当用户手动选择风速时,应先关闭温感自动控制,否则下一次温度更新又会覆盖用户的操作。可以在控制器里设置isAutoMode标记,手动调速时将其置为false,只有用户重新开启温感模式后才允许温度回调触发自动写入。

整个链路中,扫描、连接、特征发现、写入确认、通知订阅和本地策略是一个整体。把每个环节的错误分支补全,并在固件协议上约定好字节序、单位范围和写入属性,iOS端就能用Core Bluetooth稳定控制一台蓝牙智能风扇。类似方案同样适用于加湿器、远程灯控和其他BLE小家电。

Core Bluetooth智能风扇iOS蓝牙开发修改时间:2026-10-04 14:53:21

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1004/65598.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。