├── 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 |
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 |
--------------------------------------------------------------------------------