05-STM32-ESP8266-OneNet
05-STM32-ESP8266-OneNet
官方文档:物联网开放平台
看本节内容之前先去看这个ESP8266-MQTT:
OneNet介绍
你做了一个监控花盆土壤湿度的设备,想用手机远程看到湿度数据,大致有两种办法:
✅ 方法一:自己搭服务器
- 你租一个云服务器(比如阿里云、腾讯云)
- 自己写程序,让设备把数据发到这个服务器
- 服务器再把数据转发给手机上的 APP 或网页
优点:灵活自由,想怎么玩都行
缺点:你得自己搭建、维护、写接口,比较麻烦
✅ 方法二:使用物联网云平台(比如 OneNET)
- OneNET 就是别人已经搭好的“云服务器 + 管理系统”
- 你只要把设备接入 OneNET,它帮你:
- 收数据
- 存数据
- 转发数据给 APP
- 设置自动化规则等
优点:快速上手、界面友好、不用操心底层
缺点:受平台限制,功能和自定义性不如自己搭建灵活
第一阶段--先只用esp8266
第一阶段硬件先需要一个usb-ttl和esp8266即可完成
OneNet配置图
分清产品与设备的区别
OneNet界面介绍
新建产品



新建属性

注意第四列的标识符,后面会多次用到

这里以布尔类型为例子,用来表示LED灯,其他的系统已经为我们创建过了,看上图:环境湿度,环境温度等等
创建设备


查看具体设备的属性值


三元数

命令下发调试

MQTT通信
先判断esp8266的固件是否支持mqtt:AT+MQTTUSERCFG=0,1,"ESP8266Client","myname","password",0,0,""不报错证明可用,报错的话去esp8266-mqtt通信看看怎么烧录固件
参考文档:最佳实践-物模型数据交互
云平台地址:mqtts.heclouds.com
基本信息
下面这些是我的,你要去找你的,上面有讲在哪找
| 参数 | 内容 |
|---|---|
| 产品ID | g3f7QpYaxS |
| 设备名 | produce1 |
| 设备密钥 | N05KSVhnSVFlS0pBTXhHRXF6MEFSY1lUQ2ZEcmlKSGU= |
指令生成
MQTTx配置
直接通过导出的esp8266的AT指令就可以与onenet通信了,几乎不可能会输入错误,所以这里就跳过了MQTTx的讲解,之前讲是因为之前的AT指令需要手敲,所以使用MQTTx来测试是否输入错误,不然在esp8266上不好排查
AT指令讲解
话题具体含义可以在官方文档看到:
生成的指令讲解,
需要注意的操作
命令下发
在这里下发命令
与此同时,会给订阅$sys/g3f7QpYaxS/produce1/thing/property/set的esp8266发送下图内容,我们需要回复第9条命令,注意每次云端发送的id都会变,
这里我发送一下:
onenet后台显示:
第二阶段--stm32与esp8266结合
第一阶段完全跑通之前不得进行第二阶段内容
第一阶段中是使用电脑与esp8266进行交互的,第二阶段使用stm32与esp8266进行交互,本质上还是发送串口信息,主要有以下几点要注意:
- c语言需要转义,比如在串口助手是
AT+CWJAP="your_wifi_name","12345678",在c语言里是"AT+CWJAP=\"your_wifi_name\",\"12345678\",处理起来比较麻烦,这个看上去比较简单,但是这个呢:uart_print(&huart1, "AT+MQTTPUB=0,\"$sys/g3f7QpYaxS/produce1/thing/property/post\",\"{\\\"id\\\":\\\"123\\\"\\,\\\"version\\\":\\\"1.0\\\"\\,\\\"params\\\":{\\\"Power\\\":{\\\"value\\\":12345}\\,\\\"temp\\\":{\\\"value\\\":233.6}\\,\\\"fasdf\\\":{\\\"value\\\":\\\"fasdf\\\"}\\,\\\"dasf\\\":{\\\"value\\\":34345}\\,\\\"asdf\\\":{\\\"value\\\":2323}\\,\\\"fadsf\\\":{\\\"value\\\":234}}}\",0,0\r\n");,虽然上面的工具也生成了c代码,但是如果项目的模板无法满足你的需求,需要手动更改代码,可以使用我写的这个工具检查 - 在stm32不方便看esp8266返回了什么信息,我这里使用usart2来打印esp8266返回的内容
- stm32不知道什么时候发送完了,要不要接着发送,万一esp8266在busy的同时stm32在发送怎么办
主要围绕这几个注意事项来编写代码
硬件准备
- 嘉立创天空星-stm32f407vet6(其他stm32类单片机均可)
- OLED屏幕
- esp8266(我使用的是正点原子的,其他的也可(需要额外烧录mqtt固件))
- usb-ttl和stlink
cubemx配置
串口部分:

OLED

LED

主要代码讲解
代码主要分为以下 7 个核心模块:
1. 🆔 宏定义与身份配置区
这是设备的“身份证”和“通讯录”
#define WIFI_SSID "aaa"
#define WIFI_PASSWORD "aaaaaaaa"
#define MQTT_SERVER "mqtts.heclouds.com"
// ... 各种 OneNet 参数亮点技巧:代码中使用了 C 语言特有的“字符串字面量自动拼接”特性来生成 Topic。
#define TOPIC_POST_REPLY "$sys/" PRODUCT_ID "/" DEVICE_NAME "/thing/property/post/reply"编译器会自动把它们组合成完整的字符串,彻底告别了使用 sprintf 拼接长字符串带来的内存消耗和易错问题。
2. 📥 串口中断接收基石
这部分相当于单片机的“信箱”。
volatile uint8_t uart_rx_byte; // 接收单字节的中转站
char uart_rx_buf[512]; // 存放完整消息的全局大信箱
volatile uint16_t uart_rx_len = 0; // 当前信箱里的信件长度
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { ... }原理解析:
ESP8266 发来的数据是一个字节一个字节过来的。每次收到一个字节,就会触发一次 RxCpltCallback 中断,代码将这个字节存入 uart_rx_buf,并在末尾补上 \0(方便后续作为 C 语言字符串处理),然后再开启下一次中断接收。
3. 🛠️ 辅助与显示工具集
这里包含 OLED 屏幕的刷新函数和最核心的串口发送函数。
void uart_print(UART_HandleTypeDef *huart, char* format, ...)
{
static char buf[512] = {0}; // 【防爆栈神器】
// ...
vsnprintf(buf, sizeof(buf), format, ap);
// ...
}原理解析:
这里使用了 vsnprintf 和可变参数 ...,让我们能像用 printf 一样给串口发数据。
核心安全设计:使用了 static char buf[512]。如果不加 static,这 512 字节会占用极为紧张的“栈(Stack)”内存,极易导致单片机崩溃重启。加上 static 后,它被移到了安全的全局静态内存区。
4. ⏳ 阻塞式指令发送函数 (仅用于初始化)
int8_t Send_Cmd_Wait_Resp_IT(UART_HandleTypeDef *huart, char *cmd, char *expected_resp, uint32_t timeout_ms, uint8_t max_retries)原理解析:
顾名思义:发送 -> 死等回复 -> 超时重试。
比如发了 AT+CWJAP 连 WiFi,必须死等它返回 OK 才能进行下一步。这个函数自带超时和重试机制(默认重试 3 次),并且一旦收到预期回复,会立刻清空接收缓存,防止污染后续的数据解析。它主要用在开机初始化阶段。
5. 🪄 核心黑科技:万能 JSON 构建器
这是整套代码最优雅的地方,专门解决 AT 指令发 JSON 时痛苦的转义问题。
void Build_OneNet_Cmd(char *out_buf, const char *topic, const char *msg_id, uint8_t param_count, ...)原理解析:
它接受不固定数量的参数(param_count)。内部通过 va_list 依次提取你要发送的属性名和数值。
- 遇到
'i':按整数处理 (%d) - 遇到
'f':按浮点数处理 (%.2f) - 遇到
's':按字符串处理,并自动在两边加上\"转义保护。 - 自动在多个参数中间插入
\,转义逗号。
只需一行代码,就能拼出绝对符合 OneNet 和 ESP8266 规范的超级嵌套 JSON。
6. 🚀 开机初始化流程 (mycode_run 上半部分)
单片机上电后,开始按部就班地唤醒 ESP8266:
- 复位模块 (
AT+RST):并盲等 2 秒跳过乱码期。 - 入网与连接:配置 Station 模式 -> 连 WiFi -> 配置 MQTT Token -> 连 OneNet。
- 订阅话题:订阅
post/reply(上报回执)和set(云端控制下发)。 - 在每一个步骤间,都会通过
Update_Top_Status(步骤号)在 OLED 屏幕左上角实时显示进度,卡在哪一步一目了然。
7. ♾️ 异步主循环 (mycode_run 下半部分)
初始化完成后进入 while(1) 死循环,这里主要并发处理两件事:
任务 A:定时非阻塞上报(发送)
if(uwTick / 1000 % 2 == 0) { ... } // 每 2 秒触发一次调用 Build_OneNet_Cmd 生成数据后,直接 uart_print 甩给串口发送,绝对不死等云端的 reply,直接去干别的事。
任务 B:异步指令解析(接收)
if (uart_rx_len > 0) {
HAL_Delay(50); // 防数据碎片化断层
// ...
}只要信箱里有数据,先等 50ms 让数据彻底收完。
如果是云端下发了控制指令 (/set):
- 执行动作:用
strstr寻找"LED":true,执行开灯或关灯。 - 提取 ID 并回复:提取这串长 JSON 里的
msg_id,并立刻向set_reply话题发布状态码200,告诉云端“我已办妥”。
最后,无论收到什么乱七八糟的数据,统统清空信箱 (memset),轻装上阵迎接下一轮通信。
第三阶段--android studio-app开发
第二阶段完成前禁止观看下面的内容
app使用Android Studio进行开发,代码我全程使用ai写的,这里主要用于命令下发来控制stm32,之前使用的平台的应用模拟器,明显是不方便的,所以可以做一个app来完成应用模拟器所具有的功能,这里简单讲解一下源码:
一、整体介绍



