树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统

树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统

通过arduino采集室内室外温湿度数据,通过串口以发送json数据的形式,将采集到的数据传递给树莓派,树莓派中有一个服务,获取到来自串口的数据,将数据写入rrd文件。树莓派上再制作一个脚本,每5分钟生成图形,放在http服务器目录下,以便通过浏览器查看。
树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统
树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统

项目用到的材料清单如下
必备材料:
1、树莓派一块
2、欧西亚(Oregon Scientific)RTGR328N室外传感器一台。(欧西亚家用气象站的室外温度湿度传感器,后面会有简单的说明)
3、Arduino nano一块
4、RXB60 433MHz超外差接收模块一块(也使用过那种很便宜的超再生模块,似乎抗干扰能力很差,后来换用这种模块后就稳定多了)
5、连接线若干
6、433MHz弹簧天线一根(RXB60接收模块本身不含天线,需要自己在模块ANT针上连接一根天线)
7、170孔面包板两块(实际上这次使用两块170孔面包板比使用400孔面包板更为紧凑、空间利用率更高)
8、DHT11或者DHT22温湿度模块(主要用来获取室内温湿度,这里我用的是DHT22)

树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统
树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统

软件方面:
1、树莓派最好用debian或者ubuntu的系统
2、安装rrdtool和python3,
3、再装个nginx或者apache之类的http服务器,主要是为了通过网页查看rrdtool出的图。

为什么要使用欧西亚的温湿度传感器?
最初是为了利用家里现有的国产易美特气象站433MHz室外温湿度传感器来得到室外温湿度数据,也测试过另一款很便宜的标识为UPM的洋垃圾433Mhz温湿度传感器,发现对于我这个并不精通无线电这方面的门外汉来说,解码这些传感器数据困难很大,在研究过程中发现欧西亚气象站在外国使用量很大(因为欧西亚就是家用气象站的鼻祖嘛),研究的朋友也很多,有现成且很成熟的解码方案,遂在某鱼上低价入手一台二手的用来研究。有朋友会问了,为什么不用arduino、ESP8266\ESP32自己做一台蓝牙、WIFI或者433Mhz的室外传感器呢?我只是觉得像欧西亚这种成品传感器有它独特的优点:现成的可以在恶劣室外环境下工作的外壳(主要是保护主要器件防水、防尘、传感器有专门的开口)、433Mhz相对功耗比较低、使用干电池比较方便也能更好适应室外恶劣的环境,最后,最重要的一个有点就是拿来就用(我的这个传感器型号是RTGR328N,需要两节5号电池,有电波钟功能,只是频率和国内不同,需要改造才能使用)。

为什么不直接使用树莓派连接433Mhz接收器和DHT22传感器?
原因很简单,因为arduino有现成的欧西亚传感器解码代码,我们的原则是尽量拿来就用。

Arduino nano传感器连接:

天线部分,因为原来就是直接找了一根铜连接线插在面包板上,感觉总是拖个小尾巴,干脆就在某宝买了2.54转1.27的小电路板和弹簧天线,将天线焊在电路板1.27的孔内,2.54孔位焊上排针,做成了如下图的天线:

树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统
树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统
树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统

Arduino代码

欧西亚传感器解码部分来自于https://github.com/rene-d/oregon,在此基础上,我做了些修改,加入了读取DHT22传感器数据的代码,以及改为通过串口以json格式发送数据。
使用到的arduino库有ArduinoJson和DHT sensor library两个库。

