距离上一篇《张高兴的 .NET Core IoT 入门指南》系列博客的发布已经过去 2 年的时间了,2 年的时间 .NET 版本发生了巨大的变化,.NET Core 也已不复存在,因此本系列博客更名为 《张高兴的 .NET IoT 入门指南》,我也重新审阅了之前的内容进行了相应的更改以保证内容的时效性。

和单片机不同,使用 Linux 开发板、现成的传感器套件以及合适的后端技术几乎可以做成任何东西。为了更好的整合前面章节介绍的内容,本文将制作一个简单的气象站(也许叫环境信息收集装置更合适),至于为何选择制作一个气象站,因为难度不高制作不复杂,并且温湿度传感器花费较低的价格即可获得,可以以低廉的价格换取一个 cool stuff。本文将使用 .NET 6 编写一个控制台应用程序,通过本文你可以学到:

  1. I2C I2cDevice 类的使用;
  2. 摄像头设备 VideoDevice 类的使用;
  3. Iot.Device.Bindings NuGet 包的使用;
  4. 时序数据库 TimescaleDB 的简单使用;
  5. Quartz 定时任务的使用;
  6. 在控制台应用中进行依赖注入;
  7. 使用 Docker 拉取镜像、部署应用。

硬件需求

名称 描述 数量
Orange Pi Zero Linux 开发板 x1
BME280 提供温度、湿度以及气压数据 x1
USB 摄像头 提供环境图像 x1
杜邦线 传感器与开发板的连接线 若干

电路

传感器 接口 开发板接口
BME280 SDA TWI0_SDA (Pin 3)
SCL TWI0_SCK (Pin 5)
VCC 5V (Pin 4)
GND GND (Pin 6)
USB 摄像头 USB USB

准备工作

配置 TimescaleDB 数据库

TimescaleDB 是一款基于 PostgreSQL 插件的时序数据库。考虑到收集的环境数据是按时间进行索引,并且数据基本上都是插入,没有更新的需求,因此选用了时序数据库作为数据存储。TimescaleDB 是 PostgreSQL 的一款插件,可以通过先安装 PostgreSQL 之后再安装插件的形式部署 TimescaleDB,这里直接使用 TimescaleDB 的 Docker 镜像进行部署。

  1. 拉取 TimescaleDB 镜像:
docker pull timescale/timescaledb:latest-pg14
  1. 创建卷,用于持久化数据库数据:
docker volume create tsdb_data
  1. 运行镜像,端口映射为 54321,密码配置为弱密码 @Passw0rd
docker run -d --name timescaledb -p 54321:5432 --restart=always -e POSTGRES_PASSWORD='@Passw0rd' -e TZ='Asia/Shanghai' -e ALLOW_IP_RANGE=0.0.0.0/0 -v tsdb_data:/var/lib/postgresql timescale/timescaledb:latest-pg14
  1. 使用熟悉的数据库管理工具(如 Navicat)创建数据库 WeatherMetrics
CREATE DATABASE "WeatherMetrics"
WITH OWNER = postgres ENCODING = 'UTF8'; CREATE TABLE metrics (
time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT 'now()',
device_id VARCHAR(50) NULL,
weather_type VARCHAR(50) NULL,
temperature DECIMAL(5, 2) NULL,
humidity DECIMAL(5, 2) NULL,
pressure DECIMAL(8, 2) NULL,
image_base64 TEXT NULL
); SELECT create_hypertable('metrics', 'time');

time 表示采集数据的时间,device_id 记录采集设备的 id,weather_type 记录从心知天气获取的天气名,temperature 记录传感器获取的温度,humidity 记录传感器获取的湿度,pressure 记录传感器获取的气压,image_base64 记录摄像头采集的图像。

提示

在数据库中存储任何字符类型以外的数据都是愚蠢的,这里是为了演示,并且只是低分辨率的图像。

