396 lines
14 KiB
C
396 lines
14 KiB
C
/* sensor.c - 485 传感器采集(采集终端核心模块)
|
||
*
|
||
* 硬件: USART2 接 485 总线,PA2(TX), PA3(RX),9600-8N1,DMA+IDLE 定帧接收。
|
||
* 协议: Modbus RTU 读保持寄存器(0x03),各传感器地址/寄存器见 sensor.h 表头注释。
|
||
* 业务: 每 10s 本地查询一遍传感器刷新缓存(供 OLED 实时显示);
|
||
* 上传调度锚定在"OTA 已确认"(联网就绪)时刻:+5s/+30s/+60s 各一次,之后每 5 分钟一次。
|
||
* 传感器无应答不影响其它传感器,离线传感器跳过不上报。
|
||
*/
|
||
|
||
#include "sensor.h"
|
||
#include "adc.h"
|
||
#include "uart.h"
|
||
#include "log.h"
|
||
#include "network.h"
|
||
#include "FreeRTOS.h"
|
||
#include "task.h"
|
||
#include <string.h>
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
sensor_data_t g_sensor;
|
||
uint16_t g_sensor_online = 0;
|
||
|
||
/*==== Modbus 底层 ====*/
|
||
|
||
static uint16_t crc16_modbus(const uint8_t *data, uint16_t len)
|
||
{
|
||
uint16_t crc = 0xFFFF;
|
||
for (uint16_t i = 0; i < len; i++) {
|
||
crc ^= data[i];
|
||
for (uint8_t j = 0; j < 8; j++) {
|
||
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : (crc >> 1);
|
||
}
|
||
}
|
||
return crc;
|
||
}
|
||
|
||
/* 发 8 字节查询并等一帧应答(IDLE 定帧)。
|
||
* 返回帧长(>0),0=超时无帧。自动跳过自己发出去的 485 回声帧。 */
|
||
static int sensor_query(const uint8_t *req, uint8_t *resp, uint16_t max_len, uint32_t timeout_ms)
|
||
{
|
||
USART2_FlushRxBuf(); /* 清软件缓冲残留(回声/垃圾),硬件接收常开 */
|
||
HAL_UART_Transmit(&huart2, (uint8_t *)req, 8, 100);
|
||
|
||
uint32_t t0 = HAL_GetTick();
|
||
while ((HAL_GetTick() - t0) < timeout_ms) {
|
||
int n = USART2_PollFrame(resp, max_len);
|
||
if (n > 0) {
|
||
if (n == 8 && memcmp(resp, req, 8) == 0) continue; /* 回声,继续等真应答 */
|
||
return n;
|
||
}
|
||
vTaskDelay(2);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static void dump_frame(const char *tag, const uint8_t *buf, int len)
|
||
{
|
||
char hex[3 * 32 + 1] = {0};
|
||
int n = len > 32 ? 32 : len;
|
||
for (int i = 0; i < n; i++) snprintf(hex + i * 3, 4, "%02X ", buf[i]);
|
||
log_info("> SENSOR: %s 收 %d 字节: %s", tag, len, hex);
|
||
}
|
||
|
||
/* 读保持寄存器:addr 从机地址,start 起始寄存器,num 寄存器数。
|
||
* 成功返回 0 且 resp 为完整应答帧/长度 resp_len;失败返回非 0(1=无应答 2=CRC错 3=帧异常) */
|
||
static int modbus_read(uint8_t addr, uint16_t start, uint8_t num,
|
||
uint8_t *resp, uint16_t *resp_len, const char *tag)
|
||
{
|
||
uint8_t req[8] = { addr, 0x03,
|
||
(uint8_t)(start >> 8), (uint8_t)(start & 0xFF),
|
||
0x00, num, 0x00, 0x00 };
|
||
uint16_t crc = crc16_modbus(req, 6);
|
||
req[6] = (uint8_t)(crc & 0xFF);
|
||
req[7] = (uint8_t)(crc >> 8);
|
||
|
||
uint16_t expect = (uint16_t)(5 + num * 2);
|
||
int n = sensor_query(req, resp, 40, 400);
|
||
if (n <= 0) {
|
||
log_warn("> SENSOR: %s 无应答", tag);
|
||
return 1;
|
||
}
|
||
dump_frame(tag, resp, n);
|
||
if (n != (int)expect || resp[1] != 0x03) {
|
||
log_warn("> SENSOR: %s 帧异常(长%d 期望%d)", tag, n, (int)expect);
|
||
return 3;
|
||
}
|
||
uint16_t calc = crc16_modbus(resp, (uint16_t)(n - 2));
|
||
uint16_t recv = (uint16_t)resp[n - 1] << 8 | resp[n - 2];
|
||
if (calc != recv) {
|
||
log_warn("> SENSOR: %s CRC错(calc=%04X recv=%04X)", tag, calc, recv);
|
||
return 2;
|
||
}
|
||
*resp_len = (uint16_t)n;
|
||
return 0;
|
||
}
|
||
|
||
/* 有符号温度解析:高字节 0xFF 为负(参考工程按补码处理) */
|
||
static float parse_temp(uint8_t hi, uint8_t lo)
|
||
{
|
||
if (hi == 0xFF) {
|
||
lo = (uint8_t)(~lo + 1);
|
||
return -(float)(lo / 10.0f);
|
||
}
|
||
return (float)((hi * 256 + lo) / 10.0f);
|
||
}
|
||
|
||
/*==== 溶解氧 DO59 专用(UNESCO 公式,饱和度%换算 mg/L,盐度0)====*/
|
||
static float do59_mg_l(float per, float temp, float pressure)
|
||
{
|
||
float T = 273.15f + temp;
|
||
float lnX1 = -173.4292f + 249.6339f * (100.0f / T) + 143.3483f * logf(T / 100.0f) + (-21.8492f) * (T / 100.0f);
|
||
float X1 = expf(lnX1);
|
||
float U = powf(10.0f, 8.10765f - 1750.286f / (235.0f + temp));
|
||
float Phmg = pressure * 760.0f / 101.325f;
|
||
float X2 = (Phmg - U) / (760.0f - U);
|
||
return per * X1 * X2 * 1.4276f / 100.0f;
|
||
}
|
||
|
||
/* DO59 两阶段:先"启动测量"(0x2500),下次再"读数据"(0x2600 温度+饱和度)。
|
||
* 返回 0=本次读到有效数据,1=无数据(未启动完成/无应答/校验错) */
|
||
static int do59_read(void)
|
||
{
|
||
static uint8_t started = 0;
|
||
uint8_t resp[16];
|
||
uint16_t rlen = 0;
|
||
int rc;
|
||
|
||
if (!started) {
|
||
rc = modbus_read(0xFF, 0x2500, 1, resp, &rlen, "溶解氧启动");
|
||
if (rc == 0) {
|
||
started = 1;
|
||
log_info("> SENSOR: 溶解氧已启动测量");
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
rc = modbus_read(0xFF, 0x2600, 4, resp, &rlen, "溶解氧");
|
||
if (rc != 0) {
|
||
started = 0; /* 下次重新启动 */
|
||
return 1;
|
||
}
|
||
float temp, sat;
|
||
memcpy(&temp, &resp[3], 4); /* 温度 float 小端 */
|
||
memcpy(&sat, &resp[7], 4); /* 饱和度% float 小端 */
|
||
float press = ((g_sensor_online & (1u << 1)) && g_sensor.air_press > 0.0f)
|
||
? g_sensor.air_press : 101.325f; /* 大气压传感器在线用实测,否则标准大气压 */
|
||
g_sensor.water_t = temp;
|
||
g_sensor.do_sat = sat;
|
||
g_sensor.do_mgl = do59_mg_l(sat, temp, press);
|
||
return 0;
|
||
}
|
||
|
||
/*==== 各传感器读取 + 解码(0=成功)====*/
|
||
|
||
static int read_phec(void) /* 0x01 水体PH-EC */
|
||
{
|
||
uint8_t r[16]; uint16_t rl;
|
||
if (modbus_read(0x01, 0x0000, 2, r, &rl, "PH-EC") != 0) return 1;
|
||
g_sensor.ph = (float)(r[3] * 256 + r[4]) / 10.0f;
|
||
g_sensor.ec = (float)(r[5] * 256 + r[6]) / 10.0f;
|
||
return 0;
|
||
}
|
||
|
||
static int read_airpress(void) /* 0x02 大气压温湿度(王子壳),读 12 个寄存器 */
|
||
{
|
||
uint8_t r[40]; uint16_t rl;
|
||
if (modbus_read(0x02, 0x0000, 12, r, &rl, "大气压") != 0) return 1;
|
||
g_sensor.air_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
|
||
g_sensor.air_t = parse_temp(r[5], r[6]);
|
||
/* 气压在字节 24~26(3 字节大端,单位 0.01mbar) */
|
||
uint32_t ph = ((uint32_t)r[24] << 16) | ((uint32_t)r[25] << 8) | r[26];
|
||
g_sensor.air_press = ph / 100.0f / 10.0f; /* -> kPa */
|
||
return 0;
|
||
}
|
||
|
||
static int read_byx(void) /* 0x06 大气百叶箱,读 9 个寄存器 */
|
||
{
|
||
uint8_t r[32]; uint16_t rl;
|
||
if (modbus_read(0x06, 0x0000, 9, r, &rl, "百叶箱") != 0) return 1;
|
||
g_sensor.by_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
|
||
g_sensor.by_t = parse_temp(r[5], r[6]);
|
||
g_sensor.by_co2 = (uint16_t)(r[13] * 256 + r[14]);
|
||
g_sensor.by_hv = (uint16_t)(r[19] * 256 + r[20]);
|
||
return 0;
|
||
}
|
||
|
||
static int read_leaf(void) /* 0x07 叶面温湿度 */
|
||
{
|
||
uint8_t r[16]; uint16_t rl;
|
||
if (modbus_read(0x07, 0x0020, 2, r, &rl, "叶面温湿") != 0) return 1;
|
||
g_sensor.leaf_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
|
||
g_sensor.leaf_t = parse_temp(r[5], r[6]);
|
||
return 0;
|
||
}
|
||
|
||
static int read_soilth(void) /* 0x08 土壤温湿度 */
|
||
{
|
||
uint8_t r[16]; uint16_t rl;
|
||
if (modbus_read(0x08, 0x0012, 2, r, &rl, "土壤温湿") != 0) return 1;
|
||
g_sensor.soil_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
|
||
g_sensor.soil_t = parse_temp(r[5], r[6]);
|
||
return 0;
|
||
}
|
||
|
||
static int read_npk(void) /* 0x09 土壤氮磷钾 */
|
||
{
|
||
uint8_t r[16]; uint16_t rl;
|
||
if (modbus_read(0x09, 0x001E, 3, r, &rl, "氮磷钾") != 0) return 1;
|
||
g_sensor.soil_n = (float)(r[3] * 256 + r[4]);
|
||
g_sensor.soil_p = (float)(r[5] * 256 + r[6]);
|
||
g_sensor.soil_k = (float)(r[7] * 256 + r[8]);
|
||
return 0;
|
||
}
|
||
|
||
static int read_soilph(void) /* 0x0A 土壤PH */
|
||
{
|
||
uint8_t r[16]; uint16_t rl;
|
||
if (modbus_read(0x0A, 0x0006, 1, r, &rl, "土壤PH") != 0) return 1;
|
||
g_sensor.soil_ph = (float)(r[3] * 256 + r[4]) / 100.0f;
|
||
return 0;
|
||
}
|
||
|
||
static int read_soilec(void) /* 0x0B 土壤电导率 */
|
||
{
|
||
uint8_t r[16]; uint16_t rl;
|
||
if (modbus_read(0x0B, 0x0015, 1, r, &rl, "土壤EC") != 0) return 1;
|
||
g_sensor.soil_ec = (float)(r[3] * 256 + r[4]);
|
||
return 0;
|
||
}
|
||
|
||
/*==== 轮询调度 ====*/
|
||
|
||
typedef struct {
|
||
const char *name; /* 日志/OLED 用名 */
|
||
int (*read)(void); /* 读取+解码,0=成功 */
|
||
void (*report)(void); /* 组包上报 */
|
||
} sensor_entry_t;
|
||
|
||
static void report_phec(void) { char b[64]; snprintf(b, sizeof(b), "\"PH\":%.1f,\"EC\":%.1f", g_sensor.ph, g_sensor.ec); NET_publish_props(b); }
|
||
static void report_air(void) { char b[96]; snprintf(b, sizeof(b), "\"BS_T\":%.2f,\"BS_RH\":%.2f,\"BS_DO\":%.2f", g_sensor.air_t, g_sensor.air_rh, g_sensor.air_press); NET_publish_props(b); }
|
||
static void report_do(void) { char b[64]; snprintf(b, sizeof(b), "\"DS_T\":%.2f,\"DO\":%.2f", g_sensor.water_t, g_sensor.do_mgl); NET_publish_props(b); }
|
||
static void report_byx(void) { char b[112]; snprintf(b, sizeof(b), "\"BY_TP\":%.1f,\"BY_RH\":%.1f,\"BY_HV\":%u,\"BY_CO2\":%u", g_sensor.by_t, g_sensor.by_rh, g_sensor.by_hv, g_sensor.by_co2); NET_publish_props(b); }
|
||
static void report_leaf(void) { char b[80]; snprintf(b, sizeof(b), "\"YSMD_TP\":%.1f,\"YSMD_RH\":%.1f", g_sensor.leaf_t, g_sensor.leaf_rh); NET_publish_props(b); }
|
||
static void report_sth(void) { char b[80]; snprintf(b, sizeof(b), "\"SOIL_TP\":%.1f,\"SOIL_RH\":%.1f", g_sensor.soil_t, g_sensor.soil_rh); NET_publish_props(b); }
|
||
static void report_npk(void) { char b[112]; snprintf(b, sizeof(b), "\"SOIL_NV\":%.1f,\"SOIL_PV\":%.1f,\"SOIL_KV\":%.1f", g_sensor.soil_n, g_sensor.soil_p, g_sensor.soil_k); NET_publish_props(b); }
|
||
static void report_sph(void) { char b[48]; snprintf(b, sizeof(b), "\"SOIL_PH\":%.2f", g_sensor.soil_ph); NET_publish_props(b); }
|
||
static void report_sec(void) { char b[48]; snprintf(b, sizeof(b), "\"SOIL_RV\":%d", (int)g_sensor.soil_ec); NET_publish_props(b); }
|
||
|
||
static const sensor_entry_t s_table[] = {
|
||
{ "PH-EC", read_phec, report_phec },
|
||
{ "大气压", read_airpress, report_air },
|
||
{ "溶解氧", do59_read, report_do },
|
||
{ "百叶箱", read_byx, report_byx },
|
||
{ "叶面温湿", read_leaf, report_leaf },
|
||
{ "土壤温湿", read_soilth, report_sth },
|
||
{ "氮磷钾", read_npk, report_npk },
|
||
{ "土壤PH", read_soilph, report_sph },
|
||
{ "土壤EC", read_soilec, report_sec },
|
||
};
|
||
#define SENSOR_COUNT (sizeof(s_table) / sizeof(s_table[0]))
|
||
|
||
static int online_count(void)
|
||
{
|
||
int n = 0;
|
||
for (uint16_t i = 0; i < SENSOR_COUNT; i++) {
|
||
if (g_sensor_online & (1u << i)) n++;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* 本地读取周期:10s(只更新缓存/OLED,不上报) */
|
||
#define SENSOR_READ_MS 10000u
|
||
|
||
/* 上传调度:锚定"OTA 已确认"(联网就绪),+5s/+30s/+60s 暖机三次,之后每 SENSOR_CYCLE_MS */
|
||
static const uint16_t s_pub_warmup_s[3] = {5, 30, 60};
|
||
static uint32_t s_read_tick = 0;
|
||
static uint32_t s_pub_anchor = 0; /* 0=未锚定(OTA 未确认/未联网) */
|
||
static uint8_t s_pub_stage = 0; /* 0~2=暖机阶段,3=常规 5 分钟周期 */
|
||
static uint32_t s_pub_last = 0; /* 常规阶段上次上传时刻 */
|
||
|
||
void sensor_init(void)
|
||
{
|
||
log_info("> SENSOR: 初始化, %d 种传感器, 10s 本地刷新, 联网后 +5s/+30s/+60s 上传, 之后每 %u 分钟",
|
||
(int)SENSOR_COUNT, (unsigned)(SENSOR_CYCLE_MS / 60000u));
|
||
}
|
||
|
||
/* 由 app_task 在"OTA 已确认"时调用,锚定上传计划 */
|
||
void sensor_notify_ota_confirmed(void)
|
||
{
|
||
if (s_pub_anchor == 0) {
|
||
s_pub_anchor = HAL_GetTick();
|
||
s_pub_stage = 0;
|
||
log_info("> SENSOR: 上传计划已锚定(+5s/+30s/+60s, 之后每 %u 分钟)",
|
||
(unsigned)(SENSOR_CYCLE_MS / 60000u));
|
||
}
|
||
}
|
||
|
||
/* 全量查询一遍传感器,只更新数值缓存和在线掩码 */
|
||
static void scan_sensors(void)
|
||
{
|
||
for (uint16_t i = 0; i < SENSOR_COUNT; i++) {
|
||
if (s_table[i].read() == 0) {
|
||
g_sensor_online |= (1u << i);
|
||
} else {
|
||
g_sensor_online &= ~(1u << i);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 上传当前缓存的在线传感器数据 + 电池电量 */
|
||
static void publish_all(void)
|
||
{
|
||
if (!g_net_mqtt_ready) return;
|
||
for (uint16_t i = 0; i < SENSOR_COUNT; i++) {
|
||
if (g_sensor_online & (1u << i)) {
|
||
s_table[i].report();
|
||
vTaskDelay(300); /* 上报之间留间隙,避免 AT 通道挤压 */
|
||
}
|
||
}
|
||
/* 电池电量随每轮上传一次 */
|
||
{
|
||
int bat = battery_percent();
|
||
char b[32];
|
||
snprintf(b, sizeof(b), "\"BAT_VAL\":%d", bat);
|
||
NET_publish_props(b);
|
||
log_info("> SENSOR: 电池电量 %d%%", bat);
|
||
}
|
||
}
|
||
|
||
void sensor_process(void)
|
||
{
|
||
uint32_t now = HAL_GetTick();
|
||
|
||
/* 10s 本地读取刷新(未联网也读,OLED 实时显示用) */
|
||
if ((now - s_read_tick) >= SENSOR_READ_MS) {
|
||
s_read_tick = now;
|
||
scan_sensors();
|
||
}
|
||
|
||
/* 上传调度 */
|
||
if (s_pub_anchor == 0) return;
|
||
if (s_pub_stage < 3) {
|
||
/* 暖机阶段:+5s/+30s/+60s */
|
||
if ((now - s_pub_anchor) >= (uint32_t)s_pub_warmup_s[s_pub_stage] * 1000u) {
|
||
publish_all();
|
||
s_pub_stage++;
|
||
if (s_pub_stage == 3) s_pub_last = now;
|
||
}
|
||
} else if ((now - s_pub_last) >= SENSOR_CYCLE_MS) {
|
||
s_pub_last = now;
|
||
publish_all();
|
||
log_info("> SENSOR: 周期上传完成, 在线 %d/%d", online_count(), (int)SENSOR_COUNT);
|
||
}
|
||
}
|
||
|
||
/*==== OLED 摘要(每 5s 翻一页,一页一路传感器,R2/R3 两行)====
|
||
* 行宽约束: 128px,全角 16px/字,半角 8px/字,每行不超过 128px */
|
||
int sensor_oled_summary(char *l1, char *l2, int cap)
|
||
{
|
||
int total = online_count();
|
||
if (total == 0) return 0;
|
||
|
||
int want = (int)((HAL_GetTick() / 5000u) % (uint32_t)total);
|
||
int idx = -1;
|
||
for (int i = 0, seen = 0; i < (int)SENSOR_COUNT; i++) {
|
||
if (g_sensor_online & (1u << i)) {
|
||
if (seen == want) { idx = i; break; }
|
||
seen++;
|
||
}
|
||
}
|
||
if (idx < 0) return 0;
|
||
|
||
l1[0] = l2[0] = 0;
|
||
switch (idx) {
|
||
case 0: snprintf(l1, cap, "水体PH:%.1f", g_sensor.ph);
|
||
snprintf(l2, cap, "电导率:%.0fuS/cm", g_sensor.ec); break;
|
||
case 1: snprintf(l1, cap, "大气温度:%.1fC", g_sensor.air_t);
|
||
snprintf(l2, cap, "湿%.0f%% 压%.0fkPa", g_sensor.air_rh, g_sensor.air_press); break;
|
||
case 2: snprintf(l1, cap, "溶解氧:%.2fmg/L", g_sensor.do_mgl);
|
||
snprintf(l2, cap, "水温%.1fC 饱%.0f%%", g_sensor.water_t, g_sensor.do_sat); break;
|
||
case 3: snprintf(l1, cap, "温%.1fC 湿%.0f%%", g_sensor.by_t, g_sensor.by_rh);
|
||
snprintf(l2, cap, "CO2:%u 光:%u", g_sensor.by_co2, g_sensor.by_hv); break;
|
||
case 4: snprintf(l1, cap, "叶面温度:%.1fC", g_sensor.leaf_t);
|
||
snprintf(l2, cap, "叶面湿度:%.1f%%", g_sensor.leaf_rh); break;
|
||
case 5: snprintf(l1, cap, "土壤温度:%.1fC", g_sensor.soil_t);
|
||
snprintf(l2, cap, "土壤湿度:%.1f%%", g_sensor.soil_rh); break;
|
||
case 6: snprintf(l1, cap, "氮:%.0f 磷:%.0f", g_sensor.soil_n, g_sensor.soil_p);
|
||
snprintf(l2, cap, "钾:%.0fmg/kg", g_sensor.soil_k); break;
|
||
case 7: snprintf(l1, cap, "土壤PH:%.2f", g_sensor.soil_ph); break;
|
||
case 8: snprintf(l1, cap, "土壤电导率:%.0f", g_sensor.soil_ec); break;
|
||
default: return 0;
|
||
}
|
||
return 1;
|
||
}
|