// oregon.ino
// rene-d 01/2019
#include "DHT.h"
#include <ArduinoJson.h>
#include "Oregon.h"
#include "decode.h"
#define DHTPIN 3     // Digital pin connected to the DHT sensor
// Define pin where is 433Mhz receiver (here, pin 2)
#define MHZ_RECEIVER_PIN 2
//#define DHTTYPE DHT11   // DHT 11
#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321
//#define DHTTYPE DHT21   // DHT 21 (AM2301)
DHT dht(DHTPIN, DHTTYPE);
void setup()
{
    Serial.begin(115200);
    //DHTxx
    dht.begin();
    

    Serial.println(">>>>Arduino-Oregon RTGR328N Example<<<<");
    //Serial.println("Setup started");

    memset(&sensor_clock, 0, sizeof(sensor_clock));
    sensor_clock.day = 1;
    sensor_clock.month = 1;
    pinMode(LED_BUILTIN, OUTPUT);

    // Setup received data
    attachInterrupt(digitalPinToInterrupt(MHZ_RECEIVER_PIN), ext_int_1, CHANGE);

    //Serial.println("Setup completed");
    //Serial.println("Begin receive 433MHz Oregon RTGR328N sensors...");
}

void loop()
{
    //------------------------------------------
    // Start process new data from Oregon sensors
    //------------------------------------------
    noInterrupts(); // Disable interrupts
    word p = pulse;
    pulse = 0;
    interrupts(); // Enable interrupts

    if (p != 0)
    {
        if (orscV2.nextPulse(p))
        {
            // Decode Hex Data once
            byte length;
            const byte *osdata = DataToDecoder(orscV2, length);

            print_hexa(osdata, length);
//            Serial.println(F("Outdoor:"));
            oregon_decode(osdata, length);
            float h = dht.readHumidity();
            float t = dht.readTemperature();

            if (isnan(h) || isnan(t)) {
              Serial.println(F("Failed to read from DHT sensor!"));
              return;
            }

            float hic = dht.computeHeatIndex(t, h, false);
//            Serial.println(F("Indoor:"));
//            Serial.print(F("Humidity: "));
//            Serial.print(h);
//            Serial.print(F("%  Temperature: "));
//            Serial.print(t);
//            Serial.print(F("°C "));
//            Serial.print(F("Heat index: "));
//            Serial.print(hic);
//            Serial.println(F("°C"));


StaticJsonDocument<200> doc;

doc["name"] = "RTGR328N";

JsonObject data = doc.createNestedObject("data");

JsonObject data_indoor = data.createNestedObject("indoor");
data_indoor["temperature"] = t;
data_indoor["humidity"] = h;
data_indoor["heat-index"] = hic;

JsonObject data_outdoor = data.createNestedObject("outdoor");
data_outdoor["humidity"] = sensor_info.hum;
data_outdoor["temperature"] = sensor_info.temp;
data_outdoor["channel"] = sensor_info.channel;
data_outdoor["id"] = sensor_info.id;
data_outdoor["time"] = date_time;
data_outdoor["bat"] = sensor_info.bat;
data_outdoor["RAW"] = raw_result;
serializeJson(doc, Serial);
//Serial.println();
//serializeJsonPretty(doc, Serial);

        }
    }

    update_clock();

    if (Serial.available() > 0)
    {
        // read the incoming byte:
        byte incomingByte = Serial.read();

        if (incomingByte == 't')
        {
            // print the current time
            print_hexa(&incomingByte, 0);
        }
    }
}

int days_in_month(byte month, byte year)
{
    static byte days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if (month == 2)
    {
        // we will never reach 2100
        if (year % 4 == 0)
            return 29;
    }
    return days[month];
}

void update_clock()
{
    unsigned long now = millis();
    unsigned long o = now - sensor_clock.now;
    if (o >= 1000)
    {
        digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));

        sensor_clock.now = now;

        while (o >= 1000)
        {
            // advance the clock by one second

            o -= 1000;

            // dirty code done dirt cheap...
            sensor_clock.second++;
            if (sensor_clock.second >= 60)
            {
                sensor_clock.second = 0;
                sensor_clock.minute++;
                if (sensor_clock.minute >= 60)
                {
                    sensor_clock.minute = 0;
                    sensor_clock.hour++;
                    if (sensor_clock.hour >= 24)
                    {
                        sensor_clock.hour = 0;
                        sensor_clock.day++;
                        if (sensor_clock.day > days_in_month(sensor_clock.month, sensor_clock.year))
                        {
                            sensor_clock.day = 1;
                            sensor_clock.month++;
                            if (sensor_clock.month > 12)
                            {
                                sensor_clock.month = 1;
                                sensor_clock.year++;
                            }
                        }
                    }
                }
            }
        }
    }
}

void print_hexa(const byte *data, byte length)
{
    raw_result = "";
    //Serial.println();

    // print the sensor/board clock
    char buf[32];
    snprintf(buf, 32, "%04u/%02u/%02u %02u:%02u:%02u.%03lu",
             2000 + sensor_clock.year, sensor_clock.month, sensor_clock.day,
             sensor_clock.hour, sensor_clock.minute, sensor_clock.second,
             millis() - sensor_clock.now);
    date_time = buf;
    //Serial.println(buf);

    // print data in byte order, that's NOT the nibble order
    //for (byte i = 0; i < length; ++i)
    //{
    //    Serial.print(data[i] >> 4, HEX);
    //    Serial.print(data[i] & 0x0F, HEX);
    //}
    //Serial.println();
    for (byte i = 0; i < length; ++i)
    {
        raw_result += tohex(data[i] >> 4);
        raw_result += tohex(data[i] & 0x0F);
    }

}

decode.h文件主要是修改了原来直接输出到串口的部分代码,变更为将数据存储在全局变量中,以便最后统一输出为json格式。

// decode.h
// rene-d 01/2019

// internal clock, set from OS sensor
struct
{
    byte year, month, day;
    byte hour, minute, second;
    unsigned long now;
} sensor_clock;
struct
{
    byte id, channel;
    float temp;
    byte hum, bat;
} sensor_info;
String raw_result = "";
String date_time = "";
//
// get a nibble (a half byte)
//
byte nibble(const byte *osdata, byte n)
{
    if (n & 1)
    {
        return osdata[n / 2] >> 4;
    }
    else
    {
        return osdata[n / 2] & 0xf;
    }
}

//
// calculate the packet checksum (sum of all nibbles)
//
bool checksum(const byte *osdata, byte last, byte check)
{
    byte calc = 0;

    for (byte i = 1; i <= last; i++)
    {
        calc += nibble(osdata, i);
    }

    return (check == calc);
}

//
// print a buffer in nibble order, with nibbles packed by field
//
void print_nibbles(const byte *osdata, size_t len, const char *def)
{
    static const char digits[] = "0123456789ABCDEF";
    char hexa[128];
    size_t i = 0;
    size_t j = 0;
    size_t k = 0;
    char c, n;

    n = def[0];
    if (n)
        n -= '0';
    c = n;

    while (i < len * 2 && j < sizeof(hexa) - 3)
    {
        hexa[j++] = digits[nibble(osdata, i++)];
        if (c > 0)
        {
            c--;
            if (c == 0)
            {
                // reverse last def[k] chars
                if (n > 1)
                {
                    for (char z = 0; z < n / 2; ++z)
                    {
                        c = hexa[j - 1 - z];
                        hexa[j - 1 - z] = hexa[j - n + z];
                        hexa[j - n + z] = c;
                    }
                }
                hexa[j++] = ' ';
                n = def[++k];
                if (n)
                    n -= '0';
                c = n;
            }
        }
    }
    hexa[j] = 0;

    Serial.print("data: ");
    Serial.println(hexa);
}