超表(hypertable)是 TimescaleDB 的一个重要概念,由若干个块(chunks)组成,将超表中的数据按照时间列(即 metrics 表中的 time 字段)分成若干个块存储,而使用 PostgreSQL 层面上的表(table)实现 SQL 接口的暴露,因此使用 create_hypertable() 将表转换为超表。上面创建的 metrics 表并不是真正意义上的表,表中不存在主键字段,而是类似视图(view)一样的抽象结构。

安装摄像头的依赖库

VideoDevice 类是使用 PInvoke 操作实现的,依赖于 Video for Linux 2(V4L2),因此还需要安装 V4L2 工具:

sudo apt install v4l-utils

实现时还引用了 System.Drawing NuGet 包,因此还需要安装 System.Drawing 的前置依赖:

sudo apt install libc6-dev libgdiplus libx11-dev

编写代码

项目地址:https://github.com/ZhangGaoxing/weather-metrics

项目结构

创建一个控制台应用和类库,项目结构如下:

项目依赖

WeatherMetrics.ConsoleApp 添加如下 NuGet 包引用:

<ItemGroup>
<PackageReference Include="Iot.Device.Bindings" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Quartz" Version="3.3.3" />
<PackageReference Include="System.Device.Gpio" Version="2.0.0" />
</ItemGroup>

WeatherMetrics.Models 添加如下 NuGet 包引用:

<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
</ItemGroup>

数据库上下文与实体类

TimescaleDB 本质上就是一个 PostgreSQL 数据库,因此数据库访问使用 Npgsql 驱动。首先添加实体类 Metrics.cs

public class Metrics
{
[Column("time")]
public DateTime Time { get; set; } = DateTime.Now; [Column("device_id")]
public string DeviceId { get; set; } [Column("weather_type")]
public string WeatherType { get; set; } [Column("temperature")]
public double Temperature { get; set; } [Column("humidity")]
public double Humidity { get; set; } [Column("pressure")]
public double Pressure { get; set; } [Column("image_base64")]
public string ImageBase64 { get; set; }
}

接着添加数据库上下文 WeatherContext.cs

public class WeatherContext : DbContext
{
private readonly string _connectString; public WeatherContext(string connectString)
{
_connectString = connectString;
} protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
optionsBuilder.UseNpgsql(_connectString);
} protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Metrics>()
.ToTable("metrics")
.HasNoKey();
}
}

这里使用了一个传递数据库连接字符串的构造函数,连接字符串从 appsettings.json 文件中读取。由于 metrics 表是无主键的,还需要使用 HasNoKey() 进行标记。EF Core 由于使用了实体跟踪,因此无法对无主键的表进行修改,只能通过执行 SQL 的方式插入数据,在 Metrics.cs 中新增方法:

public static bool Insert(DbContext context, Metrics metrics)
{
int row = context.Database.ExecuteSqlRaw("INSERT INTO metrics VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6})", metrics.Time, metrics.DeviceId, metrics.WeatherType, metrics.Temperature, metrics.Humidity, metrics.Pressure, metrics.ImageBase64); return row > 0;
}

️ 警告

请不要在 SQL 中使用字符串内插。

配置文件

appsettings.json 中添加如下内容:

{
// 数据库连接字符串
"ConnectionString": "Server=localhost;Port=54321;Database=WeatherMetrics;User Id=postgres;Password=@Passw0rd;",
// 定时任务设置
"QuartzCron": "0 0/1 * * * ? *",
// 心知天气的配置
"Xinzhi": {
"Key": "",
"Location": "34.24:117.16"
}
}

初始化与依赖注入配置

新建一个静态类 AppConfig,用于保存依赖注入的 ServiceProvider 变量:

public static class AppConfig
{
public static IServiceProvider ServiceProvider { get; set; }
}

Program.cs 中添加初始化代码:

