基于Adafruit Feather nRF52832与iOS的BLE数据采集与实时图表显示
2026/6/1 14:00:58 网站建设 项目流程

1. 项目概述与核心价值

如果你手头有一块Adafruit Feather nRF52832开发板,想让它采集的传感器数据实时显示在iPhone上,却苦于找不到一个现成、好用的App,那么这个项目就是为你准备的。我最近完成了一个从硬件固件到iOS App的完整开发流程,核心目标就是让Feather nRF52832通过蓝牙低功耗(BLE)向iPhone发送数据,并在手机端用漂亮的折线图实时展示出来。整个过程涉及Arduino编程、iOS的CoreBluetooth框架以及Charts图表库的使用,算是一个典型的物联网端到端应用原型。

这个方案的价值在于,它为你提供了一个可复用的基础框架。无论是想监测环境温湿度、记录运动传感器的加速度,还是想远程查看某个设备的运行状态,你都可以基于这个框架快速搭建起自己的数据采集与可视化系统。Adafruit Feather nRF52832本身集成了强大的nRF52832 SoC,蓝牙功能稳定且功耗极低,非常适合作为各种物联网项目的“大脑”。而iOS设备作为数据接收和展示终端,拥有出色的交互体验和普及度。通过这个项目,你将掌握如何打通这两个平台,实现数据的无线传输与可视化,为你的创意项目增添一个专业的移动端界面。

2. 硬件准备与Arduino环境搭建

2.1 开发板选型与核心特性解析

我选择Adafruit Feather nRF52832作为硬件核心,主要基于几个关键考量。首先,它采用了Nordic Semiconductor的nRF52832系统级芯片,这颗芯片集成了ARM Cortex-M4F内核、512KB Flash和64KB RAM,性能足以应对多数嵌入式任务。更重要的是,它内置了蓝牙5.0低功耗射频模块,这意味着我们无需外接复杂的蓝牙模块,简化了硬件设计和连线。其次,Feather生态系统的兼容性很好,板载了锂聚合物电池充电管理芯片和STEMMA QT/Qwiic连接器,方便后续扩展各种I2C传感器。最后,Adafruit提供了完善的Arduino核心支持库,使得我们可以用熟悉的Arduino IDE和语法进行开发,大大降低了嵌入式编程的门槛。

在开始之前,你需要准备以下硬件:

  1. Adafruit Feather nRF52832开发板一块。
  2. 一台运行macOS的电脑(用于iOS开发和Arduino编程)。
  3. 一根Micro-USB数据线,用于给开发板供电和上传程序。
  4. 一部iPhoneiPad(系统版本建议iOS 14及以上),用于运行和测试我们开发的App。需要特别注意的是,iOS模拟器无法模拟蓝牙硬件,因此必须在真机上测试。

2.2 Arduino IDE配置与Bootloader更新

要让Arduino IDE识别并支持Feather nRF52832,我们需要先添加对应的板卡支持包。打开Arduino IDE,进入“文件”->“首选项”。在“附加开发板管理器网址”框中,粘贴以下URL(每行一个):

https://adafruit.github.io/arduino-board-index/package_adafruit_index.json https://sandeepmistry.github.io/arduino-nRF5/package_nRF5_boards_index.json

注意:不同教程可能推荐不同的JSON文件地址。adafruit.github.io这个地址是Adafruit官方维护的,包含了他们所有板子的定义,是最稳定可靠的选择。添加多个源有时会导致冲突。

添加完成后,点击“确定”关闭首选项。接着,进入“工具”->“开发板”->“开发板管理器”。在顶部的搜索框中输入“Adafruit nRF52”,等待列表刷新后,你应该能看到“Adafruit nRF52 by Adafruit”这个条目。点击它,然后选择安装最新版本。这个过程会下载并安装所有必要的工具链和库文件,包括ARM GCC编译器、nRF5x命令行工具以及Adafruit的BLE库等。

安装完成后,在“工具”->“开发板”菜单下,你应该能找到“Adafruit nRF52 Boards”分组,选择其中的“Adafruit Feather nRF52832”。接下来还需要选择正确的“编程器”。对于通过USB上传代码,我们需要使用“Adafruit nRF52 Bootloader”作为编程器。端口(Port)选择你的Feather板子所连接的USB串口。