其他均默认操作
具体来说,这个App可以:
- 控制设备:比如点一下手机上的“开灯”按钮,灯就亮了;点“关灯”,灯就灭了。
- 查询设备数据:比如输入一个你想查的数据名字(例如温度、湿度),App就会去问设备当前是多少度,然后把结果显示在屏幕上。
设备和手机之间不是直接连接的,而是通过一个叫 OneNET 的云平台(可以理解为一个专门管理设备的中转站)来传递消息。你的手机把指令发给OneNET,OneNET再转发给设备;设备上报的数据也会先到OneNET,然后你的手机去OneNET查询。
所以,这个App其实是一个云平台的客户端,它通过 HTTP协议 和云平台说话。
二、代码整体结构
代码是用 Kotlin 语言写的(Android开发常用语言)。一个App的界面和功能通常写在 MainActivity 这个类里。可以把这个类想象成一个房子的设计图,里面有各种房间(变量)和功能(方法)。
代码主要分成几大块:
- 开头部分:导入需要用到的工具包(好比建房子前要买好砖头、水泥)。
MainActivity类:这是主界面,包含了:- 成员变量:界面上那些输入框、按钮、显示文字的控件(就像房子里的门窗、开关)。
onCreate方法:App启动时自动执行,负责“装修”房子,把设计图变成真实的样子。initViews方法:把代码里的变量和界面上的控件对应起来(比如找到那个叫“开灯”的按钮,把它和变量btnLightOn绑定)。setupListeners方法:给按钮设置“监听器”,也就是告诉按钮,当用户点击你时,要执行什么动作。queryDataViaHttp方法:查询设备数据的具体逻辑。sendCommandViaHttp方法:控制设备的具体逻辑。log方法:在屏幕上显示日志信息,方便我们看App运行的过程。
下面我们逐行拆解。
三、导入工具包(import ...)
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit这些 import 语句就像你做饭前要准备食材。每一行表示我们要用到一个“工具”:
android.os.Bundle等:是安卓系统自带的基础工具,用来创建界面、操作按钮、文本框等。okhttp3.*:这是一个别人写好的、非常流行的网络通信工具包。我们的App要上网和OneNET说话,就靠它。org.json.JSONObject:这是处理JSON格式数据的工具。什么是JSON?就像一种大家约定好的“书信格式”,比如{"name":"小明","age":18},这样机器能看懂。java.io.IOException:处理输入输出错误(比如网络断了)。java.util.concurrent.TimeUnit:用来设置超时时间,比如等10秒没反应就认为失败。
四、定义 MainActivity 类
class MainActivity : AppCompatActivity() {
...
}这行代码定义了一个叫 MainActivity 的类,它继承自 AppCompatActivity。简单理解:AppCompatActivity 是安卓提供的一个“基础房子模板”,我们在此基础上装修成自己的房子。
五、成员变量
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()这里创建了一个 OkHttpClient 对象,并给它起名叫 client。这个 client 就是负责发送网络请求的“邮递员”。我们告诉这个邮递员:每次送信,最多等10秒连接、10秒发送、10秒接收,超时就不等了。
接着定义了很多UI组件(界面上的控件):
private lateinit var etProductId: EditText
private lateinit var etDeviceName: EditText
private lateinit var etAuthToken: EditText
private lateinit var btnLightOn: Button
private lateinit var btnLightOff: Button
private lateinit var etQueryId: EditText
private lateinit var btnQuery: Button
private lateinit var tvResult: TextView
private lateinit var tvLog: TextView
private lateinit var scrollLog: androidx.core.widget.NestedScrollViewetProductId:输入框,让你输入“产品ID”(每个设备都有一个唯一标识)。etDeviceName:输入设备名字。etAuthToken:输入授权令牌(相当于密码,证明你有权限操作这个设备)。btnLightOn、btnLightOff:开灯和关灯按钮。etQueryId:查询时输入你要查的数据名字(比如温度标识符叫temp)。btnQuery:查询按钮。tvResult:显示查询结果的文本框。tvLog:显示日志的文本框。scrollLog:一个可以滚动的容器,包住日志框,防止日志太多显示不全。lateinit var的意思是“这个变量我先声明,稍后再初始化”,因为要到onCreate里才能找到这些控件。
六、onCreate 方法
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initViews()
setupListeners()
}onCreate 是Activity的生命周期方法,当这个界面被创建时自动执行。好比你一进房子,自动开灯。
setContentView(R.layout.activity_main):加载界面布局文件(activity_main.xml),把设计好的按钮、输入框摆出来。initViews():调用下面的方法,把代码里的变量和界面上的实际控件连接起来。setupListeners():调用下面的方法,给按钮设置点击监听。
七、initViews 方法
private fun initViews() {
etProductId = findViewById(R.id.et_product_id)
etDeviceName = findViewById(R.id.et_device_name)
etAuthToken = findViewById(R.id.et_auth_token)
btnLightOn = findViewById(R.id.btn_light_on)
btnLightOff = findViewById(R.id.btn_light_off)
etQueryId = findViewById(R.id.et_query_id)
btnQuery = findViewById(R.id.btn_query)
tvResult = findViewById(R.id.tv_result)
tvLog = findViewById(R.id.tv_log)
scrollLog = findViewById(R.id.scroll_log)
}findViewById 是一个“找东西”的方法。它根据你在布局文件里给每个控件起的ID(比如 et_product_id),找到这个控件,然后赋值给对应的变量。这样,后面代码里操作 etProductId,就相当于操作界面上的那个输入框。
八、setupListeners 方法
private fun setupListeners() {
btnLightOn.setOnClickListener { sendCommandViaHttp(true) }
btnLightOff.setOnClickListener { sendCommandViaHttp(false) }
btnQuery.setOnClickListener { queryDataViaHttp() }
}setOnClickListener 就是“设置点击监听器”。括号里的 { ... } 是一个代码块,当按钮被点击时,就会执行里面的代码。
- 点“开灯”按钮,就调用
sendCommandViaHttp(true)(true表示开)。 - 点“关灯”按钮,就调用
sendCommandViaHttp(false)(false表示关)。 - 点“查询”按钮,就调用
queryDataViaHttp()。
九、查询数据的方法(queryDataViaHttp)
这个方法很长,我们分段解释。
1. 获取用户输入
val productId = etProductId.text.toString().trim()
val deviceName = etDeviceName.text.toString().trim()
val authToken = etAuthToken.text.toString().trim()
val targetIdentifier = etQueryId.text.toString().trim()从输入框里拿到用户填的内容,.text 得到输入的文字,.toString() 转成字符串,.trim() 去掉开头和结尾的空格。
2. 检查是否为空
if (productId.isEmpty() || deviceName.isEmpty() || authToken.isEmpty() || targetIdentifier.isEmpty()) {
log("⚠️ 请先填写完整配置参数和需要查询的标识符!")
return
}如果任何一项为空,就调用 log 方法在日志框里显示警告,然后 return 退出方法,不再执行后续代码。
3. 构造请求的网址
val url = "https://iot-api.heclouds.com/thingmodel/query-device-property?product_id=$productId&device_name=$deviceName"这里拼接了一个网址(URL),格式是固定的,由OneNET平台提供。$productId 和 $deviceName 会把用户输入的值替换进去。这个网址告诉OneNET:我想查询某个产品的某个设备的属性。
4. 更新界面提示
log("⏳ 正在查询 [$targetIdentifier] 的实时数据...")
tvResult.text = "查询中..."
tvResult.setTextColor(android.graphics.Color.parseColor("#8E8E93"))在日志框里显示“正在查询...”,然后把结果文本框的文字改为“查询中...”,颜色设为灰色,提示用户正在等待。
5. 创建 HTTP 请求对象
val request = Request.Builder()
.url(url)
.get()
.addHeader("Authorization", authToken)
.build()这相当于写一封信:
- 收信地址(url)就是上面构造的网址。
- 请求方法是
GET(表示要获取数据)。 - 加一个“头信息”(Header),里面放
Authorization(授权),值就是用户输入的令牌(密码)。OneNET收到信后,会验证这个令牌是否有效。
6. 发送请求(异步)
client.newCall(request).enqueue(object : Callback {
...
})client 就是之前创建的“邮递员”。newCall(request) 把信交给邮递员,.enqueue(...) 意思是:邮递员你拿着信出发,但不要堵在门口等回信,我留个电话给你,有回复了打我电话(也就是回调)。这样App不会卡住,界面还能继续操作。object : Callback 是创建了一个匿名的回调对象,里面有两个必须实现的方法:onFailure 和 onResponse。
7. onFailure 方法(请求失败时回调)
override fun onFailure(call: Call, e: IOException) {
log("❌ 查询失败 (网络错误): ${e.message}")
runOnUiThread { tvResult.text = "网络错误" }
}如果网络不通、超时等,就会执行这里。log 显示失败信息,然后用 runOnUiThread 更新界面(因为网络回调是在后台线程,不能直接修改UI,所以要用这个切换到主线程),把结果显示为“网络错误”。
8. onResponse 方法(收到回复时回调)
override fun onResponse(call: Call, response: Response) {
val responseBodyStr = response.body?.string() ?: ""
runOnUiThread {
if (response.isSuccessful) {
...
} else {
tvResult.text = "请求失败"
log("⚠️ HTTP 错误代码: ${response.code}\n错误详情: $responseBodyStr")
}
}
}当OneNET服务器返回了内容(无论成功还是失败),就会执行这里。
response.body?.string()把返回的内容(JSON格式)读出来,如果为空则给空字符串。- 用
runOnUiThread切换到主线程更新UI。 response.isSuccessful判断HTTP状态码是否成功(如200)。如果成功,则处理成功的数据;否则显示错误代码。
成功时的处理
log("✅ 查询成功!\n返回: $responseBodyStr")
try {
val rootObj = JSONObject(responseBodyStr)
if (rootObj.optInt("code") == 0) {
val dataArray = rootObj.optJSONArray("data")
var foundValue: String? = null
if (dataArray != null) {
for (i in 0 until dataArray.length()) {
val item = dataArray.getJSONObject(i)
if (item.optString("identifier") == targetIdentifier) {
foundValue = item.optString("value")
break
}
}
}
if (foundValue != null) {
tvResult.text = foundValue
tvResult.setTextColor(android.graphics.Color.parseColor("#FF9500"))
} else {
tvResult.text = "暂无数据"
tvResult.setTextColor(android.graphics.Color.parseColor("#FF3B30"))
log("⚠️ 在云端未找到标识符为 [$targetIdentifier] 的数据。")
}
} else {
tvResult.text = "查询出错"
log("⚠️ 云端报错: ${rootObj.optString("msg")}")
}
} catch (e: Exception) {
tvResult.text = "解析异常"
log("❌ JSON 解析错误: ${e.message}")
}这段代码是解析服务器返回的JSON数据。
- 先把整个返回的字符串
responseBodyStr变成一个JSONObject对象(这样就能方便地取里面的字段)。 rootObj.optInt("code")取里面的code字段,如果为0表示业务成功(OneNET自己的业务码)。- 如果成功,再取
data数组,里面包含了所有属性的最新值。 - 然后用一个循环遍历数组,找到我们想要的那个
identifier(比如用户输入的temp),取出它的value。 - 如果找到了,就显示在结果文本框;没找到就显示“暂无数据”。
- 如果
code不是0,说明业务失败,显示错误信息。 - 如果解析过程中发生异常(比如返回的不是合法JSON),就捕获异常,显示“解析异常”。
十、控制设备的方法(sendCommandViaHttp)
这个方法稍微复杂一点,因为它有自动重试机制。
1. 获取输入并检查
val productId = etProductId.text.toString().trim()
val deviceName = etDeviceName.text.toString().trim()
val authToken = etAuthToken.text.toString().trim()
if (productId.isEmpty() || deviceName.isEmpty() || authToken.isEmpty()) {
log("⚠️ 请先填写完整 Product ID、Device Name 和 API Token!")
return
}和查询类似,先取输入,检查空值。
2. 构造 URL 和 JSON 数据
val url = "https://iot-api.heclouds.com/thingmodel/set-device-property"
val jsonBody = JSONObject().apply {
put("product_id", productId)
put("device_name", deviceName)
val params = JSONObject().apply {
put("LED", turnOn) // 注意这里用的是 LED
}
put("params", params)
}.toString()- 控制指令的网址是固定的,用来设置设备属性。
- 我们要构造一个JSON对象,包含产品ID、设备名、以及要设置的参数。参数也是一个JSON对象,里面放
LED这个属性(这是物模型里定义好的属性名字),值就是turnOn(true或false)。 .toString()把整个JSON变成字符串,比如{"product_id":"123","device_name":"dev1","params":{"LED":true}}。
3. 判断是否需要首次日志
if (retryCount == 3) {
log("⏳ 正在发送控制指令...\n内容: $jsonBody")
}这个 retryCount 参数是重试次数,默认是3(从 sendCommandViaHttp(true) 调用时没传,所以是3)。只有第一次调用时(重试计数为3)才打印日志,重试时不重复打印。
4. 创建请求对象
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request.Builder()
.url(url)
.post(requestBody)
.addHeader("Authorization", authToken)
.build()- 因为要发送数据,所以请求方法是
POST。 - 把JSON字符串封装成
requestBody,并指定内容类型为application/json。 - 同样加上授权头。
5. 发送请求(异步)
client.newCall(request).enqueue(object : Callback {
...
})和查询一样,异步发送。
onFailure 中的重试逻辑
override fun onFailure(call: Call, e: IOException) {
if (retryCount > 0) {
log("⚠️ 网络异常或超时,正在重试... (剩余 $retryCount 次)")
sendCommandViaHttp(turnOn, retryCount - 1)
} else {
log("❌ 控制请求彻底失败: ${e.message}")
}
}如果请求失败(比如超时、网络中断),并且还有剩余重试次数(retryCount > 0),就递归调用 sendCommandViaHttp,重试次数减1。这样可以自动重试最多3次,增加成功率。如果重试用完仍然失败,就记录彻底失败。
onResponse 中的处理
override fun onResponse(call: Call, response: Response) {
val responseBodyStr = response.body?.string() ?: ""
runOnUiThread {
if (response.isSuccessful) {
log("✅ 云端已接收指令!\n返回: $responseBodyStr")
} else {
log("⚠️ HTTP 错误代码: ${response.code}\n返回: $responseBodyStr")
}
}
}收到回复后,判断HTTP状态码,记录成功或失败信息。注意,这里不会重试,因为服务器已经返回了响应(可能是业务错误),重试无意义。
十一、日志方法(log)
private fun log(message: String) {
runOnUiThread {
val time = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())
tvLog.append("[$time] $message\n\n")
scrollLog.post { scrollLog.fullScroll(android.view.View.FOCUS_DOWN) }
}
}这个方法用来在 tvLog 文本框里添加一行日志,并自动滚动到底部。
- 获取当前时间(时:分:秒格式)。
- 用
tvLog.append在已有文本后面追加新的日志。 scrollLog.post { ... }让滚动容器自动滚到底部,方便看到最新日志。
十二、总结
这个App做的事情就是:
- 用户输入必要的信息(产品ID、设备名、授权令牌)。
- 点击“开/关灯”按钮,App构造一个HTTP POST请求,把指令(LED:true/false)发给OneNET平台。
- 点击“查询”按钮,App构造HTTP GET请求,从OneNET获取设备属性,并在结果框里显示。
- 所有请求都是异步的,不会卡住界面,并用日志框显示操作过程和结果。
- 控制指令有自动重试机制,防止因为网络抖动导致失败。
通过这个例子,你可以看到:
- HTTP请求:就像给服务器发消息。
- JSON:是双方约定好的消息格式。
- 回调:是当服务器回复时,通知App的方式。
- UI更新必须在主线程:
runOnUiThread保证了这一点。
十三、二次开发
我提供的这个代码可以当成一个应用demo,你可以将代码复制给ai(ui和程序),告诉ai你的需求即可,这个demo也是全程用ai编写的