// 读取配置文件
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build(); // 实例化数据库上下文
using WeatherContext context = new WeatherContext(config["ConnectionString"]); // 配置 I2C,实例化传感器
I2cConnectionSettings i2cSettings = new I2cConnectionSettings(busId: 0, deviceAddress: Bmx280Base.SecondaryI2cAddress);
using I2cDevice i2c = I2cDevice.Create(i2cSettings);
using Bme280 bme = new Bme280(i2c); // 实例化摄像头
VideoConnectionSettings videoSettings = new VideoConnectionSettings(busId: 0, captureSize: (640, 480));
using VideoDevice video = VideoDevice.Create(videoSettings); // 配置依赖注入
AppConfig.ServiceProvider = new ServiceCollection()
.AddSingleton(config)
.AddSingleton(context)
.AddSingleton(bme)
.AddSingleton(video)
.BuildServiceProvider();

配置定时任务

定时任务通过 appsettings.json 中的 QuartzCron 字段设置。Cron 表达式分为 7 个部分,从左至右分别代表:Seconds、Minutes、Hours、DayofMonth、Month、DayofWeek 以及 Year。* 出现的部分表示任意值都会触发定时任务,/ 左侧表示触发的起始时间,右侧表示触发间隔,以 appsettings.json 中的为例,表示从每小时的第 0 分开始触发,每一分钟触发一次。

新建 MetricsJob 类,用于实现定时任务:

public class MetricsJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
return Task.Run(async () =>
{
// TODO:在此处实现定时任务
// 需要完成传感器的读取,心知天气的请求,数据库的插入
});
}
}

传感器的读取

MetricsJob 类中添加方法:

private Metrics GetMetrics()
{
// 获取依赖注入的 Bme280 对象
Bme280 bme = (Bme280)AppConfig.ServiceProvider.GetService(typeof(Bme280)); // 设置传感器的电源模式
bme.SetPowerMode(Bmx280PowerMode.Normal); // 设置读取精度
bme.PressureSampling = Sampling.UltraHighResolution;
bme.TemperatureSampling = Sampling.UltraHighResolution;
bme.HumiditySampling = Sampling.UltraHighResolution; // 读取数据
bme.TryReadPressure(out UnitsNet.Pressure p);
bme.TryReadTemperature(out UnitsNet.Temperature t);
bme.TryReadHumidity(out UnitsNet.RelativeHumidity h); // 传感器休眠
bme.SetPowerMode(Bmx280PowerMode.Sleep); return new Metrics
{
DeviceId = Dns.GetHostName(),
Temperature = Math.Round(t.DegreesCelsius, 2),
Humidity = Math.Round(h.Percent, 2),
Pressure = Math.Round(p.Pascals, 2)
};
}

摄像头捕获图像

MetricsJob 类中添加方法:

private string GetImage()
{
VideoDevice video = (VideoDevice)AppConfig.ServiceProvider.GetService(typeof(VideoDevice)); byte[] image = video.Capture();
return Convert.ToBase64String(image);
}

心知天气 API 请求

通过请求心知天气 API 获得当前位置的天气名称,需要提前在 https://www.seniverse.com/api 申请 API Key。在 MetricsJob 类中添加方法:

private async Task<string> GetXinzhiWeatherAsync()
{
IConfigurationRoot config = (IConfigurationRoot)AppConfig.ServiceProvider.GetService(typeof(IConfigurationRoot)); using HttpClient client = new HttpClient(); try
{
var json = await client.GetStringAsync($"https://api.seniverse.com/v3/weather/now.json?key={config["Xinzhi:Key"]}&location={config["Xinzhi:Location"]}&language=zh-Hans&unit=c");
return (string)JsonConvert.DeserializeObject<dynamic>(json).results[0].now.text;
}
catch (Exception)
{
return string.Empty;
}
}

完善定时任务

public Task Execute(IJobExecutionContext context)
{
return Task.Run(async () =>
{
var metrics = GetMetrics();
metrics.WeatherType = await GetXinzhiWeatherAsync();
metrics.ImageBase64 = GetImage(); WeatherContext context = (WeatherContext)AppConfig.ServiceProvider.GetService(typeof(WeatherContext)); Metrics.Insert(context, metrics);
});
}

