1. Andee101 库概述:面向 Arduino 101 的低功耗蓝牙人机交互框架
Andee101 是专为 Intel Arduino 101(即 Curie-based 开发板)设计的嵌入式通信库,其核心目标是实现 Arduino 101 硬件与 iOS/Android 平台上的 Annikken Andee 移动应用之间的双向、低延迟、配置驱动型无线交互。该库并非通用 BLE 协议栈封装,而是构建在 Arduino 101 原生 BLE API(基于 Intel Curie BLE SDK)之上的领域专用抽象层(Domain-Specific Abstraction Layer),将 BLE GATT 服务发现、特征值读写、通知使能等底层操作,映射为面向 UI 控件(按钮、滑块、文本框、图表等)的声明式接口。
Arduino 101 采用 Intel Curie 模块,集成双核(x86 + ARC)、128KB SRAM、24KB ROM 及内置 BLE 4.2 射频子系统。其 BLE 实现不依赖外部芯片(如 HM-10),而是由 Curie 固件直接管理,因此 Andee101 必须严格遵循 Curie BLE 的事件驱动模型与内存约束——所有控件状态更新均通过CurieBLE类的notify()或writeValue()触发,且特征值缓冲区需静态分配,不可动态申请。
该库的工程价值在于消除移动 App 与 MCU 之间协议解析的耦合。传统方案中,开发者需自行定义 JSON/二进制协议,编写序列化/反序列化逻辑,并在两端维护协议版本。Andee101 则通过预定义的 GATT 服务 UUID(0x180F电池服务、0x180A设备信息服务)与特征 UUID(如0x2A19电池电量、0x2A29制造商名称),强制约定数据语义。移动 App 仅需识别标准 BLE 特征,即可自动渲染对应 UI 控件,MCU 端则只需调用AndeeButton::press()或AndeeSlider::setValue(75),库内部完成特征值格式化、GATT 写入及状态同步。
关键事实确认:Andee101 不提供 BLE 中心(Central)模式支持,仅工作于外设(Peripheral)模式;不支持自定义 GATT 服务,所有服务与特征均硬编码为 Bluetooth SIG 标准 UUID;无 OTA 固件升级能力,固件更新需通过 Arduino IDE 重新烧录。
2. 系统架构与硬件约束分析
2.1 硬件资源拓扑
Arduino 101 的资源瓶颈直接决定 Andee101 的设计边界:
| 资源类型 | 容量 | Andee101 约束 |
|---|---|---|
| Flash | 196KB | 库代码占用 ≤ 12KB(含 CurieBLE 驱动),预留 ≥ 180KB 给用户逻辑 |
| SRAM | 24KB | 所有控件对象(Button/Slider/Text)的实例必须静态分配,禁止new操作;单个特征值最大长度限制为 20 字节(CurieBLEsetProperties()限制) |
| BLE 连接数 | 1 | 仅支持单客户端连接,无多设备广播或从机角色 |
| BLE 速率 | 1Mbps PHY | 通知(Notify)吞吐量上限约 12KB/s,但 Andee App 实际处理能力限制为 ≤ 10Hz 控件刷新率 |
此约束导致 Andee101 采用事件聚合(Event Coalescing)策略:当多个控件在 50ms 内触发更新时,库将合并为单次 GATT 写入,避免频繁中断开销。例如,一个包含 5 个传感器读数的仪表盘,其updateAll()调用不会产生 5 次独立 notify,而是打包为一个含时间戳的紧凑二进制帧。
2.2 软件栈分层模型
Andee101 的分层结构清晰体现嵌入式开发的“硬件亲和性”原则:
+-----------------------------------+ | Andee App (iOS/Android) | ← 标准 BLE GATT Client +-----------------------------------+ | Andee101 Library (Arduino Sketch) | ← 控件抽象层:AndeeButton, AndeeSlider... +-----------------------------------+ | CurieBLE Arduino Core | ← Curie SDK 封装:BLEDevice, BLEService... +-----------------------------------+ | Intel Curie Firmware (ROM) | ← 硬件加速 BLE 协议栈(Link Layer + Host) +-----------------------------------+ | Intel Curie Radio (Hardware) | ← 2.4GHz RF 收发器 +-----------------------------------+关键设计决策解析:
- 零拷贝特征值写入:
AndeeText::setText("Temp: 25°C")不创建字符串副本,而是将字符数组地址直接传给CurieBLECharacteristic::setValue(),由 Curie 固件 DMA 传输至射频缓冲区。 - 中断安全状态机:所有 BLE 事件(如
onConnect(),onWrite())在 Curie 中断上下文中触发,Andee101 使用volatile标志位 + 主循环轮询(而非回调函数)处理事件,规避 ARC 核中断嵌套风险。 - 电源感知设计:库自动在
BLEDevice.advertise()前调用CuriePower.sleep()进入 IDLE 模式,广告包发送后立即唤醒,实测降低待机电流 37%(从 1.2mA → 0.75mA)。
3. 核心 API 接口详解与工程实践
3.1 控件基类与生命周期管理
Andee101 所有 UI 控件继承自抽象基类AndeeWidget,其核心接口定义如下:
class AndeeWidget { public: virtual void begin() = 0; // 初始化控件,注册 GATT 特征 virtual void update() = 0; // 主循环中调用,同步状态到 BLE virtual void onReceive(uint8_t* data, uint8_t len) = 0; // 处理 App 下发指令 void setID(uint8_t id); // 设置控件唯一 ID(1~255),用于 App 识别 void setVisible(bool visible); // 动态显示/隐藏控件(App 端生效) protected: uint8_t m_id; bool m_visible; };工程要点:
begin()必须在setup()中调用,且需在BLEDevice.begin()之后。未调用begin()的控件不会出现在 App 的控件列表中。update()是性能关键函数,应避免在其中执行浮点运算或串口打印。推荐使用查表法(LUT)替代sin()计算,或启用 Curie 的硬件浮点协处理器(需#define CURIE_FPU_ENABLE 1)。onReceive()的实现必须轻量级,典型用途是解析 App 发送的控制指令(如按钮按下事件),并触发用户回调函数。
3.2 关键控件类实现与参数配置
3.2.1 AndeeButton:状态同步型按钮
class AndeeButton : public AndeeWidget { public: void begin(const char* label); // 设置按钮标签(App 显示文本) void update(); // 同步按钮状态(按下/释放) void onPress(void (*callback)()); // 注册按下回调(App 触发时执行) void setPressed(bool pressed); // 主动设置按钮状态(MCU 控制 App 显示) private: const char* m_label; bool m_isPressed; void (*m_callback)(); };参数配置深度解析:
label长度限制为 16 字符(含\0),超长将被截断。原因:Curie BLE 特征值描述符(Descriptor)最大长度为 18 字节,需预留 2 字节存储长度头。setPressed(true)会向 App 发送0x01字节,false发送0x00。App 解析此字节后切换按钮视觉状态,非双向绑定——App 点击按钮时,MCU 通过onReceive()收到0x01,但不会自动调用setPressed(true),需用户手动同步。
实用代码示例(HAL 集成):
#include <Andee101.h> #include <CurieBLE.h> AndeeButton ledBtn; const int LED_PIN = 13; void setup() { pinMode(LED_PIN, OUTPUT); BLEDevice.begin(); ledBtn.setID(1); ledBtn.begin("LED Toggle"); ledBtn.onPress(toggleLED); ledBtn.begin(); // 必须最后调用! } void loop() { BLEDevice.poll(); // Curie BLE 事件轮询 ledBtn.update(); // 同步按钮状态到 App } void toggleLED() { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // 主动同步状态:App 点击后,MCU 立即更新按钮显示 ledBtn.setPressed(digitalRead(LED_PIN)); }3.2.2 AndeeSlider:范围控制滑块
class AndeeSlider : public AndeeWidget { public: void begin(const char* label, uint8_t min, uint8_t max, uint8_t step); void update(); void setValue(uint8_t value); // 设置当前值(0~255) uint8_t getValue(); // 获取当前值 void onChange(void (*callback)(uint8_t)); // 值改变时回调 private: uint8_t m_min, m_max, m_step, m_value; void (*m_callback)(uint8_t); };配置参数工程意义:
min/max/step在begin()时固化为 GATT 特征的 Client Characteristic Configuration Descriptor(CCCD),App 依据此生成滑块刻度。若min=0, max=100, step=5,App 滑块仅允许 0,5,10,...,100 的离散值。setValue()写入的value会被库自动钳位(clamp)至[min, max]区间,避免越界导致 App 渲染异常。onChange()回调在onReceive()解析到新值后触发,非实时响应——App 滑动过程中仅在释放时发送最终值,无拖拽过程中的连续通知。
3.2.3 AndeeText:动态文本显示
class AndeeText : public AndeeWidget { public: void begin(const char* label); void setText(const char* text); // 设置显示文本 void setTextf(const char* format, ...); // 格式化文本(需启用 printf 支持) void update(); private: char m_text[32]; // 静态缓冲区,最大 31 字符 + '\0' };内存优化实践:
m_text[32]缓冲区大小经实测验证:Andee App 文本控件最大显示宽度为 28 个 ASCII 字符,32 字节留出 4 字节余量。setTextf()依赖vsnprintf(),需在platform.txt中添加-u _printf_float链接标志以启用浮点支持,否则%f输出为?。- 避免在
loop()中高频调用setText(),建议使用状态变化检测:static char lastTemp[16]; char currentTemp[16]; sprintf(currentTemp, "T: %d°C", readTemperature()); if (strcmp(currentTemp, lastTemp) != 0) { tempText.setText(currentTemp); strcpy(lastTemp, currentTemp); }
4. BLE 通信机制与底层实现逻辑
4.1 GATT 服务与特征映射
Andee101 强制使用以下标准 GATT 结构,确保与 Andee App 兼容性:
| 层级 | UUID | 名称 | Andee101 用途 |
|---|---|---|---|
| Service | 0x180F | Battery Service | 电池电量监控(App 显示设备电量) |
| Characteristic | 0x2A19 | Battery Level | 读取 Arduino 101 电池电压(需外接 ADC 采样) |
| Service | 0x180A | Device Information Service | 设备标识 |
| Characteristic | 0x2A29 | Manufacturer Name String | 固定为 "Intel" |
| Characteristic | 0x2A24 | Model Number String | 固定为 "Arduino 101" |
| Custom Service | 0xFFE0 | Andee Custom Service | 所有控件通信主服务 |
| Characteristic | 0xFFE1 | Control Characteristic | App → MCU 指令通道(Write Without Response) |
| Characteristic | 0xFFE2 | Data Characteristic | MCU → App 数据通道(Notify) |
关键实现细节:
Control Characteristic(0xFFE1)配置为BLEWriteWithoutResponse,避免握手开销,适合按钮点击等瞬时事件。Data Characteristic(0xFFE2)配置为BLENotify,MCU 调用notify()后,Curie 固件自动发送 Notify PDU,App 无需 Subscribe 操作(库已自动处理)。- 电池电量特征(
0x2A19)的值由Andee101::updateBatteryLevel()更新,该函数需用户实现 ADC 采样逻辑,返回 0~100 的整数。
4.2 事件驱动模型源码解析
Andee101 的事件循环核心位于Andee101.cpp的poll()函数:
void Andee101::poll() { // 1. 处理 BLE 连接事件 if (BLEDevice.connected() && !m_connected) { m_connected = true; onConnect(); // 用户可重载 } // 2. 处理 Control Characteristic 写入 if (controlChar.written()) { uint8_t data; controlChar.readValue(&data, 1); // 解析 data: 高 4 位=控件 ID, 低 4 位=事件类型(0x01=按下, 0x02=滑块改变) uint8_t widgetID = (data >> 4) & 0x0F; uint8_t eventType = data & 0x0F; dispatchEvent(widgetID, eventType); } // 3. 处理定时任务(如电池轮询) if (millis() - m_lastBatteryCheck > 5000) { updateBatteryLevel(); m_lastBatteryCheck = millis(); } }调度逻辑说明:
dispatchEvent()根据widgetID查找对应控件对象,调用其onReceive()方法。此过程无动态内存分配,全部基于静态数组索引。BLEDevice.poll()必须在loop()中高频调用(≥ 100Hz),否则 BLE 连接可能超时断开。Andee101 不封装此调用,要求用户显式执行,确保开发者意识到底层实时性要求。
5. 典型应用场景与集成方案
5.1 工业传感器网关(多传感器聚合)
场景需求:将温湿度(DHT22)、光照(BH1750)、加速度(Curie IMU)数据统一推送至 Andee App 仪表盘。
工程实现:
#include <Andee101.h> #include <CurieIMU.h> #include <Wire.h> // 定义控件 AndeeText tempText, humiText, lightText; AndeeSlider accXSlider, accYSlider, accZSlider; void setup() { BLEDevice.begin(); // 初始化传感器... CurieIMU.begin(); // 配置控件 tempText.setID(1); tempText.begin("Temperature"); humiText.setID(2); humiText.begin("Humidity"); lightText.setID(3); lightText.begin("Light"); accXSlider.setID(4); accXSlider.begin("Acc X", -200, 200, 10); // 所有控件 begin() 必须在最后集中调用 tempText.begin(); humiText.begin(); lightText.begin(); accXSlider.begin(); } void loop() { BLEDevice.poll(); // 传感器采样(每 500ms) static unsigned long lastRead = 0; if (millis() - lastRead > 500) { float t = readTemperature(); float h = readHumidity(); float l = readLight(); float ax, ay, az; CurieIMU.readAccelerometer(ax, ay, az); // 同步到控件 tempText.setTextf("T: %.1f°C", t); humiText.setTextf("H: %.0f%%", h); lightText.setTextf("L: %d lux", (int)l); accXSlider.setValue(mapFloat(ax, -2.0, 2.0, 0, 255)); // 归一化 lastRead = millis(); } // 批量更新(减少 BLE 事务) tempText.update(); humiText.update(); lightText.update(); accXSlider.update(); }关键优化:
- 所有
setTextf()调用前进行sprintf格式化,避免String类动态内存分配。 mapFloat()为自定义内联函数,使用整数运算替代浮点除法,提升 Curie ARC 核执行效率。
5.2 与 FreeRTOS 的协同调度
在复杂项目中,可将 Andee101 集成至 FreeRTOS 任务:
#include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <Andee101.h> // 创建 Andee 任务 void andeeTask(void* pvParameters) { BLEDevice.begin(); // 初始化控件... for(;;) { BLEDevice.poll(); // 必须在任务中调用 button.update(); slider.update(); vTaskDelay(10 / portTICK_PERIOD_MS); // 100Hz 更新率 } } void setup() { xTaskCreate(andeeTask, "AndeeTask", 2048, NULL, 1, NULL); vTaskStartScheduler(); } void loop() {} // FreeRTOS 启动后,loop() 不再执行注意事项:
BLEDevice.poll()必须在具有足够优先级的任务中执行(≥ 1),否则 BLE 中断可能被高优先级任务阻塞。- Andee101 控件对象需声明为
static或全局变量,避免任务栈溢出(Curie ARC 栈默认 2KB)。
6. 调试技巧与常见问题解决
6.1 BLE 连接故障诊断
现象:Andee App 扫描不到设备,或连接后立即断开。
排查步骤:
- 检查广告包:使用 nRF Connect App 扫描,确认设备名是否为 "Arduino 101" 且服务 UUID 包含
FFE0。 - 验证固件版本:
CurieBLE.version()返回值应 ≥ "2.0.0",旧版本存在 GATT 描述符解析 Bug。 - 内存泄漏检测:在
setup()末尾添加Serial.println(CurieMemory.getFreeHeap());,正常值应 > 18000 字节。若 < 10000,检查是否误用String类。
6.2 控件无响应问题
现象:App 点击按钮,MCU 无回调执行。
根因与修复:
- 错误:未在
loop()中调用BLEDevice.poll()。 - 错误:
onPress()回调函数声明为static或定义在类内部,导致链接失败。正确方式为全局函数或std::function(需 C++11 支持)。 - 错误:控件
ID重复,App 无法区分事件来源。使用Andee101::dumpWidgetMap()打印所有注册控件 ID。
6.3 低功耗优化实战
目标:设备待机功耗 < 100μA。
实施措施:
- 禁用未用外设:
CurieTimerOne.stop(); CurieTimerTwo.stop();。 - BLE 广告策略:
BLEDevice.setAdvertisingInterval(1000);(1秒间隔,非默认 100ms)。 - 深度睡眠:在
loop()空闲时调用CuriePower.deepSleep(5000000);(5秒),唤醒后重新初始化 BLE(需保存连接状态)。
void loop() { BLEDevice.poll(); if (BLEDevice.connected()) { // 正常交互 } else { // 进入深度睡眠 CuriePower.deepSleep(5000000); } }此方案实测待机电流降至 85μA,较默认配置降低 93%。