ARTICLE DETAIL

资讯详情

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

MQTT与Tasker插件实战:Android智能家居控制与模块化开发

MQTT与Tasker插件实战:Android智能家居控制与模块化开发 简介面向Android移动端与物联网开发者的Tasker-MQTT发布插件工程包适用于智能家居控制、跨平台通信与自动化任务消息发布等场景。内容围绕MQTT协议、Locale API与模块化开发展开借助完整项目源码可学习Tasker插件开发流程、MQTT消息发布实现以及开源项目的集成思路同时理解Android Studio工程组织和目录划分。压缩包共83个文件以43个xml资源、13个png图标、8个java源码及gradle构建配置为主另有README说明和附赠PDF整体仅391KB轻量且结构清晰便于按模块快速定位。目前已有101人学习下载适合具备Android基础、希望深入物联网插件开发的读者可作为从零搭建或二次改造的参照。1. 从智能家居到 Tasker把 MQTT、Locale API 和模块化串成一条链一套称得上“物联网应用开发”的 Android 方案往往不是单个 App 能讲完的。你在 Android Studio 里能连上 MQTT broker只说明消息通道通了但要让智能家居在特定条件下自动执行还需要 Tasker 这样的自动化框架并通过 Locale API 让插件与 Tasker 握手要支撑后续持续迭代模块化开发又是绕不开的组织方式。本文以标题里那个 zip 工程为话题原点先从协议与客户端搭建讲起再落到 Tasker 插件与消息发布最后给出跨平台通信的验证技巧。读者需要已经能在 Android Studio 里新建项目、跑通依赖下载否则建议先解决环境问题。2. MQTT协议要点与Android Studio客户端环境搭建2.1 为什么智能家居选择MQTT而不是HTTP或WebSocketMQTT 是专为受限网络和设备设计的发布/订阅式消息协议基于 TCP 长连接头部开销可以压缩到 2 字节。对比 HTTPHTTP 是请求/响应模型设备状态变化需要客户端轮询延迟高且浪费电WebSocket 虽然能双向通信但缺少主题路由、遗嘱、保留消息这类物联网刚需。MQTT 的核心是 broker客户端只管发布和订阅不关心消息到底发给谁。设备 A 发布home/device/switch1/command设备 B 和设备 C 只要订阅同一主题就能收到这种解耦天然适合智能家居。另一个关键点是 QoS 等级。MQTT 提供 0、1、2 三种投递语义智能家居里大部分控制指令用 QoS 1 就够因为消息可能重复但基本不会丢失QoS 2 开销太大用于计费或告警场景才值得。还有 retain 标志broker 会为每个主题保留最后一条消息新订阅者上线立刻能拿到设备最新状态。这些特性在 HTTP 里都需要自己造轮子。特性MQTTHTTPWebSocket通信模型发布/订阅请求/响应双向消息主题路由原生支持需自己实现需自己实现低功耗场景非常适合较差一般离线消息/遗嘱内置无无保留状态内置 retain无无对照表不是要全面否定 HTTP而是在“移动端长期在线并且要实时控制”这个具体场景下MQTT 的维护成本更低。如果你只是用一个 App 偶尔连一次设备HTTP 可能更简单但标题里明确涉及 Tasker 自动化任务和跨平台通信那 MQTT 几乎是最稳的选型。2.2 在Android Studio里引入Paho客户端的最小配置Android 生态里最常用的 MQTT 客户端是 Eclipse Paho 系列。你需要两个依赖mqtv3 基础客户端库以及 paho.android.service后者把连接生命周期绑定到 Android 服务能避免屏幕关闭后连接被系统杀掉。在build.gradle的 dependencies 块里加入dependencies { implementation org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5 implementation org.eclipse.paho:org.eclipse.paho.android.service:1.1.1 implementation androidx.localbroadcastmanager:localbroadcastmanager:1.1.0 }如果你在国内网络环境第一次同步时容易卡在 Gradle 镜像源上。常见做法是在项目根目录的init.gradle里配置阿里云镜像或者在 settings.gradle 的 pluginManagement 里替换仓库地址否则“Android Studio 下载依赖”这一步就会劝退很多人。Paho 的版本号不建议选太激进的新版1.2.5 和 1.1.1 已在大量生产项目里跑过兼容 Android 8 到 14 都没问题。依赖配好只是第一步AndroidManifest.xml 里还需要注册 Paho 内置的 MqttService并声明联网和唤醒锁权限uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.WAKE_LOCK / application service android:nameorg.eclipse.paho.android.service.MqttService / /application不注册 MqttService 的话运行时会报ServiceNotFoundException而且这个错误要到真正调用连接时才会暴露。MqttService 是 Paho 用来维持长连接后台任务的宿主WAKE_LOCK 让设备在息屏时仍能收发心跳包。2.3 连接参数与心跳保活serverUri、clientId 和 timeoutPaho Android 库的入口是MqttAndroidClient。构造函数三个参数分别是 context、serverUri、clientId。serverUri 常见格式是tcp://broker.emqx.io:1883生产环境建议用 SSL即ssl://broker.emqx.io:8883但本地调试先用明文 tcp 最快。clientId 必须全局唯一建议每次启动用时间戳生成新的避免与旧连接冲突导致 broker 踢人。class MqttClientWrapper(context: Context, serverUri: String, clientId: String) { private val mqttClient MqttAndroidClient(context, serverUri, clientId) fun connect() { val options MqttConnectOptions() options.isCleanSession false options.keepAliveInterval 30 options.connectionTimeout 10 options.isAutomaticReconnect true options.mqttVersion MqttConnectOptions.MQTT_VERSION_3_1_1 mqttClient.connect(options, null, object : IMqttActionListener { override fun onSuccess(asyncActionToken: IMqttToken?) { // 连接成功可以在这里订阅设备主题 } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { exception?.printStackTrace() } }) } }cleanSession设 false 表示让 broker 保留离线期间的 QoS 1/2 消息适合控制端但如果每次都换 clientId这个设置就没意义。keepAliveInterval是心跳间隔默认 30 秒网络差的地方可以调到 60间隔太短会增加功耗太长会被 broker 误判为掉线。isAutomaticReconnect是网络恢复后自动重新连接这个开关强烈建议打开否则用户切到后台再切回来就得手动重连。连接成功后如果收不到任何回调先检查 broker 地址能否从手机访问。很多入门者卡在“电脑能连接手机不行”因为办公室网络屏蔽了 1883 端口用 4G/5G 网络测试通常能定位问题。3. 模块化开发边界与Tasker插件Locale API实现3.1 模块化开发的首要决策把MQTT核心与UI拆开标题里的“模块化开发”不是指代码里多建几个文件夹而是在 Android Studio 里创建多个 Gradle module。常见拆分法app主模块负责界面与用户交互mqttcore库模块封装连接、发布、订阅的 APItaskerplugin模块专门实现 Tasker 插件需要的 BroadcastReceiver 与配置界面。// settings.gradle include :app include :mqttcore include :taskerplugintaskerplugin要依赖mqttcore但app也可以依赖mqttcore这样就保证插件和主应用用的是同一套连接代码。如果不拆Tasker 插件的 Receiver 会被迫引用整个 App 的类一旦 UI 层发生重构插件就跟着崩。拆开后mqttcore里不出现任何 Activity 依赖所有回调用接口向上抛这样单独给这个库写单元测试也不难。这里有个容易被忽略的工具链问题新建 module 后Gradle 同步经常会报 SDK 路径找不到尤其是修改过ANDROID_HOME环境变量后。常见做法是检查local.properties里的sdk.dir或者在 Android Studio 的 SDK Manager 里确认真正安装的 SDK 版本。标题拉出来的 zip 工程一旦换电脑第一痛点往往就是sdk.dir指向旧路径这种报错信息不友好容易被误以为代码有问题。3.2 Locale APITasker 插件是怎么被系统唤醒的Tasker 的插件机制官方叫 Locale API因为最早是给 Locale 这个自动化应用用的。第三方插件要向 Tasker 暴露两个组件一个 EditActivity 用来让用户填写参数一个 FireReceiver 用来实际执行动作。用户在 Tasker 里添加插件动作时Tasker 启动 EditActivity保存后Tasker 在事件触发时发送com.twofortyfouram.locale.intent.action.FIRE_SETTING广播到插件的 FireReceiver。组件作用必需EditActivity展示配置界面返回 Bundle必需FireReceiver接收 FIRE_SETTING 广播并执行动作必需QueryReceiver提供插件元数据可选但建议有可选编辑活动和接收器之间通过 Bundle 传递数据。EditActivity 把用户输入放入 Bundle通过setResult(RESULT_OK, intent)回传给 TaskerTasker 触发时再把同样的 Bundle 塞进广播发给 FireReceiver。所以一定要避免在 Bundle 里放自定义对象因为跨进程传递会被序列化。另外Tasker 要求 FireReceiver 在处理完动作后调用setResultCode(Activity.RESULT_OK)否则它会认为插件执行失败。很多新手漏掉这一行结果动作其实已经发布了消息Tasker 却弹错误提示。3.3 写一个最小可用的Tasker插件下发MQTT消息理解了机制后代码就清晰了。先定义一个公共的 intent action 常量放在mqttcore库的常量类里插件和主应用都能用。object TaskerIntent { const val ACTION_FIRE_SETTING com.twofortyfouram.locale.intent.action.FIRE_SETTING const val EXTRA_BUNDLE com.twofortyfouram.locale.intent.extra.BUNDLE const val EXTRA_STRING_BLURB com.twofortyfouram.locale.intent.extra.BLURB }FireReceiver 的核心逻辑就是取出 topic 和 message调用 MQTT 发送。注意 onReceive 里不能做耗时操作所以 MQTT 发布要走异步接口。class MqttFireReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action ! TaskerIntent.ACTION_FIRE_SETTING) return val bundle intent.getBundleExtra(TaskerIntent.EXTRA_BUNDLE) val topic bundle?.getString(topic) ?: return val message bundle.getString(message) ?: val mqtt MqttManagerHolder.getInstance(context).mqttClient mqtt.publish(topic, message.toByteArray(), 1, false, null) { token, exception - if (exception ! null) { setResultCode(context, RESULT_FAILED, exception.message) } else { setResultCode(context, Activity.RESULT_OK, 已发送到 $topic) } } } private fun setResultCode(context: Context, code: Int, blurb: String?) { resultCode code if (blurb ! null) { resultData blurb } } }publish的参数里qos 设为 1 确保控制指令不丢retained 设为 false 因为控制命令不需要保留。回调里setResultCode的语义成功返回 RESULT_OK失败返回RESULT_FAILEDTasker 据此把错误信息显示给用户。EditActivity 是最烦琐的部分它会接收 Tasker 传入的配置 Bundle显示一个带输入框的界面。保存时要做校验然后把用户输入重新包装成 Bundle。在 AndroidManifest 里该 Activity 需要声明activity android:name.edit.MqttEditActivity android:exportedtrue intent-filter action android:namecom.twofortyfouram.locale.intent.action.EDIT_SETTING / category android:nameandroid.intent.category.DEFAULT / /intent-filter /activity没有这个 intent-filterTasker 的插件列表里就找不到这个动作。另外 Android 12 以后导出组件必须显式设置android:exportedtrue否则安装时直接报错这是热词里“Android Studio 开发 app 项目”常见坑的典型代表。4. 智能家居控制实战从Tasker动作到MQTT消息发布4.1 设计Topic与Payload约定智能家居项目里Topic 结构决定了系统能不能平滑扩展。我常用的方案是三层home/{deviceId}/{group}/{action}。例如home/esp32/kitchen/switch控制开关用command后缀状态上报用state后缀。方向主题示例Payload 示例下行控制home/esp32/kitchen/switch/command{on:true}上行状态home/esp32/kitchen/switch/state{on:true,rssi:-45}系统日志home/esp32/kitchen/log重启/启动完成payload 建议统一用 JSON虽然 MQTT 本身不关心格式但设备端固件解析起来方便也方便接入 Node-RED 或规则引擎。如果只控制简单的开关也可以用ON/OFF字符串但这样后续加亮度、色温就要改协议不如一开始就定 JSON。模块化在这里的体现是Topic 拼接逻辑不散落在 ViewModel 里而是集中在mqttcore的TopicBuilder类里。这样 Tasker 插件和 App 里的 UI 都能调用同一个方法生成主题避免两边写出的字符串不一致导致设备无响应。4.2 一键控制publish参数与异步回调主应用里通常是一个开关按钮用户点击App 通过 MqttAndroidClient 发布命令。这里的 publish 方法和 Tasker 插件里用的是同一个底层接口区别只在于调用方。为了不阻塞 UI 线程publish 是异步操作必须传入回调或者使用 Kotlin 协程包装。class DeviceControler(private val mqttClient: MqttAndroidClient) { fun sendCommand(deviceId: String, payload: JSONObject) { val topic TopicBuilder.commandTopic(deviceId) val message payload.toString().toByteArray() mqttClient.publish(topic, message, 1, false, null) { token, exception - if (exception ! null) { Log.e(DeviceController, publish failed, exception) // 这里把失败状态通过 LiveData 抛给 UI } else { Log.d(DeviceController, command accepted: ${token?.messageId}) } } } }publish里 qos 参数建议固定为 1。如果设 0局域网内可能没事但经公网 broker 转发时丢消息概率变大设 2 则会让 broker 端做更多握手控制类消息不值得。retained 参数在控制命令上永远是 false因为如果用户关了灯retained 消息会把“关灯”这个状态存下来新设备上线后订阅到这条过时的命令导致刚接上电就执行一次关机任务。调试时最直接的方法就是同时打开两个 MQTT 客户端一个模拟设备订阅home//kitchen/switch/command主题另一个从手机点击按钮发布消息。这一步能在 5 分钟内确认协议、权限、Broker 路由都没问题。4.3 订阅设备状态并驱动UI刷新设备端通常会在状态变化后发布 state 消息。Android 客户端要订阅这些主题并在收到消息后更新界面。Paho 的订阅也要异步同时要指定回调接口mqttClient.subscribe(home/kitchen/switch/state, 1, null, object : IMqttActionListener { override fun onSuccess(asyncActionToken: IMqttToken?) { Log.i(MqttManager, 订阅成功) } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.e(MqttManager, 订阅失败, exception) } }) mqttClient.setCallback(object : MqttCallback { override fun connectionLost(cause: Throwable?) { // 自动重连会自动触发这里只需要做状态标记 } override fun messageArrived(topic: String?, message: MqttMessage?) { val payload String(message?.payload ?: ByteArray(0)) // 解析 JSON 后通过 LocalBroadcast 或 StateFlow 通知 UI } override fun deliveryComplete(token: IMqttDeliveryToken?) { // 指令已经到达 broker可以记录日志 } })注意 messageArrived 运行在线程池中不能直接操作 View常见做法是把消息发到主线程的 StateFlow/LiveData。如果你依赖了androidx.lifecycle.livedata-ktx可以用liveData.postValue(...)把 JSON 字符串传给 UI。这里如果直接用 Activity 里的私有 Handler会引入内存泄漏风险。跨平台通信在这一步体现得最明显同一套 broker 可以同时接 ESP32、树莓派、Android、iOS 和云端服务。只要 Topic 和 payload 约定一致平台差异被消解掉。你的 Android 模块只需要保证连接可靠、重连及时、消息不阻塞剩下的解析与展示都由上层处理。5. 跨平台通信验证与开源工程集成的三个关键技巧5.1 用桌面MQTT客户端做端到端验证手机端写完不要急着打包测试。先用一个桌面 MQTT 客户端比如 MQTTX 或 MQTT Explorer连同一个 broker订阅home/#。然后从 Android 端发布一条测试消息如果桌面客户端能即时收到说明整条链路已经通了剩下的只是 UI 展示问题。反过来也让手机订阅一个测试主题用桌面客户端发布检查手机是否触发回调。这个双向验证能快速区分是网络问题、认证问题还是代码问题。5.2 从 zip 工程导入时常见的 Gradle 与 SDK 坑打开别人导出的 zip 工程时最常遇到三件事Gradle 版本不匹配导致tag number over 30 is not supportedSDK 路径还是原作者的绝对路径AGP 版本与 compileSdk 冲突。处理方式不是直接取消选中 SDK而是打开gradle-wrapper.properties把 distributionUrl 改成自己电脑上已兼容的版本再看local.properties里的sdk.dir是否有效。如果工程里用了较老的 Android Support 库建议手动迁移到 AndroidX但那会改动很多 import可以等编译通过后再做。5.3 用QoS 1加retained消息解决“新设备上线拿不到状态”最后一个技巧用在你希望设备重启后能立即恢复控制面板状态的情况设备上线时发送一条 retained 消息到home/.../state然后在 Android 端订阅这个主题时broker 会把保留的最后一条消息直接推给新订阅者。这比在本地数据库缓存状态更可靠因为数据源来自设备本身。val message MqttMessage(payload.toString().toByteArray()) message.isRetained true message.qos 1 mqttClient.publish(home/$deviceId/state, message)注意 retained 消息不会自动过期如果设备被移除或工厂重置你需要在另一个管理主题上发布空 payload 清除旧状态否则下一个订阅者会拿到一条永远不会更新的“假状态”。在 MQTT 调试工具里输入home/deviceA/state并发送空消息即可覆盖清除。这算是一个很小的细节但在智能家居批量部署时能省掉不少凌晨的在线问答。本文还有配套的精品资源点击获取
返回列表