更新记录

1.1.0(2026-09-11)

1.1.0(2026-09-08)

  • 适配普通 uni-app(vue3)项目,与 uni-app x 项目共用一套 API(通过条件编译自动区分按键监听方案)
  • 新增 requestPermission API:封装存储权限申请,Android 11+ 自动引导"所有文件访问"授权页
  • 新增 unlockBarcodeKey / lockBarcodeKey API:切换把枪按键归属,避免盘点与系统 iScan 扫码同时触发
  • 修复:SDK 实例创建移至主线程执行,解决非 Looper 线程初始化抛出 Can't create handler 导致初始化失败的问题
  • 优化:初始化、盘点各失败分支输出日志(logcat 过滤 UHFNative),便于真机排查

1.0.0(2026-09-07)

  • 首次发布
  • 支持 UHF 模块初始化 / 释放、功率设置、标签盘点、标签回调、硬件把枪按键监听
  • 支持模块型号:UM / SLR / RM / GX / YRM

平台兼容性

uni-app(5.14)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - - - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(5.14)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - - - -

uhf-rfid-scanner

iData 手持机 UHF RFID 盘点插件(Android 原生 UTS 插件),基于 iData UHF SDK(UHFJar V1.4.06)封装,一套 API 同时适配 uni-app xuni-app(vue3) 项目。

功能特性

  • UHF 模块初始化 / 释放,支持 UM / SLR / RM / GX / YRM 五种模块型号
  • 标签盘点(开始 / 停止),通过回调实时返回 EPC、RSSI、DATA、EXTRA 数据
  • 发射功率设置
  • 硬件把枪按键监听(按键触发盘点,拦截系统 iScan 扫码服务)
  • 把枪按键归属切换(盘点期间禁用扫码,退出后自动恢复系统扫码)
  • 存储权限申请封装(含 Android 11+ "所有文件访问" 权限引导)
  • SDK 实例创建自动调度至主线程(规避 Looper 线程限制),调用方无需关心线程问题

平台兼容性

平台 支持情况
App(Android) √(Android 6.0+,仅支持 iData 系列 UHF 手持机)
App(iOS) x
App(Harmony) x
Web / 各小程序 x
HBuilderX 最低版本 4.25

本插件依赖 iData 设备 UHF 硬件,在其他品牌设备上无法完成初始化。

插件目录结构

uni_modules/uhf-rfid-scanner
├── readme.md                        插件文档(本文件)
├── changelog.md                     更新日志
├── package.json                     插件配置
└── utssdk
    ├── interface.uts                统一接口定义(API 类型、数据结构、错误码)
    ├── unierror.uts                 错误码定义
    └── app-android                  Android 平台实现
        ├── index.uts                平台胶水层(双端适配入口)
        ├── config.json              abi(armeabi-v7a / arm64-v8a)、minSdkVersion 23
        ├── UHFNative.kt             原生混编实现(Kotlin)
        └── libs
            └── UHFJar_V1.4.06.aar   iData UHF SDK

快速开始

1. 配置权限(必须)

本插件依赖存储权限完成 UHF 模块串口初始化,需在项目 manifest.json 源码视图中声明:

uni-app x 项目app-android 节点):

"app-android": {
  "distribute": {
    "permissions": [
      "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
      "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
      "<uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>"
    ]
  }
}

uni-app 项目app-plus 节点):

"app-plus": {
  "distribute": {
    "android": {
      "permissions": [
        "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
        "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
        "<uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>"
      ]
    }
  }
}

2. 制作自定义基座

本插件为 UTS 原生插件,标准基座无法运行,请通过 HBuilderX「运行 → 运行到手机或模拟器 → 制作自定义调试基座」打包。

  • 首次打包耗时较长(需编译原生 Kotlin 代码)属正常现象
  • 修改 manifest 权限或插件原生代码后,必须重新制作基座;若 HBuilderX 提示"基座已是最新版本"但代码未生效,请修改 manifest.json 中的 versionCode 强制更新

3. 导入插件

// uni-app x(.uvue,注意 UTS 类型)
import { initialize, onTag, startInventory } from "@/uni_modules/uhf-rfid-scanner";
// uni-app(.vue,普通 JS)
import { initialize, onTag, startInventory } from '@/uni_modules/uhf-rfid-scanner';

