介绍

本次购买的RS485信号小型超声波一体式气象站由山东塞恩电子科技有限公司制造。
设备型号:SN-3003-FSXCS-N01
包含气温,湿度,大气压强,风速,风向,光学雨量共6个气象要素。供电方面支持10V-30V DC宽电压,可配合DC12v-TypeC与ESP32一同供电。

关注几个主要参数:大气压强测量精度只有1hPa,最大误差±1.5hPa,这一点对于测量天气系统已经基本死刑。气温测量精度0.1℃,最大误差±0.5℃

以及,虽然它的气温探头在百叶箱中,但是形同虚设。置于室外环境中,气温波动猛烈,在太阳直照仪器时可探测到40℃高温。

作为超声波测量仪器,其风速精度较高,误差优于国家标准站的标准,最大误差仅±(0.5+0.02V)m/s(0.5m/s启动风速)。也正由于测量原理,采样方式与风杯机械结构不同,易导致采样区间内的极值被平滑低估?

传输方式

RS485信号转TTL信号连接到ESP32,进入loop发送问询并读取应答数据,通过GPRS/WiFi传输原始6要素数据到中转服务器,数据换算(海平面气压,露点,分钟级降水)、入库。

使用的ESP32需要从NTP获取网络时间或者使用GNSS授时。

数据帧结构

地址码:为变送器的地址,在通讯网络中是唯一的(出厂默认0x01)

功能码:主机所发指令功能指示

数据区:数据区是具体通讯数据,16bits数据高字节

CRC码:二字节的校验码

主机问询帧结构

地址码功能码寄存器起始地址寄存器长度校验码低字节校验码高字节
1字节1字节2字节2字节1字节1字节

从机应答帧结构

地址码功能码有效字节数数据一区数据二区数据N区校验码低字节校验码高字节
1字节1字节1字节2字节2字节2字节1字节1字节

寄存器地址

寄存器地址PLC或组态地址内容支持功能码定义说明
50040501风速值0x03/0x04实际值的100倍
50140502风力0x03/0x04实际值(当前风速对应的风级值)
50240503风向(0-7档)0x03/0x04实际值(正北方向为0,顺时针增加数值,正东方为2)
50340504风向(0-360°)0x03/0x04实际值(正北方向为0°顺时针增加度数,正东方为90°)
50440505湿度值0x03/0x04实际值的10倍
50540506温度值0x03/0x04实际值的10倍
50940510大气压值(单位kPa)0x03/0x04实际值的10倍
51340514光学雨量雨量值(单位:mm)0x03/0x04实际值的10倍
寄存器地址内容支持功能码定义说明
6000 H小型超声波风向偏移寄存器0x060代表正常方向 1代表方向偏移180°
6001 H小型超声波风速调零寄存器0x06写入0xAA,等待10s后,设备调零
6002 H光学雨量调零寄存器0x06写入0x5A,雨量值调零
6003 H光学雨量灵敏值0x06默认为11 H,改小后可增加雨量灵敏度

构造问询帧

const uint8_t REQUEST_FRAME[][8] = {
  {0x01, 0x03, 0x01, 0xF8, 0x00, 0x02, 0x44, 0x06}, // 温湿度
  {0x01, 0x03, 0x01, 0xF4, 0x00, 0x01, 0xC4, 0x04}, // 风速
  {0x01, 0x03, 0x01, 0xFD, 0x00, 0x01, 0x14, 0x06}, // 大气压强
  {0x01, 0x03, 0x02, 0x01, 0x00, 0x01, 0xD4, 0x72}, // 总降雨量(自通电后)
  {0x01, 0x06, 0x60, 0x02, 0x00, 0x5A, 0xB6, 0x31}  // 清除降雨
}
void ReadT(HardwareSerial &serial, float &temperature, float &humidity) {
  serial.eventQueueReset();
  serial.write(REQUEST_FRAME[0], sizeof(REQUEST_FRAME[0])); 
  delay(500);
  if (serial.available())
  {
    uint8_t buffer[128];
    int len = serial.readBytes(buffer, sizeof(buffer));
    if (len > 0 && len == 9 && buffer[0] == 0x01 && buffer[1] == 0x03)
    {
      int t = buffer[5] * 256 + buffer[6];
      temperature = t / 10.0;
      int rh = buffer[3] * 256 + buffer[4];
      humidity = rh / 10.0;
    }
  }
}
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <HTTPClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>
#include <math.h>