//
// message 3EA8 or 3EC8 : clock
//
void decode_date_time(const byte *osdata, size_t len)
{
    if (len < 12)
        return;

    byte crc = osdata[11];
    bool ok = checksum(osdata, 21, crc);

    byte channel = (osdata[2] >> 4);
    byte rolling_code = osdata[3];

    int year = (osdata[9] >> 4) + 10 * (osdata[10] & 0xf);
    int month = (osdata[8] >> 4);
    int day = (osdata[7] >> 4) + 10 * (osdata[8] & 0xf);
    int hour = (osdata[6] >> 4) + 10 * (osdata[7] & 0xf);
    int minute = (osdata[5] >> 4) + 10 * (osdata[6] & 0xf);
    int second = (osdata[4] >> 4) + 10 * (osdata[5] & 0xf);

    channel = nibble(osdata, 5);

#ifdef ARDUINO
    if (!ok)
        Serial.println(" bad crc");

    if (ok && (nibble(osdata, 8) & 2) != 0)
    {
        // update the sensor clock
        sensor_clock.now = millis();
        sensor_clock.year = year;
        sensor_clock.month = month;
        sensor_clock.day = day;
        sensor_clock.hour = hour;
        sensor_clock.minute = minute;
        sensor_clock.second = second;
        Serial.println("update sensor clock!");
    }

    //Serial.print(" id: ");
    //Serial.println(rolling_code);
    //Serial.print(" channel: ");
    //Serial.println(channel);

    char buf[100];
    snprintf(buf, sizeof(buf), " date: 20%02d/%02d/%02d", year, month, day);
    //Serial.println(buf);
    snprintf(buf, sizeof(buf), " time: %02d:%02d:%02d", hour, minute, second);
    //Serial.println(buf);
#else
    print_nibbles(osdata, len, "14121222211212");

    char buf[80];
    snprintf(buf, sizeof(buf), "channel=%d crc=$%02X %s id=%d channel=%d state=%d clock=20%02d/%02d/%02d %02d:%02d:%02d",
             channel, crc, ok ? "OK" : "KO", rolling_code,
             channel, nibble(osdata, 8),
             year, month, day, hour, minute, second);
    Serial.println(buf);

    static const char *label[] = {
        "alwaysA",        // 0    b0    A=1010 - not part of the message
        "id_msg",         // 1    b0
        "id_msg",         // 2    b1
        "id_msg",         // 3    b1
        "id_msg",         // 4    b2
        "channel",        // 5    b2
        "rolling code",   // 6    b3
        "rolling code",   // 7    b3
        "clock state",    // 8    b4    0,4,8: date is invalid, 2 or 6: date is valid
        "second (units)", // 9    b4
        "second (tens)",  // 10   b5
        "minute (unit)",  // 11   b5
        "minute (tens)",  // 12   b6
        "hour (units)",   // 13   b6
        "hour (tens)",    // 14   b7
        "day (units)",    // 15   b7
        "day (tens)",     // 16   b8
        "month",          // 17   b8
        "day of week",    // 18   b9
        "year (units)",   // 19   b9
        "year (tens)",    // 20   b10
        "?",              // 21   b10
        "crc",            // 22   b11
        "crc",            // 23   b11
    };

    for (int i = 0; i < 24; ++i)
    {
        snprintf(buf, sizeof(buf), "  nibble %2d : %X  %s", i, nibble(osdata, i), label[i]);
        Serial.println(buf);
    }
#endif
}

