BLE推送二维码/广告的成熟方案与代码

一、硬件选择与成熟案例

以下是几种经过市场验证的成熟硬件方案:

1. 开发板/模块(适合原型开发)

硬件特点成本适合场景
ESP32 双核MCU + BLE/Wi-Fi,社区支持好 ¥30-80 原型验证,快速开发
nRF52840 (Adafruit/Seeed) BLE 5.0,功耗低,性能强 ¥80-150 量产前测试,高性能需求
TI CC2640/CC2652 专业BLE SoC,功耗极低 ¥50-100 电池设备,长续航需求
Raspberry Pi Pico W 低功耗 + BLE,性价比高 ¥40-70 简单原型

**推荐起点:**ESP32,因为其生态系统完善,文档多。

2. 商业成品设备(可直接部署)

设备特点价格备注
Estimote Beacon 成熟的iBeacon/Eddystone设备 $25-50/个 工业级,防水,可编程
Kontakt.io Beacon 企业级Beacon,管理平台完善 $20-40/个 适合大规模部署
RadBeacon 开源Beacon,可深度定制 $30/个 支持多种广播格式
国内:云里物里 中国品牌,支持BLE 5.0 ¥100-200 有完整管理平台

推荐商业方案: 使用 Estimote Beacon云里物里 的成品,它们都提供SDK和API,可以直接编程广播自定义数据。


二、核心实现原理

广播方式选择

  1. iBeacon格式(Apple标准)

    • 结构:UUID + Major + Minor + TX Power

    • 优点:iOS兼容性好,系统级支持

    • 限制:广播数据量小(31字节)

  2. Eddystone格式(Google标准)

    • 支持Eddystone-URL(广播一个URL)

    • 优点:Android兼容性好,可直接在通知栏显示

    • 限制:URL需要缩短服务

  3. 自定义Manufacturer Data

    • 在广播包中加入自定义数据

    • 优点:灵活,可广播任意数据(二维码内容、JSON等)

    • 要求:必须有配套App解析

**推荐方案:**同时广播iBeacon+自定义数据,兼顾兼容性和功能。


三、完整代码示例(ESP32 + Arduino)

1. ESP32固件代码-广播二维码和广告信息

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// 设备配置
#define DEVICE_NAME "SmartBeacon_001"
#define COMPANY_ID 0x1234 // 替换为您公司的ID

// iBeacon参数
#define BEACON_UUID "a0b1c2d3-e4f5-a6b7-c8d9-e0f1a2b3c4d5"
#define MAJOR 1001
#define MINOR 2001
#define TX_POWER 0xC9 // -55dBm

// 二维码和广告内容
String qrCodeData = "https://example.com/pay?id=123&amount=50";
String adContent = "今日特惠:咖啡买一送一";

BLEAdvertising *pAdvertising;

void setup() {
Serial.begin(115200);

// 初始化BLE
BLEDevice::init(DEVICE_NAME);
BLEServer *pServer = BLEDevice::createServer();
pAdvertising = pServer->getAdvertising();

// 设置广播参数
setupBeacon();
setupCustomData();

// 开始广播
pAdvertising->start();
Serial.println("Beacon开始广播...");

// 动态更新内容(示例:每10秒切换广告)
updateAdvertisingContent();
}

void loop() {
// 这里可以添加动态更新逻辑
delay(10000);
updateAdvertisingContent();
}

void setupBeacon() {
// 设置iBeacon广播数据
BLEAdvertisementData advertisementData;

// iBeacon数据包结构
uint8_t beaconData[25] = {
0x02, 0x15, // iBeacon前缀
0xa0, 0xb1, 0xc2, 0xd3, 0xe4, 0xf5, 0xa6, 0xb7, // UUID部分
0xc8, 0xd9, 0xe0, 0xf1, 0xa2, 0xb3, 0xc4, 0xd5,
(MAJOR >> 8) & 0xFF, MAJOR & 0xFF, // Major
(MINOR >> 8) & 0xFF, MINOR & 0xFF, // Minor
TX_POWER
};

advertisementData.setManufacturerData(COMPANY_ID, beaconData, sizeof(beaconData));
pAdvertising->setAdvertisementData(advertisementData);
}

void setupCustomData() {
// 自定义数据广播
BLEAdvertisementData scanResponseData;

// 构建自定义数据包(包含二维码和广告)
String fullData = "QR:" + qrCodeData + ";AD:" + adContent;

// 如果数据太长,需要分段广播(BLE限制31字节)
if (fullData.length() > 28) {
fullData = fullData.substring(0, 28);
}

scanResponseData.setManufacturerData(COMPANY_ID, (uint8_t*)fullData.c_str(), fullData.length());
pAdvertising->setScanResponseData(scanResponseData);
}