仅支持导入插件根目录,不支持 @/uni_modules/uhf-rfid-scanner/index.uts 这类内部路径。

API 参考

initialize(options) : boolean

初始化 UHF 模块并上电。

参数 类型 必填 说明
options.moduleType UHFModuleType 模块型号:"UM_MODULE" | "SLR_MODULE" | "RM_MODULE" | "GX_MODULE" | "YRM_MODULE"
options.power number 初始化后设置的功率,如 30
  • 返回 true 表示初始化成功;失败时可通过 adb logcat -s UHFNative 查看具体原因
  • 耗时操作:uni-app x 项目建议在 IO 线程调用(见下方完整示例);uni-app 项目可直接调用
  • 模块型号选择:SLR_MODULE 为 iData 50/60 等常见把枪设备型号,具体以设备规格为准
const ok = initialize({ moduleType: "SLR_MODULE", power: 30 });

setPower(power) : boolean

设置发射功率,需在 initialize 成功后调用。

参数 类型 必填 说明
power number 功率值,不同机型上限不同(常见 5~33)
setPower(30);

startInventory() : boolean

开始盘点。成功后标签数据持续通过 onTag 回调返回。重复调用幂等(已在盘点中返回 true)。

startInventory();

stopInventory() : boolean

停止盘点。

stopInventory();

isInitialized() : boolean

判断模块是否已初始化(且未释放)。

if (isInitialized()) {
  startInventory();
}

onTag(callback) / offTag(callback)

注册 / 注销标签数据回调。

参数 类型 必填 说明
callback (tag: UHFTag) => void 标签回调;注销时传 null 可清除全部回调
tag UHFTag - { epc: string, rssi: string, data: string, extra: string, receivedAt: number }

注意事项:

  • 盘点期间回调触发频率高(每秒可达数十次),请勿在回调中执行同步 IO 或复杂 UI 操作
  • 重复注册同一个函数会自动替换旧回调,不会叠加;onTag 回调可能来自非 UI 线程,更新 UI 前请自行切线程(uni-app x 示例见下)
  • receivedAt 为标签读取时刻的毫秒时间戳
const tagHandler = function (tag: UHFTag): void {
  console.log(`EPC: ${tag.epc}, RSSI: ${tag.rssi}, 时间: ${tag.receivedAt}`);
};

onTag(tagHandler);    // 注册
offTag(tagHandler);   // 注销指定回调
offTag(null);         // 清除全部回调

onHardwareKey(callback) / offHardwareKey()

监听 / 取消监听设备把枪按键。回调参数为 Android 键值(number / Int)。

插件识别的触发键列表:

按键 keyCode
KEYCODE_F8 134
KEYCODE_F4 131
KEYCODE_BUTTON_4 310
KEYCODE_BUTTON_3 309
KEYCODE_PROG_RED 286
  • 长按只触发一次(忽略 repeat 事件)
  • 触发键按下时 App 层会拦截事件,但仍需配合 unlockBarcodeKey() 释放系统 iScan 服务的按键归属(见下)
onHardwareKey((keyCode) => {
  // 典型用法:按键切换盘点状态
});
offHardwareKey(); // 取消监听

unlockBarcodeKey() / lockBarcodeKey()

把枪按键归属切换:

  • unlockBarcodeKey():发送系统广播 android.intent.action.BARCODEUNLOCKSCANKEY,通知系统 iScan 扫码服务释放把枪按键(按键不再触发扫码/亮灯),由 App 接收按键事件。建议在页面 onShow / onMounted 时调用
  • lockBarcodeKey():发送广播 android.intent.action.BARCODELOCKSCANKEY,恢复系统 iScan 扫码服务接管按键。建议在页面 onHide / onUnmounted、或调用 destroy() 前执行

不调用 unlockBarcodeKey() 时,把枪按键会导致系统扫码与 RFID 盘点同时触发。

requestPermission(callback) : void

申请插件所需的存储权限(一次性封装,内部已做已授权检查):

  • Android 11+:检查"所有文件访问"(MANAGE_EXTERNAL_STORAGE)权限,未授予时自动跳转系统设置页,用户开启后返回需重新调用 initialize
  • Android 6.0 ~ 10:弹出运行时权限申请(READ/WRITE_EXTERNAL_STORAGE)
