微信小程序接入NFC,使用HCE模拟主机卡完成NFC刷卡发送消息

本文涉及的产品
数据可视化DataV,5个大屏 1个月
可视分析地图(DataV-Atlas),3 个项目,100M 存储空间
简介: 微信小程序接入NFC,使用HCE模拟主机卡完成NFC刷卡发送消息

NFC相信大家都很熟悉,现实中经常使用的门禁卡,公交卡,地铁卡,饭卡等都是采用NFC功能,那么你知道吗,NFC也可以用微信小程序来实现。使用微信小程序可以读取/写入让手机成为一个刷卡器,也可以使用微信小程序模拟一个主机卡,来刷开门禁/饭卡等等。本章就带大家来一起看看微信小程序的NFC有何不同!


一、什么是NFC

NFC是一种采用13.56MHz频带的近距离无线通讯技术,虽然通讯距离仅为10cm左右,不过和非接触式IC卡技术一样,我们只需要“触碰一下”即可在不同的电子产品之间交换数据。


与非接触IC卡不同,NFC与非接触式IC卡不同,NFC可进行双向通信。只要是支持NFC的产品和IC卡,就可以读出或写入数据。还可在手机等便携产品间进行通信。数据传输速度不高,有106kbit/秒、212kbit/秒、424kbit/秒以及848kbit/秒四种速度可供选择。


NFC介绍


二、NFC可以做什么


NFC具有“卡模拟”、“读写器模拟”以及“产品间通信(P2P)”三种功能。


1. 卡模拟:举例来说,我们可以用手机来模拟门禁卡,公交卡,饭卡等等。但是要注意的是,它是模拟出来的,也就是说并不实体存在的一个卡。


2. 读写器模拟:指的就是,你可以模拟一个读写器,别人用NFC卡来你这里刷卡,你就可以获取其卡片上的信息。


3. 通讯讲就是数据通信,使用NFC卡在刷的时候都会进行数据交换。用来做一些事情,比如:配置网络,设置信息,传输文件等等。


6f10c173ef604f4eadec24668db37374.png


三、微信小程序的NFC


NFC | 微信开放文档


我们打开文档可以看到,对于NFC,微信给它的名字叫:近场通讯


首先一定要强调的是,因为苹果手机权限的缘故,所以NFC在微信小程序中只支持:Android


微信将NFC分为了三部分


HCE(基于主机的卡模拟),也就是将安卓手机模拟成实体的智能卡。我们可以通过模拟的智能卡来刷对应的读卡器,给读卡器传递数据。

支持NFC读写,也就是将手机作为读卡器来使用。也就是我们可以将一些实体的NFC卡通过贴在手机上从而实现读取卡内容。

NFC标签打开小程序。指的就是我们可以通过NFC卡片触碰手机,快速唤起小程序页面的能力。

1. HCE(基于主机的卡模拟)的使用场景:


使用手机做门禁卡/公交卡/地铁卡

使用手机给读卡器传递数据(配网、登记)

2. NFC读写的使用场景:


使用手机做刷卡器,来获取NFC卡的信息。

3. NFC标签打开小程序的使用场景


设备的快速配网

文件快速传输等快捷控制

官网文档写的也很齐全了,大家可以根据自己的需求选择不同的场景来进行使用。


本章我就选择了HCE(基于主机的卡模拟)场景。


四、使用步骤


1.研究API

wx.stopHCE(Object object) | 微信开放文档


微信官方文档中,卡模拟一共有六个API。


分别为:


wx.stopHCE 关闭NFC模块。


wx.startHCE 初始化NFC


wx.sendHCEMessage 发送NFC消息


wx.onHCEMessage  监听接收NFC设备消息事件。


wx.offHCEMessage  移除接收 NFC 设备消息事件的监听函数


wx.getHCEState 判断当前设备是否支持 HCE 能力。


再次特别要强调的是,NFC仅在Android系统下支持。


