ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android Java实现RFID扫描应用开发实战

Android Java实现RFID扫描应用开发实战 简介这是一套面向Android开发初学者与RFID应用实践者的完整开源项目源码聚焦于仓储物流场景下的物料全流程数字化管理。项目基于Java语言开发实现了盘询标签、物料入托、托盘/物料入库与出库等核心功能可直接用于中小型仓库的RFID终端系统快速验证与二次开发。资源共578个文件包含263个Java类承载业务逻辑、71个Java源文件含MaterialIncomingActivity、ProductOutActivity等关键页面、72个XML配置文件界面与权限定义、139个PNG资源图UI组件、16个JAR依赖包及1个可安装APK整体压缩包仅12.76MB轻量易部署。目前已有326人学习下载结构清晰含Eclipse项目配置.classpath/.project、模块化代码目录如ScanLablex、readme说明及编译产物dex、so、ap_等便于理解Android传统构建流程与RFID硬件集成逻辑。1. 这不是“写个App读卡”那么简单Java开发Android RFID扫描应用本质是串口/USB通信硬件抽象层适配实时数据流处理很多开发者看到“Android RFID扫描应用”第一反应是调个SDK、接个API、吐出UID就完事。但真实产线场景里你面对的可能是无厂商文档的国产UHF模块、需要手动配置跳频参数的ISO18000-6C读写器、或必须绕过Android USB权限模型直连CP2102芯片的定制设备。这类项目的核心矛盾从来不是“能不能读”而是“在Android Runtime约束下如何稳定、低延迟、可维护地把物理层信号变成业务层结构化数据”。本方案完全基于Java不依赖Kotlin或NDK聚焦Android 8.0–13主流版本覆盖USB Host模式OTG、串口通过CH340/CP2102转接、以及部分厂商提供的AIDL服务接口三种主流接入方式。适合有Java基础、熟悉Android Activity/Service生命周期、但对嵌入式通信协议不熟悉的中级开发者——你不需要懂RFID物理层调制但必须清楚readTag()调用背后发生了几次USB控制传输、缓冲区如何避免溢出、以及为什么onNewIntent()里解析Intent Extra比轮询更可靠。2. 从硬件连接到Java层抽象构建可插拔的RFID通信适配器架构2.1 为什么不能直接用厂商SDK——解耦硬件差异的必要性市面上主流RFID模块如Impinj Speedway、Zebra FX750、国产R2000芯片方案提供两种接入方式AIDL服务型厂商预装System App暴露Binder接口如IRFIDService需签名匹配且仅限系统级App调用USB/串口裸协议型设备以CDC ACM或自定义Vendor ID挂载需Java层实现协议解析如ISO15693指令集或自定义二进制帧。若直接耦合厂商SDK更换读写器时需重写80%业务逻辑。我们采用分层抽象// 定义统一接口屏蔽底层差异 public interface RfidReader { boolean connect(); // 启动连接USB权限申请/串口打开 void disconnect(); ListRfidTag readTags(int timeoutMs); // 返回标准Tag对象 void setPowerLevel(int dbm); // 统一功率控制 }提示RfidTag必须包含uid字节数组、epcEPC编码字符串、rssi信号强度、timestamp毫秒级时间戳四个字段这是后续库存盘点、防重读、移动轨迹分析的基础。2.2 USB Host模式下的权限与设备发现实战Android USB Host需动态申请权限并监听设备插拔事件。关键步骤如下2.2.1 在AndroidManifest.xml中声明USB权限和过滤器uses-feature android:nameandroid.hardware.usb.host / uses-permission android:nameandroid.permission.USB_PERMISSION / !-- 声明支持的Vendor ID/Product ID -- activity android:name.MainActivity intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / /intent-filter meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /activityres/xml/device_filter.xml内容需精确匹配目标设备?xml version1.0 encodingutf-8? resources !-- CP2102常见VID/PID -- usb-device vendor-id4292 product-id60000 / !-- CH340常见VID/PID -- usb-device vendor-id6790 product-id29987 / !-- 某UHF读写器自定义VID/PID -- usb-device vendor-id1234 product-id5678 / /resources2.2.2 Java层设备枚举与权限请求UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, UsbDevice deviceList usbManager.getDeviceList(); UsbDevice targetDevice null; for (UsbDevice device : deviceList.values()) { if (device.getVendorId() 4292 device.getProductId() 60000) { targetDevice device; break; } } if (targetDevice ! null) { PendingIntent permissionIntent PendingIntent.getBroadcast( this, 0, new Intent(ACTION_USB_PERMISSION), 0); usbManager.requestPermission(targetDevice, permissionIntent); // 触发系统弹窗 }注意requestPermission()后需在BroadcastReceiver中监听结果。若用户拒绝UsbDeviceConnection将为null后续bulkTransfer()必失败。务必在onReceive()中检查UsbManager.EXTRA_PERMISSION_GRANTED布尔值。2.3 串口通信的健壮性设计避免Buffer Overflow与超时阻塞当RFID模块通过CH340转接至手机OTG口时实际走的是串口协议。android-serialport-api库已过时我们直接使用UsbSerialDriver来自usb-serial-for-android// 初始化串口驱动需提前添加依赖 implementation com.github.mik3y:usb-serial-for-android:3.4.6 UsbSerialDriver driver drivers.get(0); // drivers由UsbSerialProber获取 UsbSerialPort port driver.getPorts().get(0); UsbSerialConnection connection new UsbSerialConnection(port); // 关键配置禁用流控、设置超时、启用接收缓冲区 connection.open(); port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); // 设置接收缓冲区大小默认256字节易丢包 port.setReadTimeout(500); // 单次read()最大等待500ms port.setWriteTimeout(100);提示RFID响应帧常含不定长EPC数据如EPC-96标准为12字节但厂商扩展可能达24字节。若read()返回字节数小于预期需循环读取直至收到完整帧头如0x02帧尾如0x03并用ByteBuffer动态扩容而非固定长度数组。3. RFID协议解析与标签数据标准化从原始字节流到业务可用对象3.1 解析ISO15693指令响应以读取单个标签为例多数中低端RFID模块采用ISO15693协议非NFC Forum标准其典型读取流程为发送0x20Inventory命令获取UID列表对每个UID发送0x21Read Single Block读取EPC区响应帧格式[STX][LEN][CMD][STATUS][DATA...][ETX]STX0x02, ETX0x03。Java解析代码需严格校验帧完整性private ListRfidTag parseInventoryResponse(byte[] rawBytes) { ListRfidTag tags new ArrayList(); ByteBuffer buffer ByteBuffer.wrap(rawBytes); while (buffer.hasRemaining()) { // 查找帧头0x02 if (buffer.get() ! 0x02) continue; // 读取长度字节后续len字节为有效载荷 int len buffer.get() 0xFF; if (len buffer.remaining() - 1) break; // 防止越界 byte[] payload new byte[len]; buffer.get(payload); // 校验帧尾0x03 if (buffer.get() ! 0x03) continue; // 解析payload前8字节为UIDISO15693 UID为8字节 byte[] uid Arrays.copyOf(payload, 8); String epc parseEpcFromPayload(payload); // 自定义EPC提取逻辑 tags.add(new RfidTag(uid, epc, calculateRssi(payload), System.currentTimeMillis())); } return tags; } private String parseEpcFromPayload(byte[] payload) { // EPC通常位于payload偏移10处长度12字节EPC-96 if (payload.length 22) { byte[] epcBytes Arrays.copyOfRange(payload, 10, 22); return HexDump.toHexString(epcBytes).toUpperCase(); // 转十六进制字符串 } return ; }注意HexDump.toHexString()来自android.util.HexDump无需额外依赖。若厂商使用自定义EPC编码如Base64需替换parseEpcFromPayload()实现。3.2 处理多标签并发读取的防重与去抖策略产线环境下同一时刻可能有数十张标签进入读取区域。原始readTags()返回的UID列表常含重复项因信号反射导致多次响应。必须在Java层做轻量级去重// 使用LinkedHashSet保持插入顺序同时去重 private ListRfidTag deduplicateTags(ListRfidTag rawTags) { MapString, RfidTag latestMap new LinkedHashMap(); for (RfidTag tag : rawTags) { String uidHex HexDump.toHexString(tag.getUid()).toUpperCase(); // 若UID已存在保留RSSI更高的版本信号更强更可信 RfidTag existing latestMap.get(uidHex); if (existing null || tag.getRssi() existing.getRssi()) { latestMap.put(uidHex, tag); } } return new ArrayList(latestMap.values()); } // 调用示例 ListRfidTag cleanTags deduplicateTags(parseInventoryResponse(rawBytes));提示单纯用Set去重会丢失顺序而产线常需按读取时间排序。LinkedHashMap兼顾顺序与O(1)查找比TreeSet更高效。3.3 RSSI值校准与温度补偿针对工业级UHF读写器高端UHF模块如Impinj提供原始RSSI值但Android端读取的数值受手机USB供电波动影响。需做两点校准基线校准空载时连续读取100次RSSI取中位数作为baselineRssi温度补偿若模块支持温度传感器通过0x1A指令读取按公式compensatedRssi rawRssi 0.1 * (currentTemp - 25)调整。// 获取温度并补偿示例 private int compensateRssi(int rawRssi, int tempCelsius) { final int BASELINE_TEMP 25; double delta tempCelsius - BASELINE_TEMP; return (int) Math.round(rawRssi 0.1 * delta); }注意温度补偿系数0.1需根据模块Datasheet实测确定不同芯片差异可达±0.3。4. Android UI与后台服务协同实现低功耗持续扫描与状态反馈4.1 使用Foreground Service规避Android 8.0后台限制从Android 8.0起隐式广播和后台Service被严格限制。RFID持续扫描必须使用前台Service// 在Service中启动前台通知 private void startForegroundService() { NotificationCompat.Builder builder new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(RFID扫描服务运行中) .setContentText(正在读取标签...) .setSmallIcon(R.drawable.ic_rfid) .setPriority(NotificationCompat.PRIORITY_LOW); // 必须创建NotificationChannelAndroid 8.0 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( CHANNEL_ID, RFID Service, NotificationManager.IMPORTANCE_LOW); NotificationManager manager getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } startForeground(1, builder.build()); }提示startForeground()必须在onStartCommand()内调用且Service需在AndroidManifest.xml中声明android:foregroundServiceTypespecialUseAndroid 12要求。4.2 RecyclerView高效渲染百级标签列表DiffUtil优化扫描结果常达50–200条/秒直接notifyDataSetChanged()会导致UI卡顿。使用DiffUtil实现增量更新public class TagDiffCallback extends DiffUtil.Callback { private final ListRfidTag oldList; private final ListRfidTag newList; public TagDiffCallback(ListRfidTag oldList, ListRfidTag newList) { this.oldList oldList; this.newList newList; } Override public int getOldListSize() { return oldList.size(); } Override public int getNewListSize() { return newList.size(); } Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).getUidHash() newList.get(newItemPosition).getUidHash(); } Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).equals(newList.get(newItemPosition)); } } // 在Adapter中触发更新 public void updateTags(ListRfidTag newTags) { ListRfidTag oldTags new ArrayList(currentTags); currentTags.clear(); currentTags.addAll(newTags); DiffUtil.DiffResult result DiffUtil.calculateDiff( new TagDiffCallback(oldTags, currentTags)); result.dispatchUpdatesTo(this); }注意getUidHash()需在RfidTag中重写返回Arrays.hashCode(uid)避免String转换开销。4.3 实时状态反馈用ProgressBar模拟扫描进度用Snackbar提示异常UI层需直观反映扫描状态。关键交互点扫描启动时显示旋转ProgressBar每秒统计标签数用TextView显示已读取: 127连接失败时用Snackbar提示具体原因如USB权限被拒绝请重新授权。// 在Activity中绑定Service回调 private RfidService.RfidCallback callback new RfidService.RfidCallback() { Override public void onConnected() { progressBar.setVisibility(View.VISIBLE); snackbar.setText(连接成功).show(); } Override public void onDisconnected(String reason) { progressBar.setVisibility(View.GONE); snackbar.setText(断开连接: reason).show(); } Override public void onTagsRead(ListRfidTag tags) { tagAdapter.updateTags(tags); tagCountView.setText(已读取: tags.size()); } };提示Snackbar需使用CoordinatorLayout作为父容器才能正确避让FloatingActionButton否则可能被遮挡。5. 生产环境排错与性能调优解决USB断连、内存泄漏、冷启动延迟三大痛点5.1 USB设备热插拔导致的Connection Null Pointer异常现象用户反复插拔OTG线UsbDeviceConnection.bulkTransfer()抛NullPointerException。根本原因是UsbDeviceConnection对象未及时释放新连接时旧引用仍存在。解决方案强制重置连接对象private void safeReconnect() { if (connection ! null) { connection.close(); // 显式关闭 connection null; } // 重新获取UsbDeviceConnection connection usbManager.openDevice(targetDevice); if (connection null) { Log.e(TAG, Failed to open USB device - check permissions); return; } // 重新配置端点 connection.claimInterface(port.getInterface(), true); }注意claimInterface()必须在openDevice()后立即调用否则bulkTransfer()会返回-1。5.2 避免Handler内存泄漏扫描线程与Activity生命周期解耦常见错误在Activity中创建Handler处理扫描结果Activity销毁后Handler仍持有引用导致内存泄漏。正确做法使用WeakReference 静态内部类private static class ScanResultHandler extends Handler { private final WeakReferenceMainActivity activityRef; ScanResultHandler(MainActivity activity) { this.activityRef new WeakReference(activity); } Override public void handleMessage(Message msg) { MainActivity activity activityRef.get(); if (activity null || activity.isFinishing() || activity.isDestroyed()) { return; // Activity已销毁不处理 } switch (msg.what) { case MSG_TAGS_READ: activity.updateUi((ListRfidTag) msg.obj); break; } } }提示Handler必须声明为static否则隐式持有外部类引用。WeakReference确保GC可回收Activity。5.3 冷启动扫描延迟优化预加载USB驱动与缓存协议解析器首次启动时从插上设备到读出第一个标签常耗时3秒。可通过两项优化压缩至800ms内优化项实现方式效果USB驱动预加载在Application.onCreate()中初始化UsbSerialDriver探针避免首次扫描时动态加载耗时协议解析器复用将parseInventoryResponse()改为静态方法复用ByteBuffer实例减少GC压力提升解析速度30%// Application类中预加载 public class RfidApplication extends Application { private UsbSerialDriver driverCache; Override public void onCreate() { super.onCreate(); // 启动后台线程预扫描USB设备 new Thread(() - { UsbSerialProber prober UsbSerialProber.getDefaultProber(); ListUsbSerialDriver drivers prober.findAllDrivers( (UsbManager) getSystemService(Context.USB_SERVICE)); if (!drivers.isEmpty()) { driverCache drivers.get(0); } }).start(); } }注意预加载不占用主线程但需确保UsbSerialProber在onCreate()中可用避免Context未初始化。5.4 关键参数速查表调试时优先检查的5个配置项当扫描失败时按此顺序排查90%问题源于以下配置参数位置典型值错误表现检查命令USB权限adb shell dumpsys usbgrantedtrueUsbManager.requestPermission()无响应adb shell dumpsys usb | grep granted串口波特率UsbSerialPort.setParameters()115200帧乱码、无法识别STX用串口调试助手验证硬件输出读取超时port.setReadTimeout()500read()阻塞、ANR日志中搜索read timeoutRSSI阈值RfidReader.setPowerLevel()-10dBm读取距离过短调高至0测试是否改善Foreground Service通道NotificationChannelIMPORTANCE_LOWService被系统杀死adb shell dumpsys notification | grep RFID提示adb shell dumpsys usb输出中若出现no permissions说明device_filter.xml的VID/PID与实际设备不符需用lsusb -v在Linux主机上确认真实值。用adb logcat | grep -i rfid实时捕获日志时重点关注UsbSerialPort类的read()返回值——若持续返回0说明硬件未发送数据若返回负数如-1代表USB传输错误需检查OTG线材质量。本文还有配套的精品资源点击获取
返回列表