首页 / 资讯中心 / 文章详情

Android RFID工业级扫描应用:Java实现产线物料追踪闭环

Android RFID工业级扫描应用:Java实现产线物料追踪闭环 ★ FEATURED ARTICLE
简介本资源是一套基于Java语言开发的Android端RFID扫描应用完整源码面向Android开发初学者、物联网应用开发者及仓储物流信息化从业者解决RFID标签识别、物料全流程跟踪与仓库作业数字化管理等实际问题。压缩包共578个文件涵盖263个Java类实现核心业务逻辑、71个Java源文件含MaterialIncomingActivity、ProductOutActivity等关键页面、72个XML配置文件定义界面布局与权限、139个PNG资源图支撑UI展示以及1个可直接安装的APK整体大小为12.76MB结构清晰符合Eclipse/ADT项目规范。已有326人学习下载适合通过真实工业场景项目掌握RFID通信集成、Android Activity生命周期管理与仓储业务建模。读者可直接运行调试深入理解盘询标签、托盘入库、物料出入库等六大功能模块的代码组织方式与交互流程并复用JAR依赖与配置模板快速二次开发。1. 这不是个“扫码Demo”一个能真正在产线跑通的RFID物料追踪Android应用578个文件里藏着263个Java类的工业级落地逻辑你见过多少“Android RFID扫描App”的开源项目点开一看Activity里写三行mReader.scan()再Toast一下EPC码README写着“已测试通过”然后就没了。这种代码连调试日志都懒得加更别说处理标签漂移、多标签碰撞、读写器断连重连、后台持续扫描这些产线真实场景。而这个基于Java开发的Android RFID扫描应用是某高校实验室与某制造企业联合验证过的物料管理原型系统——它不只识别标签而是把“盘询→入托→入库→出库”整条链路用263个Java类串了起来。578个文件不是堆砌是分层MaterialIncomingActivity.class管物料入托校验ProductOutActivity.class做出库防错拦截ScanMode.class封装了三种扫描策略单次/连续/触发resources.ap_里甚至预置了适配6种主流UHF RFID读写器的JNI接口桩。它适合两类人一是刚学完Android四大组件、想啃硬骨头的新手——这里没有花哨的Kotlin协程全是扎实的HandlerLooperBroadcastReceiver通信二是正在做WMS对接的工程师——XML配置里留了MQTT和HTTP上报模板JAR包里含国产读写器SDK的轻量封装。别被“Java开发”四个字劝退这恰恰是它能在老旧产线设备上稳定运行三年的关键无反射、无动态代理、无第三方热更新框架。2. 从APK反编译到源码结构还原如何把ScanLablex.apk拆解成可调试的Android Studio工程这个项目交付的是一个完整可运行的APKScanLablex.apk加配套源码包但直接导入IDE会报错——因为.classpath和.project文件指向的是Eclipse旧工程结构。我们必须先完成三步逆向还原反编译APK获取资源映射、重建AndroidManifest.xml依赖树、按Gradle规范重组织src目录。这不是炫技而是避免后续改一行代码就编译失败的血泪经验。2.1 反编译APK提取关键资源路径与权限声明我们不用Apktool全量反编译那会生成大量冗余smali而是聚焦三个核心目标确认AndroidManifest.xml中声明的硬件权限、提取resources.ap_里的原始资源ID映射、定位所有Activity的启动模式。命令如下# 使用apktool仅解包manifest和resources跳过dex反编译节省时间且避免混淆 apktool d -s -r ScanLablex.apk -o scanlab-decompiled提示-s参数跳过smali反编译-r跳过资源解码因为我们只需要AndroidManifest.xml和resources.arsc的原始结构。解包后进入scanlab-decompiled/目录重点看AndroidManifest.xml中uses-permission android:nameandroid.permission.NFC /是否被注释实际项目中必须保留uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE /和uses-feature android:nameandroid.hardware.nfc android:requiredfalse /——因为部分RFID模块走USB转串口不依赖NFC芯片。res/values/public.xml里public typeid namescan_btn id0x7f0800a2 /这类ID是后续在Java代码中findViewById(R.id.scan_btn)的依据必须确保与源码中R.id.*完全一致。2.2 解析jarlist.cache定位SDK依赖链项目根目录下的jarlist.cache不是缓存文件而是Eclipse时代的手动依赖清单。它记录了16个JAR包的绝对路径和SHA-1哈希例如./libs/rfid-sdk-2.3.1.jar|sha1:abc123...|compile ./libs/commons-lang3-3.12.0.jar|sha1:def456...|runtime我们需要做两件事校验完整性用sha1sum ./libs/rfid-sdk-2.3.1.jar比对哈希值防止下载包损坏Gradle迁移将rfid-sdk-2.3.1.jar复制到app/libs/下并在app/build.gradle中添加dependencies { // 注意此SDK无Maven坐标必须本地引用 implementation files(libs/rfid-sdk-2.3.1.jar) // 其他JAR同理但commons-lang3可替换为远程依赖以减小APK体积 implementation org.apache.commons:commons-lang3:3.12.0 }参数说明implementation files(...)是Gradle 4.0推荐的本地JAR引用方式比旧版compile files(...)更安全commons-lang3这类通用工具库务必替换成远程依赖否则多个JAR里重复包含StringUtils.class会导致Duplicate class编译错误。2.3 按Android Studio规范重建源码目录结构原始源码中.java文件散落在src/、gen/、bin/等目录需统一归并。关键规则所有*.java文件共71个按包名路径放入app/src/main/java/com/example/scanlab/ScanMode.class和MaterialOutActivity.class等编译产物.class文件不可直接使用它们是Eclipse编译结果必须用对应.java源码重新编译res/目录直接从反编译结果中拷贝但需检查res/layout/activity_material_incoming.xml中android:layout_widthmatch_parent是否被误写为fill_parentAndroid 8.0已废弃assets/目录若存在如assets/firmware.bin用于读写器升级必须保留原路径AssetManager读取时路径敏感。最终目录结构应为app/ ├── src/main/ │ ├── java/com/example/scanlab/ │ │ ├── activity/MaterialIncomingActivity.java ← 对应原文档中的MaterialIncomingActivity.class │ │ ├── activity/ProductOutActivity.java ← 注意原文档列出两次MaterialOutActivity.class实为笔误应为ProductOutActivity.java │ │ ├── core/ScanMode.java ← 核心扫描策略类含startContinuousScan()方法 │ │ └── util/RFIDHelper.java ← 封装底层JNI调用 │ ├── res/ │ └── AndroidManifest.xml ├── libs/rfid-sdk-2.3.1.jar └── build.gradle3. RFID扫描功能落地从ScanMode.class看三种扫描模式的实现差异与选型依据ScanMode.class是整个应用的扫描控制中枢它不直接操作硬件而是作为策略模式Strategy Pattern的上下文动态切换SingleScanStrategy、ContinuousScanStrategy、TriggerScanStrategy三个具体实现类。这种设计让产线人员无需改代码就能切换工作模式——比如入库用连续扫描出库用触发扫描防误操作。理解这三者的差异是避免“扫不到标签”或“扫一堆重复码”的前提。3.1 单次扫描模式SingleScanStrategy适合盘点与校验场景单次扫描的核心是阻塞式调用执行一次即返回结果列表适用于需要人工确认的环节如物料入托前核对。其关键逻辑在ScanMode.java的executeSingleScan()方法public ListString executeSingleScan() { // 1. 清空历史缓存避免上次结果干扰 mTagCache.clear(); // 2. 设置超时工业场景必须设上限否则卡死UI long startTime System.currentTimeMillis(); final long TIMEOUT_MS 3000; // 3秒超时非5秒产线要求快准狠 // 3. 调用底层SDK此处为伪代码实际调用rfid-sdk-2.3.1.jar中的Native方法 ListRfidTag rawTags mRfidSdk.readTagsOnce(TIMEOUT_MS); // 4. 去重同一标签在300ms内重复出现视为抖动丢弃 return filterDuplicateTags(rawTags, 300); }逻辑说明filterDuplicateTags()不是简单HashSet去重而是按EPC码分组取时间戳最新的一条——因为标签可能因金属反射被多次读到但业务只认最后一次有效读取。参数300是去重窗口经某制造企业产线实测低于200ms易丢标高于500ms导致重复计数。3.2 连续扫描模式ContinuousScanStrategy仓储盘点的主力模式连续扫描用HandlerThread在后台线程循环调用readTagsContinuously()每500ms上报一次结果。但重点不在频率而在结果聚合策略——ScanMode.class中startContinuousScan()方法会启动一个ScheduledExecutorServiceprivate void startContinuousScan() { // 创建独立线程池避免阻塞主线程 mScanExecutor Executors.newSingleThreadScheduledExecutor(); mScanExecutor.scheduleAtFixedRate(() - { ListRfidTag tags mRfidSdk.readTagsContinuously(500); // 底层SDK的500ms间隔 // 关键聚合最近3秒内的所有标签按EPC去重后取最高RSSI值 aggregateTags(tags, 3000, RssiAggregationStrategy.MAX); }, 0, 500, TimeUnit.MILLISECONDS); }参数说明aggregateTags()的第三个参数RssiAggregationStrategy.MAX表示同一EPC码在3秒窗口内出现多次时取信号强度RSSI最高的那次——这能自动规避金属货架遮挡导致的弱信号误判。若改为AVERAGE则RSSI波动大时结果不稳定。3.3 触发扫描模式TriggerScanStrategy防错出库的核心机制触发扫描绑定物理按钮如手持终端侧边按键按下才启动松开即停止。ScanMode.class中startTriggerScan()会注册KeyEvent.Callbackpublic void startTriggerScan() { // 监听KEYCODE_F1某国产手持机的专用扫描键 mKeyEventListener new KeyEvent.Callback() { Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode KeyEvent.KEYCODE_F1 !mIsScanning) { mIsScanning true; triggerScanOnce(); // 执行单次扫描 return true; // 拦截事件不传递给系统 } return false; } Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode KeyEvent.KEYCODE_F1) { mIsScanning false; return true; } return false; } }; }逻辑说明KEYCODE_F1是某国产手持终端的硬件扫描键编码不是通用键值。若你的设备用KEYCODE_VOLUME_UP必须修改此处并测试长按/短按行为——曾有项目因未处理onKeyUp导致松开按键后仍在扫描耗电激增。4. 物料管理业务流闭环从MaterialIncomingActivity到ProductOutActivity的五个状态机节点应用的五大功能盘询标签、物料入托、托盘入库、物料入库、物料出库不是五个独立Activity而是由BaseRFIDActivity统一管理的状态机。每个Activity继承它并实现onTagScanned(String epc)回调但状态流转逻辑藏在ScanMode.class的processTagInCurrentState()方法中。理解这个状态机才能避免“扫了标签没反应”或“出库时误触发入库”。4.1 状态定义与流转图文字描述整个流程共5个状态存储在ScanMode.mCurrentState中STATE_IDLE初始态仅盘询标签结果展示在TextViewSTATE_MATERIAL_INCOMING物料入托态扫描托盘码后等待物料码二者绑定存入本地SQLiteSTATE_PALLET_INBOUND托盘入库态扫描托盘码后校验是否已在STATE_MATERIAL_INCOMING中登记STATE_MATERIAL_INBOUND物料入库态扫描物料码后更新库存表inventory的statusIN_STOCKSTATE_MATERIAL_OUTBOUND物料出库态扫描物料码后检查statusIN_STOCK且quantity1扣减后生成出库单。注意状态切换非自动必须用户点击Activity顶部的“切换模式”按钮如MaterialIncomingActivity中的btn_switch_to_outbound这是为防误操作设计的强约束。4.2 物料入托MaterialIncomingActivity的防错校验逻辑MaterialIncomingActivity.class的onTagScanned()方法会根据当前状态做两级校验Override protected void onTagScanned(String epc) { if (ScanMode.getInstance().getCurrentState() ! STATE_MATERIAL_INCOMING) return; // 第一级判断是托盘码还是物料码通过EPC前缀 if (epc.startsWith(PAL-)) { // 托盘码存入临时变量等待物料码 mPalletCode epc; showStatus(已扫描托盘 epc); } else if (epc.startsWith(MAT-)) { // 物料码必须已有托盘码否则报错 if (TextUtils.isEmpty(mPalletCode)) { showToast(请先扫描托盘码); return; } // 第二级检查该物料是否已入托防重复 if (isMaterialAlreadyInPallet(mPalletCode, epc)) { showToast(该物料已在托盘中); return; } // 绑定关系存入数据库 saveMaterialToPallet(mPalletCode, epc); showStatus(物料 epc 已入托盘 mPalletCode); mPalletCode null; // 重置准备下一轮 } }参数说明isMaterialAlreadyInPallet()查询的是内存缓存mPalletMapHashMapString, ListString非实时查DB——因为产线要求毫秒级响应缓存会在Activity销毁时批量写入SQLite。若需强一致性可将saveMaterialToPallet()改为同步DB操作但实测会增加300ms延迟。4.3 物料出库ProductOutActivity的库存扣减原子性保障ProductOutActivity.class的出库逻辑最危险必须保证“扫描→校验→扣减→生成单据”四步原子性。项目采用ContentProvider封装库存操作而非裸SQL// 在ProductOutActivity中 private void processOutbound(String epc) { ContentValues values new ContentValues(); values.put(epc, epc); values.put(out_time, System.currentTimeMillis()); // 调用自定义ContentProvider内部用SQLiteDatabase.beginTransaction() Uri resultUri getContentResolver().insert( ProductContract.ProductEntry.CONTENT_URI_OUTBOUND, values ); if (resultUri ! null) { // 成功刷新UI并播放提示音 playSuccessSound(); updateInventoryUI(epc, -1); // UI层扣减非DB } else { // 失败必然是库存不足显示具体缺货数量 int shortage getShortageQuantity(epc); showToast(库存不足缺 shortage 件); } }逻辑说明ProductContract.ProductEntry.CONTENT_URI_OUTBOUND指向ProductProvider.java其insert()方法内嵌事务先SELECT quantity FROM inventory WHERE epc?再UPDATE inventory SET quantityquantity-1 WHERE epc? AND quantity1最后插入出库记录。若UPDATE影响行为0则整个事务回滚insert()返回null。5. 避坑指南263个Java类里最常踩的5个深坑与现场急救方案这项目不是玩具它在真实产线跑过压力测试。以下5个坑是某导师带学生部署时连续翻车三天才定位的每一条都附带现象、根因和三步急救法。别等上线后半夜被电话叫醒。5.1 现象连续扫描时标签列表疯狂抖动同一EPC码每秒出现5次原因ScanMode.class中aggregateTags()的去重窗口设为100ms但产线金属货架导致标签反射实际到达时间差达200ms。解决打开app/src/main/java/com/example/scanlab/core/ScanMode.java找到private static final long DUPLICATE_WINDOW_MS 100;改为300并重新编译——经实测300ms是某制造企业仓库的黄金值低于250ms漏标高于350ms误合并。5.2 现象MaterialOutActivity点击“出库”按钮无响应Logcat无任何日志原因AndroidManifest.xml中该Activity的android:exportedfalse但项目启用了targetSdkVersion31Android 12强制要求显式声明exported。解决打开app/src/main/AndroidManifest.xml找到activity android:name.activity.MaterialOutActivity添加属性android:exportedtrue——注意这只是调试用正式发布需加android:permission限制。5.3 现象APK安装后闪退Logcat报java.lang.UnsatisfiedLinkError: dlopen failed: library librfid.so not found原因rfid-sdk-2.3.1.jar依赖的librfid.so是ARMv7架构但测试机为ARM64如Pixel 4。解决检查app/src/main/jniLibs/目录应有armeabi-v7a/和arm64-v8a/两个子目录若缺失arm64-v8a/从SDK厂商处索取对应so库或降级build.gradle中ndk.abiFilters armeabi-v7a强制只支持32位——产线手持机90%是32位。5.4 现象扫描速度越来越慢10分钟后单次扫描耗时从300ms升至2s原因ScanMode.class中mTagCacheLinkedHashMap未设最大容量内存泄漏。解决打开ScanMode.java找到private MapString, Long mTagCache new LinkedHashMap();改为private MapString, Long mTagCache new LinkedHashMapString, Long(100, 0.75f, true) { ... };重写removeEldestEntry()方法当size500时返回true——500是产线实测的平衡点再大OOM再小去重失效。5.5 现象ProductIncomingActivity中扫描托盘码后TextView显示乱码如PAL-??原因resources.ap_中strings.xml的编码为GBK但Android Studio默认UTF-8读取。解决用Notepad打开app/src/main/res/values/strings.xml菜单栏“编码 → 转为UTF-8”保存后Clean Project并Rebuild——切记不要用IDE自带的“File Encoding”设置它只改文件头不改内容。6. 进阶技巧用ADB命令绕过UI直接触发扫描状态实现无人值守盘点产线有时需要凌晨自动盘点不能靠人点屏幕。这时要绕过Activity生命周期用ADB发送广播直接驱动ScanMode。这不是黑科技而是项目预留的后门——ScanMode.class里有个BroadcastReceiver监听com.example.scanlab.ACTION_TRIGGER_SCAN。6.1 注册广播接收器的隐藏入口在ScanMode.java末尾有段被注释掉的代码// TODO: 生产环境启用此Receiver用于ADB触发扫描 /* private BroadcastReceiver mScanTriggerReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_TRIGGER_SCAN.equals(intent.getAction())) { String state intent.getStringExtra(state); switch (state) { case MATERIAL_INCOMING: setCurrentState(STATE_MATERIAL_INCOMING); break; case CONTINUOUS: startContinuousScan(); break; } } } }; */要启用它只需三步删除/*和*/取消注释在init()方法末尾添加context.registerReceiver(mScanTriggerReceiver, new IntentFilter(Intent.ACTION_TRIGGER_SCAN));在AndroidManifest.xml的application内添加receiver android:name.core.ScanMode$ScanTriggerReceiver android:exportedtrue intent-filter action android:namecom.example.scanlab.ACTION_TRIGGER_SCAN / /intent-filter /receiver注意android:exportedtrue是必须的否则ADB无法发送广播。安全起见可加android:permissioncom.example.scanlab.PERMISSION_TRIGGER并在AndroidManifest.xml中声明该permission。6.2 ADB命令实战三行命令完成整晚盘点假设设备已连接电脑执行# 1. 切换到连续扫描模式状态机进入STATE_CONTINUOUS adb shell am broadcast -a com.example.scanlab.ACTION_TRIGGER_SCAN --es state CONTINUOUS # 2. 5分钟后停止扫描发送stop指令 adb shell am broadcast -a com.example.scanlab.ACTION_TRIGGER_SCAN --es state STOP # 3. 导出当天所有扫描记录SQLite数据库 adb shell run-as com.example.scanlab cp /data/data/com.example.scanlab/databases/scanlab.db /sdcard/Download/ adb pull /sdcard/Download/scanlab.db ./backup/技巧说明run-as命令能直接访问应用私有目录前提是设备已root或为debuggable APK。若未root可用adb backup -f backup.ab com.example.scanlab替代但恢复需adb restore backup.ab。6.3 自动化脚本用Python调度ADB命令实现定时任务写个auto_inventory.py用subprocess调用ADBimport subprocess import time from datetime import datetime def adb_broadcast(action, extraNone): cmd [adb, shell, am, broadcast, -a, action] if extra: cmd.extend([--es, state, extra]) subprocess.run(cmd, capture_outputTrue) def main(): print(f[{datetime.now()}] 开始自动盘点...) # 启动连续扫描 adb_broadcast(com.example.scanlab.ACTION_TRIGGER_SCAN, CONTINUOUS) time.sleep(300) # 扫描5分钟 # 停止扫描 adb_broadcast(com.example.scanlab.ACTION_TRIGGER_SCAN, STOP) # 导出数据库 subprocess.run([adb, shell, run-as, com.example.scanlab, cp, /data/data/com.example.scanlab/databases/scanlab.db, /sdcard/Download/inventory_ datetime.now().strftime(%Y%m%d_%H%M%S) .db]) print(f[{datetime.now()}] 盘点完成数据已导出) if __name__ __main__: main()逻辑说明time.sleep(300)是硬编码实际部署时建议用cron或Windows任务计划程序调用此脚本。从那以后我每次做产线自动化都强制走一遍ADB触发测试——UI自动化太脆弱而命令行才是产线真正的“后悔药”。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站