创建定时任务触发器

Program.cs 中添加:

// 创建一个触发器
var trigger = TriggerBuilder.Create()
.WithCronSchedule(config["QuartzCron"])
.Build(); // 创建任务
var jobDetail = JobBuilder.Create<MetricsJob>()
.WithIdentity("job", "group")
.Build(); // 绑定调度器
ISchedulerFactory factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
await scheduler.ScheduleJob(jobDetail, trigger);
await scheduler.Start();

这样一个一分钟采集一次数据的简易气象站就完成了。

部署应用

发布到文件

  1. 切换到 WeatherMetrics.ConsoleApp 项目运行发布命令:
dotnet publish -c release -r linux-arm
  1. 将发布后的文件通过 FTP 等方式复制到 Linux 开发板;
  2. WeatherMetrics.ConsoleApp 文件增加可执行权限
sudo chmod +x WeatherMetrics.ConsoleApp
  1. 运行程序
sudo ./WeatherMetrics.ConsoleApp

构建 Docker 镜像

  1. 查看 TimescaleDB 容器的 IP,并修改 appsettings.json 的数据库连接字符串:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' timescaledb
  1. 在项目的根目录中创建 Dockerfile,并将整个项目复制到 Linux 开发板中:
FROM mcr.microsoft.com/dotnet/core/sdk:6.0-focal-arm32v7 AS build
WORKDIR /app # publish app
COPY src .
WORKDIR /app/WeatherMetrics.ConsoleApp
RUN dotnet restore
RUN dotnet publish -c release -r linux-arm -o out ## run app
FROM mcr.microsoft.com/dotnet/core/runtime:6.0-focal-arm32v7 AS runtime
WORKDIR /app
COPY --from=build /app/WeatherMetrics.ConsoleApp/out ./ # install native dependencies
RUN apt update && \
apt install -y --allow-unauthenticated v4l-utils libc6-dev libgdiplus libx11-dev ENTRYPOINT ["dotnet", "WeatherMetrics.ConsoleApp.dll"]
  1. 切换到项目目录,构建镜像:
docker build -t weather-metrics -f Dockerfile .
  1. 运行镜像:
docker run --rm -it --device /dev/video0 --device /dev/i2c-0 weather-metrics

后续工作

程序运行一段时间后,使用标准的 SQL 查询一下数据:

SELECT * FROM metrics
ORDER BY time DESC

硬件是软件的基础,对收集到的数据后续可以使用其他技术进行处理,比如可以使用 ASP.NET 编写 WEB 应用对数据进行展示,或者可以使用 ML.NET 构建机器学习模型对天气进行预测等等。

