因为搭了一个大棚,为了方便检测大棚的信息,使用DFRobot FireBeetle 2 ESP32-C5,做了一个大棚环境监测系统,具体可见:FireBeetle 2 ESP32-C5之大棚环境监测系统
在之前的大棚环境监测系统系统中,我让ESP32-C5通过WiFi网络,连接到MQTT服务器,发布采集到的温湿度等信息。
现在,有了TKB-623,我进行了升级,让ESP32-C5通过TKB-623的远程无线通信功能,来发布采集到的信息。
相较于WiFi网络的方式,使用TKB-623无需再提供WiFi网络接入,远程采集数据更方便了。如果是在大规模种植农作物的大棚应用环境中,将大幅节省成本,简化检测设备部署环境。
下面,就分享基于TKB-623远程通信的大棚环境监测系统的具体过程。
一、系统设计
因为 FireBeetle 2 ESP32-C5之大棚环境监测系统 是在大棚中使用,并且集成了太阳能电池板,所以做了休眠和唤醒设计,系统的运行状态如图:

检测系统使用的传感器包括温湿度传感器、其他传感器、光线传感器,数据采集部分的逻辑流程如下:

系统的整体运行流程如下:

因为原有的已经实现了基于MQTT的通信,能够稳定运行了,我的做法是实现一个MQTT的伪装类,来接管MQTT的调用。
参考Arduino中Adafruit_MQTT的实现,做了TBK-623调用的伪造类实现,仅需要实现技术的功能即可。
参考之前 ESP32结合TKB-623评估板收发信息 中的处理,来做对应的具体实现。
初始化流程:
构造函数 → 引脚初始化 → 串口配置 → 模块唤醒 → AT命令测试 → 工作模式配置
数据发布流程:
publish() → 连接检查 → JSON序列化 → 十六进制转换 → AT+SENDB命令 → 响应验证
需要实现的类包括:


二、硬件准备
因为使用了电池和太阳能电池板,所以要考虑低功耗运行,实际运行时,让系统休眠300s后自动唤醒。
ESP32-C5本身支持深度随眠模式,并且 DFRobot FireBeetle 2 ESP32-C5 扩展板上,还有一个引脚,可以控制3.3V电压的输出:

具体位于扩展板上的如下位置:

另外,TKB-623本身也支持低功耗休眠模式:

可以通过AT指令,设置休眠唤醒参数:

因为是连接到了ESP32-C5,所以我使用了引脚唤醒模式:

唤醒引脚使用了GPIO_0:

要让TKB-623进入随眠模式,只需要使用下面的AT指令即可:

最终,具体的硬件连线如下:
| FireBeetle 2 ESP32-C5扩展板 |
传感器引脚 |
|---|
| 3V |
SHT30-VIN |
| GND |
SHT30-GND |
| C-10 |
SHT30-SCL |
| D-9 |
SHT30-SDA |
|
|
| 3V |
BMP180-3.3 |
| GND |
BMP180-GND |
| C-10 |
BMP180-SCL |
| D-9 |
BMP180-SDA |
|
|
| 3V |
光线传感器-3.3 |
| GND |
光线传感器-GND |
| 2 |
光线传感器-A |
|
|
| BAT+ |
锂电池+ |
| BAT- |
锂电池- |
| BAT_ADC-IO1 |
电池电压检测 |
|
|
| VIN:5V |
太阳能电池板+ |
| GND |
太阳能电池板- |
|
|
| SPI-24 |
TKB-623: UART_RXD |
| SPI-23 |
TKB-623: UART_TXD |
| SPI-3V3 |
TKB-623: 3V3_M |
| SPI-GND |
TKB-623: GND |
| 3 |
TKB-623: GPIO_0 |
三、代码开发
在 FireBeetle 2 ESP32-C5之大棚环境监测系统
中,已经提供了原有的MQTT版本的代码,这里主要说明TK8620_MQTT部分。
最终实现的`TK8620_MQTT.h如下:
#ifndef TK8620_MQTT_H
#define TK8620_MQTT_H
#include <Arduino.h>
#include <HardwareSerial.h>
#include <ArduinoJson.h>
#define TK8620_TX 24
#define TK8620_RX 23
#define TK8620_WAKEUP_PIN 3
#define TK8620_SERIAL_NUM 1
#define TK8620_AT_DELAY 200
enum TK8620_Error {
TK8620_CONNECTION_ACCEPTED = 0,
TK8620_CONNECTION_REFUSED_PROTOCOL_VERSION = 1,
TK8620_CONNECTION_REFUSED_IDENTIFIER_REJECTED = 2,
TK8620_CONNECTION_REFUSED_SERVER_UNAVAILABLE = 3,
TK8620_CONNECTION_REFUSED_BAD_USERNAME_PASSWORD = 4,
TK8620_CONNECTION_REFUSED_NOT_AUTHORIZED = 5,
TK8620_CONNECTION_TIMEOUT = -1,
TK8620_CONNECTION_LOST = -2,
TK8620_CONNECTION_FAILED = -3
};
class TK8620_MQTT_Client {
private:
HardwareSerial* _serial;
bool _connected;
bool _sleeping;
String _server;
uint16_t _port;
String _username;
String _password;
bool configureModule();
bool sendATCommand(const char* command);
bool sendATCommandWithRetry(const char* command, int retries = 3);
String readSerialResponse();
String stringToHex(const String& input);
void wakeupModule();
void sleepModule();
public:
TK8620_MQTT_Client(void* client, const char* server, uint16_t port, const char* username, const char* password);
bool connected();
bool isSleeping();
int8_t connect();
void disconnect();
bool publish(const char* topic, const char* message);
void enterSleep();
void wakeup();
const char* connectErrorString(int8_t error);
};
class TK8620_MQTT_Publish {
private:
TK8620_MQTT_Client* _client;
String _topic;
public:
TK8620_MQTT_Publish(TK8620_MQTT_Client* client, const char* topic);
bool publish(const char* message);
bool publish(float message);
bool publish(int message);
bool publish(uint32_t message);
};
TK8620_MQTT_Client::TK8620_MQTT_Client(void* client, const char* server, uint16_t port,
const char* username, const char* password) {
_serial = &Serial1;
_connected = false;
_sleeping = true;
_server = String(server);
_port = port;
_username = String(username);
_password = String(password);
pinMode(TK8620_WAKEUP_PIN, OUTPUT);
digitalWrite(TK8620_WAKEUP_PIN, LOW);
}
void TK8620_MQTT_Client::wakeupModule() {
if (_sleeping) {
Serial.println("? 唤醒TK8620模块...");
digitalWrite(TK8620_WAKEUP_PIN, HIGH);
delay(100);
_sleeping = false;
unsigned long startTime = millis();
while (millis() - startTime < 3000) {
if (sendATCommand("AT")) {
Serial.println("✅ TK8620模块已唤醒并就绪");
break;
}
delay(TK8620_AT_DELAY);
}
}
}
void TK8620_MQTT_Client::sleepModule() {
if (!_sleeping) {
Serial.println("? 让TK8620模块进入休眠...");
if (sendATCommand("AT+ENTERSLEEP")) {
Serial.println("✅ TK8620休眠命令发送成功");
} else {
Serial.println("⚠️ TK8620休眠命令发送失败,强制休眠");
}
digitalWrite(TK8620_WAKEUP_PIN, LOW);
_sleeping = true;
_connected = false;
while(_serial->available()) {
_serial->read();
}
}
}
bool TK8620_MQTT_Client::configureModule() {
Serial.println("? 配置TK8620模块为变长突发模式...");
bool success = true;
if (!sendATCommandWithRetry("AT", 10)) {
Serial.println("❌ AT测试失败");
success = false;
} else {
Serial.println("✅ AT测试成功");
}
delay(TK8620_AT_DELAY);
if (!sendATCommandWithRetry("AT+WAKEUPCFG=0,1")) {
success = false;
Serial.println("❌ 唤醒配置失败");
} else {
Serial.println("✅ 唤醒配置成功");
}
delay(TK8620_AT_DELAY);
if (!sendATCommandWithRetry("AT+WORKMODE=21")) {
success = false;
Serial.println("❌ 工作模式设置失败");
}
delay(TK8620_AT_DELAY);
if (!sendATCommandWithRetry("AT+FREQ=490300000,490300000,490300000")) {
success = false;
Serial.println("❌ 频率设置失败");
}
delay(TK8620_AT_DELAY);
if (!sendATCommandWithRetry("AT+TXP=15")) {
success = false;
Serial.println("❌ 发射功率设置失败");
}
delay(TK8620_AT_DELAY);
if (!sendATCommandWithRetry("AT+RATE=6")) {
success = false;
Serial.println("❌ 通信速率设置失败");
}
delay(TK8620_AT_DELAY);
if (!sendATCommandWithRetry("AT+ADDRFILTER=0")) {
success = false;
Serial.println("❌ 地址过滤设置失败");
}
delay(TK8620_AT_DELAY);
if (success) {
Serial.println("✅ TK8620变长突发模式配置完成");
_connected = true;
} else {
Serial.println("⚠️ TK8620配置有部分失败,但将继续运行");
_connected = true;
}
return success;
}
bool TK8620_MQTT_Client::sendATCommandWithRetry(const char* command, int retries) {
for (int i = 0; i < retries; i++) {
if (sendATCommand(command)) {
return true;
}
Serial.print("? 重试命令 (");
Serial.print(i + 1);
Serial.print("/");
Serial.print(retries);
Serial.println(")...");
delay(TK8620_AT_DELAY);
}
return false;
}
bool TK8620_MQTT_Client::sendATCommand(const char* command) {
wakeupModule();
while(_serial->available()) {
_serial->read();
}
_serial->println(command);
#if (DEBUG_MODE == 1)
Serial.print("? 发送AT: ");
Serial.println(command);
#endif
String response = readSerialResponse();
if (response.length() > 0) {
#if (DEBUG_MODE == 1)
Serial.print("? 原始响应: ");
for (unsigned int i = 0; i < response.length(); i++) {
char c = response[i];
if (c == '\r') {
Serial.print("\\r");
} else if (c == '\n') {
Serial.print("\\n");
} else {
Serial.print(c);
}
}
Serial.println();
#endif
if (response.indexOf("AT_OK") >= 0) {
#if (DEBUG_MODE == 1)
Serial.println("✅ 检测到AT_OK");
#endif
return true;
} else if (response.indexOf("OK") >= 0) {
#if (DEBUG_MODE == 1)
Serial.println("✅ 检测到OK");
#endif
return true;
} else if (response.indexOf("ERROR") >= 0) {
#if (DEBUG_MODE == 1)
Serial.println("❌ 检测到ERROR");
#endif
}
} else {
#if (DEBUG_MODE == 1)
Serial.println("❌ 无响应");
#endif
}
return false;
}
String TK8620_MQTT_Client::readSerialResponse() {
String response = "";
unsigned long startTime = millis();
while (millis() - startTime < 3000) {
if (_serial->available()) {
break;
}
delay(10);
}
while (millis() - startTime < 3000) {
if (_serial->available()) {
char c = _serial->read();
response += c;
if (response.indexOf("AT_OK") >= 0 || response.indexOf("AT_ERROR") >= 0) {
delay(100);
break;
}
unsigned long lastDataTime = millis();
while (millis() - lastDataTime < 200) {
if (_serial->available()) {
break;
}
delay(10);
}
if (millis() - lastDataTime >= 200) {
break;
}
} else {
delay(10);
}
}
return response;
}
String TK8620_MQTT_Client::stringToHex(const String& input) {
String hexString = "";
for (size_t i = 0; i < input.length(); i++) {
char hex[3];
sprintf(hex, "%02X", (unsigned char)input[i]);
hexString += hex;
}
return hexString;
}
bool TK8620_MQTT_Client::connected() {
return _connected && !_sleeping;
}
bool TK8620_MQTT_Client::isSleeping() {
return _sleeping;
}
int8_t TK8620_MQTT_Client::connect() {
if (_connected && !_sleeping) {
return TK8620_CONNECTION_ACCEPTED;
}
wakeupModule();
_serial->begin(115200, SERIAL_8N1, TK8620_RX, TK8620_TX);
delay(2000);
_serial->flush();
while(_serial->available()) {
_serial->read();
}
Serial.println("? 初始化TK8620无线模块...");
Serial.print(" TK8620 TX -> ESP32 引脚 ");
Serial.println(TK8620_TX);
Serial.print(" TK8620 RX -> ESP32 引脚 ");
Serial.println(TK8620_RX);
Serial.print(" TK8620 WAKEUP -> ESP32 引脚 ");
Serial.println(TK8620_WAKEUP_PIN);
if (configureModule()) {
return TK8620_CONNECTION_ACCEPTED;
} else {
return TK8620_CONNECTION_FAILED;
}
}
void TK8620_MQTT_Client::disconnect() {
_connected = false;
}
bool TK8620_MQTT_Client::publish(const char* topic, const char* message) {
if (!_connected || _sleeping) {
if (connect() != TK8620_CONNECTION_ACCEPTED) {
return false;
}
}
StaticJsonDocument<200> doc;
doc["t"] = topic;
doc["m"] = message;
String jsonString;
serializeJson(doc, jsonString);
#if (DEBUG_MODE == 1)
Serial.println("\n? 准备通过TK8620发送数据:");
Serial.print(" 主题: ");
Serial.println(topic);
Serial.print(" 消息: ");
Serial.println(message);
Serial.print(" 原始JSON: ");
Serial.println(jsonString);
#endif
String hexData = stringToHex(jsonString);
#if (DEBUG_MODE == 1)
Serial.print(" 十六进制数据: ");
Serial.println(hexData);
#endif
int retries = 3;
bool ret = false;
for(int i=0; i<retries; i++) {
String atCommand = "AT+SENDB=" + hexData;
_serial->println(atCommand);
#if (DEBUG_MODE == 1)
if (i>0) {
Serial.print("? 重试AT命令: ");
} else {
Serial.print("? 发送AT命令: ");
}
Serial.println(atCommand);
#endif
String response = readSerialResponse();
if (response.indexOf("AT_OK") >= 0) {
Serial.println("✅ TK8620发送成功!");
ret = true;
break;
} else {
Serial.println("❌ TK8620发送失败!");
#if (DEBUG_MODE == 1)
Serial.print(" 响应: ");
Serial.println(response);
#endif
}
delay(TK8620_AT_DELAY);
}
return ret;
}
void TK8620_MQTT_Client::enterSleep() {
sleepModule();
}
void TK8620_MQTT_Client::wakeup() {
wakeupModule();
}
const char* TK8620_MQTT_Client::connectErrorString(int8_t error) {
switch (error) {
case TK8620_CONNECTION_ACCEPTED:
return "Connection accepted";
case TK8620_CONNECTION_REFUSED_PROTOCOL_VERSION:
return "Connection refused, unacceptable protocol version";
case TK8620_CONNECTION_REFUSED_IDENTIFIER_REJECTED:
return "Connection refused, identifier rejected";
case TK8620_CONNECTION_REFUSED_SERVER_UNAVAILABLE:
return "Connection refused, server unavailable";
case TK8620_CONNECTION_REFUSED_BAD_USERNAME_PASSWORD:
return "Connection refused, bad user name or password";
case TK8620_CONNECTION_REFUSED_NOT_AUTHORIZED:
return "Connection refused, not authorized";
case TK8620_CONNECTION_TIMEOUT:
return "Connection timeout";
case TK8620_CONNECTION_LOST:
return "Connection lost";
case TK8620_CONNECTION_FAILED:
return "Connection failed";
default:
return "Unknown error";
}
}
TK8620_MQTT_Publish::TK8620_MQTT_Publish(TK8620_MQTT_Client* client, const char* topic) {
_client = client;
_topic = String(topic);
}
bool TK8620_MQTT_Publish::publish(const char* message) {
return _client->publish(_topic.c_str(), message);
}
bool TK8620_MQTT_Publish::publish(float message) {
char buffer[20];
dtostrf(message, 1, 2, buffer);
return publish(buffer);
}
bool TK8620_MQTT_Publish::publish(int message) {
char buffer[20];
itoa(message, buffer, 10);
return publish(buffer);
}
bool TK8620_MQTT_Publish::publish(uint32_t message) {
char buffer[20];
utoa(message, buffer, 10);
return publish(buffer);
}
#endif
这部分的代码逻辑,在前面已经说明过了。
在主代码中,添加一个选项,表示是否启用:
#define ENABLE_TK8620 1
然后,根据定义来调用对应的头文件即可:
#if (ENABLE_MQTT == 1 && ENABLE_TK8620 == 0)
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"
#elif (ENABLE_TK8620 == 1)
#include "TK8620_MQTT.h"
#define Adafruit_MQTT_Client TK8620_MQTT_Client
#define Adafruit_MQTT_Publish TK8620_MQTT_Publish
#endif
最后,在ESP32-C5进入深度睡眠之前,让TKB-623先进入休眠模式:
#if (ENABLE_SLEEP == 1)
if (loopCnt >= 1 && !skipSleep) {
Serial.println("进入休眠状态," + String(TIME_TO_SLEEP) + "秒后自动唤醒");
Serial.println("长按BOOT按钮可取消休眠");
Serial.flush();
#if (ENABLE_MQTT == 1)
delay(1000);
if (mqtt.connected()) {
mqtt.disconnect();
}
#if (ENABLE_TK8620 == 0)
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
#elif (ENABLE_TK8620 == 1)
mqtt.enterSleep();
Serial.println("✅ TK8620模块已进入休眠");
#endif
#endif
digitalWrite(POWER_PIN, LOW);
esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
esp_deep_sleep_start();
}
#endif
经过这样的处理,同一套代码,能够兼容MQTT和TKB-623两种数据发布的模式。
然后,PC端,参考 ESP32结合TKB-623评估板收发信息,将收到的json信息解码,然后发布到MQTT即可。
四、实测运行
修改代码后,编译烧录到ESP32-C5,运行效果如下:

ESP32-C5启动/唤醒后,会自动进行数据采集,并唤醒TKB-623,并进行运行模式设置:

然后发送采集到的传感器的数据:

发送完成后,会自动进入深度随眠,并让TKB-623休眠:

而在pc接收端,启动后,会线进行TKB-623工作模式配置:

然后,接受到数据后,会转发到MQTT服务器:

在MQTT服务上,也可以看到定时发布的信息:

五、部署运行
测试完成后,将设备放置到大棚里面:

就可以通过手持终端,查看检测大棚的状态了:
