Questions on data reading for rs485 protocol sensor on ESP32

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); // 采样节奏
}```

Your topic does not indicate a problem with IDE 2.x and therefore has been moved to a more suitable category of the forum.

Did you try with
0x0004
0x0005
0x0006
instead?

Also, you should debug your problem separately from other hardware and code, connecting esp32 to computer and Serial.print both the request you make and the reply (if any) on serial monitor.

1 Like

Without a schematic it is hard to guess. Try these links.