65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 REDIS 和 GO 语言的 IoT 数据采集系统。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2020年2月2日

CPOL

10分钟阅读

viewsIcon

25411

downloadIcon

170

面向传感器数据采集模块的 IoT 架构提案,使用 REDIS 数据库和 GO 语言。

引言

物联网数据采集系统由一个传感器网络组成,该网络将数据发送到一个或多个中央单元,这些单元进而处理和存储这些数据,使您能够监控和控制本地或远程的设备。

在本文中,我将描述接收器模块的架构。

背景

接收器模块的目的是允许存储由本地或远程分布的微控制器网络发送的数据。

  • 每个微控制器设备都有一个传感器集,其测量值通过不同的通信协议发送到中央接收器。
  • 数据由类似消息代理或网关的聚合器接收。
  • 然后,这些数据将极快地存储在内存缓存中,可供监控应用程序处理。

为了更好地理解,请参见下图

  1. 传感器网络 → 传感器连接到将数据发送到聚合器的微控制器设备。所使用的板卡型号没有限制,可以是 Arduino、ESP8266、ESP32、PIC 等。

    实际上,应根据所需的通信形式选择板卡。

    但是,在设备能够连接到系统之前,必须遵循操作和软件标准。

  2. 通信形式和协议 → 设备与聚合器之间的通信提供了以下标准
    • Wi-fi / 以太网 (Websocket / HTTP / REST)
    • RF / LORA
    • 串行 / 蓝牙通信
    • GSM / GPRS
  3. 接收例程 → GO 语言中的例程,用于接收来自传感器的原始数据、处理它并将其发送到 REDIS 缓存。对于第 2 项中描述的每种通信协议,都有一个专门的例程。
  4. REDIS 缓存 → 临时或永久存储传感器数据。如有必要,可以镜像(复制)缓存。

还可以创建一个类似于 MQQT 协议的消息发布和订阅方案,但这在此处不涵盖。

注释

  1. 根据通信形式和组件之间的距离,可能需要额外的硬件/软件层进行互连。本介绍是对架构的通用和简化描述;
  2. 每个层级的服务器分布不是由架构决定的,但理想情况下,应该有一个专用的 LINUX 服务器用于 REDIS。
  3. 这里我们只描述数据采集模块。控制模块将在稍后详细介绍。

技术

现在我们将列出聚合器模块中使用的主要软件技术

  1. REDIS 是一个内存数据存储系统,具有出色的性能,可用作 NOSQL 数据库、缓存或消息代理。

    REDIS 使用键值数据结构,如链表、字符串、集合等。此外,还可以配置数据为临时的或以多种方式持久化。

    我还重点介绍了对以下功能的支持

    • 复制
    • 存储过程
    • 分区;
    • 等等。
  2. GO 或 Golang 是由 Google 创建的一种编程语言,于 2009 年 11 月开源发布。它是一种静态类型、编译型语言,专注于生产力和并发编程(维基百科)。

    Go 是为当今计算机的多核现实而设计的。因此,由于其健壮性,它被广泛用于大规模网络服务器和分布式系统的编程。

开发环境

现在我们将了解如何在 Linux 操作系统上准备开发环境。

安装 REDIS

要在 Linux 上安装 REDIS,请使用 apt

sudo apt update
sudo apt install redis

将 REDIS 服务器作为服务启动

sudo systemctl start redis-server

检查服务器状态或停止服务

sudo systemctl status redis
sudo systemctl stop redis

REDIS 包带有一个客户端应用程序 redis-cli,可用于维护和测试。要查看一切是否正常,请运行以下命令

redis-cli
set hello world
get hello

如果系统回答“world”,则表示一切正常,您刚刚创建了一个键为“hello”,值为“world”的字符串数据结构。

要退出客户端,请键入:quit

您也可以选择安装一个功能更强大、更易于使用的客户端:Redis Desktop Manager

要了解更多关于 REDIS 的信息

GO 语言安装

第一步是从官方网站下载安装包

https://golang.ac.cn/dl/

然后使用以下命令解压缩文件

sudo tar -C /usr/local -xzf package_name

其中 package_name 是您下载的文件的名称。

例如:go1.13.5.linux-amd64.tar.gz

下一步是设置环境变量。

我不知道这好坏,但在我们之间,GO 语言在许多方面都是权威的,您在准备开发环境时就能体会到这一点。