参数 类型 必填 说明
callback (granted: boolean) => void 授权结果,true 表示可继续初始化
requestPermission((granted) => {
  if (granted) {
    initialize({ moduleType: "SLR_MODULE" });
  }
});

destroy() : void

释放设备:停止盘点、模块下电、注销标签回调、按键监听并恢复系统扫码。页面卸载或退出前必须调用,否则设备串口将保持占用,其他应用无法使用 UHF。

destroy();

完整示例

典型使用流程

requestPermission(申请权限)
        ↓ granted === true
initialize(初始化模块,成功后 onHardwareKey / onTag 生效)
        ↓
unlockBarcodeKey(接管把枪按键,系统扫码不再响应)
        ↓
onTag 回调持续返回标签 → startInventory / stopInventory 切换盘点
        ↓ 页面退出
lockBarcodeKey(恢复系统扫码)→ offTag / offHardwareKey / destroy(释放设备)

调用时序要求:

  1. initialize 必须在 requestPermission 授权成功之后调用;
  2. startInventory 必须在 initialize 成功之后调用;
  3. onTag 建议在开始盘点前注册、停止盘点后注销,避免无谓回调;
  4. 页面卸载前必须 destroy(),否则设备串口持续被占用;
  5. unlockBarcodeKey / lockBarcodeKey 必须成对使用,退出时忘记 lockBarcodeKey 会导致把枪键一直无法扫码。

示例一:uni-app x 项目(.uvue,组合式 API)

新建 pages/rfid/rfid.uvue,并在 pages.json 中注册:

{
  "path": "pages/rfid/rfid",
  "style": {
    "navigationBarTitleText": "UHF RFID 盘点"
  }
}

页面完整代码(可直接复制运行,功能:权限申请 → 初始化 → 盘点 → 把枪键触发 → EPC 去重计数 → 释放):

<template>
  <view class="page">
    <text class="status">{{ status }}</text>
    <button class="btn" :disabled="isIniting" @click="initDevice">
      {{ isIniting ? "初始化中..." : "初始化" }}
    </button>
    <button class="btn" :disabled="!ready" @click="toggleReading">
      {{ reading ? "停止盘点" : "开始盘点" }}
    </button>
    <button class="btn" :disabled="!ready" @click="releaseDevice">释放设备</button>
    <text class="summary">共 {{ list.length }} 种标签,读取 {{ total }} 次</text>
    <view v-for="(item, index) in list" :key="item.epc" class="row">
      <text class="row-index">{{ index + 1 }}</text>
      <text class="row-epc">{{ item.epc }}</text>
      <text class="row-count">{{ item.count }}</text>
      <text class="row-rssi">{{ item.rssi }}</text>
    </view>
  </view>
</template>

<script setup lang="uts">
import {
  requestPermission, initialize, isInitialized,
  onTag, offTag, startInventory, stopInventory,
  onHardwareKey, offHardwareKey,
  unlockBarcodeKey, lockBarcodeKey, destroy, UHFTag
} from "@/uni_modules/uhf-rfid-scanner";

type TagItem = { epc: string, count: number, rssi: string };

const status = ref("未初始化");
const isIniting = ref(false);
const reading = ref(false);
const ready = ref(false);
const list = ref<Array<TagItem>>([]);
const total = ref(0);

function updateTag(tag: UHFTag): void {
  // 回调来自非 UI 线程,更新 UI 需切回主线程
  UTSAndroid.getDispatcher("main").async(function (_: Any | null): void {
    total.value += 1;
    const idx = list.value.findIndex((item: TagItem): boolean => item.epc == tag.epc);
    if (idx >= 0) {
      list.value[idx].count += 1;
      list.value[idx].rssi = tag.rssi;
    } else {
      list.value.push({ epc: tag.epc, count: 1, rssi: tag.rssi } as TagItem);
    }
  }, null);
}

const tagHandler = function (tag: UHFTag): void { updateTag(tag); };

function toggleReading(): void {
  if (reading.value) {
    stopInventory();
    offTag(tagHandler);
    reading.value = false;
  } else {
    onTag(tagHandler);
    reading.value = startInventory();
  }
}

