Hi, I am a university student currently working on a group project regarding a soil monitoring IoT product. However, I am facing some problems with my code as the rs485 NPK sensoor, particularly the model sn-3002-tr-npk-n01 does not have any values read. The OLED display shows "waiting".
* ESP32 Soil Monitor: DS18B20 + SEN0308 + NPK(485)
* OLED (SSD1306 I2C) + Blynk + RGB + Buzzer
* Pins:
* DS18B20 -> GPIO15 (DQ, 4.7k pull-up to 3V3)
* SEN0308 AO -> GPIO36 (VP / ADC1_CH0)
* RS485->TTL TXD -> ESP32 RX2 = GPIO16
* RXD <- ESP32 TX2 = GPIO17
* (可选) RS485 DE/RE -> 自定义引脚(见下方宏)
* OLED I2C -> SDA=21, SCL=22 (addr 0x3C)
* RGB LED -> R=26, G=27, B=33 (common cathode; each 220~330Ω)
* Buzzer -> GPIO25
****************************************************/
// <<< 必须在所有 Blynk 头文件之前 >>>
#define BLYNK_TEMPLATE_ID "TMPL6CEzt9vwJ"
#define BLYNK_TEMPLATE_NAME "sms"
#define BLYNK_AUTH_TOKEN "-TAXuFXkcT2SU-3Gxf04Z8ML5yYZ4Y1D"
// (可选调试)#define BLYNK_PRINT Serial
#include <Arduino.h>
// ---------- WiFi & Blynk ----------
#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
const char* WIFI_SSID = "Student";
const char* WIFI_PASS = "xmustudent";
// Blynk 虚拟引脚映射
// V0: 温度(°C)
// V1: 湿度(%)
// V2: 氮N (mg/kg)
// V3: 磷P (mg/kg)
// V4: 钾K (mg/kg)
// V5: 系统状态(文本)
// V6: OK计数(0~3)
BlynkTimer blynkTimer;
// ---------- OLED ----------
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
#define SDA_PIN 21
#define SCL_PIN 22
TwoWire I2COLED = TwoWire(0);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2COLED, -1);
// ---------- DS18B20 ----------
#include <OneWire.h>
#include <DallasTemperature.h>
#define DS18B20_PIN 15
OneWire oneWire(DS18B20_PIN);
DallasTemperature sensors(&oneWire);
// ---------- SEN0308 (Analog) ----------
#define SOIL_ADC_PIN 36 // VP
// 校准:极干/极湿时的 ADC 原始值(需实测微调)
const int SOIL_ADC_DRY = 3200;
const int SOIL_ADC_WET = 800;
// ---------- NPK (RS485 Modbus,自研底层,替换 ModbusMaster) ----------
#define UART2_RX 16 // ESP32 RX2
#define UART2_TX 17 // ESP32 TX2
// 是否需要方向控制(MAX485 等半双工模块需要);自动收发模块请置 0
#define USE_RS485_DIR 0
#define RS485_RE_DE 4 // 若 USE_RS485_DIR=1,请把此引脚接 DE 与 /RE(并联)
// 设备参数
#define NPK_BAUD 4800 // 若设备已改为 9600,请改这里
#define NPK_ADDR 0x01 // 默认从站地址
#define FC_READ_HOLD 0x03
// 常见 JXBS-3001-NPK-RS:N/P/K 寄存器
const uint16_t REG_N = 0x001E;
const uint16_t REG_P = 0x001F;
const uint16_t REG_K = 0x0020;
// 通信健壮性参数
const uint16_t NPK_READ_TIMEOUT_MS = 200;
const uint8_t NPK_MAX_RETRIES = 3;
// ---------- RGB & Buzzer ----------
#define PIN_R 26
#define PIN_G 27
#define PIN_B 33
#define PIN_BUZZER 25
// ---------- 阈值(最佳生长区间)----------
float TEMP_MIN = 18.0;
float TEMP_MAX = 28.0;
int SOIL_MIN = 35; // %
int SOIL_MAX = 60;
// NPK 阈值(mg/kg)
int N_MIN = 80, N_MAX = 200;
int P_MIN = 50, P_MAX = 150;
int K_MIN = 100, K_MAX = 300;
// ---------- 稳定/静音控制 ----------
unsigned long bootMs = 0;
bool npkEverOK = false;
bool lastAlert = false;
// 工具:约束并映射
int mapConstrain(int x, int in_min, int in_max, int out_min, int out_max) {
x = constrain(x, min(in_min, in_max), max(in_min, in_max));
long val = (long)(x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
return (int)val;
}
bool inRangeFloat(float v, float lo, float hi) { return (v >= lo && v <= hi); }
bool inRangeInt(int v, int lo, int hi) { return (v >= lo && v <= hi); }
// -------------------- NPK 低层工具函数(基于 Serial2) --------------------
HardwareSerial& NPKSER = Serial2;
static void npkClearRx() {
while (NPKSER.available()) (void)NPKSER.read();
}
static uint16_t modbusCRC(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; ++b) {
if (crc & 1) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc; // 小端:Lo 在前,Hi 在后
}
static bool readBytesExact(uint8_t* buf, size_t len, uint16_t timeout_ms) {
size_t got = 0; unsigned long t0 = millis();
while (got < len) {
if (NPKSER.available()) buf[got++] = NPKSER.read();
else if (millis() - t0 >= timeout_ms) return false;
}
return true;
}
// 连续读取 qty 个保持寄存器(qty<=3 足够 N/P/K)
static bool readHoldingRegs(uint8_t devAddr, uint16_t startReg, uint8_t qty, uint16_t* outVals) {
if (qty == 0 || qty > 3) return false;
uint8_t req[8] = {
devAddr, FC_READ_HOLD, (uint8_t)(startReg >> 8), (uint8_t)startReg,
0x00, qty, 0x00, 0x00
};
uint16_t crc = modbusCRC(req, 6);
req[6] = (uint8_t)(crc & 0xFF); // Lo
req[7] = (uint8_t)((crc >> 8) & 0xFF);// Hi
for (uint8_t attempt = 0; attempt < NPK_MAX_RETRIES; ++attempt) {
npkClearRx();
#if USE_RS485_DIR
digitalWrite(RS485_RE_DE, HIGH); // 发送
#endif
NPKSER.write(req, sizeof(req));
NPKSER.flush();
#if USE_RS485_DIR
digitalWrite(RS485_RE_DE, LOW); // 接收
#endif
// 期望响应:addr func byteCnt (= qty*2) data... crcLo crcHi
const uint8_t expectLen = 5 + qty * 2;
uint8_t resp[5 + 3*2]; // 最大 11
if (!readBytesExact(resp, expectLen, NPK_READ_TIMEOUT_MS)) continue;
// 基本校验
if (resp[0] != devAddr || resp[1] != FC_READ_HOLD || resp[2] != qty * 2) continue;
// CRC 校验
uint16_t crc2 = modbusCRC(resp, expectLen - 2);
uint8_t crcLo = (uint8_t)(crc2 & 0xFF), crcHi = (uint8_t)(crc2 >> 8);
// 等待剩余 2 字节 CRC(有些模块会在前面一起到达;保险起见严格按 expectLen 读)
uint8_t crcRecv[2];
if (!readBytesExact(crcRecv, 2, NPK_READ_TIMEOUT_MS)) continue;
if (crcRecv[0] != crcLo || crcRecv[1] != crcHi) continue;
// 解析数据(大端)
for (uint8_t i = 0; i < qty; ++i) {
outVals[i] = ((uint16_t)resp[3 + i*2] << 8) | resp[4 + i*2];
}
return true;
}
return false;
}
// 对外:一次性读 N/P/K
static bool readNPK_triplet(int &N, int &P, int &K) {
uint16_t vals[3] = {0};
bool ok = readHoldingRegs(NPK_ADDR, REG_N, 3, vals);
if (ok) { N = vals[0]; P = vals[1]; K = vals[2]; }
return ok;
}
// -------------------- 其他传感器函数 --------------------
float readTemperatureC() {
sensors.requestTemperatures();
return sensors.getTempCByIndex(0);
}
int readSoilPercent() {
int raw = analogRead(SOIL_ADC_PIN); // 0~4095
int pct = mapConstrain(raw, SOIL_ADC_DRY, SOIL_ADC_WET, 0, 100);
return constrain(pct, 0, 100);
}
// RGB 显示(共阴极:高电平点亮)
void setRGB(bool r, bool g, bool b) {
digitalWrite(PIN_R, r ? HIGH : LOW);
digitalWrite(PIN_G, g ? HIGH : LOW);
digitalWrite(PIN_B, b ? HIGH : LOW);
}
// 蜂鸣器短鸣
void beepOnce(uint16_t freq = 2000, uint16_t ms = 300) {
tone(PIN_BUZZER, freq, ms);
delay(ms + 10);
noTone(PIN_BUZZER);
}
// OLED 刷屏
void drawOLED(float tC, int soilPct, int N, int P, int K, const String &statusText) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Temp: "); display.print(tC, 1); display.println(" C");
display.setCursor(0, 10);
display.print("Soil: "); display.print(soilPct); display.println(" %");
display.setCursor(0, 20);
if (npkEverOK) {
display.print("N:"); display.print(N);
display.print(" P:"); display.print(P);
display.print(" K:"); display.print(K);
} else {
display.print("NPK: waiting...");
}
display.setCursor(0, 35);
display.print("Status: "); display.println(statusText);
display.setCursor(0, 50);
display.print("WiFi: ");
display.print(WiFi.status() == WL_CONNECTED ? "OK " : "...");
display.print(" RSSI:"); display.print(WiFi.RSSI());
display.display();
}
// Blynk 定时上报
void blynkReport(float tC, int soilPct, int N, int P, int K, const String &statusText, int okCount) {
Blynk.virtualWrite(V0, tC);
Blynk.virtualWrite(V1, soilPct);
Blynk.virtualWrite(V2, N);
Blynk.virtualWrite(V3, P);
Blynk.virtualWrite(V4, K);
Blynk.virtualWrite(V5, statusText);
Blynk.virtualWrite(V6, okCount);
}
void setup() {
Serial.begin(115200);
// GPIO
pinMode(PIN_R, OUTPUT);
pinMode(PIN_G, OUTPUT);
pinMode(PIN_B, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
setRGB(false, false, false);
digitalWrite(PIN_BUZZER, LOW);
#if USE_RS485_DIR
pinMode(RS485_RE_DE, OUTPUT);
digitalWrite(RS485_RE_DE, LOW); // 初始接收
#endif
// OLED
I2COLED.begin(SDA_PIN, SCL_PIN, 400000); // 400kHz
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
// OLED 初始化失败也不阻塞
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Soil Monitor Boot...");
display.display();
// 温度
sensors.begin();
// NPK (RS485) — 使用 Serial2,波特率见 NPK_BAUD
NPKSER.begin(NPK_BAUD, SERIAL_8N1, UART2_RX, UART2_TX);
// WiFi + Blynk
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Blynk.config(BLYNK_AUTH_TOKEN);
unsigned long t0 = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - t0) < 10000UL) {
delay(200);
}
if (WiFi.status() == WL_CONNECTED) {
Blynk.connect(5000);
}
// 定时上报到 Blynk(每 2 秒) -> 实际数据上报在 loop 中完成
blynkTimer.setInterval(2000, []() {});
// 首屏
drawOLED(0, 0, 0, 0, 0, "Booting...");
bootMs = millis();
}
void loop() {
Blynk.run();
blynkTimer.run();
// 1) 读各传感器
float tC = readTemperatureC();
int soilPct = readSoilPercent();
int N = -1, P = -1, K = -1;
bool npkReadOK_now = readNPK_triplet(N, P, K);
if (npkReadOK_now) npkEverOK = true;
// 2) 判定区间
bool okTemp = inRangeFloat(tC, TEMP_MIN, TEMP_MAX);
bool okSoil = inRangeInt(soilPct, SOIL_MIN, SOIL_MAX);
bool okNPK = false;
if (npkReadOK_now) {
bool okN = inRangeInt(N, N_MIN, N_MAX);
bool okP = inRangeInt(P, P_MIN, P_MAX);
bool okK = inRangeInt(K, K_MIN, K_MAX);
okNPK = okN && okP && okK;
}
int okCount = (okTemp ? 1 : 0) + (okSoil ? 1 : 0) + (okNPK ? 1 : 0);
bool isAlert = (okCount <= 1);
// 3) RGB + 状态文本
String statusText;
if (okCount == 3) {
setRGB(false, true, false); // 绿
statusText = "ALL GOOD";
} else if (okCount == 2) {
setRGB(false, false, true); // 蓝
statusText = "2 OK";
} else {
setRGB(true, false, false); // 红
statusText = "ALERT";
}
// 4) 告警短鸣(节制)
if ((millis() - bootMs > 15000) && npkEverOK && isAlert && !lastAlert) {
beepOnce(2200, 250);
}
lastAlert = isAlert;
// 5) OLED 刷新(npkEverOK 前显示 waiting...)
drawOLED(tC, soilPct, npkEverOK ? N : 0, npkEverOK ? P : 0, npkEverOK ? K : 0, statusText);
// 6) Blynk 上报
blynkReport(tC, soilPct, npkEverOK ? N : 0, npkEverOK ? P : 0, npkEverOK ? K : 0, statusText, okCount);
delay(500); // 采样节奏
}```