张高兴的 .NET IoT 入门指南:(七)制作一个气象站的更多相关文章

  1. Asp.Net MVC4.0 官方教程 入门指南之四--添加一个模型

    Asp.Net MVC4.0 官方教程 入门指南之四--添加一个模型 在这一节中,你将添加用于管理数据库中电影的类.这些类是ASP.NET MVC应用程序的模型部分. 你将使用.NET Framewo ...

  2. Asp.Net MVC4.0 官方教程 入门指南之三--添加一个视图

    Asp.Net MVC4.0 官方教程 入门指南之三--添加一个视图 在本节中,您需要修改HelloWorldController类,从而使用视图模板文件,干净优雅的封装生成返回到客户端浏览器HTML ...

  3. 张高兴的 .NET Core IoT 入门指南:(二)GPIO 的使用

    什么是 GPIO GPIO 是 General Purpose Input Output 的缩写,即"通用输入输出". Raspberry Pi 有两行 GPIO 引脚, Rasp ...

  4. 张高兴的 .NET Core IoT 入门指南:(一)环境配置、Blink、部署

    如何在 Raspberry Pi 的 Raspbian 上构建使用 GPIO 引脚的 IoT 程序?你可能会回答使用 C++ 或 Python 去访问 Raspberry Pi 的引脚.现在,C# 程 ...

  5. 张高兴的 .NET Core IoT 入门指南:(四)使用 SPI 进行通信

    什么是 SPI 和上一篇文章的 I2C 总线一样,SPI(Serial Peripheral Interface,串行外设接口)也是设备与设备间通信方式的一种.SPI 是一种全双工(数据可以两个方向同 ...

  6. 张高兴的 .NET Core IoT 入门指南:(三)使用 I2C 进行通信

    什么是 I2C 总线 I2C 总线(Inter-Integrated Circuit Bus)是设备与设备间通信方式的一种.它是一种串行通信总线,由飞利浦公司在1980年代为了让主板.嵌入式系统或手机 ...

  7. 张高兴的 .NET Core IoT 入门指南:(五)PWM 信号输出

    什么是 PWM 在解释 PWM 之前首先来了解一下电路中信号的概念,其中包括模拟信号和数字信号.模拟信号是一种连续的信号,与连续函数类似,在图形上表现为一条不间断的连续曲线.数字信号为只能取有限个数值 ...

  8. 张高兴的 .NET Core IoT 入门指南:(五)串口通信入门

    在开始之前,首先要说明的是串口通信所用到的 SerialPort 类并不包含在 System.Device.Gpio NuGet 包中,而是在 System.IO.Ports NuGet 包中.之所以 ...

  9. unity入门—五分钟制作一个理论上的游戏

    unity入门 前言:这可不是标题党,虽然都是基础的操作,不过含括了基本的流程,比起脑海中的五花八门的画面,入门还是这个现实一点. 这里插两句,unity国外官网下载会推荐你看一个简短的视频,国内官网 ...

随机推荐

  1. 解决点击cell时,UILabel的背景颜色消失的问题

    -(void)setSelected:(BOOL)selected animated:(BOOL)animated{ [super setSelected:selected animated:anim ...

  2. 基于hadoop的数据仓库工具:Hive概述

    Hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张数据库表,并提供完整的sql查询功能,可以将sql语句转换为MapReduce任务进行运行.其优点是学习成本低,可以通过类 ...

  3. 初级Oracle和SQL学习者的学习笔记。韩顺平-玩转oracle。

    我自己就是一个oracle和sql的初学者,前段时间看了韩顺平老师的oracle视频教程,觉得很深入浅出,收获了很多.同时自己也做了不少笔记,现在想将纸质笔记以自己的话总结出来.俗话说得好:教学总是相 ...

  4. 敏感字符串加密处理(PHP实现)

    /** * 敏感字符串加密处理 * @param $raw_str 原始字符串 * @param $before 前面保留的显示位数 * @param $after 后面保留的显示位数 * @para ...

  5. javascript和c#aes加密方法互解

    关键信息如下. javascript function Encrypt() { var key = CryptoJS.enc.Utf8.parse('8080808080808080'); var i ...

  6. UIAlertControllerStyleActionSheet 崩溃。

    即使Devices 设置为iPhone模式,在审核时还是运行在iPad的小屏模式下.因此必须 UIActivityViewController UIAlertControllerStyleAction ...

  7. mac安装docker,显示无效的命令

    mac安装完docker后,显示无效的命令. 在应用程序中找到docker安装的路径,将该路径写到/etc/paths里面

  8. Eigen学习

    Eigen 是一个基于C++的线性代数库,其中实现大量常用的线性代数算法,包括常规矩阵计算,矩阵变换,矩阵分解,矩阵块操作.Eigen 广泛地应用在开源项目中,例如OpenCV,PCL(Point C ...

  9. RabbitMQ 安装和监控[原,转]

    在Windows上安装Rabbit MQ 指南,最好的是这篇<Rabbit MQ Windows Installation guide>,其中还包括了使用.NET RabbitMQ.Cli ...

  10. 354. Russian Doll Envelopes

    You have a number of envelopes with widths and heights given as a pair of integers (w, h). One envel ...