function initDevice(): void {
  isIniting.value = true;
  requestPermission(function (granted: boolean): void {
    if (!granted) {
      isIniting.value = false;
      status.value = "缺少存储权限";
      return;
    }
    UTSAndroid.getDispatcher("io").async(function (_: Any | null): void {
      const ok = initialize({ moduleType: "SLR_MODULE" });
      UTSAndroid.getDispatcher("main").async(function (_: Any | null): void {
        isIniting.value = false;
        ready.value = ok;
        status.value = ok ? "设备已就绪" : "初始化失败";
      }, null);
    }, null);
  });
}

function handleKey(_keyCode: number): void {
  toggleReading();
}

function releaseDevice(): void {
  lockBarcodeKey();     // 恢复系统扫码
  offHardwareKey();
  offTag(tagHandler);
  destroy();
  reading.value = false;
  ready.value = false;
  status.value = "设备已释放";
}

onMounted(() => {
  onHardwareKey(handleKey);
  unlockBarcodeKey();   // 接管按键,禁止按键触发扫码
});

onUnmounted(() => {
  lockBarcodeKey();     // 恢复系统扫码
  offHardwareKey();
  offTag(tagHandler);
  destroy();
});
</script>

<style>
.page {
  display: flex;
  flex-direction: column;
  padding: 24px;
  background-color: #f4f6f8;
}

.status {
  font-size: 16px;
  color: #5f6b7a;
  margin-bottom: 12px;
}

.btn {
  height: 44px;
  margin-bottom: 12px;
  background-color: #126b70;
  color: #ffffff;
}

.summary {
  font-size: 13px;
  color: #6d7784;
  margin: 12px 0;
}

.row {
  display: flex;
  flex-direction: row;
  min-height: 40px;
  background-color: #ffffff;
  align-items: center;
}

.row-index {
  width: 12%;
  text-align: center;
  font-size: 12px;
}

.row-epc {
  width: 50%;
  font-size: 12px;
}

.row-count {
  width: 18%;
  text-align: center;
  font-size: 12px;
  color: #126b70;
}

.row-rssi {
  width: 20%;
  text-align: center;
  font-size: 12px;
}
</style>

示例二:uni-app 项目(.vue,vue3 选项式 API)

新建 pages/rfid/rfid.vuepages.json 注册方式与示例一相同。与 uni-app x 的差异:页面为 .vue、无 UTS 类型注解、回调直接更新 data(框架已桥接线程)、生命周期用 onLoad / onUnloadinitializesetTimeout 避免阻塞 UI。

<template>
  <view class="page">
    <text class="status">{{ status }}</text>
    <button class="btn" :disabled="isIniting" @click="initDevice">
      {{ isIniting ? '初始化中...' : '初始化' }}
    </button>
    <button class="btn" :disabled="!ready" @click="toggleReading">
      {{ isReading ? '停止盘点' : '开始盘点' }}
    </button>
    <button class="btn" :disabled="!ready" @click="releaseDevice">释放设备</button>
    <text class="summary">共 {{ tagList.length }} 种标签,读取 {{ totalReadCount }} 次</text>
    <view v-for="(item, index) in tagList" :key="item.epc" class="row">
      <text class="row-index">{{ index + 1 }}</text>
      <text class="row-epc">{{ item.epc }}</text>
      <text class="row-count">{{ item.count }}</text>
      <text class="row-rssi">{{ item.rssi }}</text>
    </view>
  </view>
</template>

<script>
import {
  requestPermission, initialize, isInitialized,
  onTag, offTag, startInventory, stopInventory,
  onHardwareKey, offHardwareKey,
  unlockBarcodeKey, lockBarcodeKey, destroy
} from '@/uni_modules/uhf-rfid-scanner';