//
// message 3CCx or 02D1: temperature humidity
//
void decode_temp_hygro(const byte *osdata, size_t len)
{
    if (len < 9)
        return;

    byte crc = osdata[8];
    bool ok = checksum(osdata, 15, crc); // checksum = nibbles 1-15, result is nibbles 17..16

    byte channel = (osdata[2] >> 4);
    byte rolling_code = osdata[3];

    int temp = ((osdata[5] >> 4) * 100) + ((osdata[5] & 0x0F) * 10) + ((osdata[4] >> 4));
    if (osdata[6] & 0x08)
        temp = -temp;

    byte hum = ((osdata[7] & 0x0F) * 10) + (osdata[6] >> 4);
//
    byte BatteryLevel = (osdata[4] & 0x4) ? 10 : 90;
//    Serial.println("Oregon battery level: " + String(BatteryLevel));
//
    byte bat = osdata[4] & 0x4;
//    byte bat = osdata[4] & 0x0F;

#ifdef ARDUINO
    if (!ok)
        Serial.println(" bad crc");
    //Serial.print(" id: [");
    //Serial.print(rolling_code);
    //Serial.print("] channel: [");
    //Serial.print(channel);

    //Serial.print("] temperature: [");
    //Serial.print(temp / 10.);
    //Serial.print("] humidity: [");
    //Serial.print(hum);

    //Serial.print("] bat: [");   
    //Serial.print(BatteryLevel);
//    Serial.print(bat);
    //if ((bat & 4) != 0)
    //{
    //    Serial.println(" low]");
    //}
    //else
    //{
    //    Serial.println(" ok]");
    //}
        sensor_info.id = rolling_code;
        sensor_info.channel = channel;
        //sensor_info.temp = temp;
        sensor_info.temp = temp / 10.;
        sensor_info.hum = hum;
        sensor_info.bat = BatteryLevel;
#else
    print_nibbles(osdata, len, "141214212");
    char buf[80];
    snprintf(buf, sizeof(buf), "channel=%d crc=$%02X %s id=%d temp=%.1lf°C hum=%d%% bat=%d",
             channel, crc, ok ? "OK" : "KO", rolling_code,
             temp / 10., hum, bat);
    Serial.println(buf);

    static const char *label[] = {
        "alwaysA",              // 0    b0 lsb  always A=1010
        "id_msg",               // 1    b0 msb
        "id_msg",               // 2    b1 lsb
        "id_msg",               // 3    b1 msb
        "id_msg",               // 4    b2 lsb
        "channel",              // 5    b2 msb
        "rolling code",         // 6    b3 lsb
        "rolling code",         // 7    b3 msb
        "battery",              // 8    b4 lsb  bit 3=1 => low?
        "temperature (tenths)", // 9    b4 msb
        "temperature (units)",  // 10   b5 lsb
        "temperature (tens)",   // 11   b5 msb
        "temperature (sign)",   // 12   b6 lsb
        "humidity (units)",     // 13   b6 msb
        "humidity (tens)",      // 14   b7 lsb
        "comfort",              // 15   b7 msb  comfort ??? (according to RFLink) 0: normal, 4: comfortable, 8: dry, C: wet
        "crc",                  // 16   b8 lsb
        "crc",                  // 17   b8 msb
    };

    for (int i = 0; i < 18; ++i)
    {
        snprintf(buf, sizeof(buf), "  nibble %2d : %X  %s", i, nibble(osdata, i), label[i]);
        Serial.println(buf);
    }
#endif
}

void oregon_decode(const byte *osdata, size_t len)
{
    // we need 2 bytes at least
    //  - the preamble A
    //  - the ID on four nibbles
    if (len < 3)
        return;

    uint16_t id = (((uint16_t)nibble(osdata, 4)) << 12) +
                  (((uint16_t)nibble(osdata, 3)) << 8) +
                  (((uint16_t)nibble(osdata, 2)) << 4) +
                  (((uint16_t)nibble(osdata, 1)));

#ifndef ARDUINO
    char buf[32];
    snprintf(buf, 32, "message: %04X len=%zu", id, len);
    Serial.println(buf);
#endif

    if ((id & 0xFFF0) == 0x3CC0 || id == 0x02D1)
    {
        decode_temp_hygro(osdata, len);
    }
    else if (id == 0x3EA8 || id == 0x3EC8)
    {
        decode_date_time(osdata, len);
    }
    else
    {
#ifndef ARDUINO
        print_nibbles(osdata, len, "1412");
        Serial.println("UNKNOWN");
#endif
    }
}
String tohex(int n) {
  if (n > 15) {
    return "0"; 
  }
  String result = "";
  char _16[] = {
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
  };
  result = _16[n];
  return result;
}

树莓派上首先要自己写一个服务,在后台读取USB 串口传过来的json数据,写入rrd文件。
首先,在/home/ubuntu/目录下新建service_read_sensor.py文件,内容如下,主要作用是
service文件调用的

#!/usr/bin/python3
import serial
import threading
import json
import time
import math
import rrdtool
import sys