在安装之前,您必须已经规划好您的工作区(Workspace)将放在哪里,即您的源代码文件将放在哪里。对此有一个模式。这不是规则,但最好遵守 • ᴗ •

假设您的工作区将在 /home/user/gocode 文件夹中。然后,在该文件夹下,必须创建 3 个相同级别的子文件夹

  • pkg 存放您下载或创建的、将在程序中引用的包和库的文件夹
  • src 您的源代码文件
  • bin 这里存放您创建并从 src 文件夹安装的可执行文件。它就像一个部署文件夹。

还需要创建三个环境变量

  • GOPATH 工作区的路径。可以有多个,在这种情况下,用分隔。
  • GOROOT GO 的安装路径。通常是:/usr/local/go
  • GOBIN GO 应该生成可执行文件的文件夹。可以是工作区的 bin 文件夹。但这由您决定。

此外,强烈建议更改 PATH 变量以指向 GO 二进制文件,通常是:/usr/local/go/bin

要创建这些环境变量,我们使用 export 命令。为此,请编辑 ~/.profile 文件(sudo gedit ~/.profile),并将 export 命令放在文件末尾。

在我的例子中,它是这样的

export PATH=$PATH:/usr/local/go/bin
export GOROOT=/usr/local/go
export GOPATH=/home/josecintra/code/go
export GOBIN=/home/josecintra/code/go/bin

还有一点:Golang 强调使用版本控制系统,例如 GIT。您需要一个才能安装第三方库包。

如果您尚未在 Linux 系统上安装,请在此 链接 上查看安装说明。

好了!现在我们只需要选择一个 IDE。

GO 语言的 IDE

这是一个个人问题。我非常喜欢 Geany。它已经配置好用于编辑、编译和运行 GO 程序。

要安装,请使用以下命令

sudo add-apt-repository ppa:geany-dev/ppa
sudo apt-get update
sudo apt-get install geany geany-plugins-common

如果您不想使用 Geany,这里有一份支持 GO 语言的 IDE 列表

要了解更多关于 GO 语言的信息

数据建模

现在我们来谈谈 REDIS 以及它将如何用于这个项目模块。

REDIS 将充当监控系统的主要 DBMS 的辅助数据库。REDIS 的主要功能是存储传感器网络的数据,以便(几乎)实时显示(和处理)。

由于其出色的性能,REDIS 非常适合此类应用,同时还能节省系统的主数据库。

在监控系统模块中,REDIS 将具有其他未在此处介绍的功能。

场景

我们想要建模的场景如下:本地或远程分布的多个传感器将定期将测量数据发送到服务器。收集例程(GOLANG)会将数据存储在 REDIS 中,以便监控应用程序可以处理并在一仪表板上显示信息。

我们希望从传感器存储的信息如下

  • 发送信息的传感器标识
  • 发送日期和时间
  • 测量值

其他信息,如传感器类型、位置、测量单位等,不属于 REDIS 的职责。这将由监控 Web 应用程序负责。

REDIS 在此模块中的作用仅仅是加快数据接收速度,将复杂性留给监控系统。

传感器标识:这是传感器 ID,一个由 3 个字母和数字序列组成的 String,用管道符("|")分隔。

例如

  • 例如,一个温度传感器,无论标准或型号如何,其 ID 可能为“TMP | 1”
  • 湿度传感器可以有 ID“HMD | 100”,依此类推。

显然,ID 不会重复,也就是说,不能有两个具有相同标识的传感器,就像主键一样!

对于 REDIS 来说,传感器 ID 只是一个普通的键,它对此一无所知,除非它可能是唯一的。

发送日期和时间:为提高效率,日期将以 Unix 时间戳格式 存储。

测量值:这是一个存储数值的字符串。同样,REDIS 对此值或其来源一无所知。

注意:传感器 ID 及其测量值由连接传感器的微控制器板(Arduino 系列)提供。发送日期由 GO 语言开发的接收应用程序负责。这是为了标准化和经济考虑(板卡不需要 RTC 模块)。

数据结构

对于使用关系数据库的人来说,REDIS 中数据的存储和访问方式可能会很奇怪。关于这一点,我们在之前的文章中已经介绍了很多教程,所以我们直奔主题……