2.使用方法


有过开发经验的同学其实比较清楚。

以上API 很明显就是一个生命周期。

从开始到销毁。我们该如何去操作这个NFCapi呢?


1. 首先调用 wx.getHCEState, 判断设备是否支持NFC


2. 调用 wx.startHCE(OBJECT) 初始化手机的NFC模块;


3. 初始化完成后,调用 wx.onHCEMessage 监听芯片响应的消息;


4. 点击页面上的“询卡”按钮,调用 wx.sendHCEMessage发送询卡指令;


5. 这时 wx.onHCEMessage(应该可以收到带有uid信息的芯片响应数据;


6. 业务处理。


7.  全部操作完成后之后,调用 wx.stopHCE停止手机的NFC模块


好了,废话不多说,直接进入我们的正式项目。


一:新项目


我们需要利用微信开发者工具,来新建一个我们的项目。APPID大家如果有的话就用自己的,没有的话则使用测试号即可。


244fe1b0146b43329f2f0c02f0c589b9.png


二:设置简单页面及对应的js


ba6491b2570a46ce8161b5c025b0b663.png


三:根据上述我们理清的 NFC生命周期顺序来搭建我们的NFC项目。


顺序自然为:页面打开初始化的时候就需要 getHCEState 判断设备是否支持NFC。不支持则需要做兼容。

799dba49ede04803a6935e931e12c563.png

当返回值的errCode为 0  的时候则一切正常,是支持NFC的。

同时,为了更有效的避免大家判断code,所以我们可以将官网的 getHCEState返回code 声明出来,以便后面使用。

6d72f607ccb74e0ca246188e3c25f9c4.png


四:需要开始初始化StartHCE(初始化NFC,将手机初始化为一个主机模拟卡)

76f30058e378419f9859d5b108e58587.png


wx.startHCE接收一个参数为 aid_list。

a8f7e74614e147259312fac80db05370.png

官方解释意思为,需要注册到系统AID列表中,AID列表其实就是每个刷卡器的唯一标识,不清楚的就问下你们的安卓或者默认填写:F22222222。与其它API 类似,它也有自己的返回值,默认0为正常成功的。


五:onHCEMEssage 监听

在说这里之前,网上许多朋友碰到的类似,wx.onHCEMessage,不管怎么调都没有返回值?

d64ff788635a4e83845bb7f96625db79.png

不知道有没有伙伴仔细看我上面的话,初始化完成后,onHCEMessage其实就要开始监听。至于为什么没有返回值。是因为,wx.onHCEMessage是一个监听。它需要读卡器来响应它,也就是说要给需要读卡器(设备)通过render 指令 给它发消息,这时候它才能拿得到值。

 abaa95b46fa445bfbf371cee133017bc.png

它有三个返回值,但是我们只一般取第一个也就是 messageType。


messageType的值 = 1 时,也就是说,读卡器给我们响应了,说它接收到了NFC。这时候我们就可以调用 sendHCEMessage 来给读卡器发送消息了。


messageType的值 = 2 时,就是说,手机已经从读卡器上拿开了。


所以我们在这里只判断 messageType的值为1. 当等于1 的时候我们就可以开始发消息了。


六:使用wx.sendHCEMessage 发送NFC消息

51f48515887241ec9fb5402de65092ad.png


细心的朋友会发现,发个消息不就是 wx.sendHCEMessage就够了。为啥我的有一大堆的ArrayBuffer 甚至还有comm.


这是因为文档有标注:

7d4221e088634916867c4eb0818cf448.png

wx.sendHCEMessage 的data 必须是一个二进制数据。也就是我们不能将普通的JSON,字符串等数据 传递给它。否则会报错。

那这时候我们需要怎么办呢?

转换!

我在项目中的comm 就是一个封装的 转换文件。内容如下:

comm.js


const formatTime = date => {
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()
  const hour = date.getHours()
  const minute = date.getMinutes()
  const second = date.getSeconds()
  return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
/**
 * 生成指定长度随机数
 */
function genRandom(n) {
  let a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; //生成的随机数的集合  
  let res = [];
  for (let i = 0; i < n; i++) {
    let index = parseInt(Math.random() * (a.length));    //生成一个的随机索引,索引值的范围随数组a的长度而变化  
    res.push(a[index]);
    a.splice(index, 1)  //已选用的数,从数组a中移除, 实现去重复  
  }
  return res.join('');
} 
/**
    * 字符串转换为时间
    * @param  {String} src 字符串
    */
function strToDate(dateObj){
  dateObj = dateObj.replace(/T/g, ' ').replace(/\.[\d]{3}Z/, '').replace(/(-)/g, '/')
  dateObj = dateObj.slice(0, dateObj.indexOf("."))
  return new Date(dateObj)
}
function isFunctinMethod(name) {
  if (name != undefined && typeof name === 'function') {
    return true
  }
  return false
}
const formatNumber = n => {
  n = n.toString()
  return n[1] ? n : '0' + n
}
// ArrayBuffer转16进度字符串
function ab2hex(buffer) {
  var hexArr = Array.prototype.map.call(
    new Uint8Array(buffer),
    function (bit) {
      return ('00' + bit.toString(16)).slice(-2)
    }
  )
  return hexArr.join('');
}
//十六进制字符串转字节数组  
function hex2Bytes(str) {
  var pos = 0;
  var len = str.length;
  if (len % 2 != 0) {
    return null;
  }
  len /= 2;
  var hexA = new Array();
  for (var i = 0; i < len; i++) {
    var s = str.substr(pos, 2);
    var v = parseInt(s, 16);
    hexA.push(v);
    pos += 2;
  }
  return hexA;
}
function hex2ArrayBuffer(hex){
  var pos = 0;
  var len = hex.length;
  if (len % 2 != 0) {
    return null;
  }
  len /= 2;
  var buffer = new ArrayBuffer(len)
  var dataview=new DataView(buffer)
  for (var i = 0; i < len; i++) {
    var s = hex.substr(pos, 2);
    var v = parseInt(s, 16);
    dataview.setInt16(i,v)
    pos += 2;
  }
  return buffer
}
/**
 * string转16进制
 */
function stringToHex(str) {
  var val = "";
  for (var i = 0; i < str.length; i++) {
    if (val == "")
      val = str.charCodeAt(i).toString(16);
    else
      val += str.charCodeAt(i).toString(16);
  }
  return val;
}
/**
 * 16进制转string
 */
function hexCharCodeToStr(hexCharCodeStr) {
    var trimedStr = hexCharCodeStr.trim();
    var rawStr =
      trimedStr.substr(0, 2).toLowerCase() === "0x"
        ?
        trimedStr.substr(2)
        :
        trimedStr;
    var len = rawStr.length;
    if (len % 2 !== 0) {
        alert("Illegal Format ASCII Code!");
        return "";
    }
    var curCharCode;
    var resultStr = [];
    for (var i = 0; i < len; i = i + 2) {
        curCharCode = parseInt(rawStr.substr(i, 2), 16); // ASCII Code Value
        resultStr.push(String.fromCharCode(curCharCode));
    }
    return resultStr.join("");
}
function pad(num, n) {
  var len = num.toString().length;
  while (len < n) {
    num = "0" + num;
    len++;
  }
  return num;
}
function strToHexCharCode(str) {
    if (str === "")
        return "";
    var hexCharCode = [];
    hexCharCode.push("0x");
    for (var i = 0; i < str.length; i++) {
        hexCharCode.push((str.charCodeAt(i)).toString(16));
    }
    return hexCharCode.join("");
}
/**
 * string转byte数组
 */
function stringToByteArray(str) {
  var bytes = new Array();
  var len, c;
  len = str.length;
  for (var i = 0; i < len; i++) {
    c = str.charCodeAt(i);
    if (c >= 0x010000 && c <= 0x10FFFF) {
      bytes.push(((c >> 18) & 0x07) | 0xF0);
      bytes.push(((c >> 12) & 0x3F) | 0x80);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
    } else if (c >= 0x000800 && c <= 0x00FFFF) {
      bytes.push(((c >> 12) & 0x0F) | 0xE0);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
    } else if (c >= 0x000080 && c <= 0x0007FF) {
      bytes.push(((c >> 6) & 0x1F) | 0xC0);
      bytes.push((c & 0x3F) | 0x80);
    } else {
      bytes.push(c & 0xFF);
    }
  }
  return bytes;
}
/**
 * byte数组转string
 */
function byteToString(bytearr) {
  if (typeof arr === 'string') {
    return arr;
  }
  var str = '',
    _arr = arr;
  for (var i = 0; i < _arr.length; i++) {
    var one = _arr[i].toString(2),
      v = one.match(/^1+?(?=0)/);
    if (v && one.length == 8) {
      var bytesLength = v[0].length;
      var store = _arr[i].toString(2).slice(7 - bytesLength);
      for (var st = 1; st < bytesLength; st++) {
        store += _arr[st + i].toString(2).slice(2);
      }
      str += String.fromCharCode(parseInt(store, 2));
      i += bytesLength - 1;
    } else {
      str += String.fromCharCode(_arr[i]);
    }
  }
  return str;
}
/**
 * 二进制转10
 */
function bariny2Ten(byte){
  return parseInt(byte, 2)
}
function bariny2Hex(a){
  return parseInt(a, 16)
}
/**
 * 10/16进制转2进制
 */
function ten2Bariny(ten){
  return ten.toString(2)
}
function str2Hex(str){
  return parseInt(str, 10).toString(16)
}
/**
 * 16进制转2进制
 */
function hex2bariny(hex){
  return parseInt(hex, 16).toString(2)
}
module.exports = {
  formatTime: formatTime,
  isFunctinMethod: isFunctinMethod,
  ab2hex: ab2hex,
  hex2Bytes: hex2Bytes,
  stringToByteArray: stringToByteArray,
  byteToString: byteToString,
  hex2ArrayBuffer: hex2ArrayBuffer,
  bariny2Ten: bariny2Ten,
  bariny2Hex: bariny2Hex,
  ten2Bariny: ten2Bariny,
  str2Hex: str2Hex,
  hex2bariny: hex2bariny,
  genRandom: genRandom,
  stringToHex: stringToHex,
  hexToString: hexCharCodeToStr,
  pad: pad
}

它里面包括了 string 转 字节数组等常用转换方法。


至于 ArrayBuffer  以及 DataView 等方法,相信各位也都知道,就不在这里做详解。不了解的可以在CSDN上搜索学习。


由于我在这里给后台需要传递的是一个JSON,所以我就将JSON转换为字符串。然后再将字符串转换为字节数组,传递给了消息。

ca7c4b69b3d6439fac9c019cd7475812.png


comm就是 上述封装的转化文件。comm.js  

stringToByteArray 方法就是 comm.js 文件中封装的 string转 字节数组的方法。

a098b942d3dd455fb182f4f6638c88b1.png


当我们完成这一切后使用“真机调试”前往对应刷卡器跟前刷卡即可!会发现前端已经成功了,并且读卡器也已经在控制台上打印出了我们传递的数据。

至此,我们的项目就结束了。


总结


以上就是今天分享给大家的微信小程序HCE模拟主机卡的功能。

但是其中不缺乏有一些坑。我列出来供大家参考!

1. wx.onHCEMessage 没有返回值。


这个可能是我见过最多的问题了,其实它没有返回值的原因就是,读卡器没有 返回信息。通俗点讲就是,你手机开启了NFC,当你挨着读卡器的时候,手机不知道你有没有挨上读卡器,所以需要读卡器给你说一句,你挨上我了,可以发消息了。所以这块的处理是需要对应的客户端开发来处理的,小程序端已经结束了。如果客户端开发不会的话,在此处放一个大佬的文章,可以让客户端开发参考下。微信小程序 NFC HCE卡模拟+AndroidNFC读取优必果


2. 我的手机刷卡没反应。是我哪里写错了吗?


如果你是按照上面的步骤同时结合官网文档来进行的,那么就基本没错。如果刷卡没反应,并且这时候客户端已经是处理好了的。那么只有一个可能,手机兼容性不够。你换个安卓手机试试。(PS:我试了 vivo跟华为。vivo 有两个手机不支持。华为目前使用的一款是支持的)、


3. aid_list 是什么,怎么找呢?


其实最简单的就是找你们客户端开发,他们有办法找到你的读卡器的 aid, 默认一般都是F222222222 。如果不是那就找一找你们的客户端开发,让他帮你找一下。


好了,文章就到这里。后面大家若是在开发过程有疑问,欢迎提问、私信我。

相关实践学习
DataV Board用户界面概览
本实验带领用户熟悉DataV Board这款可视化产品的用户界面
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
目录
相关文章
|
6月前
|
存储 缓存 JavaScript
自己动手做一个Server酱-cloudflare workers实现通过微信公众号发送消息
自己动手做一个Server酱-cloudflare workers实现通过微信公众号发送消息
356 0
PyWin32库操作微信自动发送消息
闲得无聊随便练练
136 0
|
开发者
手把手教你微信公众号如何给指定用户发送消息提醒
消息提醒功能是提升用户满意度的最有效方式,基于微信聊天的消息提醒也是现在最常见的消息提醒方式之一,
手把手教你微信公众号如何给指定用户发送消息提醒
|
测试技术
clswindow使用案例:控制电脑版微信发送消息(含源码)
clswindow使用案例:控制电脑版微信发送消息(含源码)
270 0
clswindow使用案例:控制电脑版微信发送消息(含源码)
|
Java 开发者 数据格式
微信企业号开发之发送消息问题
微信企业号开发之发送消息问题
微信企业号开发之发送消息问题
微信公众平台开发(12)--主动发送消息(客服消息)
本文目录 1. 前言 2. 代码 3. 测试 4. 发送其他类型消息 5. 小结
397 0
|
测试技术 Python
python骚操作,指定微信好友发送消息
指定好友发送消息 大家在测试的时候尽量使用微信小号 不要不改代码就测试 这里调用了金山词霸的每日一句,你也可以指定文字信息发送,七夕快到了,你懂得。 from threading import Timer from wxpy import * i...
1774 0
|
JSON 数据格式
.NET 往微信公众号 发送消息
public string gettocken()         {           string url = @"https://qyapi.
1130 0
|
新零售
2017微信数据报告 截至9月日登录用户超9亿日发送消息380亿次
  9日,在2017腾讯全球合作伙伴大会上,微信公布了《2017微信数据报告》。 微信今年9月平均日登陆用户达9.02亿,同比增长17%,其中55~70岁老年用户达到5000万人。微信用户日发送消息380亿次,同比增长25%;日发送语音61亿次,同比增长26%;日成功通话次数2.05亿次,同比增长106%。
1371 0
|
JSON 移动开发 Java
Java企业微信开发_04_消息推送之发送消息(主动)
源码请见: Java企业微信开发_00_源码及资源汇总贴 一、本节要点 1.发送消息与被动回复消息 (1)流程不同:发送消息是第三方服务器主动通知微信服务器向用户发消息。而被动回复消息是 用户发送消息之后,微信服务器将消息传递给 第三方服务器,第三方服务器接收到消息后,再对消息做出相应的回复消息。
1653 0