Skip to content

Weather Station

This project is about replacing this commercial weather station from Germany:

Unlike this rendering, the real thing is virtually unreadable without shining a light on it ...

It synchronises to the German DCF77 time standard, shows the (local) weather predictions for the next 3 days (also via DCF77), and shows inside + outside temperature and humidy, picking up the 868 MHz RF signal from an external sensor.

I'd like to replace it with my own home-grown gadget. The hardware will be based on a 2.9" ePaper module, an STM32L052 µC, an RFM69 radio module, a DCF77 receiver module, and a LiPo or LiFePo battery. The software is going to be based on JeeH (of course). This should be a good test case for ultra-low power software design.

Note

It may take a while before I actually get to work on this, but it's now "officially" on my to-do list!

Prototype

DCF77 decoding

To get started, here is a mini-setup with just a DCF77 receiver and a small OLED display:

It uses a fairly simplistic decoder, which has now been added to JeeH:

jee/util/dcf77.h
namespace jeeh::util {

struct DCF77 {
    enum { HZ = 256, RATE = 8, BINS = HZ/RATE };

    uint8_t tally [BINS], prev [3] ={};
    uint16_t count =0, edge =HZ;
    uint64_t bits =0;
    uint16_t meteo =0;
    bool valid =false;
    DateTime now;

    int feed (bool on) {
        if (count == 0)
            memset(tally, 0, sizeof tally);

        auto bin = count/RATE;
        (tally[bin] += on);
        count = (count + 1) % HZ;

        if (count % RATE == 0) {
            prev[0] = prev[1];
            prev[1] = prev[2];
            prev[2] = tally[bin];
            if (prev[0] == 0 && (prev[1] == RATE || prev[2] == RATE))
                edge = (bin-1+BINS) % BINS; // found a clean rising edge
        }

        if (count != 0 || edge >= BINS)
            return 0; // ongoing

        auto sym = symbol();
        bits >>= 1;
        if (sym > 0)
            bits |= 1ull << 59;

        if (sym == -1) {
            meteo = bits;
            if (decode()) {
                valid = true;
                return 1; // new result
            }
        }

        return -1; // starting up
    }

private:
    int symbol () {
        auto sum = 0;
        for (auto i = 0; i < BINS; ++i) {
            auto e = tally[(edge+i) % BINS];
            if (!valid) {
                uint16_t b = e ? e+'0' : '.';
                logWriter(&b, 1);
            }
            if (i < BINS/4)
                sum += e;
        }
        // thresholds are: absent < 5%, "0" < 15%, "1" < 25%, else error
        auto sym = sum < HZ/20 ? -1 : sum < 3*HZ/20 ? 0 : sum < HZ/4 ? 1 : -2;
        if (!valid)
            logf(" %3d %2d %d", edge, sum, sym);
        return sym;
    }

    bool decode () {
        auto f = ((int) bits & 0x01) * 0x10 +
                 ((int) (~bits>>20) & 0x01) * 0x08 +
                 (parity((bits>>21) & 0xFF)) * 0x04 +
                 (parity((bits>>29) & 0x7F)) * 0x02 +
                 (parity((bits>>36) & 0x7FFFFF));
        DateTime dt = { fromBcd((bits >> 50) & 0xFF),
                        fromBcd((bits >> 45) & 0x1F),
                        fromBcd((bits >> 36) & 0x3F),
                        fromBcd((bits >> 29) & 0x3F),
                        fromBcd((bits >> 21) & 0x7F),
                        (int) (bits >> 42) & 0x07 }; // weekday as seconds
        dt.ms = (bits >> 17) & 0x01;                 // dst as subseconds
        if (f == 0 && dt.yr > 25 && dt.mo > 0 && dt.dy > 0)
            now = dt;
        else
            logf("check %02x: %07x%08x",
                f, (uint32_t) (bits>>32), (uint32_t) bits);
        bits = 0;
        return f == 0;
    }

    static bool parity (uint32_t v) {
        return __builtin_parity(v);
    }

