├── public ├── scripts │ ├── config.sample.js │ ├── debug.js │ └── demo.js ├── views │ └── index.html └── style │ └── device.css ├── .gitignore ├── config └── _sample.json ├── route └── handle.js ├── package.json ├── app.js ├── README.md └── util └── weixin.js /public/scripts/config.sample.js: -------------------------------------------------------------------------------- 1 | var baseUrl = "http://domain.com"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dump.rdb 3 | bin 4 | config/* 5 | redis.conf 6 | !config/_sample.json 7 | public/scripts/config.js 8 | access_token.txt 9 | jsapi_ticket.txt 10 | *~ -------------------------------------------------------------------------------- /config/_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "weixin": { 3 | "id": "your weixin id", 4 | "token": "your token", 5 | "appid": "your app_id", 6 | "app_secret": "your app_secret" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /route/handle.js: -------------------------------------------------------------------------------- 1 | var config = require('config'); 2 | var weixin = require('../util/weixin'); 3 | 4 | module.exports = function(app){ 5 | 6 | /** 7 | * jssdk 签名, 供H5页面调用 8 | */ 9 | app.get('/sign', function(req, res){ 10 | var url = req.query.url; 11 | if (!url) { 12 | return res.status(400).json({errMsg: "need url"}); 13 | } 14 | url = decodeURIComponent(url); 15 | weixin.api.getTicketToken(function(err, token){ 16 | if (!token) return res.json({}); 17 | var ret = weixin.util.getJsConfig(token.ticket, url); 18 | ret.appid = config.weixin.appid; 19 | console.log('token: ', token); 20 | console.log('signData: ', ret); 21 | res.json(ret); 22 | }); 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weixin-device-demo", 3 | "version": "2.0.1", 4 | "description": "微信H5页面控制蓝牙硬件demo", 5 | "main": "app.js", 6 | "dependencies": { 7 | "config": "^1.12.0", 8 | "ejs": "^2.3.4", 9 | "express": "^4.12.3", 10 | "morgan": "^1.6.1", 11 | "weixin-trap": "1.0.5" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/liuxiaodong/weixin-device-demo.git" 20 | }, 21 | "keywords": [ 22 | "weixin", 23 | "device", 24 | "weixin-device", 25 | "iot" 26 | ], 27 | "author": "leaf", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/liuxiaodong/weixin-device-demo/issues" 31 | }, 32 | "homepage": "https://github.com/liuxiaodong/weixin-device-demo" 33 | } 34 | -------------------------------------------------------------------------------- /public/scripts/debug.js: -------------------------------------------------------------------------------- 1 | function dump_obj(myObject) { 2 | var s = ''; 3 | for (var property in myObject) { 4 | s += '' + property +": " + myObject[property] + ''; 5 | } 6 | return s; 7 | } 8 | 9 | var i = 0; 10 | console.log = (function(old_funct, div_log) { 11 | return function(func, text) { 12 | old_funct(text); 13 | var p = ''; 14 | if (i%2 == 0) 15 | p = '

'; 16 | else 17 | p = '

'; 18 | 19 | if (typeof text === "object") 20 | div_log.innerHTML += p + func + ': ' + JSON.stringify(text) + '

'; 21 | // div_log.innerHTML += p + dump_obj(text) + '

'; 22 | else 23 | div_log.innerHTML += p + text + '

'; 24 | 25 | div_log.scrollTop = div_log.scrollHeight; 26 | i += 1; 27 | }; 28 | } (console.log.bind(console), document.getElementById("debug"))); 29 | console.error = console.debug = console.info = console.log 30 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var morgan = require('morgan'); 4 | var weixin = require('./util/weixin'); 5 | 6 | var app = express(); 7 | app.set('port', process.env.PORT || 3000); 8 | 9 | app.use(morgan('dev')); 10 | app.use(express.static(path.join(__dirname, 'public'))); 11 | app.set('views', __dirname + '/public/views'); 12 | app.engine('html', require('ejs').renderFile); 13 | app.set('view engine', 'html'); 14 | 15 | // 微信公众号配置的URL 路由 16 | app.use('/wechat', weixin.trap); 17 | 18 | // H5页面需要的接口(获取签名) 19 | require('./route/handle')(app); 20 | 21 | // H5 的demo页面 22 | app.get('/*', function(req, res, next){ 23 | res.render('index'); 24 | }); 25 | 26 | app.use(function(req, res, next) { 27 | var err = new Error('Not Found'); 28 | err.status = 404; 29 | next(err); 30 | }); 31 | 32 | /* jshint unused:false */ 33 | if (app.get('env') === 'development') { 34 | app.use(function(err, req, res, next) { 35 | res.status(err.status || 500); 36 | res.json({ 37 | env: 'development', 38 | message: err.message, 39 | error: err 40 | }); 41 | }); 42 | } 43 | 44 | app.use(function(err, req, res, next) { 45 | res.status(err.status || 500); 46 | res.json({ 47 | message: err.message, 48 | error: {} 49 | }); 50 | }); 51 | 52 | var server = app.listen(app.get('port'), function(a, b){ 53 | console.log('weixin server listening on port ' + server.address().port); 54 | }); 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##微信蓝牙设备DEMO 2 | * 测试微信蓝牙硬件流程,由于硬件不熟悉,所以只写了服务器和H5部分。 3 | 4 | ####步骤 5 | 1. 当然需要一个公众号,现在可以申请测试公众号。 6 | 7 | http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login 8 | 9 | * 现在好像需要找微信申请开通硬件权限 10 | 11 | 12 | 2. 还需要一个满足微信蓝牙协议的蓝牙模块。 13 | 14 | 最好用微信的AirSyncDebugger测试通过 15 | http://iot.weixin.qq.com/doc/blue/%E5%BE%AE%E4%BF%A1%E8%93%9D%E7%89%99%E5%8D%8F%E8%AE%AE%E8%B0%83%E8%AF%95%E5%B7%A5%E5%85%B7AirSyncDebugger%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3%20v2.0.pdf 16 | 17 | 18 | 3. 配置好公众号的各种信息 19 | 20 | URL, Token, JS接口安全域名等信息 21 | URL 最好 `http://your_domain.com/wechat` 22 | 不然需要修改 app.js 中的 `app.use('/wechat', weixin.trap);` 路由 23 | 24 | 4. 克隆代码 25 | 26 | `git clone git@github.com:liuxiaodong/weixin-device-demo.git` 27 | 28 | 29 | 5. 安装依赖包并修改配置文件 30 | 31 | `cd weixin-device-demo` 32 | 33 | `sudo npm install` 34 | 35 | `cp config/_sample.json development.json` 36 | 37 | 修改 development.json 里面配置,比如 weixin 部分的 id, token, appid, app_secret 为自己公众号的配置信息。 38 | 39 | 6. 拷贝前端页面配置文件 40 | 41 | `cp public/scripts/config.sample.js public/scripts/config.js` 42 | 43 | 并修改里面的 baseUrl 为自己域名的url 44 | 45 | 7. 发布到服务,打开微信关注公众号进入链接 http://your_domain/wechat/demo 即可测试微信蓝牙硬件流程。 46 | * 在调用其他接口前必须先调用 初始化设备库(openWXDeviceLib) 接口 47 | -------------------------------------------------------------------------------- /public/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 硬件JSAPI测试 12 | 13 | 14 | 15 |
16 |
17 |
18 | API初始化 19 | API释放 20 |
21 |
22 | 设备状态 23 |
24 |
25 | 开始扫描 26 | 停止扫描 27 |
28 |
29 | 连接设备 30 | 断开设备 31 | 发送数据 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/style/device.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0px; 3 | padding: 0px; 4 | } 5 | body { 6 | background: none repeat scroll 0% 0% rgb(255, 255, 255); 7 | font-family: YaHei,Helvetica,Tahoma,sans-serif; 8 | font-size: 16px; 9 | color: #cccccc; 10 | margin: 0px; 11 | padding: 0px; 12 | background-color: #343434; 13 | } 14 | #debug { 15 | background-color: white; 16 | border-radius: 5px; 17 | -webkit-border-radius: 5px; 18 | border: 1px solid #000; 19 | margin: 10px; 20 | height: 270px; 21 | overflow: scroll; 22 | color: black; 23 | font-size: 12px; 24 | } 25 | #debug p { 26 | padding: 3px 5px; 27 | word-wrap: break-word; 28 | } 29 | .gray { 30 | background-color: #ddd; 31 | } 32 | #debug span { 33 | display: block; 34 | word-wrap: break-word; 35 | margin-bottom: 2px; 36 | } 37 | #buttons { 38 | text-align: center; 39 | } 40 | .button { 41 | display: inline-block; 42 | margin: 5px 5px; 43 | padding: 7px 10px; 44 | text-align: center; 45 | text-decoration: none; 46 | 47 | text-shadow: 1px 1px 1px rgba(255,255,255, .22); 48 | 49 | -webkit-border-radius: 30px; 50 | -moz-border-radius: 30px; 51 | border-radius: 30px; 52 | 53 | -webkit-box-shadow: 1px 1px 1px rgba(0,0,0, .29), inset 1px 1px 1px rgba(255,255,255, .44); 54 | -moz-box-shadow: 1px 1px 1px rgba(0,0,0, .29), inset 1px 1px 1px rgba(255,255,255, .44); 55 | box-shadow: 1px 1px 1px rgba(0,0,0, .29), inset 1px 1px 1px rgba(255,255,255, .44); 56 | 57 | -webkit-transition: all 0.15s ease; 58 | -moz-transition: all 0.15s ease; 59 | -o-transition: all 0.15s ease; 60 | -ms-transition: all 0.15s ease; 61 | transition: all 0.15s ease; 62 | 63 | color: #19667d; 64 | background: #70c9e3; /* Old browsers */ 65 | background: -moz-linear-gradient(top, #70c9e3 0%, #39a0be 100%); /* FF3.6+ */ 66 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#70c9e3), color-stop(100%,#39a0be)); /* Chrome,Safari4+ */ 67 | background: -webkit-linear-gradient(top, #70c9e3 0%,#39a0be 100%); /* Chrome10+,Safari5.1+ */ 68 | background: -o-linear-gradient(top, #70c9e3 0%,#39a0be 100%); /* Opera 11.10+ */ 69 | background: -ms-linear-gradient(top, #70c9e3 0%,#39a0be 100%); /* IE10+ */ 70 | background: linear-gradient(top, #70c9e3 0%,#39a0be 100%); /* W3C */ 71 | } -------------------------------------------------------------------------------- /util/weixin.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var config = require('config'); 4 | 5 | var accessTokenFile = path.join(__dirname, '../access_token.txt'); 6 | 7 | if (!fs.existsSync(accessTokenFile)) { 8 | fs.appendFileSync(accessTokenFile, '', {encoding: 'utf8'}); 9 | } 10 | 11 | var jsApiTicketFile = path.join(__dirname, '../jsapi_ticket.txt'); 12 | 13 | if (!fs.existsSync(jsApiTicketFile)) { 14 | fs.appendFileSync(jsApiTicketFile, '', {encoding: 'utf8'}); 15 | } 16 | 17 | var weixin = require("weixin-trap")({ 18 | attrNameProcessors: 'underscored', 19 | saveToken: function(token, callback){ 20 | token.saveTime = new Date().getTime(); 21 | var tokenStr = JSON.stringify(token); 22 | fs.writeFile(accessTokenFile, tokenStr, {encoding: 'utf8'}, callback); 23 | }, 24 | getToken: function(callback){ 25 | fs.readFile(accessTokenFile, {encoding: 'utf8'}, function(err, str){ 26 | var token; 27 | if (str) { 28 | token = JSON.parse(str); 29 | } 30 | var time = new Date().getTime(); 31 | if (token && (time - token.saveTime) < ((token.expireTime - 120) * 1000) ) { 32 | return callback(null, token); 33 | } 34 | callback(); 35 | }); 36 | }, 37 | saveTicketToken: function(appid, type, token, callback) { 38 | token.saveTime = new Date().getTime(); 39 | var tokenStr = JSON.stringify(token); 40 | fs.writeFile(jsApiTicketFile, tokenStr, {encoding: 'utf8'}, callback); 41 | }, 42 | getTicketToken: function(callback) { 43 | fs.readFile(jsApiTicketFile, {encoding: 'utf8'}, function(err, str){ 44 | var token; 45 | if (str) { 46 | token = JSON.parse(str); 47 | } 48 | var time = new Date().getTime(); 49 | if (token && (time - token.saveTime) < ((token.expireTime - 120) * 1000) ) { 50 | return callback(null, token); 51 | } 52 | weixin.api.getTicket(config.weixin.id, 'jsapi', function(err, token){ 53 | if (err) { 54 | console.log('获取 jsapi 签名出错: ', err); 55 | } 56 | callback(null, token); 57 | }); 58 | }); 59 | }, 60 | config: { 61 | id: config.weixin.id, // 微信公众号 id 62 | appid: config.weixin.appid, 63 | token: config.weixin.token, 64 | appsecret: config.weixin.app_secret 65 | //encryptkey: config.weixin.encryptkey 66 | } 67 | }); 68 | 69 | 70 | /** 71 | * 处理微信用户发送到公众号的文本消息 72 | */ 73 | weixin.trap.text(/\S/, function(req, res){ 74 | var id = req.body.to_user_name; 75 | var openid = req.body.from_user_name; 76 | var content = req.body.content; 77 | if(content) content = content.trim(); 78 | console.log("收到消息来自微信用户 %s 的消息:%s", openid, content); 79 | res.text("hello"); 80 | /** 81 | * // 发送消息给设备 82 | res.text("收到 openid: " + openid + " 发来的数据: " + content); 83 | weixin.api.transferMessage(id, id, device_id, openid, content, contefunction(err, ret){ 84 | var replyText = "写入数据成功: " + content; 85 | if (err) { 86 | replyText = "给设备写数据失败: " + JSON.stringify(err); 87 | } 88 | weixin.api.sendText(id, openid, replyText); 89 | }); 90 | */ 91 | }); 92 | 93 | /** 94 | * 接受设备发送到的消息并回复 95 | */ 96 | /** 97 | weixin.trap.device(function(req, res){ 98 | //res.device(new Buffer("1111", "hex")); // 响应设备 99 | var openid = req.body.from_user_name; 100 | var content = req.body.content; 101 | if(content) { 102 | content = content.trim(); 103 | content = new Buffer(content, 'base64').toString(); 104 | } 105 | var id = req.body.device_id; 106 | var replyText = id + ' 说: '; 107 | if (content) { 108 | replyText += content; 109 | } else { 110 | replyText += '什么都不想说'; 111 | } 112 | weixin.api.sendText(appid, openid, replyText); 113 | }); 114 | */ 115 | 116 | module.exports = weixin; -------------------------------------------------------------------------------- /public/scripts/demo.js: -------------------------------------------------------------------------------- 1 | (function(window, undefined){ 2 | 3 | var url = location.href.replace(location.hash, ""); 4 | url = encodeURIComponent(url); 5 | 6 | var deviceId = "112233445566"; // 需要连接设备的deviceID 7 | var buf = "aGVsbG8="; // 发送给设备的数据,base64编码 8 | var signData = {}; 9 | /** 10 | * 去后端获取 config 需要的签名 11 | * @param url 本页面的url(去掉hash部分) 12 | */ 13 | $.get(baseUrl + "/sign?url="+url, function(data){ 14 | signData = { 15 | "verifyAppId" : data.appid, 16 | "verifyTimestamp" : data.timestamp, 17 | "verifySignType" : "sha1", 18 | "verifyNonceStr" : data.nonceStr, 19 | "verifySignature" : data.signature 20 | }; 21 | wx.config({ 22 | debug: false, 23 | appId: data.appid, 24 | timestamp: data.timestamp, 25 | nonceStr: data.nonceStr, 26 | signature: data.signature, 27 | jsApiList: [ 28 | 'openWXDeviceLib', 29 | 'closeWXDeviceLib', 30 | 'getWXDeviceInfos', 31 | 'startScanWXDevice', 32 | 'stopScanWXDevice', 33 | 'connectWXDevice', 34 | 'disconnectWXDevice', 35 | 'sendDataToWXDevice' 36 | ] 37 | }); 38 | }); 39 | 40 | /** 41 | * config 完成后绑定各种事件 42 | */ 43 | wx.ready(function (){ 44 | console.log("config", "ready"); 45 | WeixinJSBridge.on('onWXDeviceBindStateChange', function(argv) { 46 | console.log("onWXDeviceBindStateChange", argv); 47 | }); 48 | 49 | WeixinJSBridge.on('onWXDeviceStateChange', function(argv) { 50 | console.log("onWXDeviceStateChange", argv); 51 | }); 52 | 53 | WeixinJSBridge.on('onReceiveDataFromWXDevice', function(argv) { 54 | console.log("onReceiveDataFromWXDevice", argv); 55 | }); 56 | 57 | WeixinJSBridge.on('onWXDeviceBluetoothStateChange', function(argv) { 58 | console.log("onWXDeviceBluetoothStateChange", argv); 59 | }); 60 | 61 | WeixinJSBridge.on('onScanWXDeviceResult', function(argv){ 62 | console.log("onScanWXDeviceResult", argv); 63 | }); 64 | 65 | onConfigReady(); 66 | 67 | }); 68 | 69 | /** 70 | * config 失败 71 | */ 72 | wx.error(function (res) { 73 | alert(JSON.stringify(res)); 74 | }); 75 | 76 | /** 77 | * 事件绑定初始化 78 | */ 79 | function onConfigReady() { 80 | document.querySelector('#openWXDeviceLib').addEventListener('touchend', function(e){ 81 | openWXDeviceLib(); 82 | }); 83 | 84 | document.querySelector('#closeWXDeviceLib').addEventListener('touchend', function(e){ 85 | closeWXDeviceLib(); 86 | }); 87 | 88 | document.querySelector('#getWXDeviceInfos').addEventListener('touchend', function(e){ 89 | getWXDeviceInfos(); 90 | }); 91 | 92 | document.querySelector('#startScanWXDevice').addEventListener('touchend', function(e){ 93 | startScanWXDevice(); 94 | }); 95 | 96 | document.querySelector('#stopScanWXDevice').addEventListener('touchend', function(e){ 97 | stopScanWXDevice(); 98 | }); 99 | 100 | document.querySelector('#connectWXDevice').addEventListener('touchend', function(e){ 101 | connectWXDevice(); 102 | }); 103 | 104 | document.querySelector('#disconnectWXDevice').addEventListener('touchend', function(e){ 105 | disconnectWXDevice(); 106 | }); 107 | 108 | document.querySelector('#sendDataToWXDevice').addEventListener('touchend', function(e){ 109 | sendDataToWXDevice(); 110 | }); 111 | } 112 | 113 | /* 114 | * jsapi接口的封装 115 | */ 116 | function checkJsApi(){ 117 | wx.checkJsApi({ 118 | jsApiList: ['getWXDeviceTicket'], 119 | success: function(res) { 120 | //alert(JSON.stringify(res)); 121 | } 122 | }); 123 | } 124 | 125 | /** 126 | * 各个 JSSDK 的 API 接口实现 127 | */ 128 | 129 | function openWXDeviceLib(){ 130 | WeixinJSBridge.invoke('openWXDeviceLib', signData, function(res){ 131 | console.log("openWXDeviceLib", res); 132 | }); 133 | } 134 | 135 | function closeWXDeviceLib(){ 136 | WeixinJSBridge.invoke('closeWXDeviceLib', signData, function(res){ 137 | console.log("closeWXDeviceLib", res); 138 | }); 139 | } 140 | 141 | 142 | function getWXDeviceInfos(){ 143 | WeixinJSBridge.invoke('getWXDeviceInfos', signData, function(res){ 144 | console.log("getWXDeviceInfos", res); 145 | }); 146 | } 147 | 148 | function connectWXDevice(){ 149 | var _data = mixin({"deviceId":deviceId}, signData); 150 | WeixinJSBridge.invoke('connectWXDevice', _data, function(res){ 151 | console.log("connectWXDevice", res); 152 | }); 153 | } 154 | 155 | function disconnectWXDevice(){ 156 | var _data = mixin({"deviceId":deviceId}, signData); 157 | WeixinJSBridge.invoke('disconnectWXDevice', _data, function(res){ 158 | console.log("disconnectWXDevice", res); 159 | }); 160 | } 161 | 162 | function sendDataToWXDevice(deviceId, buf, cb){ 163 | var _data = mixin({"deviceId":deviceId, "base64Data": buf}, signData); 164 | WeixinJSBridge.invoke('sendDataToWXDevice', _data, function(res){ 165 | console.log("sendDataToWXDevice", res); 166 | }); 167 | } 168 | 169 | function startScanWXDevice(cb){ 170 | var _data = mixin({btVersion:'ble'}, signData); 171 | WeixinJSBridge.invoke('startScanWXDevice', _data, function(res){ 172 | console.log("startScanWXDevice", res); 173 | }); 174 | } 175 | 176 | function stopScanWXDevice(){ 177 | WeixinJSBridge.invoke('stopScanWXDevice', signData, function(res){ 178 | console.log("stopScanWXDevice", res); 179 | }); 180 | } 181 | 182 | function mixin(target, src) { 183 | Object.getOwnPropertyNames(src).forEach(function(name) { 184 | var descriptor = Object.getOwnPropertyDescriptor(src, name); 185 | Object.defineProperty(target, name, descriptor); 186 | }); 187 | return target; 188 | } 189 | 190 | })(window); 191 | --------------------------------------------------------------------------------