继续阅读完整内容
支持我们的网站,请点击查看下方广告
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,可以直接编程广播自定义数据。
二、核心实现原理
广播方式选择
-
iBeacon格式(Apple标准)
-
结构:UUID + Major + Minor + TX Power
-
优点:iOS兼容性好,系统级支持
-
限制:广播数据量小(31字节)
-
-
Eddystone格式(Google标准)
-
支持Eddystone-URL(广播一个URL)
-
优点:Android兼容性好,可直接在通知栏显示
-
限制:URL需要缩短服务
-
-
自定义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. 开发流程
-
硬件原型:购买ESP32开发板,刷入上述Arduino代码
-
手机端测试:安装Android/iOS测试App,验证数据接收
-
服务器搭建:部署简单的Node.js服务器
-
内容管理:开发Web管理后台,配置不同设备的内容
2. 性能优化
-
广播间隔:调整广播间隔(100ms-1s),平衡功耗和发现速度
-
数据压缩:对二维码URL使用短链接服务(如Bit.ly API)
-
缓存策略:手机端缓存已获取的内容,减少重复请求
3. 合规实施
-
隐私政策:在App中明确说明数据收集和使用方式
-
用户授权:首次运行时请求蓝牙和通知权限
-
退订功能:提供简单的广告关闭选项
五、商业方案考虑
如果您希望快速商用而不想深入开发:
-
使用商业Beacon平台:
-
Google Nearby:提供完整的附近设备互动API
-
微信摇一摇周边:直接使用微信生态,用户无需安装新App
-
阿里云物联网:提供设备管理、数据推送一站式服务
-
-
OEM解决方案:
-
联系硬件厂家(如云里物里)定制固件
-
使用他们的云端管理平台
-
专注开发自己的业务逻辑
-
这个方案可以在2-4周内实现基本原型,是验证商业模式的低成本方式。