const char* SSID = "TQM_DORMITORY";
const char* PASSWORD = "TQM_DORMITORY";
const char* API_URL = "https://SDKAPI/api/receive_meteo_data_json";

const uint8_t WIND_SPEED_QUERY[] = {0x02, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x39};

class Module {
public:
    Module() : timeClient(ntpUDP, "pool.ntp.org", 0, 60000) {}

    void begin() {
        Serial.begin(115200);
        Serial2.begin(9600, SERIAL_8N1, 16, 17);

        connectWiFi();
        initBME280();
        timeClient.begin();
    }

    void run() {
        timeClient.update();
        float temperature = bme280.readTemperature();
        float humidity = bme280.readHumidity();
        float pressure = bme280.readPressure() / 100;
        float windSpeed = readWindSpeed();
        float gps_h = 30;

        String jsonPayload = initPayload(temperature, humidity, pressure, windSpeed, gps_h);
        sendData(jsonPayload);

        delay(10000);
    }

private:
    WiFiUDP ntpUDP;
    NTPClient timeClient;
    Adafruit_BME280 bme280;

    void connectWiFi() {
        Serial.print("Connecting to WiFi");
        WiFi.begin(SSID, PASSWORD);
        while (WiFi.status() != WL_CONNECTED) {
            delay(500);
            Serial.print(".");
        }
        Serial.println("Connected!");
    }

    void initBME280() {
        if (!bme280.begin(0x76)) {
            Serial.println("Sensor not found");
            while (1) delay(10);
        }
    }

    float readWindSpeed() {
        Serial2.write(WIND_SPEED_QUERY, sizeof(WIND_SPEED_QUERY));
        delay(1000);

        if (Serial2.available()) {
            uint8_t buffer[7];
            int len = Serial2.readBytes(buffer, sizeof(buffer));

            if (len == 7 && buffer[0] == 0x02 && buffer[1] == 0x03) {
                int fs10 = buffer[3] * 256 + buffer[4];
                return fs10 / 10.0;
            }
        }

        Serial.println("Failed to read wind speed");
        return -1;
    }

    String initPayload(float t, float rh, float p, float ws, float gps_h) {
        StaticJsonDocument<256> jsonDoc;
        jsonDoc["timestamp"] = timeClient.getEpochTime();
        jsonDoc["instrument"] = "Station";
        jsonDoc["observation"]["t"] = t;
        jsonDoc["observation"]["rh"] = rh;
        jsonDoc["observation"]["p"] = p;
        jsonDoc["observation"]["dt"] = dewPointFast(t, rh);
        jsonDoc["observation"]["mslp"] = mslp(p, t, gps_h);
        jsonDoc["wind"]["spd"] = ws;
        jsonDoc["wind"]["dir"] = 0;
        jsonDoc["gps"]["lat"] = 0;
        jsonDoc["gps"]["long"] = 0;
        jsonDoc["gps"]["alt"] = gps_h;
        jsonDoc["gps"]["dir"] = 0;
        jsonDoc["gps"]["speed"] = 0;

        String jsonString;
        serializeJson(jsonDoc, jsonString);
        return jsonString;
    }

    void sendData(const String& payload) {
        if (WiFi.status() == WL_CONNECTED) {
            HTTPClient http;
            http.begin(API_URL);
            http.addHeader("Content-Type", "application/json");

            int responseCode = http.POST(payload);
            http.end();
        } else {
            Serial.println("WiFi not connected");
        }
    }

    float mslp(float p, float t, float h) {
        return p * pow((1 - 0.0065 * h / (t + 0.0065 * h + 273.15)), -5.257);
    }

    float dewPointFast(float celsius, float humidity) {
        float a = 17.271, b = 237.7;
        float temp = (a * celsius) / (b + celsius) + log(humidity / 100.0);
        return round((b * temp) / (a - temp) * 100) / 100.0;
    }
};

Module collector;

void setup() {
    collector.begin();
}

void loop() {
    collector.run();
}

总结

总之就是很不推荐购买使用。


寂静在喧嚣里低头不语,沉默在黑夜里与目光结交