├── package.json ├── zb-simulator.js ├── README.md └── ble-simulator.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weixin-iot", 3 | "version": "1.0.0", 4 | "description": "WeChat IoT developer utilities", 5 | "main": "ble-simulator.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/luluxie/weixin-iot.git" 12 | }, 13 | "keywords": [ 14 | "ble", 15 | "simulator" 16 | ], 17 | "author": "luluxie", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/luluxie/weixin-iot/issues" 21 | }, 22 | "homepage": "https://github.com/luluxie/weixin-iot#readme", 23 | "dependencies": { 24 | "bleadvertise": "^0.1.1", 25 | "bleno": "^0.4.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /zb-simulator.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var bleno = require('bleno'); 3 | // 在微信摇一摇周边后台 https://zb.weixin.qq.com 注册后得到的设备信息 4 | var uuid = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'; 5 | var major = XXXXX; // 0x0000 - 0xffff 6 | var minor = XXXX; // 0x0000 - 0xffff 7 | var measuredPower = -59; // -128 - 127 8 | //蓝牙设备名称,与微信内显示的名称无关 9 | var DEVICE_NAME = '摇一摇设备'; 10 | // 在运行环境变量中设置设备名称 11 | process.env['BLENO_DEVICE_NAME'] = DEVICE_NAME; 12 | 13 | console.log('bleno - iBeacon'); 14 | 15 | bleno.on('stateChange', function(state) { 16 | console.log('on -> stateChange: ' + state); 17 | 18 | if (state === 'poweredOn') { 19 | bleno.startAdvertisingIBeacon(uuid, major, minor, measuredPower); 20 | } else { 21 | bleno.stopAdvertising(); 22 | } 23 | }); 24 | 25 | bleno.on('advertisingStart', function() { 26 | console.log('on -> advertisingStart'); 27 | }); 28 | 29 | bleno.on('advertisingStop', function() { 30 | console.log('on -> advertisingStop'); 31 | }); 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 微信蓝牙设备模拟器 2 | 3 | * `ble-simulator.js` 微信运动,微信蓝牙设备发现模拟器。可模拟微信运动步数上报,设备发现逻辑。 4 | 5 | * `zb-simulator.js` 摇一摇周边模拟器。可模拟真实的iBeacon设备,在微信摇一摇周边中出现。 6 | 7 | # 蓝牙协议模拟器 8 | 9 | `ble-simulator.js` 10 | 11 | 基于对微信AirSync蓝牙协议的理解写的模拟器,主要用于演示BLE设备与微信的通讯原理,方便其它开发者快速了解和上手。 12 | 13 | 微信有专属的蓝牙`Service UUID`,同时还有指定的特征值要实现才能被微信识别和发现。如果是开发微信运动精简协议,还需要多两个特征值。 14 | 15 | ```javascript 16 | // 微信蓝牙服务和特征值专属UUID 17 | var WX_SERVICE_UUID = 'FEE7'; 18 | var WX_CHARC_UUID_WRITE = 'FEC7'; 19 | var WX_CHARC_UUID_INDICATE = 'FEC8'; 20 | var WX_CHARC_UUID_READ = 'FEC9'; 21 | // 微信运动精简协议专属UUID 22 | var WERUN_PEDOMETER_UUID = 'FEA1'; 23 | var WERUN_TARGET_UUID = 'FEA2'; 24 | ``` 25 | 26 | ## 主要特性 27 | 28 | 微信运动 29 | - 支持模拟微信运动精简协议 30 | 31 | 设备发现 32 | - 支持模拟微信蓝牙近场发现 33 | 34 | ## 使用方法 35 | ### Step 1 36 | 37 | $ sudo node ble-simulator.js 38 | ### Step 2 39 | 打开AirSyncDebugger工具点击`扫描`蓝牙设备, 点击`精简协议`, 点击`记步器测试`。测试通过后设备即可被微信发现,同时也支持了微信运动的接入。 40 | 41 | ## 接入微信 42 | 如果只是调试蓝牙协议的话Step 1和Step 2即可。Step 3和Step 4也可以预先完成,此两步主要在微信硬件平台登记产品型号,和获得接口授权。Step 4会消耗`授权配额`,微信针对每个型号都有授权配额,认证前默认只有100个,需要后续免费申请才可获得更多。 43 | 44 | ### Step 3 45 | 要让微信运动可以添加该设备为数据源,还需要在微信硬件平台中录入该设备基本信息,图标,默认显示名称等。在申请到的公众号后台开通“设备功能”插件,即可以添加一款新设备。 46 | 47 | 公众号需要做过微信认证,如果只是临时开发调试用,也可以使用公众号测试账号: 48 | 49 | http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login 50 | 51 | 添加设备时,需要指明接入方案: 52 | - 微信硬件云标准接入方案 53 | - 设备直连微信硬件云通道 54 | - 厂商云连接微信硬件云通道 55 | - 平台基础接入方案 56 | 57 | 接入微信运动精简协议,选`平台基础接入方案`即可。 58 | 59 | ### Step 4 60 | 登记成功后会得到一个微信硬件的`型号编码`即`PRODUCT_ID`,准备好公众号的`appid`,`access_token`,设备`MAC`地址列表等。 61 | 62 | 1.通过调用微信的设备编号API接口,得到设备编号。请求参数中`PRODUCT_ID`即`型号编码`。 63 | 64 | https://api.weixin.qq.com/device/getqrcode?access_token=ACCESS_TOKEN&product_id=PRODUCT_ID 65 | 66 | 2.通过调用微信的授权接口将设备`MAC`更新到设备编号上。 67 | 68 | https://api.weixin.qq.com/device/authorize_device?access_token=ACCESS_TOKEN 69 | 70 | # 摇一摇模拟器 71 | 72 | `zb-simulator.js` 73 | 74 | 快速模拟一个标准的iBeacon设备,运行时可以在微信`摇一摇>周边`中出现。需要在微信摇一摇网站注册设备,并得到`UUID`,`Major`,`Minor`等参数才可以被微信识别。 75 | 76 | ## 主要特性 77 | 78 | 微信摇一摇 79 | - 支持微信摇一摇周边 80 | 81 | ## 使用方法 82 | ### Step 1 83 | 在微信摇一摇周边网站注册新的iBeacon设备,等待审核。 84 | 85 | https://zb.weixin.qq.com/ 86 | 87 | 审核通过后得到`UUID`,`Major`,`Minor`等参数,在zb-simulator.js中更新对应的三个参数。 88 | 89 | ```javascript 90 | var uuid = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'; 91 | var major = XXXXX; // 0x0000 - 0xffff 92 | var minor = XXXX; // 0x0000 - 0xffff 93 | ``` 94 | 95 | ### Step 2 96 | 97 | $ sudo node zb-simulator.js 98 | ### Step 3 99 | 打开微信进入`摇一摇`, 等待`周边`页面出现后摇一摇手机。即可摇出模拟的设备。 100 | 101 | ## 依赖 102 | 103 | 推荐在Linux环境下运行,系统需要有蓝牙适配器和bluez蓝牙工具包。bleno库主要完成BLE设备的服务与特征值构建,在非Linux环境下运行时需要满足bleno的依赖条件。 104 | 105 | Linux 106 | * bluez 107 | * libbluetooth-dev 108 | * nodejs 109 | 110 | Node.js modules 111 | * bleno 112 | * bleadvertise 113 | 114 | ## 测试环境 115 | 116 | 代码已经在以下环境运行测试通过 117 | 118 | ### 软件环境 119 | * bluez 版本 4.101 120 | * nodejs 版本 4.4.5 121 | * bleno 版本 0.4.0 122 | * bleadvertise 版本 0.1.1 123 | 124 | ### 硬件环境 125 | |项目|参数| 126 | |----|----| 127 | |平台 | Raspberry Pi 2 Model B| 128 | |OS | Ubuntu 14.04.4| 129 | |内核 | 3.18.0-20-rpi2| 130 | |蓝牙模块 | CSR Bluetooth 4.0 USB module| 131 | 132 | ## 备注 133 | AirSyncDebugger是微信提供的蓝牙协议调试工具。 134 | -------------------------------------------------------------------------------- /ble-simulator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 已在以下环境下运行测试通过 3 | * 4 | * [软件环境] 5 | * bluez: 版本 4.101 6 | * nodejs: 版本 4.4.5 7 | * bleno: 版本 0.4.0 8 | * bleadvertise: 版本 0.1.1 9 | * 10 | * [硬件环境] 11 | * 平台: Raspberry Pi 2 Model B 12 | * OS: Ubuntu 14.04.4 13 | * 内核: 3.18.0-20-rpi2 14 | * 蓝牙模块: CSR Bluetooth 4.0 USB module 15 | * 16 | * [Usage] 17 | * sudo node ble-simulator.js 18 | * 19 | * [备注] 20 | * AirSyncDebugger是微信提供的蓝牙协议调试工具 21 | * 22 | */ 23 | 24 | var util = require('util'); 25 | var bleno = require('bleno'); 26 | var parser = require('bleadvertise'); 27 | 28 | // 微信蓝牙服务和特征值专属UUID <-------------------------------[重要] 29 | var WX_SERVICE_UUID = 'FEE7'; 30 | var WX_CHARC_UUID_WRITE = 'FEC7'; 31 | var WX_CHARC_UUID_INDICATE = 'FEC8'; 32 | var WX_CHARC_UUID_READ = 'FEC9'; 33 | 34 | // 微信运动精简协议专属UUID 35 | var WERUN_PEDOMETER_UUID = 'FEA1'; 36 | var WERUN_TARGET_UUID = 'FEA2'; 37 | 38 | // 蓝牙广播COMPANY IDENTIFIER,设备厂商在BLUETOOTH SIG申请的公司编号 39 | var DEVICE_COMPANY_ID = '013A'; 40 | // 蓝牙广播LOCAL NAME,AirSyncDebugger显示该名称 41 | var BLE_LOCAL_NAME = 'WeChat BLE'; 42 | // 蓝牙设备名称,与微信内显示的名称无关 43 | var DEVICE_NAME = '微信互联硬件'; 44 | // 设备MAC地址 45 | var DEVICE_MAC_ADDR; 46 | 47 | // 在运行环境变量中设置设备名称 48 | process.env['BLENO_DEVICE_NAME'] = DEVICE_NAME; 49 | 50 | var BlenoPrimaryService = bleno.PrimaryService; 51 | var BlenoCharacteristic = bleno.Characteristic; 52 | 53 | // ///////////////////////////////////////////////////////// START 54 | // 定义Write特征值 55 | var WxWriteOnlyChar = function() { 56 | WxWriteOnlyChar.super_.call(this, { 57 | uuid : WX_CHARC_UUID_WRITE, // 0xFEC7 58 | properties : [ 'write', 'writeWithoutResponse' ] 59 | }); 60 | }; 61 | 62 | util.inherits(WxWriteOnlyChar, BlenoCharacteristic); 63 | 64 | WxWriteOnlyChar.prototype.onWriteRequest = function(data, offset, 65 | withoutResponse, callback) { 66 | console.log('WxWriteOnlyChar write request: 0x' + data.toString('hex') + ' ' 67 | + offset + ' ' + withoutResponse); 68 | callback(this.RESULT_SUCCESS); 69 | }; 70 | // 定义Write特征值 --END 71 | 72 | // 定义Indicate特征值 73 | var WxIndicateOnlyChar = function() { 74 | WxIndicateOnlyChar.super_.call(this, { 75 | uuid : WX_CHARC_UUID_INDICATE, // 0xFEC8 76 | properties : [ 'indicate' ] 77 | }); 78 | }; 79 | 80 | util.inherits(WxIndicateOnlyChar, BlenoCharacteristic); 81 | // 定义Indicate特征值 --END 82 | 83 | // 定义Read特征值 84 | var WxStaticReadOnlyChar = function() { 85 | WxStaticReadOnlyChar.super_.call(this, { 86 | uuid : WX_CHARC_UUID_READ, // 0xFEC9 87 | properties : [ 'read' ], 88 | // 设备MAC地址 <------------------------------------------[重要] 89 | value : new Buffer(DEVICE_MAC_ADDR, 'hex') 90 | }); 91 | }; 92 | 93 | util.inherits(WxStaticReadOnlyChar, BlenoCharacteristic); 94 | // 定义Read特征值 --END 95 | // ///////////////////////////////////////////////////////// END 96 | 97 | /////////////////////////////////////////////////////////// START 98 | // 定义微信运动计步器特征值 99 | var WeRunPedoMeterChar = function() { 100 | WeRunPedoMeterChar.super_.call(this, { 101 | uuid : WERUN_PEDOMETER_UUID, 102 | properties : [ 'read', 'indicate' ], 103 | // 返回计步器数据,此处为示例仅返回固定值:0x01 0x102700 104 | // 数据格式: XX(flag) XXXX..(value, Little-Endian) 105 | // FLAG: 0x01:步数(必选) 0x02:距离单位m(可选) 0x04:卡路里(可选) 106 | // VALUE: 步数,距离,卡路里 107 | value : new Buffer('01' + '102700', 'hex') 108 | }); 109 | }; 110 | 111 | util.inherits(WeRunPedoMeterChar, BlenoCharacteristic); 112 | 113 | // onSubscribe 114 | WeRunPedoMeterChar.prototype.onSubscribe = function(maxValueSize, 115 | updateValueCallback) { 116 | console.log('WeRunPedoMeterChar subscribe'); 117 | this.changeInterval = setInterval(function() { 118 | // 返回计步器数据,此处为示例仅返回固定值:0x01 0x102700 119 | var data = new Buffer('01' + '102700', 'hex'); 120 | console.log('WeRunPedoMeterChar update value: 0x' + data.toString('hex')); 121 | updateValueCallback(data); 122 | this.counter++; 123 | }.bind(this), 1000); 124 | }; 125 | // onUnsubscribe 126 | WeRunPedoMeterChar.prototype.onUnsubscribe = function() { 127 | console.log('WeRunPedoMeterChar unsubscribe'); 128 | if (this.changeInterval) { 129 | clearInterval(this.changeInterval); 130 | this.changeInterval = null; 131 | } 132 | }; 133 | // onIndicate 134 | WeRunPedoMeterChar.prototype.onIndicate = function() { 135 | console.log('WeRunPedoMeterChar on indicate'); 136 | }; 137 | // 定义微信运动计步器特征值 --END 138 | 139 | // 定义微信运动运动目标特征值 140 | var WeRunTargetChar = function() { 141 | WeRunTargetChar.super_.call(this, { 142 | uuid : WERUN_TARGET_UUID, 143 | properties : [ 'read', 'indicate', 'write' ], 144 | // 返回运动目标,此处示例返回目标值为1万步:0x01 0x102700 145 | // 数据格式: XX(flag) XXXX..(value, Little-Endian) 146 | // FLAG: 0x01:步数(必选) 147 | // VALUE: 步数 148 | value : new Buffer('01' + '102700', 'hex') 149 | }); 150 | }; 151 | 152 | util.inherits(WeRunTargetChar, BlenoCharacteristic); 153 | 154 | // onWrite 155 | WeRunTargetChar.prototype.onWriteRequest = function(data, offset, 156 | withoutResponse, callback) { 157 | console.log('WeRunTargetChar write request: 0x' + data.toString('hex') + ' ' 158 | + offset + ' ' + withoutResponse); 159 | callback(this.RESULT_SUCCESS); 160 | }; 161 | // onSubscribe 162 | WeRunTargetChar.prototype.onSubscribe = function(maxValueSize, 163 | updateValueCallback) { 164 | console.log('WeRunTargetChar subscribe'); 165 | this.changeInterval = setInterval(function() { 166 | // 返回运动目标,此处示例返回目标值为1万步:0x01 0x102700 167 | var data = new Buffer('01' + '102700', 'hex'); 168 | console.log('WeRunTargetChar update value: 0x' + data.toString('hex')); 169 | updateValueCallback(data); 170 | this.counter++; 171 | }.bind(this), 1000); 172 | }; 173 | // onUnsubscribe 174 | WeRunTargetChar.prototype.onUnsubscribe = function() { 175 | console.log('WeRunTargetChar unsubscribe'); 176 | if (this.changeInterval) { 177 | clearInterval(this.changeInterval); 178 | this.changeInterval = null; 179 | } 180 | }; 181 | // onIndicate 182 | WeRunTargetChar.prototype.onIndicate = function() { 183 | console.log('WeRunTargetChar on indicate'); 184 | }; 185 | // 定义微信运动运动目标特征值 --END 186 | // ///////////////////////////////////////////////////////// END 187 | 188 | function WxBLEService() { 189 | WxBLEService.super_.call(this, { 190 | uuid : WX_SERVICE_UUID, 191 | characteristics : [ 192 | new WxWriteOnlyChar(), 193 | new WxIndicateOnlyChar(), 194 | new WxStaticReadOnlyChar(), 195 | // 添加微信运动精简协议特征值 196 | new WeRunPedoMeterChar(), new WeRunTargetChar() ] 197 | }); 198 | } 199 | 200 | util.inherits(WxBLEService, BlenoPrimaryService); 201 | 202 | bleno.on('stateChange', function(state) { 203 | // 获取设备MAC地址 <--------------------------------------------------------------------------[重要] 204 | DEVICE_MAC_ADDR = bleno.address.replace(/:/g,''); 205 | console.log('\r\n'); 206 | console.log('------------------------------------------------------------------'); 207 | console.log('MAC=' + bleno.address + ', DevName=' + DEVICE_NAME + ', LocalName=' + BLE_LOCAL_NAME); 208 | console.log('------------------------------------------------------------------'); 209 | console.log('on -> stateChange ' + state); 210 | if (state === 'poweredOn') { 211 | // 拼装广播数据包 212 | var data = { 213 | // 蓝牙flags设置不正确会导致微信或AirSyncDebugger连接不成功 <------------------------------[重要] 214 | flags : [0x04], 215 | // 在广播的服务UUIDs中加入微信专属UUID:0xFEE7 <------------------------------------------[重要] 216 | incompleteUUID16 : [ WX_SERVICE_UUID ], 217 | // LOCAL NAME,AirSyncDebugger中扫描到的名称 218 | completeName : BLE_LOCAL_NAME, 219 | // 微信蓝牙协议Manufacturer Data固定结尾格式: XXXX(2 bytes) XXXXXXXXXXXX(6 bytes) <------[重要] 220 | // 微信iOS版本发现设备时以advertise data中的MAC为准,微信Android版本发现时以设备的物理MAC为准 221 | mfrData : new Buffer(DEVICE_COMPANY_ID + DEVICE_MAC_ADDR, 'hex') 222 | }; 223 | var advertisementData = parser.serialize(data); 224 | bleno.startAdvertisingWithEIRData(advertisementData, null); 225 | } else { 226 | bleno.stopAdvertising(); 227 | } 228 | }); 229 | 230 | bleno.on('advertisingStart', function(error) { 231 | console.log('on -> advertisingStart ' 232 | + (error ? 'error' + error : 'success')); 233 | if (!error) { 234 | // 此处示例仅添加了微信必须的Service,可在此添加更多的业务Service 235 | bleno.setServices([ new WxBLEService() ]); 236 | // bleno.setServices([ new WxBLEService(), new OtherService() ]); 237 | } 238 | }); 239 | --------------------------------------------------------------------------------