在 REDIS 提供的各种数据结构中,我们选择了链表,因为在列表末尾进行插入和删除操作速度极快,这正是我们所需要的。

例如,我们来看看列表的插入和删除操作的样子。

假设在“2020/01/01”的“01:00:05”时,“TMP | 5”代码传感器发送了值“25.25”。

我们的列表键将简单地是传感器 ID。因此,每个传感器将有一个列表。

将日期转换为 Unix 时间戳,我们得到:“1577840405”。因此,将此数据存储在列表末尾的命令将是:

RPUSH TMP|5 1577840405|25.25

要获取“TMP | 5”传感器发送的最后一个值,我们使用以下命令

RPOP TMP|5

注意:另外,为了可读性,我们可以使用 JSON 格式表示值,这在支持此技术的 REDIS 世界中非常常见。

开发测试应用程序

我们将开发一个使用 GOLANG 和 REDIS 发送和接收传感器数据的测试应用程序(P.O.C.)。

对于这个特定的测试,我们将使用一个在 Arduino IDE 中编程的 NodeMCU 板。该板将每 30 秒通过 WebSockets 发送一个 NTC 温度传感器的原始数据。

Golang 中的接收应用程序将接收数据,解析并将其存储在 REDIS 中,使其可供 Web 应用程序使用。

我们将从描述 NodeMCU 应用程序开始。

发送站 nodemcu

原型

Components

  1. NodeMCU 板卡。实际上,它可以是任何兼容 Arduino IDE 的 ESP*. * 系列板卡(ESP8266、NodeMCU、Wemos、ESP32 等)
  2. 10K NTC 热敏电阻型温度传感器
  3. 跳线,以及可选的实验板

所需软件

  • 要创建 websocket 服务器,我们需要安装 ArduinoWebsockets ,可以通过 Arduino IDE 本身的库管理器进行安装。
  • 传感器中的温度数据将每 3 分钟发送到 Web 服务器。为了简化代码,我们将使用 NeoTimer 库(也可在管理器中找到)来计算时间,该库使用 millis。
  • 温度读取和计算将通过互联网上提供的以下例程完成:Thermistor Interfacing with NodeMCU

草图

/*
  ESP8266-WS_Clent.ino - Esp8266 Websockets Client
  2020, by José Augusto Cintra 
  http://www.josecintra.com/blog

  This is free and unencumbered software released into the public domain.
  Anyone is free to copy, modify, publish, use, compile, sell, or
  distribute this software, either in source code form or as a compiled
  binary, for any purpose, commercial or non-commercial, and by any
  means.
  For more information, please refer to <http: unlicense.org="">

	This sketch:
        1. Connects to a WiFi network
        2. Connects to a Websockets server
        3. Periodically sends data from a temperature sensor to the server
        4. Receives return messages from the server
*/

// From library Manager
#include <arduinowebsockets.h> 
#include <esp8266wifi.h>
#include <neotimer.h>

const char* ssid = "x";  //Enter your SSID
const char* password = "y"; //Enter your Password
const char* websockets_server_host = "192.168.0.0"; //Enter your server address
const uint16_t websockets_server_port = 8080; // Enter server port

Neotimer mytimer = Neotimer(10000); // Time interval for sending sensor data
String tempString;
String sensor = "TMP|5";
using namespace websockets;

WebsocketsClient client;
void setup() {
    Serial.begin(9600);
    
    // Connect to wifi
    WiFi.begin(ssid, password);

    // Wait some time to connect to wifi
    for(int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) {
        Serial.print(".");
        delay(1000);
    }

    // Check if connected to wifi
    if(WiFi.status() != WL_CONNECTED) {
        Serial.println("No Wifi!");
        return;
    }

    Serial.println("Connected to Wifi, Connecting to server.");
    // try to connect to Websockets server
    bool connected = client.connect(websockets_server_host, websockets_server_port, "/");
    if(connected) {
        Serial.println("Connecetd!");
        //client.send("Hello Server");
    } else {
        Serial.println("Not Connected!");
    }
    
    // run callback when messages are received
    client.onMessage([&](WebsocketsMessage message) {
        Serial.print("Got Message: ");
        Serial.println(message.data());
    });
}

void loop() {
  // let the websockets client check for incoming messages
  if(client.available()) {
    client.poll();
  }
  // Periodic sending of temperature sensor data to the customer
  if (mytimer.repeat()) {
    tempString = readTemperature();
    Serial.println(tempString);
    client.send(sensor + "@" +tempString);
  }
}