export default {
  data() {
    return {
      status: '未初始化',
      isIniting: false,
      isReading: false,
      ready: false,
      tagList: [],
      totalReadCount: 0
    };
  },
  onLoad() {
    onTag(this.handleTag);              // tag 结构 { epc, rssi, data, extra, receivedAt }
    onHardwareKey(this.handleHardwareKey);
    unlockBarcodeKey();                 // 接管把枪按键,禁止按键触发扫码
  },
  onUnload() {
    lockBarcodeKey();                   // 恢复系统扫码
    offHardwareKey();
    offTag(this.handleTag);
    destroy();
  },
  methods: {
    handleTag(tag) {
      this.totalReadCount += 1;
      const index = this.tagList.findIndex((item) => item.epc === tag.epc);
      if (index >= 0) {
        this.tagList[index].count += 1;
        this.tagList[index].rssi = tag.rssi;
      } else {
        this.tagList.push({ epc: tag.epc, count: 1, rssi: tag.rssi });
      }
    },
    handleHardwareKey() {
      this.toggleReading();
    },
    initDevice() {
      if (this.isIniting) return;
      this.isIniting = true;
      this.status = '正在检查存储权限...';
      requestPermission((granted) => {
        if (!granted) {
          this.isIniting = false;
          this.status = '缺少存储权限,请在系统设置中授权后重新初始化';
          return;
        }
        this.status = '正在初始化设备...';
        // initialize 为耗时串口操作,延迟执行避免阻塞 UI
        setTimeout(() => {
          const ok = initialize({ moduleType: 'SLR_MODULE' });
          this.isIniting = false;
          this.isReading = false;
          this.ready = ok;
          this.status = ok ? '设备已就绪' : '初始化失败';
        }, 50);
      });
    },
    toggleReading() {
      if (!isInitialized()) {
        this.status = '请先初始化设备';
        return;
      }
      if (this.isReading) {
        stopInventory();
        this.isReading = false;
      } else {
        this.isReading = startInventory();
      }
    },
    releaseDevice() {
      lockBarcodeKey();
      offHardwareKey();
      offTag(this.handleTag);
      destroy();
      this.isReading = false;
      this.ready = false;
      this.status = '设备已释放';
    }
  }
};
</script>

<style>
.page {
  padding: 24rpx;
  background-color: #f4f6f8;
  min-height: 100vh;
  box-sizing: border-box;
}

.status {
  font-size: 28rpx;
  color: #5f6b7a;
  margin-bottom: 20rpx;
}

.btn {
  margin-bottom: 20rpx;
  background-color: #126b70;
  color: #ffffff;
}

.summary {
  font-size: 24rpx;
  color: #6d7784;
  margin: 16rpx 0;
}

.row {
  display: flex;
  flex-direction: row;
  min-height: 72rpx;
  background-color: #ffffff;
  align-items: center;
  border-bottom: 1rpx solid #edf0f2;
}

.row-index {
  width: 12%;
  text-align: center;
  font-size: 24rpx;
}

.row-epc {
  width: 50%;
  font-size: 24rpx;
}

.row-count {
  width: 18%;
  text-align: center;
  font-size: 24rpx;
  color: #126b70;
}

.row-rssi {
  width: 20%;
  text-align: center;
  font-size: 24rpx;
}
</style>

错误码

错误码 说明
9010101 UHF 模块未初始化
9010102 不支持的 UHF 模块型号
9010103 UHF 模块上电失败
9010104 启动盘点失败

常见问题(FAQ)

初始化失败如何排查?

  1. 确认已配置 manifest 权限并重新打包了自定义基座;
  2. 确认设备为 iData UHF 手持机、模块型号选择正确;
  3. 通过 adb 过滤插件日志定位失败环节:adb logcat -s UHFNative,日志会明确输出是实例创建、上电(powerOn)还是功率设置(powerSet)失败及异常堆栈。

Android 11+ 授权"所有文件访问"后仍初始化失败?

授权页返回 App 后,权限不会自动生效到已结束的初始化流程,需重新点击初始化。

按把枪键同时触发了扫码和盘点?

调用 unlockBarcodeKey() 接管按键;页面关闭或释放设备时调用 lockBarcodeKey() 恢复系统扫码。

标签回调里 UI 没刷新?

标签回调可能来自原生轮询线程:uni-app x 项目更新响应式数据需切回主线程(见完整示例);uni-app 项目框架已做桥接,可直接更新 data。

修改权限 / 原生代码后运行不生效?

必须重新制作自定义基座。HBuilderX 按版本号判断基座是否更新,必要时提升 manifest.jsonversionCode 后重新运行。

同一 EPC 会重复回调吗?

会。盘点模式下每读取到一次标签即回调一次(含重复标签),业务侧通常按 EPC 去重计数(见完整示例的 count 逻辑)。

运行环境要求

  • HBuilderX 4.25 及以上
  • Android 6.0+(minSdkVersion 23,abi:armeabi-v7a / arm64-v8a)
  • iData 系列 UHF 手持机(SDK:UHFJar V1.4.06)

许可

仅供合法授权的资产管理、仓储盘点等业务场景使用。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>", "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>", "<uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>"

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。