The Renke RS-FSXCS-N01-*-EX weather station is a compact, all-in-one environmental monitoring solution capable of measuring key atmospheric parameters such as wind speed and direction, temperature, humidity, noise levels, air quality (PM2.5, PM10), light intensity, barometric pressure, rainfall, and CO₂ concentration.
This sensor communicates over the Modbus RTU protocol, typically via RS485, returning raw data in 16-bit register formats. These registers must be parsed and interpreted according to the Renke documentation.
Measurement Principles in Brief
Wind speed and direction are typically measured using ultrasonic or mechanical sensors.
Particulate Matter (PM2.5/PM10) values come from a laser scattering sensor.
CO₂ is measured using NDIR (Non-Dispersive Infrared) technology. Either the CO2 sensor or the PM sensor are installed, not both at the same time.
Light is captured with photodiode arrays for both illumination and solar radiation.
Rainfall is detected optically, based on droplet presence and refraction.
Below is a full C implementation that:
Validates CRC using the Modbus standard (0xA001 polynomial),
Decodes register data into physical quantities (e.g., °C, dB, ppm),
Converts wind direction codes into cardinal directions,
Estimates PM1.0 because it is not explicitly reported.
/*
* RENKE RS-FSXCS-N01-*-EX Ultrasonic Integrated Weather Station
*
* (C) 2025 Radu Motisan , www.pocketmagic.net
*
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
// === CRC16 MODBUS standard (polinom 0xA001) ===
uint16_t modbus_crc16(const uint8_t *data, uint8_t length) {
uint16_t crc = 0xFFFF;
for (uint8_t i = 0; i < length; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
const char* wind_direction_cardinal(uint8_t direction_code) {
switch (direction_code) {
case 0: return "N"; // Nord
case 1: return "NE"; // Nord-Est
case 2: return "E"; // Est
case 3: return "SE"; // Sud-Est
case 4: return "S"; // Sud
case 5: return "SW"; // Sud-Vest
case 6: return "W"; // Vest
case 7: return "NW"; // Nord-Vest
default: return "Unknown"; // Cod invalid
}
}
uint16_t estimate_pm1(uint16_t pm25, uint16_t pm10) {
if (pm10 == 0) {
return (uint16_t)(pm25 * 0.7); // fallback
}
float ratio = (float)(pm10 - pm25) / pm10;
float pm1 = pm25 * (1.0f - ratio);
// Limite de siguranță:
if (pm1 < 0) pm1 = 0;
if (pm1 > pm25) pm1 = pm25;
return (uint16_t)(pm1 + 0.5f); // rotunjire
}
// === Decodează un cadru Modbus ===
void parse_modbus_response(const uint8_t *frame, uint8_t length) {
if (length < 5) {
printf("Data too short or incomplete\n");
return;
}
uint16_t crc_calc = modbus_crc16(frame, length - 2);
uint16_t crc_recv = frame[length - 2] | (frame[length - 1] << 8);
if (crc_calc != crc_recv) {
printf("CRC invalid! Computed: 0x%04X, Received: 0x%04X\n", crc_calc, crc_recv);
return;
}
if (frame[1] != 0x03) {
printf("Unknown MODBUS function: 0x%02X\n", frame[1]);
return;
}
uint8_t byte_count = frame[2];
if (byte_count + 5 != length) {
printf("Wrong length\n");
return;
}
for (uint8_t i = 0; i < byte_count / 2; i++) {
uint16_t reg = (frame[3 + i * 2] << 8) | frame[4 + i * 2];
switch (i) {
case 0: printf("500 Wind speed: %.2f m/s\n", reg / 100.0); break;
case 1: printf("501 Wind force: %u (Beaufort)\n", reg); break;
case 2: printf("502 Wind direction (encoded): %u %s\n", reg, wind_direction_cardinal(reg)); break;
case 3: printf("503 Wind direction (degrees): %u°\n", reg); break;
case 4: printf("504 Air Humidity: %.1f%%\n", reg / 10.0); break;
case 5: printf("505 Air Temperature: %.1f°C\n", reg / 10.0); break;
case 6: printf("506 Noise: %.1f dB\n", reg / 10.0); break;
case 7: printf("507 PM2.5: %u µg/m³\n", reg); break;
case 8: {
printf("508 PM10: %u µg/m³\n", reg);
uint16_t pm25 = ((uint32_t)(frame[3 + 2*7] << 8) | frame[4 + 2*7]); // pm25
uint16_t pm1 = estimate_pm1(pm25, reg);
printf("PM1: %u µg/m³\n", pm1);
}
break;
case 9: printf("509 Barometric Pressure: %.1f kPa\n", reg / 10.0); break;
case 10: // Lux high
printf("510 Light illumination (high): 0x%04X\n", reg);
break;
case 11: { // Lux low
printf("511 Light illumination (low): 0x%04X\n", reg);
uint32_t lux = ((uint32_t)(frame[3 + 2*10] << 8) | frame[4 + 2*10]); // high
lux <<= 16;
lux |= reg; // low
printf("Light illumination: %lu lux\n", lux);
}
break;
case 12: printf("512 Light illumination (radiation): %u x100 Lux = %u Lux\n", reg, reg * 100); break;
case 13: printf("513 Rain (optical): %.1f mm\n", reg / 10.0); break;
case 14: printf("514 CO2 (NDIR): %u ppm\n", reg); break;
case 15: printf("515 Light total radiation: %u W/m²\n", reg); break;
default: break;
}
}
}
int main() {
uint8_t frame1[] = {
0x01, 0x03, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x45, 0x00, 0xC8, 0x01,
0xDE, 0x00, 0x07, 0x00, 0x11, 0x03, 0xEF, 0x00,
0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x5C, 0x42
};
uint8_t frame2[] = {
0x01, 0x03, 0x20, 0x00, 0x12, 0x00, 0x02, 0x00,
0x05, 0x00, 0xCB, 0x02, 0x41, 0x00, 0xCD, 0x02,
0x69, 0x00, 0x06, 0x00, 0x07, 0x03, 0xEF, 0x00,
0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x5B, 0x88
};
uint8_t frame3[] = {
0x01, 0x03, 0x20, 0x00, 0x23, 0x00, 0x03, 0x00, 0x06, 0x01, 0x09, 0x02,
0x2F, 0x00, 0xD4, 0x02, 0xC3, 0x00, 0x05, 0x00, 0x06, 0x03, 0xEF, 0x00,
0x00, 0x1A, 0x5B, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xA1, 0x28
};
parse_modbus_response(frame3, sizeof(frame3));
return 0;
}
This code is designed to run on embedded platforms or microcontrollers where Renke data is received over serial/RS485 and needs to be validated, decoded, and displayed in real-world units. The above sample data will output:
500 Wind speed: 0.35 m/s
501 Wind force: 3 (Beaufort)
502 Wind direction (encoded): 6 W
503 Wind direction (degrees): 265°
504 Air Humidity: 55.9%
505 Air Temperature: 21.2°C
506 Noise: 70.7 dB
507 PM2.5: 5 µg/m³
508 PM10: 6 µg/m³
PM1: 4 µg/m³
509 Barometric Pressure: 100.7 kPa
510 Light illumination (high): 0x0000
511 Light illumination (low): 0x1A5B
Light illumination: 6747 lux
512 Light illumination (radiation): 67 x100 Lux = 6700 Lux
513 Rain (optical): 0.0 mm
514 CO2 (NDIR): 0 ppm
515 Light total radiation: 63 W/m²
Platform Compatibility
This code is written in standard C and uses only basic constructs—uint8_t
, printf
, arrays, and simple loops—making it highly portable and well-suited for 8-bit microcontrollers, including AVR devices like the ATmega328P or ATmega1284P. With minimal modifications (e.g., replacing printf
with uart_puts()
), this logic can be embedded in firmware running on low-power MCUs with limited resources, enabling robust, real-time environmental monitoring in the field.
Let me know how this works for you and feel free to share pictures with your projects! I will include them here.