// Temperature reading and calculation by the thermistor
double readTemperature() {
  // Code extracted from the 'Thermistor Interfacing with NodeMCU' tutorial available at:
  // https://www.electronicwings.com/nodemcu/thermistor-interfacing-with-nodemcu

  const double VCC = 3.3; // NodeMCU on board 3.3v vcc
  const double R2 = 10000; // 10k ohm series resistor
  const double adc_resolution = 1023; // 10-bit adc
  const double A = 0.001129148; // thermistor equation parameters
  const double B = 0.000234125;
  const double C = 0.0000000876741;
  double Vout, Rth, temperature, adc_value;

  adc_value = analogRead(A0);
  Vout = (adc_value * VCC) / adc_resolution;
  Rth = (VCC * R2 / Vout) - R2;

  temperature = (1 / (A + (B * log(Rth)) + (C * pow((log(Rth)), 3))));  // Temperature in kelvin
  temperature = temperature - 273.15;  // Temperature in degree celsius
  delay(500);
  return (temperature);
}

Golang 接收站

所需软件

我们之前已经展示了如何安装 REDIS 和 GO 语言。但是,要与 REDIS 建立连接并接受 Node 的 websockets 连接,我们需要安装一些库包

  • GORILLA → GO 语言的 Web Toolkit
  • REDIGO → Go 客户端 for REDIS

要安装这些包,请在虚拟终端中输入以下命令

go get github.com/gorilla/websocket
go get github.com/gomodule/redigo/redis

开始吧!

/*
  WS-redis.GO - Golang Websockets Server with REDIS integration
	2020, by José Augusto Cintra 
	http://www.josecintra.com/blog

  This is free and unencumbered software released into the public domain.
  Anyone is free to copy, modify, publish, use, compile, sell, or
  distribute this software, either in source code form or as a compiled
  binary, for any purpose, commercial or non-commercial, and by any
  means.
  For more information, please refer to <http: unlicense.org="">

	This routine:
        1. Starts a Websockets server
        3. Waits for client messages with sensor data  
        4. Parses the data received
        5. Stores data in a REDIS database 
*/

package main

import (
	"fmt"
	"strconv"
	"strings"
	"time"
	"log"
	"github.com/gomodule/redigo/redis" 
	"github.com/gorilla/websocket"
	"net/http"
)

// HTTP request handler
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin:     func(r *http.Request) bool { return true },
}

func main() {

  // Starts the server connection 
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

func handler(writer http.ResponseWriter, request *http.Request) {
	socket, err := upgrader.Upgrade(writer, request, nil)
	if err != nil {
		fmt.Println(err)
	}
	for {
		// reading the message received via Websocket
		msgType, msg, err := socket.ReadMessage() // Dados recebido do sensor
		if err != nil {
			fmt.Println(err)
			return
		}

		// Tratar a mensagem recebida
		var msg_split []string = strings.Split(string(msg), "@")   // Splitted data
		currentTime := strconv.FormatInt(time.Now().Unix(), 10)    // Current date and time
		key := msg_split[0]                                        // Key to the linked list
                                                                   // on Redis
		value := currentTime + "|" + msg_split[1]                  // Value to the linked 
                                                                   // list on Redis

		// Send the data to REDIS
		conn, err := redis.Dial("tcp", "localhost:6379")
		if err != nil {
			log.Fatal(err)
		}
		defer conn.Close()
		_, err = conn.Do("RPUSH", key, value)
		if err != nil {
			log.Fatal(err)
		}

		// Optional: Returning the received message back to the customer
		err = socket.WriteMessage(msgType, msg)
		if err != nil {
			fmt.Println(err)
			return
		}
	}
}

结论

GO 语言和 REDIS 数据库的结合使您能够编写健壮且可扩展的应用程序,并具有出色的性能,使其成为物联网世界的理想选择。

此处显示的示例代码是教学性质的。此应用程序可以在多个方面进行改进

  • 加密和安全功能
  • 通过 GO 例程进行并发
  • 模块化
  • 等等。

下次,我们将在此处展示如何开发用于控制和监控的 Web 应用程序。

在此之前!

参考文献

历史

  • 2020-02-01 - 初始发布
© . All rights reserved.