void updateAdvertisingContent() {
// 模拟动态更新广告内容
static int adIndex = 0;
String ads[] = {
"今日特惠:咖啡买一送一",
"新用户首单立减10元",
"周末特价:蛋糕8折",
"关注公众号领取优惠券"
};

adContent = ads[adIndex % 4];
adIndex++;

// 重新设置广播数据
setupCustomData();
pAdvertising->start();

Serial.println("更新广告内容: " + adContent);
}

2. Android手机端代码(Kotlin + Android Studio)

// MainActivity.kt
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.*
import android.os.Bundle
import android.os.ParcelUuid
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject

class MainActivity : AppCompatActivity() {
private lateinit var bluetoothAdapter: BluetoothAdapter
private lateinit var bluetoothLeScanner: BluetoothLeScanner
private var scanning = false

// 自定义服务的UUID(与设备端匹配)
private val SERVICE_UUID = ParcelUuid.fromString("0000ABCD-0000-1000-8000-00805F9B34FB")

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 初始化BLE
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner

// 请求权限(需要在AndroidManifest.xml中添加权限)
requestPermissions()

// 开始扫描
startScan()
}

private fun startScan() {
if (scanning) return

val filters = mutableListOf()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()

bluetoothLeScanner.startScan(filters, settings, scanCallback)
scanning = true
}

private fun stopScan() {
if (!scanning) return
bluetoothLeScanner.stopScan(scanCallback)
scanning = false
}

// 扫描回调
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
val device = it.device
val scanRecord = it.scanRecord

// 1. 检查是否是iBeacon
if (isIBeacon(scanRecord?.bytes)) {
processIBeacon(scanRecord.bytes)
}

// 2. 检查自定义数据
val manufacturerData = scanRecord?.getManufacturerSpecificData(0x1234)
manufacturerData?.let { data ->
val receivedString = String(data)
processCustomData(receivedString)
}

// 3. 检查服务UUID
scanRecord?.serviceUuids?.forEach { uuid ->
if (uuid == SERVICE_UUID) {
// 连接设备获取更多数据
connectToDevice(device)
}
}
}
}
}

private fun isIBeacon(scanRecord: ByteArray?): Boolean {
scanRecord ?: return false
if (scanRecord.size < 25) return false

// iBeacon标识:0x02 0x15
return scanRecord[0].toInt() == 0x02 && scanRecord[1].toInt() == 0x15
}

private fun processIBeacon(data: ByteArray) {
// 解析iBeacon数据
val uuid = bytesToHex(data.copyOfRange(2, 18))
val major = ((data[18].toInt() and 0xFF) shl 8) or (data[19].toInt() and 0xFF)
val minor = ((data[20].toInt() and 0xFF) shl 8) or (data[21].toInt() and 0xFF)

// 根据UUID/Major/Minor从服务器获取对应内容
fetchContentFromServer(uuid, major, minor)
}

private fun processCustomData(data: String) {
// 解析自定义格式数据:QR:xxx;AD:yyy
val parts = data.split(";")
parts.forEach { part ->
when {
part.startsWith("QR:") -> {
val qrContent = part.substring(3)
showQRCode(qrContent)
}
part.startsWith("AD:") -> {
val adContent = part.substring(3)
showNotification(adContent)
}
}
}
}

private fun fetchContentFromServer(uuid: String, major: Int, minor: Int) {
// 异步请求服务器获取二维码和广告内容
Thread {
try {
// 这里替换为您的服务器API
val url = "https://your-server.com/api/content?uuid=$uuid&major=$major&minor=$minor"
// 执行网络请求...

// 假设返回JSON: {"qr": "支付链接", "ad": "广告内容"}
val json = JSONObject(response)
runOnUiThread {
showQRCode(json.getString("qr"))
showNotification(json.getString("ad"))
}
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}

private fun showQRCode(content: String) {
// 显示二维码到界面
// 可以使用ZXing库生成二维码图片
}

private fun showNotification(adContent: String) {
// 在通知栏显示广告
// 注意:Android 13+需要通知权限
}

override fun onDestroy() {
super.onDestroy()
stopScan()
}
}

3. iOS手机端代码(Swift + CoreBluetooth)

// BeaconScanner.swift
import CoreBluetooth
import UIKit

class BeaconScanner: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
private var centralManager: CBCentralManager!
private var discoveredBeacons: [CLBeacon] = []

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

func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
startScanning()
}
}

func startScanning() {
// 扫描所有设备,可添加服务UUID过滤
centralManager.scanForPeripherals(
withServices: nil,
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
)
}

