Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in /var/www/html/plugins/system/falangdriver/falangdriver.php on line 545

Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in /var/www/html/plugins/system/t4/src/t4/MVC/Router/T4.php on line 394
BLE推送二维码/广告的成熟方案与代码 - bluetooth蓝牙技术

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周内实现基本原型,是验证商业模式的低成本方式。


登陆


Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in /var/www/html/libraries/CBLib/CB/Legacy/cbPluginHandler.php on line 323

Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in /var/www/html/libraries/CBLib/CB/Legacy/cbPluginHandler.php on line 323

Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in /var/www/html/libraries/CBLib/CB/Legacy/cbPluginHandler.php on line 323

Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in /var/www/html/libraries/CBLib/CB/Legacy/cbPluginHandler.php on line 323

Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in /var/www/html/libraries/CBLib/CB/Legacy/cbPluginHandler.php on line 323

Deprecated: Using null as an array offset is deprecated, use an empty string instead in /var/www/html/components/com_comprofiler/plugin/user/plug_cbjdownloads/cbjdownloads.php on line 49

Deprecated: Using null as an array offset is deprecated, use an empty string instead in /var/www/html/components/com_comprofiler/plugin/user/plug_cbblogs/cbblogs.php on line 48

Deprecated: Using null as an array offset is deprecated, use an empty string instead in /var/www/html/components/com_comprofiler/plugin/user/plug_cbarticles/cbarticles.php on line 47

Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in /var/www/html/libraries/CBLib/CB/Legacy/cbPluginHandler.php on line 323

Deprecated: Using null as an array offset is deprecated, use an empty string instead in /var/www/html/libraries/src/Menu/AbstractMenu.php on line 164

Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in /var/www/html/plugins/system/falangdriver/falangdriver.php on line 97

Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in /var/www/html/plugins/system/falangdriver/falangdriver.php on line 97
mysqli object is already closed (500 Whoops, looks like something went wrong.)

Error

HTTP 500 Whoops, looks like something went wrong.

mysqli object is already closed

Exception

Error

  1. */
  2. public function disconnect()
  3. {
  4. // Close the connection.
  5. if (\is_callable([$this->connection, 'close'])) {
  6. $this->connection->close();
  7. }
  8. parent::disconnect();
  9. }
  1. */
  2. public function disconnect()
  3. {
  4. // Close the connection.
  5. if (\is_callable([$this->connection, 'close'])) {
  6. $this->connection->close();
  7. }
  8. parent::disconnect();
  9. }
  1. *
  2. * @since 2.0.0
  3. */
  4. public function __destruct()
  5. {
  6. $this->disconnect();
  7. }
  8. /**
  9. * Alter database's character set.
  10. *
DatabaseDriver->__destruct()

Stack Trace

Error
Error:
mysqli object is already closed

  at /var/www/html/libraries/vendor/joomla/database/src/Mysqli/MysqliDriver.php:318
  at mysqli->close()
     (/var/www/html/libraries/vendor/joomla/database/src/Mysqli/MysqliDriver.php:318)
  at Joomla\Database\Mysqli\MysqliDriver->disconnect()
     (/var/www/html/libraries/vendor/joomla/database/src/DatabaseDriver.php:495)
  at Joomla\Database\DatabaseDriver->__destruct()                

Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in /var/www/html/plugins/system/falangdriver/falangdriver.php on line 97

Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in /var/www/html/plugins/system/falangdriver/falangdriver.php on line 97