Receive weather station data with Arduino

 Posted by:   Posted on:   Updated on:  2024-01-27T14:20:08Z

Receive temperature and humidity data from the outdoor unit of a weather station with 433.92 MHz receiver and Arduino

A while ago (to be more specific two years ago) I used software defined radio to capture and decode RF signal from the outdoor unit of a weather station. This allowed me to emulate the protocol with an Arduino and a cheap 433.92 MHz transmitter and send my own data to the indoor station. I can make my own units if the original outdoor unit fails. The outdoor unit uses on-off-keying (OOK) and sends pulse distance modulated bits, explained in detail in the linked post.

But what about receiving data from outdoor unit(s) with an Arduino? One can add an ESP8266 to capture temperature and humidity and publish data to MQTT, Home Assistant or other IoT servers. Capturing and analyzing pulse timings of a signal was a daunting task for me. However it turned out to be easier than I thought, using an interrupt routine. In this post, I'll explain all the steps required to make a pulse distance modulation (PDM) decoder.

Quick intro

The outdoor unit is part of a weather station available in Europe from Lidl under the brand name Auriol. It employs the protocol of a device named "Nexus-TH" according to rtl_433 (decoder #19). Using OOK it transmits:

  • Bit 0 with 500 μs carrier on (radio on) followed by 1000 μs pause;
  • Bit 1 with 500 μs carrier on followed by 2000 μs pause;
  • Sync marker (at the end of the bit stream) with 500 μs carrier on followed by 4000 μs pause.

36 bits followed by a sync marker (a packet) are transmitted 10 times for each message. There is a strange behavior of these units: first packet is missing the first 4 bits, having only 32 bits and the tenth (last) packet is missing the sync pulse. This is what each bit means:

Weather station bit mapping
Bit mapping

With this information we can start coding a decoder. By the way, I found from one of my readers that the bit between battery status and channel is set if a transmission occurs when you push the TX button on the unit.

Hardware

Successful signal decoding depends heavily on receiver. And I got the cheapest 433 MHz modules available: FS1000A transmitter with MX-RM-5V receiver. Unfortunately, the receiver is super regenerative type and no matter what I did to it, it just won't stop outputting noise. Not even when FS1000A is turned on and transmitting a continuous carrier close to 433.92 MHz, I can't get the noise from the receiver to stop. These being said, I went online and ordered some other modules. I highly recommend heterodyne receivers.

I got a few H5V4D and H3V4F receivers and some coil antennas which I could solder directly to the receiver boards. The only difference between these receivers is the supply voltage and output level (3.3 V for H3V4F and 5 V for H5V4D). They also output noise when there is no signal to receive.

433 MHz receiver modules H5V4D and H3V4F
433 MHz receiver modules H5V4D and H3V4F

However the receivers seem to perform good enough to output a clean signal when they receive it. Unfortunately either the receiver sensitivity is not really good or the coil antennas are not the best choice since I had to get the weather station unit (transmitter) closer to the receiver to get an output.

Code

I imagined the decoding process as follows:

  1. An interrupt routine captures the current micros() and finds the time passed since previous interrupt (a time frame). The interrupt is called every time the state of the pin changes. I don't know if this is the best way to handle an unknown signal, but it seems to work. I also considered using timers but that would make the code a bit more platform dependent.
  2. I use two variables to store the last two time frames which are updated at each interrupt.
  3. Each two time frames are checked whether they fit 500+1000 μs, 500+2000 μs or 500+4000 μs patterns and bits are assigned in an array.
  4. When there is a pause of more than 100 ms since the last 500+4000 μs pattern (a sync bit), the array is analyzed for completion and data is extracted. Why 100 ms? Well, a packet has 36 bits. Assuming all of them are "1", it takes (500+2000)*36 = 90,000 us to send it. I could start the data processing immediately after the last sync bit, however the last packet does not have this sync bit. Therefore I let the microcontroller capture it and afterwards I do the processing.

Let's see how the interrupt routine begins:

void mapPulseTimingsToBits() {
  currInt = micros();

  // Copy the second pulse timing to the first variable
  timeInt1 = timeInt2;

  // Update the second pulse timing with the last measured pulse
  timeInt2 = currInt - lastInt;

  // Update the most recent interrupt time
  lastInt = currInt;

...

You can see the two timeInt1 and timeInt2 variables which are volatile unsigned long type and store the last two time frames. Further, if timeInt1 is less than timeInt2 we are probably looking at a bit or sync transmission so the code checks if timeInt1 is close to 500 μs. If this is true, it then checks timeInt2, whether it is close to 1000 μs, 2000 μs or 4000 μs. As long as 500 μs time frames are recorded, a keepWaiting flag is set to true. This is the remainder of the interrupt routine:

...

  // if the first pulse timing fits the ON state
  if (timeInt1 < timeInt2 && timeInt1 > 400 && timeInt1 < 600) {
    // continue sampling the signal
    keepWaiting = true;

    // check if the pulse that followed matches bit 0
    if (timeInt2 > 900 && timeInt2 < 1100) {
      rawBits[j][i] = 0;

      if (i < BITS_PER_PACKET)
        i += 1;
      else rawBits[j][BITS_PER_PACKET] = 0;  // more than expected bits received; set error flag

      // DEBUG: Serial.print("0");
    }

    // otherwise if it maches bit 1
    else if (timeInt2 > 1900 && timeInt2 < 2100) {
      rawBits[j][i] = 1;

      if (i < BITS_PER_PACKET)
        i += 1;
      else rawBits[j][BITS_PER_PACKET] = 0;  // more than expected bits received; set error flag

      // DEBUG: Serial.print("1");
    }

    // if it is longer than that is probably a sync pulse
    else if (timeInt2 < 4500) {

      // if less than 36 bits received, set the flag to false
      if (i < (BITS_PER_PACKET - 1)) {
        rawBits[j][BITS_PER_PACKET] = 0;
      } else {
        rawBits[j][BITS_PER_PACKET] = 1;
      }

      i = 0;  // reset column

      // start filling the next row; ignore extra streams
      if (j < PACKETS_PER_STREAM - 1)
        j += 1;
        
      lastSync = currInt;  

      // DEBUG: Serial.println();
    }
  }
}

All ten packets are stored in a two dimension array (rawBits[10][37]) with 10 rows (remember that 36 bits are repeatedly transmitted 10 times). The array has a 37th column at the end which is an error flag that is set when the total number of received bits in a row is different than the expected 36. After the transmission ends, if at least one row is complete (a packet), data processing is attempted.

In loop(), a periodic check is made whether the signal goes idle. If this happens, the function which processes the raw bits is called.

void loop() {
  currMicros = micros();

  // if no sync frame received within 100 ms (assuming a 36-bit packet of "1" will be sent in 95 ms)
  if ((currMicros > lastSync + 100000) && (keepWaiting == true)) {
    // disable interrrupt for a while
    detachInterrupt(digitalPinToInterrupt(decPin));

    yield();

    // disable sampling pulses and process existing data
    keepWaiting = false;

    // clear last timeframes
    timeInt1 = 0;
    timeInt2 = 0;

    // if at least one packet received
    if (j > 0) processRawBitstream();

    j = 0;
    i = 0;  // reset indexes

    // reattach interrupt
    attachInterrupt(digitalPinToInterrupt(decPin), mapPulseTimingsToBits, CHANGE);
  }
}

Interrupt is disabled while processing sampled data. I decided to do this because the unit sends data every 56.75 seconds and because after data has been decoded, the microcontroller must do something with it. It wouldn't have been wrong to keep the interrupt disabled for 50 seconds (or something closer to 56.75), but I decided to keep it off only while the data is processed. Because I do not want to interfere with other tasks running on the microcontroller, I didn't actually disabled all interrupts, I just detached the received signal interrupt.

The following illustration summarizes what the above code does.

Sampling pulse time frames
Sampling pulse time frames

Further, because each transmission contains redundant data, a final packet is generated based on the bit majority for each column. For all the 10 rows, the columns should contain exactly the same bit, assuming proper reception. Once the packet is generated, useful data is extracted from it.

Overview

I wanted to use ESP8266 for this project which is not the best controller. Time critical routines are required to successfully decode the signal. These interfere with background tasks such as Wi-Fi connection and did trigger a lot of unwanted resets while I was testing and adjusting the code. However, by disabling the interrupt between transmissions, I have enough time to turn on Wi-Fi, connect to MQTT server and send data. It is important though, to disconnect and disable Wi-Fi when done, in order to prepare for decoding the next transmission.

Please note that I also tested the same code on Arduino Nano (ATmega328p) and it is stable enough while outputting data via serial port. This could be a starting point for a project which uses AVR based Arduino board to send data to ESP8266 using AT commands via serial port.

Resources: Arduino sketch for ESP8266 with 0.96" I2C OLED and synchronous WiFi+MQTT.

No comments :

Post a Comment

Please read the comments policy before publishing your comment.