微信小程序NFC读写案例
·229 字·1 分钟阅读
小程序
使用到的工具函数
function ab2hex(buffer) {
return Array.prototype.map
.call(new Uint8Array(buffer), (b) => ("00" + b.toString(16)).slice(-2))
.join("")
.toUpperCase();
}
function ab2str(buffer) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
初始化NFC实例和监听标签
initNFC() {
// 1. 同步获取实例
const adapter = wx.getNFCAdapter();
// 2. 注册贴卡监听
adapter.onDiscovered((res) => {
console.log('标签ID:', ab2hex(res.id))
console.log('支持技术:', res.techs)
});
// 3. 开始扫描 —— 硬件检测在此处的 fail 回调中触发
adapter.startDiscovery({
success() {
console.log('NFC 可用,已开始扫描!')
},
fail(err) {
toast.error('NFC不可用,请检查设备!')
}
})
}
读取标签数据
NFC的数据会在onDiscovered回调参数中返回
所以只需要解析这个数据即可
// 2. 注册贴卡监听
adapter.onDiscovered((res) => {
console.log("标签ID:", ab2hex(res.id));
console.log(res.techs);
// 数据读取
if (res.messages && res.messages.length > 0) {
res.messages.forEach((msg, msgIndex) => {
(msg.records || []).forEach((record, recIndex) => {
const result = methods.parseNdefRecord(record);
console.log(`[消息${msgIndex} 记录${recIndex}]`);
console.log(" 类型:", result.type);
console.log(" 内容:", result.value);
});
});
} else {
console.log("标签支持NDEF但无数据,或为空标签");
}
});
解析函数如下:
// 文本解析
parseNdefText(payload) {
const bytes = new Uint8Array(payload);
const languageCodeLength = bytes[0] & 0x3f;
const isUtf16 = (bytes[0] & 0x80) !== 0;
const textBytes = bytes.slice(1 + languageCodeLength);
if (isUtf16) {
let str = "";
for (let i = 0; i < textBytes.length; i += 2) {
str += String.fromCharCode(
(textBytes[i] << 8) | textBytes[i + 1],
);
}
return str;
}
let str = "";
for (let i = 0; i < textBytes.length; i++) {
const byte = textBytes[i];
if (byte < 0x80) {
str += String.fromCharCode(byte);
} else if (byte < 0xc0) {
continue;
} else if (byte < 0xe0) {
str += String.fromCharCode(
((byte & 0x1f) << 6) | (textBytes[++i] & 0x3f),
);
} else if (byte < 0xf0) {
str += String.fromCharCode(
((byte & 0x0f) << 12) |
((textBytes[++i] & 0x3f) << 6) |
(textBytes[++i] & 0x3f),
);
} else {
const cp =
((byte & 0x07) << 18) |
((textBytes[++i] & 0x3f) << 12) |
((textBytes[++i] & 0x3f) << 6) |
(textBytes[++i] & 0x3f);
const offset = cp - 0x10000;
str += String.fromCharCode(
0xd800 + (offset >> 10),
0xdc00 + (offset & 0x3ff),
);
}
}
return str;
},
// URI 解析
parseNdefUri(payload) {
const bytes = new Uint8Array(payload);
const prefixCode = bytes[0];
const prefix = URI_PREFIXES[prefixCode] || "";
const uriBytes = bytes.slice(1);
return prefix + ab2str(uriBytes.buffer);
},
// 统一分发:根据 type 选择解析器
parseNdefRecord(record) {
const type = ab2str(record.type);
switch (type) {
case "T":
return {
type: "TEXT",
value: this.parseNdefText(record.payload),
};
case "U":
return {
type: "URL",
value: this.parseNdefUri(record.payload),
};
case "Sp":
return {
type: "smartposter",
value: this.parseSmartPoster(record.payload),
};
default:
// TNF=3 (Absolute URI) 时 type 本身就是 URI
if (record.tnf === 3) {
return { type: "absolute-uri", value: type };
}
// 其他类型直接转字符串
return {
type: type || "unknown",
value: ab2str(record.payload),
};
}
},
// Smart Poster 内含嵌套 NDEF 消息(通常包含 URI + Text 标题)
parseSmartPoster(payload) {
// Smart Poster 的 payload 是嵌套的 NDEF records
// 微信 API 通常会自动展开嵌套记录到 messages 中
// 如果没有自动展开,返回原始数据提示
return ab2str(payload);
},
export const URI_PREFIXES = [
"",
"http://www.",
"https://www.",
"http://",
"https://",
"tel:",
"mailto:",
"ftp://anonymous:anonymous@",
"ftp://ftp.",
"ftps://",
"sftp://",
"smb://",
"nfs://",
"ftp://",
"dav://",
"news:",
"telnet://",
"imap:",
"rtsp://",
"urn:",
"pop:",
"sip:",
"sips:",
"tftp:",
"btspp://",
"btl2cap://",
"btgoep://",
"tcpobex://",
"irdaobex://",
"file://",
"urn:epc:id:",
"urn:epc:tag:",
"urn:epc:pat:",
"urn:epc:raw:",
"urn:epc:",
"urn:nfc:",
];
写入标签数据
writeNdefText(adapter, text, language = "en") {
const ndef = adapter.getNdef();
ndef.connect({
success: () => {
console.log("NDEF 已连接,准备写入");
ndef.writeNdefMessage({
texts: [{ text, language }],
success() {
console.log("写入成功:", text);
toast.success("写入成功");
},
fail(err) {
console.log("写入失败:", err.errMsg);
toast.error("写入失败");
},
complete() {
// 写完断开连接
ndef.close({});
},
});
},
fail(err) {
console.log("连接失败:", err.errMsg);
toast.error("连接标签失败");
},
});
},
指令发送
/**
* 发送 APDU 指令
* @param {NfcA|IsoDep|...} tech - 标签技术实例
* @param {string} hexCmd - 十六进制指令,如 "00:A4:04:00:07:A0:00:00:00:62:03:01:00"
* @returns {Promise<string>} 响应数据的十六进制字符串
*/
sendCommand(tech, hexCmd) {
return new Promise((resolve, reject) => {
tech.transceive({
data: hex2ab(hexCmd),
success(res) {
const response = ab2hexColon(res.data);
console.log(`发送: ${hexCmd}`);
resolve(response);
},
fail(err) {
console.log(`指令失败: ${hexCmd}`, err.errMsg);
reject(err);
},
});
});
}
使用
adapter.onDiscovered((res) => {
console.log("标签ID:", ab2hex(res.id));
console.log(res.techs);
if (res.techs.includes(adapter.tech.nfcV)) {
const nfcV = adapter.getNfcV();
nfcV.connect({
success: async () => {
// 4. 发送指令
const resp = await methods.sendCommand(
nfcV,
state.nfcValue,
);
console.log("响应:", resp);
nfcV.close({});
},
fail(err) {
console.log("连接失败:", err.errMsg);
},
});
}
});
常用指令:
| hex | 说明 | 示例 |
|---|---|---|
| 0x20 | 读单个块 | 02:20:00(读块0) |
| 0x21 | 写单个块 | 02:21:00:01:02:03:04(写块0) |
| 0x23 | 连续读多块 | 02:23:00:03(从块0读4块) |
| 0x24 | 写多块 | 02:24:00:01:AA:BB:CC:DD:EE:FF:00:11 |
| 0x22 | 锁定块 | 02:22:00(锁块0) |
| 0x2B | 读系统信息(含UID/容量) | 02:2B |
提示:写多块的指令格式为[flags] [0x24] [起始块号] [块数量-1] [数据1] [数据2] ...
flags字节说明
位置在指令的第一个字节
| flags值 | 含义 |
|---|---|
| 0x00 | 低速、非寻址 |
| 0x02 | 高速、非寻址(推荐) |
| 0x20 | 低速、寻址(需带 UID) |
| 0x22 | 高速、寻址(需带 UID) |