微信小程序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)