拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android煤矿调度管理系统:井下终端离线同步与弱网联调

简介这份资源是一篇关于基于 Android 平台构建煤矿移动调度管理系统的技术文献面向煤炭行业信息化建设者、移动应用开发初学者及计算机相关专业学生用于理解如何把调度管理从 PC 与局域网环境迁移到移动终端。压缩包内仅含 1 个 PDF 文件体积约 465KB属于单篇论文型资料便于快速通读与引用。全文围绕 Android 客户端、ASP.NET MVC4 框架下的 Web API 与 Java 后端三条技术线索展开说明了客户端通过 HTTP 与服务器交互、以 JSON 交换数据、由 SQL Server 2008 存储信息的整体结构并梳理调度通知、生产日报、生产计划、安全监测、统计分析、人员定位和安全管理等七个功能模块还涉及生产事故、掘进进尺、瓦斯检查与交接班记录等实时调度数据的管理方式。目前已有 99 人学习适合作为选题参考、方案设计模板或课程论文的补充阅读也可为移动端与后端接口设计提供可借鉴的思路。1. 井下那一屏比调度室大屏更难做煤矿调度管理系统为什么选 Android调度室的墙上通常挂着拼接大屏但真正决定一次停送电、一趟人车、一次皮带抢修快慢的往往是班长手里那块四寸屏。煤矿调度管理系统要解决的核心不是看得见而是三件更难的事井下没有稳定网络、操作者戴着厚手套点不准、一条指令发出去必须确认对方收到并回执。把这套系统落到 Android 端就意味着同时应付本安型终端的性能上限、井下环网的抖动、以及三个班次交接时的数据一致性。常见做法是把调度系统拆成井上服务端 井下 Android 终端 工业环网三段Android 端承担工单派发确认、人员定位查询、告警响应和语音对讲这四类高频动作服务端负责台账、统计和与安全监控系统的对接。Android 之所以成为首选不是因为生态花哨而是本安型防爆手机几乎全是 Android 定制 ROM加上现成的蓝牙、WiFi、音频和定位能力能省掉大量底层适配。这篇适合两类人拿到基于Android的煤矿调度管理系统设计这个题目、需要在几周内交出可演示原型的开发者以及要把原型架构讲清楚、还得回答为什么这么选的工程师。下面从业务拆解开始一路写到能跑起来的代码和现场联调命令。2. 煤矿调度管理系统的功能拆解与 Android 端选型2.1 先把调度业务拆成四类页面而不是四套系统派单、定位、告警、对讲在调度室看着是一件事在端侧却是四种完全不同的交互节拍。实时监控是秒级只读刷新用户只看不动工单派发是分钟级写操作要求落库、补发、可追溯人员定位是区域查询要求地图底图能离线加载告警确认是打断式交互要求屏幕锁着也能响。把这四类混在一个页面里结果是每次刷新都在重建列表定位查询一卡告警就延迟。我一般会按交互节拍 是否允许离线两个维度切模块监控走长连接实时推工单走本地库先落再补发定位走本地切片底图加查询接口告警走前台服务加精准闹钟。这样切完后面选型和排错都有明确的边界出了问题也知道该翻哪一块的日志。2.2 Android SDK 版本、终端机型与组件选型井下终端是选型的第一约束。本安型防爆手机需要煤安标志屏幕普遍 4 到 5 英寸内存 3 到 4GBCPU 是中低端系统多为厂商定制的 Android 8 到 10。这意味着动画要克制、图片要压缩、列表要复用别指望高刷新率。常见做法是 minSdk 定在 26因为前台服务、通知渠道、快捷方式这些基础能力都在这个版本上稳定如果甲方给的是更老的终端就只能退到 24并自己补通知渠道的判断分支。功能模块端侧组件选型理由实时监控前台服务 长连接屏幕常亮时不希望被系统回收长连接比轮询省电工单派发Room WorkManager井下断网也能建单联网后按退避策略补发人员定位自绘 View 或离线切片底图井下巷道底图来自 CAD 图纸在线地图对不上告警确认Notification 精准闹钟需要绕过免打扰且必须有人点确认才消失语音对讲AudioRecord/AudioTrack Opus井下带宽窄低码率编解码优先2.3 用 init.gradle 统一内网仓库与依赖超时矿山项目的构建机大多在隔离内网Android Studio 直连不了外网仓库新人拉下项目第一件事就是报依赖找不到。与其让每个人改自己的 build.gradle不如在用户目录放一份全局初始化脚本对所有项目生效。// ~/.gradle/init.gradle // 作用给所有 Android Studio 项目注入内网 Nexus 仓库并放宽超时 allprojects { repositories { maven { url http://192.168.10.21:8081/repository/maven-public/ // 内网私服地址 allowInsecureProtocol true // 内网走 http必须显式放开否则 Gradle 7 直接拒绝 } google() mavenCentral() } } gradle.projectsLoaded { rootProject.allprojects { buildscript { repositories { maven { url http://192.168.10.21:8081/repository/maven-public/; allowInsecureProtocol true } } } } } settingsEvaluated { settings - settings.pluginManagement { repositories { maven { url http://192.168.10.21:8081/repository/maven-public/; allowInsecureProtocol true } } } }三个块各有分工allprojects管依赖解析gradle.projectsLoaded管 buildscript 插件settingsEvaluated管新版 Gradle 的 pluginManagement缺一个就会出现插件能下、依赖不能下的诡异现象。仓库顺序会决定命中优先级内网私服放第一位能省掉每次的失败重试等待。allowInsecureProtocol只在内网 http 场景开公网环境不要这么写。3. 用 Android Studio 搭出可运行骨架工单、定位点位与同步进度条3.1 模块划分与 Gradle 配置原型阶段不要一上来就搞动态特性模块按职责切五个文件夹级别就够了app放壳与导航core-db放 Roomcore-net放长连接与重试feature-dispatch放工单feature-locate放定位与底图。每个模块只暴露必要接口好处是调试时能单独把core-net拎出来跑单元测试不用连整机。模块职责关键依赖app导航、权限、前台服务声明lifecycle、viewpager2core-db实体、DAO、数据库迁移room-runtime、room-ktxcore-net长连接、断线退避、补发调度work-runtime-ktxfeature-dispatch工单列表与详情viewbinding、pagingfeature-locate底图加载、点位渲染自绘 View// app/build.gradle.kts plugins { id(com.android.application) id(org.jetbrains.kotlin.android) id(kotlin-kapt) // Room 注解处理器需要 } android { namespace com.example.mine.dispatch compileSdk 34 defaultConfig { minSdk 26 // 井下本安终端普遍 Android 8.0 起 targetSdk 34 versionCode 1 versionName 1.0.0 } buildFeatures { viewBinding true } // 布局文件多手写 findViewById 容易错 compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { // 版本号按你本地私服实际可用的写先保证能解析再谈升级 implementation(androidx.room:room-runtime:2.6.1) implementation(androidx.room:room-ktx:2.6.1) kapt(androidx.room:room-compiler:2.6.1) implementation(androidx.work:work-runtime-ktx:2.9.0) implementation(androidx.viewpager2:viewpager2:1.1.0) implementation(androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4) }compileSdk决定能调用哪些新 APIminSdk决定哪些 API 必须写版本判断这两个值一旦定下来就别在项目中途改改一次要重跑全量回归。viewBinding打开后布局里每个 id 都有类型安全的引用井下终端屏幕小、控件多这个开关省的是低级错误。3.2 Room 存工单幂等键与待办排序工单是这套系统里唯一必须一条都不能丢的数据。井下的典型场景是班长在巷道口建了一条三采区皮带机头更换托辊的单子信号刚好断了他以为发出去了调度室那边没收到。解决办法是端侧先落库把状态标成草稿联网后按幂等键补发。Entity(tableName work_order, indices [Index(value [orderId], unique true)]) data class WorkOrder( PrimaryKey val orderId: String, // 端侧生成的 UUID作为幂等键 val areaCode: String, // 井下区域编号如 N3-1201 val content: String, val priority: Int, // 1 一般 2 紧急 3 停送电类 val createdAt: Long, // 端侧生成时间毫秒 val serverAt: Long?, // 服务端确认时间null 表示未确认 val state: Int // 0 草稿 1 已下发 2 已确认 3 已超时 ) Dao interface WorkOrderDao { Query(SELECT * FROM work_order WHERE state 2 ORDER BY priority DESC, createdAt ASC) fun pending(): FlowListWorkOrder // 待办紧急在前同优先级按时间 Insert(onConflict OnConflictStrategy.REPLACE) suspend fun upsert(o: WorkOrder) // 补发时按 orderId 覆盖不产生重复行 Query(UPDATE work_order SET state 2, serverAt :at WHERE orderId :id) suspend fun markConfirmed(id: String, at: Long) }orderId用端侧 UUID 而不是服务端自增 ID是为了让补发在服务端可去重服务端收到相同 orderId 直接返回已有记录不会生成第二条。state 2加priority DESC的排序把停送电类永远顶到最上面井下班长打开 App 第一眼看到的就是最急的事。serverAt为 null 是判断这条指令到底有没有落地的唯一依据界面上必须把它渲染成未确认的角标而不是银行卡式的加载动画。3.3 CoordinatorLayout ViewPager2 的现场适配工单页面的信息密度高工具条又要能随滚动收起用 CoordinatorLayout 加 ViewPager2 是最省事的组合上滑时 AppBarLayout 收起来给列表让出高度下滑时再展开显示筛选条件。androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent com.google.android.material.appbar.AppBarLayout android:layout_widthmatch_parent android:layout_heightwrap_content com.google.android.material.appbar.MaterialToolbar android:idid/toolbar app:title调度工单 android:layout_widthmatch_parent android:layout_height?attr/actionBarSize/ com.google.android.material.tabs.TabLayout android:idid/tabs app:tabModefixed !-- 井下只有三页固定不滚动 -- android:layout_widthmatch_parent android:layout_heightwrap_content/ /com.google.android.material.appbar.AppBarLayout androidx.viewpager2.widget.ViewPager2 android:idid/pager app:layout_behaviorstring/appbar_scrolling_view_behavior android:layout_widthmatch_parent android:layout_heightmatch_parent/ ProgressBar android:idid/syncBar style?android:attr/progressBarStyleHorizontal android:max100 android:visibilitygone !-- 由 ViewModel 控制只在补发时出现 -- android:layout_gravitybottom android:layout_widthmatch_parent android:layout_height4dp/ /androidx.coordinatorlayout.widget.CoordinatorLayoutapp:layout_behavior这一行不能漏漏了 ViewPager2 会被 AppBarLayout 盖住顶部一截。ViewPager2 内部本身就是 RecyclerView所以每个 Fragment 里不要再套一个同方向可滚动的列表容器否则滚动事件会被外层吃掉表现为列表滑不动。同步进度条用 4dp 高贴在底部比弹对话框更合适——井下操作员腾不出手去点确定。3.4 指令通道前台服务与指数退避重连长连接放在前台服务里是让系统别在锁屏后把进程干掉。Android 8.0 之后startForeground必须在 5 秒内调用否则直接抛异常。class DispatchLinkService : Service() { private val scope CoroutineScope(SupervisorJob() Dispatchers.IO) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(NOTI_ID, buildNotification()) // 8.0 必须5 秒内 scope.launch { var backoffMs 1_000L while (isActive) { try { connectAndPump() // 阻塞式收发内部处理心跳 backoffMs 1_000L // 正常后重置退避 } catch (e: IOException) { delay(backoffMs) backoffMs (backoffMs * 2).coerceAtMost(30_000L) // 上限 30s } } } return START_STICKY // 被杀后由系统尝试重建 } }NOTI_ID固定一个常量通知渠道重要性设成 LOW避免井下高频弹窗干扰工单确认。退避从 1 秒翻倍到 30 秒封顶是个权衡无限翻倍会让恢复时间拉得太长固定 1 秒重试又会在环网抖动时把电量吃光。START_STICKY只保证系统可能重建真正的兜底还得靠 WorkManager 的周期任务检查连接状态。4. ADB 无线调试、签名 SHA1 与井下弱网联调排错4.1 用 adb pair / adb connect 连上防爆终端井下终端一般挂在腰带或支架上插 USB 很不方便无线调试是常规做法。前提是调试机和终端在同一网段矿山环网通常是 5GHz 工业 WiFiSSID 分井上井下两个。adb version # 确认 platform-tools 版本高于终端系统版本 adb pair 192.168.10.77:37129 # Android 11 无线调试配对端口和配对码在终端上点开可见 adb connect 192.168.10.77:5555 # 配对后连接端口以终端显示为准 adb devices -l # 状态必须是 device出现 unauthorized 说明终端上没点允许 adb shell getprop ro.build.version.release # 确认实际系统版本定制 ROM 常与标称不符 adb shell getprop ro.product.model # 记录机型方便定位是终端问题还是代码问题unauthorized是最常见的坑无线配对成功不代表调试授权成功终端上还会弹一次允许 USB 调试井下没人点就一直卡在这个状态。另外定制 ROM 常把 5555 端口改掉adb connect报连不上时先回终端无线调试页面看当前端口。4.2 应用签名 SHA1 怎么取为什么地图和推送都认它签名 SHA1 是端侧接入第三方能力时的身份凭证离线地图 SDK、企业推送、MDM 白名单都要登记包名 签名指纹换一次签名这些登记全部失效表现为地图白屏或推送静默不达。# 方式一直接查 keystore 文件发布签名 keytool -list -v -keystore mine-release.jks -alias mine -storepass 你的口令 # 方式二用 Gradle 任务一次列出所有构建变体的签名信息推荐 ./gradlew :app:signingReport # 方式三debug 签名本地联调时用 keytool -list -v -keystore ~/.android/debug.keystore \ -alias androiddebugkey -storepass android -keypass androidsigningReport的好处是一次列全 debug 和 release 的 MD5、SHA1、SHA256不用记口令也不会把口令写进命令行历史。注意 debug 和 release 的 SHA1 是两个不同的值本地联调通了不代表发布包能用登记时一定要用 release 的那一份。4.3 弱网下的排错对照表井下环网的典型故障是丢包和切换基站表现出来却是五花八门。下面这张表是我现场最常用的对照。症状排查命令或手段说明回前台就重连adb shell dumpsys activity services 包名服务被杀检查是否漏了前台通知指令延迟 10 秒以上adb shell dumpsys deviceidle whitelist 包名加入电池优化白名单减少休眠压制定位点位不刷新adb logcat -s LocSdk:* Beacon:*分清是信标没上报还是底图没加载界面明显掉帧adb shell dumpsys gfxinfo 包名 framestats看 90 分位帧耗时定位到具体页面安装失败adb install -r -d app-release.apkINSTALL_FAILED_UPDATE_INCOMPATIBLE说明签名不一致先卸载dumpsys gfxinfo的 framestats 输出很长重点看每帧总耗时超过 16ms 的条目连续出现就说明有主线程阻塞。井下终端 CPU 弱这类问题多半来自在onBindViewHolder里做字符串拼接或数据库查询而不是布局层级太深。5. 离线同步与指令优先级的现场验证技巧5.1 断网 30 分钟的增量同步与时钟偏移井下换班时经常有半小时左右的网络空档这段时间端侧攒下的工单要能一次性补齐。常见做法是服务端给一个水位线端侧只补serverAt 水位线的记录其余按 orderId 去重。更麻烦的是时钟终端时间被人工调过之后createdAt排序就乱了紧急单可能排到普通单后面。我的处理是在握手时让服务端下发一个offset serverTime - localTime落库时统一写serverTime localTime offset并把 offset 跟着记录一起存下来排查时能还原当时的真实时间。井下终端换电池后掉时间的情况不少没有 offset 这一步后面所有按时间排序的统计都会偏。5.2 优先级不能只靠一个 int 字段priority只有 1、2、3 三档实际调度里停送电和皮带急停都算最高档两条同时到达时谁先弹我的做法是加一个category字段做二级排序停送电类固定置顶其次按区域所属班次分组当前班次负责的区域优先。这样即使服务端推送顺序乱了端侧渲染出来的顺序依然符合调度员的判断习惯。-- 待办列表的完整排序类别 优先级 时间 SELECT * FROM work_order WHERE state 2 ORDER BY category DESC, priority DESC, createdAt ASC;5.3 上线前用一台旧终端做灰度验证原型跑在开发机上永远顺滑问题都出在老旧本安终端上。发布前我会找一台和现场同型号的机器把 minSdk 抬到目标版本之前先在上面跑一遍 Monkey同时开着adb logcat抓崩溃再把屏幕亮度调到最低、开发者选项里的动画全部关掉模拟井下真实观感。一个具体的技巧是手动把网络限速到 3G 级别再走一遍完整流程观察退避重连是否会在工单确认的瞬间恰好在等待如果会就把确认请求单独走一条短连接通道不和长连接共用退避计时。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门