DATA = "" # 读取的数据
NOEND = True # 是否读取结束
sensor_data = ""

def calc_heat_index(T, RH):
    '''NOAA计算体感温度 参数为气温(摄氏度)和相对湿度(0~100或者0~1)'''
    if RH < 1:
        RH *= 100
    T = 1.8 * T + 32
    HI = 0.5 * (T + 61 + (T - 68) * 1.2 + RH * 0.094)
    if HI >= 80:  # 如果不小于 80华氏度 则用完整公式重新计算
        HI = -42.379 + 2.04901523 * T + 10.14333127 * RH - .22475541 * T * RH \
             - .00683783 * T * T - .05481717 * RH * RH + .00122874 * T * T * RH \
             + .00085282 * T * RH * RH - .00000199 * T * T * RH * RH
        if RH < 13 and 80 < T < 112:
            ADJUSTMENT = (13 - RH) / 4 * math.sqrt((17 - abs(T - 95)) / 17)
            HI -= ADJUSTMENT
        elif RH > 85 and 80 < T < 87:
            ADJUSTMENT = (RH - 85) * (87 - T) / 50
            HI += ADJUSTMENT
    return round((HI - 32) / 1.8, 2)

# 读数据的本体
def read_data(ser):
    #global DATA, NOEND, sensor_data
    global DATA, NOEND, sensor_data,logfile
    # 循环接收数据(此为死循环,可用线程实现)
    while NOEND:
        if ser.in_waiting:
            #logfile = open("\tmp\read_serial.log", "a")
            #DATA = ser.read(ser.in_waiting).decode("gbk")
            DATA = ser.readline().decode("gbk")
            logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]receive:"+DATA+"\n")
            logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]json.loads:")
            try:
                sensor_data = json.loads(DATA)
            except ValueError:
                logfile.write("error for json\n")
            else:
                logfile.write(str(sensor_data)+"\n")
                update=rrdtool.updatev('/home/ubuntu/test2.rrd','N:%s:%s:%s:%s:%s:%s' % (str(sensor_data['data']['indoor']['temperature']),str(sensor_data['data']['indoor']['humidity']),str(sensor_data['data']['indoor']['heat-index']),str(sensor_data['data']['outdoor']['temperature']),str(sensor_data['data']['outdoor']['humidity']),str(calc_heat_index(sensor_data['data']['outdoor']['temperature'],sensor_data['data']['outdoor']['humidity'])))) 
                logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]rrdtool update:"+str(update)+"\n")
            logfile.flush()
            #logfile.close()
# 打开串口
def open_seri(portx, bps, timeout):
    global logfile
    ret = False
    try:
        # 打开串口,并得到串口对象
        ser = serial.Serial(portx, bps, timeout=timeout)
        # 判断是否成功打开
        if(ser.is_open):
            ret = True
            th = threading.Thread(target=read_data, args=(ser,)) # 创建一个子线程去等待读数据
            th.start()
    except Exception as e:
        #logfile = open("\tmp\open_serial.log", "a")
        logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]open serial error:"+e+"\n")
        logfile.flush()
        #logfile.close()
    return ser, ret

# 关闭串口
def close_seri(ser):
    global NOEND
    NOEND = False
    ser.close()

# 写数据
def write_to_seri(ser, text):
    res = ser.write(text.encode("gbk")) # 写
    return res

# 读数据
def read_from_seri():
    global DATA
    data = DATA
    DATA = "" #清空当次读取
    return data

if __name__ == "__main__":

    logfile = open("/tmp/read_serial.log", "a")
    ser, ret = open_seri('/dev/ttyUSB0', 115200, 1)

    while True:
        time.sleep(2)
    logfile.close()


在/lib/systemd/system目录下新建read_serial_sensor.service文件,内容如下:

[Unit]
Description=Read Serial Sensor Service
After=multi-user.target
Conflicts=getty@tty1.service

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/ubuntu/service_read_sensor.py
StandardInput=tty-force

[Install]
WantedBy=multi-user.target