    static uint8_t fromBcd (uint8_t v) {
        return v - 6 * (v>>4);
    }
};

} // namespace jeeh::util

As configured here, feed() needs to be called 256x each second (based on the RTC clock), with the current signal level from the DCF77 receiver. This does several things:

  • first off: locate the pulse "0" to "1" transitions to find out where each seconds tick starts
  • then count the "1" period for the next 0.25s and decide whether it's a short (0.1s) or long (0.2s) pulse, or whether there is no proper pulse
  • collect these bits for 60 seconds, ending with the absence of a pulse
  • check parity bits and decode the time signal, saved as a DateTime struct
  • return 0 while decoding is ongoing and 1 when a proper capture has been received
  • the seconds field (DateTime::ss) is actually zero, but returned as the weekday (1..7)
  • the milliseconds field (DateTime::ms) is set to one if daylight saving time is active

In short: whenever feed() returns 1, the now DateTime value contains the decoded information. It may not be accurate if a false positive passed the parity test, but two consecutive results will msot likely be correct if these results are indeed 60s apart.

The code which presents this on the OLED is here:

c011w/dcf77.cpp
#include <jee.h>
#include <jee/hal.h>
#include <jee/dev/ssd1306.h>
#include <jee/util/dcf77.h>
#include <jee/util/fonts.h>
using namespace jeeh;
#include "defs.hpp"

const Pin vcc ("A0","P+"),
          neg ("A1","U"),
          dcf ("A3","U");
const Pin pwr ("A6","P+"),
          low ("A5","P");

dev::SSD1306<128,32> oled (i2cBus);

uint8_t rtcTick () {
    return ~RTC[0x08]; // ~SSR, counts down
}

void pulse () {
    auto last =0, count =0;
    while (true) {
        auto on = !dcf;
        led = !on;
        auto dt = rtc::getDate();
        if (last != dt.ms/20) {
            last = dt.ms/20;
            count += on;
            logWriter(on ? "+" : ".", 1);
            if (last == 0) {
                logf(" %2d %s", count, rtc::getDate().asText().buf);
                count = 0;
            }
        }
    }
}

void clock () {
    bool inited = rtc::getSecs() != 0;

    util::DCF77 dcf77;

    auto last =0;
    uint32_t prev =0;
    while (true) {
        auto on = !dcf;
        led = !on;

        if (auto t = rtcTick(); last == t)
            continue;
        else
            last = t;

        if (dcf77.feed(on) > 0) {
            auto dt = dcf77.now;
            dt.ss = dt.ms = 0; // clear out dow and tz info

            if (!inited && dt == prev + 60) {
                rtc::set(dt);
                inited = true; // got two valid readings, 60s apart
            }
            prev = dt;

            auto now = rtc::getDate();
            logf("%04x  DCF %s  RTC %s  diff %d ms",
                dcf77.meteo, dt.asText().buf, now.asText().buf,
                now.todMillis() - dt.todMillis());

            char buf [9];
            constexpr const char* days [] = {
                "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"
            };

            if (!inited)
                oled.clear();
            else {
                oled.puts<font11x16>(95, 0, days[dcf77.now.ss]); // day of week
                oled.puts<font6x8>(92, 24, "WS"+dcf77.now.ms); // winter/summer
            }

            snprintf(buf, sizeof buf, "%2d:%02d", dt.hh, dt.mm);
            oled.puts<font16x32>(0, 0, buf);
            snprintf(buf, sizeof buf, "%02d/%02d", dt.dy, dt.mo);
            oled.puts<font6x8>(98, 16, buf);
            snprintf(buf, sizeof buf, "20%02d", dt.yr);
            oled.puts<font6x8>(104, 24, buf);
        }
    }
}

int main () {
    initBoard();
    rtc::init();

    i2cBus.init();
    msWait(50); // FIXME looks like the OLED needs some time to power up
    oled.init();
    oled.puts<font16x32>(0, 0, "DCF77...");

    //pulse();
    clock();
}

It's all still work-in-progress for now, but ... it's a start!