在首次上传代码前,强烈建议检查并更新板载的Bootloader。Bootloader是板子上的一段小程序,负责接收来自电脑的固件并烧录到主芯片中。一个过旧或有问题的Bootloader可能导致上传失败。更新方法很简单:在选中了正确的开发板和编程器后,点击“工具”菜单,在最下方找到“Burn Bootloader”并点击。IDE会通过USB连接,向板子写入最新的Bootloader。这个过程通常很快,完成后你的Feather nRF52832就为编程做好了准备。

3. Arduino固件开发:实现BLE数据发送

3.1 理解BLE通信模型与Adafruit BLE库

在编写代码前,我们需要简单理解一下BLE(蓝牙低功耗)的通信模型。BLE设备通常分为两类:外围设备中央设备。我们的Feather nRF52832在这个项目中扮演外围设备的角色,它像是一个服务提供者,广播自己的存在并等待连接。而iPhone则作为中央设备,主动扫描并连接外围设备。

外围设备通过服务特征值来组织数据。一个服务可以包含多个特征值。特征值是实际进行数据读写操作的最小单元。例如,我们可以定义一个“数据采集服务”,里面包含一个“模拟数据特征值”,中央设备通过订阅这个特征值,就能在外围设备数据更新时自动收到通知。

Adafruit为nRF52系列提供了强大的Adafruit_Bluefruit_nRF52库,它封装了底层复杂的BLE协议栈,提供了简单易用的API。我们将基于这个库中的一个示例进行修改。

3.2 修改bleuart示例代码

在Arduino IDE中,确保已选择“Adafruit Feather nRF52832”开发板。然后通过“文件”->“示例”->“Adafruit Bluefruit nRF52 Libraries”->“Peripheral”->“bleuart”打开示例代码。这个示例实现了一个简单的BLE UART服务,允许中央设备像使用串口一样收发文本数据,非常适合我们的场景。

我们需要对示例代码进行几处关键修改,核心目标是让板子自动、持续地读取模拟引脚A0的电压值,并通过BLE发送出去,而不是等待串口输入。

第一处修改:在loop()函数中实现自动数据发送找到原代码中loop()函数里等待串口输入并转发的那段代码(通常在while (Serial.available())循环内)。我们将用以下代码替换它,实现每500毫秒读取一次A0引脚并发送:

void loop() { // 等待足够的时间,因为我们有有限的传输缓冲区 delay(500); char buf[64]; // 从nRF52832的A0引脚读取模拟值(0-1023) int sensorValue = analogRead(A0); // 将整型数值转换为字符串,因为BLE特征值通常处理字符数据 String valueString = String(sensorValue); // 将字符串转换为字符数组,以便通过BLE发送 valueString.toCharArray(buf, 64); // 通过BLE UART服务发送字符数组 bleuart.write(buf, strlen(buf)); }

这段代码的逻辑很清晰:analogRead(A0)读取引脚电压(映射到0-1023的整数值),转换成字符串,然后通过bleuart.write()函数发送出去。delay(500)控制了数据发送的频率,这里设置为每秒发送2次数据,对于多数传感器监控场景已经足够,你也可以根据需求调整。

第二处修改:引入连接状态控制循环原示例的while (Serial.available())循环意味着只有串口监视器有输入时才会进入循环发送数据。我们希望App一连接,就自动开始发送。为此,我们需要一个标志位来跟踪BLE连接状态。

  1. 在文件顶部,定义全局变量区,添加一个布尔变量:
    bool bleConnected = false;
  2. loop()函数中的while (Serial.available())条件改为while (bleConnected)。这样,只要bleConnectedtrue,循环就会持续执行我们上面添加的数据发送代码。
  3. 在BLE连接成功的回调函数connect_callback()中,添加一行代码,在连接建立时将标志位置true
    bleConnected = true;
  4. 在BLE断开连接的回调函数disconnect_callback()中,添加一行代码,在连接断开时将标志位置false
    bleConnected = false;

完成这些修改后,将代码上传到你的Feather nRF52832。上传成功后,打开Arduino IDE的串口监视器(波特率115200),你应该能看到板子启动并开始广播BLE信号的日志信息。此时,用手机蓝牙设置扫描,应该能发现一个名为“Adafruit Bluefruit LE”的设备(这是库的默认名称,可在代码中修改)。至此,硬件端的准备工作就全部完成了。

实操心得:在修改代码时,务必注意变量作用域和函数调用顺序。bleConnected变量必须在所有使用它的函数(如loop,connect_callback)之前声明为全局变量。另外,Adafruit的BLE库初始化需要一定时间,在setup()函数中调用Bluefruit.begin()后,建议添加一小段延时delay(500),确保蓝牙模块完全启动后再进行后续操作,可以避免一些奇怪的连接失败问题。

4. iOS开发环境配置与项目初始化

4.1 Xcode项目创建与基础设置

iOS开发需要在macOS上进行,并使用Xcode作为集成开发环境。首先,确保你的Mac上安装了最新稳定版本的Xcode(可以从Mac App Store免费下载)。打开Xcode,选择“Create a new Xcode project”。在模板选择界面,确保平台选择为“iOS”,然后选择“App”模板,点击“Next”。

接下来是项目配置页面,有几个关键项需要填写:

  • Product Name:你的应用名称,例如“BLEGraphViewer”。
  • Team:你的Apple开发者账号团队。如果你只是个人开发并在真机上测试,你需要有一个免费的Apple ID账户,并在此处登录。Xcode会自动帮你创建免费的开发证书。如果没有团队,可以点击“Add Account...”进行添加。
  • Organization Identifier:组织标识符,通常采用“com.你的名字”的反域名格式,例如“com.yourname”。这个标识符和产品名共同组成应用的Bundle Identifier,它是应用在系统内的唯一ID。
  • Interface:选择“Storyboard”。这是苹果传统的UI构建方式,对于初学者和大多数项目来说更直观易懂。
  • Language:选择“Swift”。Swift是苹果主推的现代编程语言,比Objective-C更简洁安全。
  • 确保不勾选“Use Core Data”和“Include Tests”,以保持项目简洁。

点击“Next”,选择项目保存的位置,然后点击“Create”。一个全新的iOS项目就创建好了。

4.2 配置蓝牙权限与安装Charts图表库

iOS系统出于隐私和安全考虑,对蓝牙访问有严格的权限要求。我们必须明确告知用户应用需要使用蓝牙,否则应用将无法扫描或连接任何设备。

在Xcode左侧的项目导航器中,找到并点击名为Info.plist的文件。这个文件以属性列表的形式存储了应用的各种配置信息。在任意一行上右键,选择“Add Row”。在出现的键值列表中,滚动或搜索找到以下两项,并分别添加:

  1. Privacy - Bluetooth Always Usage Description:用于描述应用为何需要始终使用蓝牙(即使应用在后台)。值可以填写为“本应用需要通过蓝牙连接您的Feather设备以接收传感器数据”。
  2. Privacy - Peripheral Usage Description:用于描述应用为何需要使用蓝牙外围设备功能。值可以填写为“用于扫描和连接您的Feather蓝牙设备”。

添加这两项后,当应用首次尝试使用蓝牙时,系统会向用户弹出提示框,显示你填写的描述,请求用户授权。

接下来,我们需要引入一个强大的第三方图表库——Charts,来绘制接收到的数据曲线。在iOS开发中,我们通常使用CocoaPods来管理第三方库依赖。CocoaPods是一个Ruby gem,需要先在系统上安装。

打开终端(Terminal),使用cd命令进入你刚才创建的Xcode项目所在的目录。确认目录下存在.xcodeproj文件。然后根据你的Mac芯片类型执行安装命令:

  • Intel芯片Mac
    sudo gem install cocoapods
  • Apple Silicon (M1/M2/M3) 芯片Mac
    sudo arch -x86_64 gem install ffi arch -x86_64 pod install

    注意:M系列芯片的Mac在安装某些Ruby gem时可能会遇到架构兼容性问题,使用arch -x86_64前缀可以强制在x86_64架构下运行安装命令,避免问题。

安装完成后,仍在项目根目录下,初始化CocoaPods并安装Charts库:

pod init

这个命令会在当前目录生成一个名为Podfile的配置文件。用文本编辑器打开它:

open Podfile

你会看到类似以下内容:

# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'BLEGraphViewer' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for BLEGraphViewer end

target 'BLEGraphViewer' doend之间,添加一行:

pod 'Charts'

保存并关闭Podfile。回到终端,运行安装命令:

pod install

CocoaPods会自动下载Charts库及其依赖,并生成一个新的.xcworkspace工作空间文件。非常重要:从此以后,你必须使用这个新生成的.xcworkspace文件来打开和编辑你的项目,而不是原来的.xcodeproj文件。关闭Xcode,双击打开BLEGraphViewer.xcworkspace,你会看到项目导航器中多了一个Pods项目,里面包含了Charts库。

5. iOS应用界面设计与Swift代码实现

5.1 使用Storyboard构建用户界面

在Xcode中打开.xcworkspace文件后,在左侧项目导航器中找到并点击Main.storyboard文件。中间区域会显示一个空白的iPhone画布,这就是我们设计界面的地方。

首先,我们需要在界面顶部添加一个导航栏。点击画布底部工具栏的“+”按钮(或使用快捷键Cmd+Shift+L)打开库面板。在搜索框中输入“Navigation Bar”,将其拖拽到画布顶部。导航栏会自动贴合状态栏。双击导航栏中间的“Title”文字,将其修改为你应用的名字,比如“BLE数据图表”。

接下来,我们从库面板中拖拽其他需要的控件到画布上:

  1. UILabel (标签):拖拽两个到画布上。一个放在左上角,用来显示连接状态,将其文本改为“未连接”,文本颜色暂时设为红色。这个控件在代码中我们将命名为connectStatusLabel。另一个放在画布中部偏左,文本改为“显示图表”,这个将命名为showGraphLabel
  2. UISwitch (开关):拖拽一个到“显示图表”标签的右侧,用于控制图表是否显示。将其命名为showGraphSwitch,默认状态设为关闭(Off)。
  3. UIButton (按钮):我们需要一个刷新/扫描按钮。由于导航栏右侧通常放置操作按钮,我们可以直接使用导航栏的UIBarButtonItem。在库中搜索“Bar Button Item”,拖拽到导航栏的右侧区域。点击这个按钮,在右侧属性检查器中,将“System Item”从“Custom”改为“Refresh”。这样它就显示为一个标准的刷新图标。我们将在代码中为其关联一个动作。

控件摆放好后,需要为它们添加约束。约束定义了控件相对于父视图或彼此之间的位置和大小关系,确保应用在不同尺寸的iPhone上都能正确布局。以“显示图表”标签为例:

  1. 选中这个UILabel
  2. 点击画布右下角的“Add New Constraints”按钮(看起来像个TIE战斗机)。
  3. 我们需要固定它的左边缘和上边缘。在弹出面板中,点击左箭头和上箭头旁边的红色实线,使其变为红色,表示添加该约束。在旁边的输入框中可以设置具体的距离值,例如左边距20点,上边距100点。
  4. 确保“Constrain to margins”复选框是未勾选状态。这样约束就是相对于屏幕边缘,而不是安全区域边距,位置更可控。
  5. 点击“Add 2 Constraints”。

用同样的方法,为开关、连接状态标签和导航栏添加上相应的约束。对于导航栏,通常只需要固定其顶部、左侧和右侧与父视图对齐即可。添加完所有约束后,可以点击画布底部的“Resolve Auto Layout Issues”按钮(三角形尺子图标),选择“Update Frames”,让Xcode根据约束自动调整控件的位置和大小。

5.2 连接界面与代码:Outlet与Action

界面设计好后,需要将其与Swift代码关联起来,这样我们才能在代码中控制这些界面元素,并响应用户的操作。

首先,确保Main.storyboardViewController.swift文件在Xcode中并排打开(可以使用右上角的“Adjust Editor Options”按钮选择并列视图)。点击画布右上角的“双环”图标,打开辅助编辑器,右侧会自动显示ViewController.swift文件。

创建Outlet(输出口):Outlet允许我们在代码中引用界面上的控件。按住Ctrl键,从“显示图表”标签拖拽一条线到ViewController.swift文件中class ViewController: UIViewController {这行下面的大括号内。松开鼠标后,会弹出一个对话框。将“Connection”类型保持为“Outlet”,在“Name”字段输入showGraphLabel,点击“Connect”。这样就创建了一个名为showGraphLabel的属性,它指向故事板中的那个标签。重复此过程,为“连接状态”标签创建connectStatusLabel,为开关创建showGraphSwitch

创建Action(动作):Action用于响应用户对控件的操作,比如点击按钮、切换开关。按住Ctrl键,从导航栏的刷新按钮拖拽到ViewController.swift文件中。在弹出对话框中,将“Connection”类型改为“Action”。在“Name”字段输入refreshButtonTapped,事件类型保持为“Touch Up Inside”,点击“Connect”。这会在代码中生成一个@IBAction方法,当用户点击刷新按钮时,这个方法就会被调用。用同样的方法,为showGraphSwitch开关创建一个Action,命名为switchValueChanged,事件类型为“Value Changed”。

5.3 实现CoreBluetooth核心逻辑

现在进入核心的蓝牙通信代码部分。首先在ViewController.swift文件顶部导入必要的框架:

import UIKit import CoreBluetooth // 用于蓝牙通信 import Charts // 用于绘制图表

接着,我们需要让ViewController类遵循CBCentralManagerDelegateCBPeripheralDelegate协议。这两个协议是CoreBluetooth框架的核心,前者用于管理中央设备(我们的手机)的状态和扫描,后者用于管理与外围设备(Feather)连接后的交互。修改类声明:

class ViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {

然后,在类内部定义一些必要的属性和常量:

// CoreBluetooth 管理器,负责扫描和连接 var centralManager: CBCentralManager! // 当前连接的外围设备 var connectedPeripheral: CBPeripheral? // 用于接收数据的特征值(从Feather读取) var rxCharacteristic: CBCharacteristic? // 用于发送数据的特征值(向Feather写入,本例未使用) var txCharacteristic: CBCharacteristic? // 存储扫描到的设备列表和信号强度 var peripheralList: [CBPeripheral] = [] var rssiList: [NSNumber] = [] // Feather nRF52832 使用的BLE服务与特征值UUID // 这些UUID必须与Arduino代码中Adafruit BLE库使用的保持一致 let BLE_Service_UUID = CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E") let BLE_Characteristic_uuid_Rx = CBUUID(string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E") // RX (Receive) let BLE_Characteristic_uuid_Tx = CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E") // TX (Transmit) // 存储接收到的数据 var receivedData: [Double] = [] // 图表显示开关状态 var showGraphIsOn = false // 定时器,用于控制扫描时长 var scanTimer: Timer?

viewDidLoad()方法中进行初始化设置:

override func viewDidLoad() { super.viewDidLoad() // 初始化中央管理器,并设置当前类为其代理 centralManager = CBCentralManager(delegate: self, queue: nil) // 初始化界面状态 connectStatusLabel.text = "未连接" connectStatusLabel.textColor = .red showGraphSwitch.isOn = false }

接下来实现CBCentralManagerDelegate的核心方法:

// 当蓝牙中心管理器状态更新时调用(例如用户打开或关闭手机蓝牙) func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOn: // 蓝牙已开启,可以开始扫描 print("蓝牙已就绪") startScanning() case .poweredOff: print("蓝牙已关闭") connectStatusLabel.text = "蓝牙未开启" connectStatusLabel.textColor = .red // 可以在这里提示用户打开蓝牙 case .unsupported, .unauthorized, .unknown, .resetting: // 处理其他不支持或未知状态 print("蓝牙不可用: \(central.state)") connectStatusLabel.text = "蓝牙不可用" connectStatusLabel.textColor = .red @unknown default: print("未知的蓝牙状态") } } // 开始扫描外围设备 func startScanning() { print("开始扫描BLE设备...") peripheralList.removeAll() rssiList.removeAll() // 扫描指定服务UUID的设备,这样能更快找到我们的Feather centralManager.scanForPeripherals(withServices: [BLE_Service_UUID], options: nil) // 设置一个10秒后停止扫描的定时器 scanTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { [weak self] _ in self?.stopScanning() } } // 停止扫描 func stopScanning() { centralManager.stopScan() print("停止扫描") // 如果扫描期间没有找到设备,可以更新UI提示 if peripheralList.isEmpty { connectStatusLabel.text = "未找到设备" } } // 当扫描发现外围设备时调用 func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { print("发现设备: \(peripheral.name ?? "未知设备"), RSSI: \(RSSI)") // 避免重复添加同一设备 if !peripheralList.contains(peripheral) { peripheralList.append(peripheral) rssiList.append(RSSI) // 通常我们连接第一个发现的设备(假设就是我们的Feather) // 在实际应用中,你可能需要让用户从列表中选择 if connectedPeripheral == nil { connectToPeripheral(peripheral) } } } // 连接到指定外围设备 func connectToPeripheral(_ peripheral: CBPeripheral) { centralManager.stopScan() // 停止扫描,准备连接 scanTimer?.invalidate() connectedPeripheral = peripheral connectedPeripheral?.delegate = self centralManager.connect(peripheral, options: nil) connectStatusLabel.text = "连接中..." connectStatusLabel.textColor = .orange } // 连接成功 func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("连接成功: \(peripheral.name ?? "未知设备")") connectStatusLabel.text = "已连接" connectStatusLabel.textColor = .systemBlue // 连接成功后,开始发现设备提供的服务 peripheral.discoverServices([BLE_Service_UUID]) } // 连接失败 func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { print("连接失败: \(error?.localizedDescription ?? "未知错误")") connectStatusLabel.text = "连接失败" connectStatusLabel.textColor = .red connectedPeripheral = nil } // 连接断开 func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { print("连接断开") connectStatusLabel.text = "未连接" connectStatusLabel.textColor = .red connectedPeripheral = nil // 可以尝试自动重连 // centralManager.connect(peripheral, options: nil) }

当连接成功并发现服务后,会进入CBPeripheralDelegate的方法流:

// 发现服务 func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { print("发现服务错误: \(error.localizedDescription)") return } guard let services = peripheral.services else { return } for service in services { print("发现服务: \(service.uuid)") // 查找我们需要的服务 if service.uuid == BLE_Service_UUID { // 发现该服务下的特征值 peripheral.discoverCharacteristics([BLE_Characteristic_uuid_Rx, BLE_Characteristic_uuid_Tx], for: service) } } } // 发现特征值 func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { print("发现特征值错误: \(error.localizedDescription)") return } guard let characteristics = service.characteristics else { return } for characteristic in characteristics { print("发现特征值: \(characteristic.uuid)") if characteristic.uuid == BLE_Characteristic_uuid_Rx { // 这是我们接收数据的特征值 rxCharacteristic = characteristic // 订阅这个特征值,这样当Feather发送新数据时,我们会自动收到通知 peripheral.setNotifyValue(true, for: characteristic) } else if characteristic.uuid == BLE_Characteristic_uuid_Tx { // 这是我们发送数据的特征值(本例中未使用) txCharacteristic = characteristic } } } // 收到特征值更新的通知(即Feather发送了新数据) func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { print("接收数据错误: \(error.localizedDescription)") return } guard let data = characteristic.value else { return } // 将接收到的数据(Data类型)转换为字符串 if let receivedString = String(data: data, encoding: .utf8) { print("收到数据: \(receivedString)") // 尝试将字符串转换为整数(Arduino发送的模拟值) if let intValue = Int(receivedString) { // 将整数转换为Double用于图表,并添加到数据数组 let dataPoint = Double(intValue) receivedData.append(dataPoint) // 如果数据太多,移除最旧的数据,保持最近100个点 if receivedData.count > 100 { receivedData.removeFirst() } // 如果图表开关打开,则更新图表 if showGraphIsOn { updateGraph() } } } }

最后,实现刷新按钮和开关的Action方法,以及更新图表的函数:

// 刷新按钮点击事件 @IBAction func refreshButtonTapped(_ sender: UIBarButtonItem) { print("手动刷新") // 如果已连接,则断开并重新扫描 if let peripheral = connectedPeripheral { centralManager.cancelPeripheralConnection(peripheral) } startScanning() } // 开关状态改变事件 @IBAction func switchValueChanged(_ sender: UISwitch) { showGraphIsOn = sender.isOn if showGraphIsOn && !receivedData.isEmpty { updateGraph() } else { // 清空图表 clearGraph() } } // 更新图表 func updateGraph() { // 1. 创建图表数据条目数组 var chartDataEntries: [ChartDataEntry] = [] for (index, value) in receivedData.enumerated() { let entry = ChartDataEntry(x: Double(index), y: value) chartDataEntries.append(entry) } // 2. 创建数据集 let dataSet = LineChartDataSet(entries: chartDataEntries, label: "传感器数据") // 自定义数据集样式 dataSet.colors = [.systemBlue] // 线条颜色 dataSet.lineWidth = 2.0 dataSet.circleColors = [.systemRed] // 数据点颜色 dataSet.circleRadius = 4.0 dataSet.drawCircleHoleEnabled = false dataSet.mode = .linear // 折线模式 dataSet.drawValuesEnabled = false // 不显示每个点的数值 // 3. 创建图表数据对象并赋值给图表视图 let chartData = LineChartData(dataSet: dataSet) // 假设你在Storyboard中拖入了一个LineChartView,并创建了Outlet命名为`lineChartView` lineChartView.data = chartData // 4. 可选:自定义图表视图外观 lineChartView.xAxis.labelPosition = .bottom lineChartView.xAxis.drawGridLinesEnabled = false lineChartView.rightAxis.enabled = false // 关闭右侧Y轴 lineChartView.leftAxis.drawGridLinesEnabled = true lineChartView.legend.enabled = false // 不显示图例 lineChartView.chartDescription?.text = "实时数据流" // 图表描述 } // 清空图表 func clearGraph() { lineChartView.clear() // 或者设置为空数据 // lineChartView.data = nil }

注意事项:在实现updateGraph函数前,你还需要回到Main.storyboard,从库中拖拽一个LineChartView到界面上(需要先在库中搜索“Chart”,因为它是第三方控件),并为其添加约束,使其占据屏幕主要区域。然后像之前创建Label的Outlet一样,为这个LineChartView创建一个名为lineChartView的Outlet。

6. 真机调试、问题排查与优化建议

6.1 真机部署与测试流程

代码编写完成后,最关键的一步是在真机上测试。请确保你的iPhone通过USB连接到Mac。

  1. 选择真机设备:在Xcode窗口顶部的Scheme工具栏中,将运行目标从模拟器(例如“iPhone 14 Pro”)改为你连接的iPhone设备名称。
  2. 签名与配置:Xcode可能会自动管理签名。如果出现签名错误,你需要:
    • 在项目导航器中选择顶层的项目文件(蓝色图标)。
    • 选择“Signing & Capabilities”标签页。
    • 确保在“Team”下拉框中选择了你的Apple ID账户。
    • Xcode会自动为你创建临时的开发描述文件。如果失败,可能需要去苹果开发者网站检查账户状态。
  3. 首次运行:点击Xcode左上角的运行(▶)按钮。Xcode会将应用编译并安装到你的iPhone上。首次安装时,可能会遇到“未受信任的开发者”提示。你需要到iPhone的“设置”->“通用”->“VPN与设备管理”(或“设备管理”)中,找到你的开发者证书,点击“信任”。
  4. 授权蓝牙:首次打开App并尝试使用蓝牙时,iOS会弹出系统对话框,请求蓝牙使用权限。务必点击“允许”,否则App将无法工作。
  5. 测试流程:确保你的Feather nRF52832已上电(通过USB连接电脑或电池)。在iPhone上打开App,点击右上角的刷新按钮。App应开始扫描,并在几秒内显示“已连接”。此时,Arduino代码应该正在以每秒2次的频率发送A0引脚的模拟值(如果A0悬空,数值会随机浮动)。打开“显示图表”开关,你应该能看到一条实时波动的折线图。

6.2 常见问题与排查技巧

在实际开发中,你可能会遇到以下问题,这里提供排查思路:

  1. App扫描不到设备

    • 检查硬件:确认Feather nRF52832已正确供电,且Arduino代码已成功上传。观察板载的红色LED,Adafruit BLE库通常会让其在广播时闪烁。
    • 检查UUID:这是最常见的问题。确保iOS代码中的BLE_Service_UUIDBLE_Characteristic_uuid_Rx/Tx与Arduino代码中Adafruit BLE库使用的UUID完全一致。Adafruit的bleuart示例使用固定的UUID,通常就是代码中写的那一串。你可以通过Arduino串口监视器查看启动日志,确认服务UUID。
    • 检查手机蓝牙:确保iPhone蓝牙已开启,并且没有连接着其他蓝牙设备(尤其是音频设备),有时这会影响扫描。
    • 重启大法:重启iPhone和Feather开发板。
  2. 连接成功但收不到数据

    • 检查特征值订阅:在didDiscoverCharacteristicsFor方法中,确保对BLE_Characteristic_uuid_Rx特征值调用了peripheral.setNotifyValue(true, for: characteristic)。没有订阅,就不会收到数据更新通知。
    • 检查数据解析:在didUpdateValueFor方法中,添加打印语句print("Raw data: \(data)"),查看接收到的原始数据。确认Arduino发送的是纯数字字符串,且iOS端用String(data: data, encoding: .utf8)能正确转换。
    • 检查Arduino端:确认Arduino的loop()函数中的delay(500)bleuart.write代码确实在执行。可以通过串口监视器打印调试信息来确认。
  3. 图表不显示或显示异常

    • 检查Outlet连接:确认Storyboard中的LineChartView是否与ViewController.swift中的lineChartView属性正确连接。可以在viewDidLoad中尝试设置lineChartView.backgroundColor = .lightGray来测试视图是否加载。
    • 检查数据格式ChartDataEntry的x, y值需要是Double类型。确保从字符串转换来的intValue被正确地转换为Double
    • 检查数据数组:在updateGraph()函数开头打印receivedData数组,确认其中有数据且格式正确。
  4. 应用崩溃

    • 检查可选值解包:Swift是强类型语言,频繁使用!强制解包可能为nil的变量会导致崩溃。尽量使用if letguard let进行安全解包。
    • 检查线程:所有UI更新必须在主线程进行。如果在蓝牙回调中直接更新UI,可能会引发问题。可以使用DispatchQueue.main.async { }将UI更新代码包起来。
    • 查看控制台:Xcode底部的控制台会输出详细的崩溃日志和错误信息,这是最重要的调试依据。

6.3 项目优化与扩展方向

这个基础项目可以沿多个方向进行优化和扩展:

  1. 数据稳定性与滤波:直接从ADC读取的数据可能带有噪声。可以在Arduino端或iOS端添加简单的软件滤波算法,如移动平均滤波或中值滤波,让曲线更平滑。
  2. 多设备连接与选择:当前代码自动连接第一个发现的设备。可以修改为扫描后列出所有发现的设备(显示名称和信号强度RSSI),让用户手动选择要连接哪一个。
  3. 数据持久化:将接收到的数据存储到iPhone本地(如使用CoreData或Realm数据库),并增加历史数据查看功能。
  4. 图表功能增强:利用Charts库的强大功能,增加缩放、拖拽、显示十字线、显示具体数值、切换不同图表类型(如柱状图、散点图)等功能。
  5. 连接稳定性:增加自动重连机制。在didDisconnectPeripheral回调中,可以尝试重新连接,并设置重连次数上限和延迟,提升用户体验。
  6. 发送控制指令:当前是单向数据流。你可以利用txCharacteristic,从iOS App向Feather发送指令(例如改变采样频率、控制一个LED开关),实现双向交互。
  7. 更换传感器:将Arduino代码中analogRead(A0)的引脚,连接到真正的传感器,如DHT11(温湿度)、MPU6050(加速度陀螺仪)、光敏电阻等,即可快速搭建不同的监测应用。

通过这个项目,你不仅实现了一个具体的BLE数据可视化应用,更重要的是掌握了iOS与嵌入式硬件通过BLE通信的完整链路。这套框架具有很强的通用性,你可以以此为起点,探索更广阔的物联网应用开发。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询