sudo systemctl start read_serial_sensor.service
sudo systemctl status read_serial_sensor.service
sudo systemctl enable read_serial_sensor.service

另外再写一个定时执行的脚本,每5分钟生成一次rrd图,图片放在/usr/local/nginx/html/images/目录下,这是我的nginx根目录下的一个图片目录。

#!/usr/bin/env python3

import sys
import time
import rrdtool

rrdtool.graph(
    '/usr/local/nginx/html/images/temp_test-1day.png',
    '--title','室内室外温度与体感温度24小时曲线图',
    '--vertical-label','temp °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
    'DEF:c=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
    'DEF:d=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
    'LINE1:a#1E90FFFF:室内温度',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C',
    'LINE1:c#87CEEBFF:室内体感',
    'GPRINT:c:LAST: Current\:%2.2lf°C',
    'GPRINT:c:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:c:MAX:Maximum\:%2.2lf°C',
    'LINE1:b#00FF7FFF:室外温度',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C',
    'LINE1:d#66CD00FF:室外体感',
    'GPRINT:d:LAST:Current\:%2.2lf°C',
    'GPRINT:d:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:d:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
    '/usr/local/nginx/html/images/temp_test-1week.png',
    '--start','end-1w',
    '--end','00:00',
    '--title','室内室外温度',
    '--vertical-label','temp °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
    'LINE1:a#00CF00FF:indoor temp',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
    'LINE1:b#002A97FF:outdoor temp',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
    '/usr/local/nginx/html/images/temp_test-1month.png',
    '--start','end-1m',
    '--end','00:00',
    '--title','室内室外温度',
    '--vertical-label','temp °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
    'LINE1:a#00CF00FF:indoor temp',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
    'LINE1:b#002A97FF:outdoor temp',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
    '/usr/local/nginx/html/images/temp_test-1year.png',
    '--start','end-1y',
    '--end','00:00',
    '--title','室内室外温度',
    '--vertical-label','temp °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
    'LINE1:a#00CF00FF:indoor temp',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
    'LINE1:b#002A97FF:outdoor temp',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
    '/usr/local/nginx/html/images/hum_test-1day.png',
    '--title','室内室外湿度',
    '--vertical-label','hum %',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','0',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
    'LINE1:a#00CF00FF:indoor hum',
    'GPRINT:a:LAST: Current\:%3.2lf%%',
    'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
    'LINE1:b#002A97FF:outdoor hum',
    'GPRINT:b:LAST:Current\:%3.2lf%%',
    'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
    '/usr/local/nginx/html/images/hum_test-1week.png',
    '--start','end-1w',
    '--end','00:00',
    '--title','室内室外湿度',
    '--vertical-label','hum %',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','0',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
    'LINE1:a#00CF00FF:indoor hum',
    'GPRINT:a:LAST: Current\:%3.2lf%%',
    'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
    'LINE1:b#002A97FF:outdoor hum',
    'GPRINT:b:LAST:Current\:%3.2lf%%',
    'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
    '/usr/local/nginx/html/images/hum_test-1month.png',
    '--start','end-1m',
    '--end','00:00',
    '--title','室内室外湿度',
    '--vertical-label','hum %',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','0',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
    'LINE1:a#00CF00FF:indoor hum',
    'GPRINT:a:LAST: Current\:%3.2lf%%',
    'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
    'LINE1:b#002A97FF:outdoor hum',
    'GPRINT:b:LAST:Current\:%3.2lf%%',
    'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
    '/usr/local/nginx/html/images/hum_test-1year.png',
    '--start','end-1y',
    '--end','00:00',
    '--title','室内室外湿度',
    '--vertical-label','hum %',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','0',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
    'LINE1:a#00CF00FF:indoor hum',
    'GPRINT:a:LAST: Current\:%3.2lf%%',
    'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
    'LINE1:b#002A97FF:outdoor hum',
    'GPRINT:b:LAST:Current\:%3.2lf%%',
    'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
    'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
    '/usr/local/nginx/html/images/hic_test-1day.png',
    '--title','室内室外体感温度',
    '--vertical-label','hic °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
    'LINE1:a#00CF00FF:indoor hic',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
    'LINE1:b#002A97FF:outdoor hic',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
    '/usr/local/nginx/html/images/hic_test-1week.png',
    '--start','end-1w',
    '--end','00:00',
    '--title','室内室外体感温度',
    '--vertical-label','hic °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
    'LINE1:a#00CF00FF:indoor hic',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
    'LINE1:b#002A97FF:outdoor hic',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
    '/usr/local/nginx/html/images/hic_test-1month.png',
    '--start','end-1m',
    '--end','00:00',
    '--title','室内室外体感温度',
    '--vertical-label','hic °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
    'LINE1:a#00CF00FF:indoor hic',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
    'LINE1:b#002A97FF:outdoor hic',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
    '/usr/local/nginx/html/images/hic_test-1year.png',
    '--start','end-1y',
    '--end','00:00',
    '--title','室内室外体感温度',
    '--vertical-label','hic °C',
    '--alt-autoscale-max',
    '--slope-mode',
    '--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
    '--lower-limit','-10',
    '--font','TITLE:10:',
    '--font','AXIS:7:',
    '--font','LEGEND:8:',
    '--font','UNIT:7:',
    '--font','WATERMARK:7:',
    'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
    'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
    'LINE1:a#00CF00FF:indoor hic',
    'GPRINT:a:LAST: Current\:%2.2lf°C',
    'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
    'LINE1:b#002A97FF:outdoor hic',
    'GPRINT:b:LAST:Current\:%2.2lf°C',
    'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
    'GPRINT:b:MAX:Maximum\:%2.2lf°C')