func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber) {

// 1. 检查iBeacon
if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
if isIBeacon(manufacturerData) {
processIBeacon(manufacturerData)
}

// 2. 处理自定义数据(公司ID: 0x1234)
if manufacturerData.count > 2 {
let companyId = (UInt16(manufacturerData[0]) << 8) | UInt16(manufacturerData[1])
if companyId == 0x1234 {
let customData = manufacturerData.subdata(in: 2..="" bool="" guard="" data.count="">= 25 else { return false }

// iBeacon前缀: 0x02 0x15
return data[0] == 0x02 && data[1] == 0x15
}

func processIBeacon(_ data: Data) {
// 解析iBeacon
let uuidBytes = data.subdata(in: 2..<18)
let major = (UInt16(data[18]) << 8) | UInt16(data[19])
let minor = (UInt16(data[20]) << 8) | UInt16(data[21])

// 转换为UUID字符串
let uuid = uuidBytes.map { String(format: "%02x", $0) }.joined()

// 从服务器获取内容
fetchContentFromServer(uuid: uuid, major: major, minor: minor)
}

func processCustomData(_ data: Data) {
if let dataString = String(data: data, encoding: .utf8) {
let components = dataString.components(separatedBy: ";")

for component in components {
if component.hasPrefix("QR:") {
let qrContent = String(component.dropFirst(3))
DispatchQueue.main.async {
self.showQRCode(qrContent)
}
} else if component.hasPrefix("AD:") {
let adContent = String(component.dropFirst(3))
DispatchQueue.main.async {
self.showLocalNotification(adContent)
}
}
}
}
}

func fetchContentFromServer(uuid: String, major: UInt16, minor: UInt16) {
let urlString = "https://your-server.com/api/content?uuid=\(uuid)&major=\(major)&minor=\(minor)"

guard let url = URL(string: urlString) else { return }

URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }

do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
if let qrContent = json["qr"] as? String {
DispatchQueue.main.async {
self.showQRCode(qrContent)
}
}
if let adContent = json["ad"] as? String {
DispatchQueue.main.async {
self.showLocalNotification(adContent)
}
}
}
} catch {
print("JSON解析错误: \(error)")
}
}.resume()
}

func showQRCode(_ content: String) {
// 生成并显示二维码
// 使用CIQRCodeGenerator生成二维码图片
}

func showLocalNotification(_ message: String) {
let content = UNMutableNotificationContent()
content.title = "附近优惠"
content.body = message
content.sound = .default

let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: nil
)

UNUserNotificationCenter.current().add(request)
}
}

4. 服务器端API示例(Node.js + Express)

// server.js
const express = require('express');
const app = express();
app.use(express.json());

// 设备配置数据库(实际应使用MongoDB/MySQL)
const deviceConfigs = {
'a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5_1001_2001': {
qrContent: 'https://example.com/pay?id=123&amount=50',
adContent: '今日特惠:咖啡买一送一',
redirectUrl: 'https://example.com/menu',
validUntil: '2024-12-31'
}
};

// API:根据设备标识获取内容
app.get('/api/content', (req, res) => {
const { uuid, major, minor } = req.query;
const deviceKey = `$

{uuid}
_$

{major}
_$

{minor}
`;

const config = deviceConfigs[deviceKey];

if (config) {
res.json({
success: true,
qr: config.qrContent,
ad: config.adContent,
redirect: config.redirectUrl,
timestamp: Date.now()
});
} else {
res.json({
success: false,
message: '设备未配置'
});
}
});

// API:管理后台更新设备配置
app.post('/api/device/update', (req, res) => {
const { deviceId, qrContent, adContent } = req.body;

// 验证权限(实际应添加JWT认证)

deviceConfigs[deviceId] = {
qrContent,
adContent,
updatedAt: Date.now()
};

res.json({ success: true });
});

// 启动服务器
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});

四、部署建议

1. 开发流程

  1. 硬件原型:购买ESP32开发板,刷入上述Arduino代码

  2. 手机端测试:安装Android/iOS测试App,验证数据接收

  3. 服务器搭建:部署简单的Node.js服务器

  4. 内容管理:开发Web管理后台,配置不同设备的内容

2. 性能优化

  • 广播间隔:调整广播间隔(100ms-1s),平衡功耗和发现速度

  • 数据压缩:对二维码URL使用短链接服务(如Bit.ly API)

  • 缓存策略:手机端缓存已获取的内容,减少重复请求

3. 合规实施

  • 隐私政策:在App中明确说明数据收集和使用方式

  • 用户授权:首次运行时请求蓝牙和通知权限

  • 退订功能:提供简单的广告关闭选项

五、商业方案考虑

如果您希望快速商用而不想深入开发:

  1. 使用商业Beacon平台

    • Google Nearby:提供完整的附近设备互动API

    • 微信摇一摇周边:直接使用微信生态,用户无需安装新App

    • 阿里云物联网:提供设备管理、数据推送一站式服务

  2. OEM解决方案

    • 联系硬件厂家(如云里物里)定制固件

    • 使用他们的云端管理平台

    • 专注开发自己的业务逻辑

这个方案可以在2-4周内实现基本原型,是验证商业模式的低成本方式。


登陆