在系统cron中添加一个任务,每5分钟执行一次脚本,生成rrd图。

*/5 * * * * sudo python3 /home/ubuntu/rrdtool_graph.py >> /tmp/temp_hum_rrdtool_graph.log 2>&1

在nginx的html根目录下新建index.html文件,将生成的rrd图放到html页面中,这样就可以通过网页查看曲线图了。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta http-equiv="refresh" content="300">
<title>温度湿度监控</title>

</head>

<body>   
<table>
    <tr>
        <td>
			<img src="images/temp_test-1day.png" alt="1day">
			1day
			</br>
			<img src="images/temp_test-1week.png" alt="1week">
			1week
			</br>
			<img src="images/temp_test-1month.png" alt="1month">
			1month
			</br>
			<img src="images/temp_test-1year.png" alt="1year">
			1year
		</td>
        <td>
			<img src="images/hum_test-1day.png" alt="1day">
			1day
			</br>
			<img src="images/hum_test-1week.png" alt="1week">
			1week
			</br>
			<img src="images/hum_test-1month.png" alt="1month">
			1month
			</br>
			<img src="images/hum_test-1year.png" alt="1year">
			1year
		</td>
        <td>
			<img src="images/hic_test-1day.png" alt="1day">
			1day
			</br>
			<img src="images/hic_test-1week.png" alt="1week">
			1week
			</br>
			<img src="images/hic_test-1month.png" alt="1month">
			1month
			</br>
			<img src="images/hic_test-1year.png" alt="1year">
			1year
		</td>
    </tr>
</table>

</body>

</html>

树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统
树莓派+欧西亚室外温度湿度传感器+Arduino Nano+433Mhz接收模块+RRDTOOL制作室内室外温湿度记录监控系统
节目预告:
下一篇文章我们将把这个系统进行改造,使用博世bme280传感器来代替DHT22传感器,通过bme280传感器我们也可以得到大气压数据。开发板改为ESP8266,将原来串口输出的数据改为wifi连接,将数据以mqtt形式发布到mqtt broker,使用domoticz来订阅这个主题,以达到将传感器数据传入domoticz的目的。

上一篇:【模块】北斗+GPS双模定位模块


下一篇:ESP8266--HTTP请求信息