使用 .NET/Blazor 与 Raspberry Pi I2C 传感器交互





5.00/5 (11投票s)
使用运行在 Raspberry Pi 上的 Blazor Server,显示来自 MCP9808 I2C 传感器的数据
引言
尽管我找到了关于在 Pi 上运行 .NET 和 Blazor 的文章,但我没有找到任何关于使用它们为 I2C 总线(集成电路间通信,通常显示为 I2C)传感器创建仪表板的内容。我希望能够在 Pi 上创建可以通过网络上的浏览器访问的 GUI 应用程序。我的第一个尝试是通过在 Windows 系统上创建一个 syslog 服务器,并让 Pi 向其写入。这并不是我想要的。我认为使用更新的 .NET/Blazor 技术在 Pi 上构建一个服务器,既具有教育意义,又实用。随着较新的 Pi4 拥有高达 8GB 的内存,有许多选项可以提供 GUI。
互联网上两位博主的帖子帮助我入门:Jeremy Lindsay (在 Wordpress 上) 和 Bradley Wells (在他自己的网站上)。像大多数搜索一样,需要根据自己的独特情况进行添加和修改。这是修改的结果。
背景
一位朋友想在办公楼里替换 1-Wire 总线温度传感器时,出现了这个需求。由于这些传感器共享总线,因此难以排除故障。本文使用的传感器是 I2C 连接的。对于多个传感器,可以使用多路复用器进行隔离(外部可用,不在 Pi 中)。
Using the Code
硬件
- Raspberry Pi (我使用的是 4 版本)
- 电源
- 传感器 MCP9808 温度模块
- 小型面包板
- 跳线公对母和公对公各 4 根 (如果您使用 T-Cobbler 分线器之一,则不需要母对公)
软件
最新版本的 Raspian (基于 Debian 构建)。从 https://www.raspberrypi.org/software/ 下载。目前,我运行的是 32 位版本。我在尝试在 64 位版本上运行 .NET 时(大部分是 BETA 版本)没有取得太多成功。如果您之前没有为 Pi 配置过操作系统,同一网站上有详细的步骤说明。
配置 Pi 时,请启用 SSH、VNC 和 I2C。首次启动时应该会提供此选项,否则请使用“首选项”。如果您希望能够从其他系统进行 FTP,请安装 vsftpd。
在 Pi 上编程
我更喜欢使用 Visual Studio Code 编写 .NET 代码,并从我的 Linux 主机远程连接。您也可以使用 Windows 系统上的 Visual Studio,然后将文件复制到 Pi,再从那里发布。当您通过 VS Code 远程连接时,会收到一条有关调试的提示,以及一个指向相关 GitHub 文章的链接。我在这里不包含该内容。这两个应用程序都相当简单。
基本操作,启动 VS Code 并通过 SSH 远程连接到 Pi:按 Ctrl-Shift-P,然后向下滚动到“Remote-SSH Connect to host”。输入 pi@<IP 地址> (例如:pi@10.0.2.29)。这假设您使用的是默认用户“pi”。我的 Pi 的地址是 10.0.2.29。打开终端 (Terminal/new terminal)。我们可以在此终端中运行以下所有命令。
安装适用于 ARM 的 .NET Core
来源:https://dotnet.microsoft.com/download/dotnet
目前,5.0.101 是最新版本。SDK 在左侧列中,您需要 32 位版本。我下载 tarball 并手动安装效果最好。您可以通过多种方式将其传输到 Pi。
- 下载到 Pi。
- 下载到您的主机并通过 FTP 传输到 Pi (您需要安装 vsftpd)。
- Sneakernet(通过 U 盘下载到主机并传输)。
- 如果主机是 Linux,将 SD 卡插入主机并复制到其中。在您的 Pi 上
将 tarball 放在 Downloads 目录中。请记住,Linux 区分大小写。Linux 使用“/”作为目录结构,而不是“\”。
安装
一旦文件位于 Pi 上的@HOME/Downloads 目录中,在线即可找到详细说明,但步骤如下
- 通过 SSH 登录到 Pi,或在 VS Code 中打开 Pi 的远程会话(我使用的就是这种方式),您也可以直接在 Pi 本身打开终端。我推荐使用 VS Code,请参阅上面的说明。请注意,您也可以从 Windows 上的 WSL 使用远程 SSH。
- 在终端会话中,执行以下命令
cd $HOME/Downloads mkdir -p $HOME/dotnet tar zxf dotnet-sdk-5.0.101-linux-arm.tar.gz -C $HOME/dotnet
请确保使用您下载的版本,我的是 5.0.101。
- tar 命令可能需要几秒钟。完成后,使用终端中的以下命令添加 2 个环境变量
export DOTNET_ROOT=$HOME/dotnet export PATH=$PATH:$HOME/dotnet
- 要使这两个环境变量在重启后仍然生效,请将它们添加到您的$HOME/.bashrc文件的末尾。请注意文件名中的“.”。您应该可以在 VS Code 中完成此操作,或者可以使用
nano $HOME/.bashrc (nano is a rather basic editor, no mouse).
要测试安装,请在终端中执行
dotnet --version 5.0.101
创建一个控制台应用程序来测试我们的设备和 .NET
cd ~
mkdir TempConsole
cd TempConsole
dotnet new console
The template "Console Application" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on /home/pi/TempConsole/TempConsole.csproj...
Determining projects to restore...
Restored /home/pi/TempConsole/TempConsole.csproj (in 75 ms).
Restore succeeded.
dotnet run
Hello World
请注意,您可能会收到关于 Linux ARM 调试器不起作用的提示,并提供一个解决方法。我尚未在 Pi 上进行测试。如果我进行大量 Blazor 工作,我更喜欢在 Visual Studio 中调试非 GPIO 部分,然后将文件复制到 Pi 以完成。您会在最终版本中看到,我在传感器部分使用了 try/catch,这允许在 Windows 系统上运行。
所以,我们有一个 .NET 控制台程序,带有标准的 Hello World。
接线
接线很简单。引脚布局并不简单。引脚编号与 GPIO 编号不匹配。对于这个设备来说,这也不是什么大问题,我将引用引脚编号(而不是 GPIO)。我们需要从 Pi 到设备的 4 根跳线,Vcc (3.3 伏,引脚 #1),Ground (引脚 #6),I2C 数据 (SDA,引脚 #3) 和 I2C 时钟 (CLK,引脚 #5)。Raspberrypi.org 网站上有引脚布局图。我的 Pi 套件中包含了一个。
修改我们刚刚创建的简单控制台应用程序,以从温度模块获取数据
该模块的数据手册可在制造商网站上找到。
我们需要一个 I2C 库来简化从温度模块获取数据的工作。如今,C# 库并不存在。嗯,即使存在,我也没找到。所以,我们将使用 Linux C 库函数与 interopServices。例如
[DllImport("libc.so.6", EntryPoint = "open")]
public static extern int Open(string fileName, int mode);
对于 open 函数。
请注意,libc.so.6 是指向当前最新版本库的符号链接,在我的 Pi 上是libc-2.28.so。总而言之,我们需要 4 个函数:open、ioctl、read 和 write。我认为 MCP9808 有点独特,因为它需要一个 write 函数来告诉我们要读取哪个寄存器。
在 Linux 中,我们在/dev 文件夹中与设备通信。I2C 总线也在这里。我们使用 open 函数打开第一个 I2C 设备
Open("/dev/i2c-1", OPEN_READ_WRITE);
这将返回一个句柄 (int)。我们使用句柄进行 Ioctl 函数,该函数挂载我们的设备。我们将客户端设置为 0x0703。我们设备的默认总线设备地址是 0x1A,还有地址引脚允许连接多个设备,您可以将它们跳接高。我们只需要默认的。write 命令告诉设备我们要从寄存器 0x05 读取。然后我们将 2 个字节读入 deviceData 字节数组。这得到了高低字节温度数据。设备数据手册给出了计算摄氏度温度的公式。一些高中物理告诉我们如何获得华氏度值。为简洁起见,我忽略了返回值,您应该检查它们,它们返回 -1 表示错误。
在您的 VS Code 编辑器中,转到 tempconsole 文件夹并打开Program.cs 文件。在Program.cs 文件中,将所有内容替换为
using System;
using System.Runtime.InteropServices;
namespace TempConsole
{
class Program
{
// constants for i2c
private static int OPEN_READ_WRITE = 2;
private static int I2C_CLIENT = 0x0703;
// externals for the i2c libraries
[DllImport("libc.so.6", EntryPoint = "open")]
private static extern int Open(string fileName, int mode);
[DllImport("libc.so.6", EntryPoint = "ioctl", SetLastError = true)]
private static extern int Ioctl(int fd, int request, int data);
[DllImport("libc.so.6", EntryPoint = "read", SetLastError = true)]
private static extern int Read(int handle, byte[] data, int length);
[DllImport("libc.so.6", EntryPoint = "write", SetLastError = true)]
private static extern int Write(int handle, byte[] data, int length);
static void Main(string[] args)
{
// read from I2C device bus 1
int i2cHandle = Open("/dev/i2c-1", OPEN_READ_WRITE);
// mount the device at address 0x1A for communication
int registerAddress = 0x1A;
int deviceReturnCode = Ioctl(i2cHandle, I2C_CLIENT, registerAddress);
Console.WriteLine(deviceReturnCode.ToString());
//set byte arrays for specifying the register and reading the data
byte[] deviceData = new byte[2];
byte[] reg=new byte[1];
//we have to tell it what register to read 0x05
reg[0]=0x05;
deviceReturnCode= Write(i2cHandle,reg,1);
//now we can read 2 bytes
deviceReturnCode= Read(i2cHandle, deviceData, deviceData.Length);
int msb=deviceData[0]; //most significant byte
int lsb=deviceData[1]; //least significant byte
//calculate according to the datasheet
msb=msb & 0x1F;
double tempc = (msb * 256) + lsb;
if (tempc > 4095)//positive
tempc -= 8192;//remove sign bit
tempc *= .0625;
//and a little high school physics for F
double tempf=32+tempc*9/5;
Console.WriteLine(tempc.ToString("N1")+(char)176+"C");
Console.WriteLine(tempf.ToString("N1")+(char)176+"F");
}
}
}
现在,执行程序。在终端中运行
dotnet run
24.0°C
75.2°F
这表明我们的模块以及 .NET 本身都在工作。您将获得自己的温度读数。
Blazor
现在,我们将使用 VS Code 终端中的远程 VS Code 连接,让 Blazor 参与进来
cd ~
mkdir tempserver
cd tempserver
dotnet new server
与我们的控制台程序类似,这应该会创建一个 Blazor 服务器应用程序,并最终显示“Restore succeeded”。我们的首要任务是让服务器可供 LAN 上的其他系统访问,默认 URL 仅限于 localhost。打开Program.cs 文件。在IHostBuilder
方法中,有一行“webBuilder.UseStartup<Startup>():
”,在其下方添加一行:WebBuilder.UseUrls("Http://*:5000");
您的方法现在应包含
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.UseUrls("Http://*:5000"); });
保存文件并运行
dotnet run
Building...
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /home/pi/tempserver
我们不再看到文本输出,而是有一个运行在 5000 端口的 Blazor 服务器。VS Code 通常会询问您是否要在主机浏览器中打开它。在我的 Pi 上,这将是http://10.0.2.29:5000。我们没有设置 https,因为我们没有证书。如果您为一个端口(例如 5001)设置 https,您的浏览器会尝试通过 https 连接,有些浏览器会拒绝打开。只需使用 http 即可。
另一个 Hello World。这对于从 Pi 开始来说已经取得了很大的进展。要退出程序,请在终端中按 Ctrl-c。让我们将控制台温度程序移到 Blazor 应用程序中,并在其中显示温度。我们将使用相同的代码来读取传感器,但将其放入一个名为GetTheTemp
的函数中。首先,我们需要添加一点(非常基本的)HTML 来显示读数。一个基本的 HTML 仪表将添加一些相当粗糙的 GUI。正如我之前所说,有很多仪表可供使用,我使用了一个供应商的免费版本。为了避免使用第三方内容,我只使用了这里的基本仪表。
<p>-50<meter id="temp" value=@theTempC min="-50" max="100" ></meter>100 </p>
其中 temp 将在变量theTempC
(摄氏度)中。一个获取读数的按钮(我们稍后会添加一个计时器)。
button class="btn btn-primary" @onclick="startRead">Start Temp Reading</button>
显示摄氏度和华氏度读数的文本。
<p>Temp is: @theTempC °C @theTempF °F </p>
显示错误信息的文本。
<p>Error: @errorMessage</p>
在 Pages 文件夹中,打开Index.razor文件。保留顶行 @page "/" 将其他所有内容替换为
@using System;
@using System.Runtime.InteropServices;
<h1>Current Temperature</h1>
<p>-50<meter id="temp"
value=@theTempC min="-50" max="100" ></meter>100 </p>
<button class="btn btn-primary" @onclick="GetTheTemp">Start Temp reading</button>
<p>Temp is: @theTempC °C @theTempF °F </p><p>Error: @errorMessage</p>
@code
{
public double currentTemp = 0.0;
public string theTempC = string.Empty;
public string theTempF = string.Empty;
int OPEN_READ_WRITE = 2;
string errorMessage = string.Empty;
[DllImport("libc.so.6", EntryPoint = "open")]
private static extern int Open(string fileName, int mode);
[DllImport("libc.so.6", EntryPoint = "ioctl", SetLastError = true)]
private static extern int Ioctl(int fd, int request, int data);
[DllImport("libc.so.6", EntryPoint = "read", SetLastError = true)]
private static extern int Read(int handle, byte[] data, int length);
[DllImport("libc.so.6", EntryPoint = "write", SetLastError = true)]
private static extern int Write(int handle, byte[] data, int length);
public void GetTheTemp()
{
try
{
int I2C_CLIENT = 0x0703; // read from I2C device bus 1
int i2cHandle = Open("/dev/i2c-1", OPEN_READ_WRITE);
// open the device at address 0x1A for communication
int registerAddress = 0x1A;
int deviceReturnCode = Ioctl(i2cHandle, I2C_CLIENT, registerAddress); //set byte
// arrays for specifying the register and reading the data
byte[] deviceData = new byte[2];
byte[] reg = new byte[1];
//we have to tell it to read register 0x05
reg[0] = 0x05;
deviceReturnCode = Write(i2cHandle, reg, 1);
//now we can read 2 bytes
deviceReturnCode = Read(i2cHandle, deviceData, deviceData.Length);
int msb = deviceData[0]; //most significant byte
int lsb = deviceData[1]; //least significant byte
//calculate according to the datasheet
msb = msb & 0x1F;
double tempc = (msb * 256) + lsb;
if (tempc > 4095) //positive
tempc -= 8192; //remove sign bit
tempc *= .0625;
//and a little high school physics for F
double tempf = 32 + tempc * 9 / 5;
currentTemp = tempc;
theTempC = Convert.ToInt32(tempc).ToString();
theTempF = Convert.ToInt32(tempf).ToString();
}
catch (Exception e)
{
errorMessage = e.ToString();
}
}
}
再次运行 dotnet run,点击按钮后,您应该能在主页上看到我们的温度。
这样不错,但我们有一个默认服务器模板的修改版本,并且当多个系统访问 Blazor 服务器时,我的经验好坏参半。所以我决定做一些更改,并将函数替换为一个服务。
- 创建一个定时器服务来刷新读数。
- 将
GetTheTemp
函数转换为服务。 - 自定义以删除模板中我们不需要/不想要的内容。
- 发布(类似于链接)解决方案,以便不每次运行时都让 .NET 进行生成。
删除Data文件夹。
在Pages文件夹中,删除Counter.razor和FetchData.razor。
编辑以下文件
在Startup.cs中,删除以下行
using tempserver.Data;
和
services.AddSingleton<WeatherForecastService>();
在Shared/NavMenu.razor文件中,删除counter
和fetchdata
的列表项
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
我还将“Home
”改为了Temperature
。
在Shared/MainLayout.razor中,我删除了About链接。<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
如果您想添加自己的About链接,请编辑它而不是删除它。
再次运行 .NET 并访问服务器,以确保我们没有破坏任何东西。您应该看到我们的修改。
现在,我们需要删除“Start temp”按钮,而应使用计时器来更新读数,而不是点击按钮。我们将把计时器创建一个服务,并将GetTheTemp
方法转换为一个服务。
在tempserver文件夹中,添加一个名为Services的文件夹。打开Services文件夹并添加一个名为TempTimer.cs的文件和一个名为GetTemp.cs的文件。
我的文件夹结构现在看起来像
打开TempTime.cs并添加以下内容
using System;
using System.Timers;
namespace tempserver.Services
{
public class TempTimer
{
private Timer aTimer;
public void SetTimer(double interval)
{
aTimer = new Timer(interval);
aTimer.Elapsed += TimedOut;
aTimer.Enabled = true;
}
public event Action OnTimeout;
private void TimedOut(Object source, ElapsedEventArgs e)
{
OnTimeout();
}
}
}
保存文件。
编辑GetTemp.cs文件,添加以下内容
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace tempserver.Services
{
public class GetTemp
{
public string theTempC = string.Empty;
public string theTempF = string.Empty;
int OPEN_READ_WRITE = 2;
public string errorMessage = "OK";
[DllImport("libc.so.6", EntryPoint = "open")]
private static extern int Open(string fileName, int mode);
[DllImport("libc.so.6", EntryPoint = "ioctl", SetLastError = true)]
private static extern int Ioctl(int fd, int request, int data);
[DllImport("libc.so.6", EntryPoint = "read", SetLastError = true)]
private static extern int Read(int handle, byte[] data, int length);
[DllImport("libc.so.6", EntryPoint = "write", SetLastError = true)]
private static extern int Write(int handle, byte[] data, int length);
public void GetTheTemp()
{
try
{
int I2C_CLIENT = 0x0703; // read from I2C device bus 1
int i2cHandle = Open("/dev/i2c-1", OPEN_READ_WRITE);
// open the device at address 0x1A for communication
int registerAddress = 0x1A;
int deviceReturnCode = Ioctl(i2cHandle, I2C_CLIENT, registerAddress);
//set byte arrays for specifying the register and reading the data
byte[] deviceData = new byte[2];
byte[] reg = new byte[1];
//we have to tell it to read register 0x05
reg[0] = 0x05;
deviceReturnCode = Write(i2cHandle, reg, 1);
//now we can read 2 bytes
deviceReturnCode = Read(i2cHandle, deviceData, deviceData.Length);
int msb = deviceData[0]; //most significant byte
int lsb = deviceData[1]; //least significant byte
//calculate according to the datasheet
msb = msb & 0x1F;
double tempc = (msb * 256) + lsb;
if (tempc > 4095) //positive
tempc -= 8192; //remove sign bit
tempc *= .0625;
//and a little high school physics for F
double tempf = 32 + tempc * 9 / 5;
theTempC = Convert.ToInt32(tempc).ToString();
theTempF = Convert.ToInt32(tempf).ToString();
}
catch (Exception e)
{
errorMessage = e.ToString();
//so we can see the exception
}
}
}
}
大部分内容是我们方法的复制。保存文件。我们需要将这两个类注册为服务。打开Startup.cs文件,并在ConfigureServices
方法中添加
services.AddTransient<Services.TempTimer>();
和
services.AddSingleton<Services.GetTemp>();
该方法应如下所示
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddTransient<Services.TempTimer>();
services.AddSingleton<Services.GetTemp>();
}
打开Index.razor。我们将删除按钮、GetTheTemp
方法,并注入我们的类。所以,只需将所有内容替换为
@inject Services.TempTimer aTimer
@inject Services.GetTemp gTemp
@page "/"
@using System;
@using System.Runtime.InteropServices;
<h1>Current Temperature</h1>
<p>-50<meter id="temp"
value=@gTemp.theTempC min="-32" max="100" ></meter>100 </p>
<p>Temperature: @gTemp.theTempC °C @gTemp.theTempF °F </p>
<p>Status: @gTemp.errorMessage</p>
@code
{
protected override void OnInitialized()
{
gTemp.GetTheTemp();
InvokeAsync(() => base.StateHasChanged());
StartTimer();
}
void StartTimer()
{
InvokeAsync(() => base.StateHasChanged());
aTimer.SetTimer(2000);
// 2 seconds
aTimer.OnTimeout += TimedOut;
}
void TimedOut()
{
gTemp.GetTheTemp();
InvokeAsync(() => base.StateHasChanged());
// refreshes the GUI
}
}
请注意,sTimer
和gTemp
是我们用于访问类的对象名称,我只是随意命名的(我不太擅长命名)。保存文件并再次运行 .NET。这是我们最终的版本,姑且这么说。
在接触模块前,先释放静电,然后将手指放在温度模块上,您应该会看到温度每 2 秒上升一次。您可以根据需要更改计时器。为避免每次都进行构建,我们将发布我们的程序。在终端中,运行
dotnet publish -r linux-arm -c Release -o /home/pi/tempserver/publish -p:PublishSingleFile=true
(after a minute or so)
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
Restored /home/pi/tempserver/tempserver.csproj (in 378 ms).
tempserver -> /home/pi/tempserver/bin/Release/net5.0/linux-arm/tempserver.dll
tempserver -> /home/pi/tempserver/bin/Release/net5.0/linux-arm/tempserver.Views.dll
tempserver -> /home/pi/tempserver/publish/
这将创建一个名为tempserver的可执行文件,位于publish目录中。该目录包含
-rw-r--r-- 1 pi pi 195 Dec 25 11:00 appsettings.Development.json
-rw-r--r-- 1 pi pi 192 Dec 25 11:00 appsettings.json
-rwxr-xr-x 1 pi pi 78M Jan 2 17:48 tempserver
-rw-r--r-- 1 pi pi 22K Jan 2 17:48 tempserver.pdb
-rw-r--r-- 1 pi pi 20K Jan 2 17:48 tempserver.Views.pdb
drwxr-xr-x 3 pi pi 4.0K Jan 2 17:48 wwwroot
请注意tempserver可执行文件的大小。78 兆。它包含 DLL 等。
可执行文件位于publish文件夹中(默认),您可以在publish命令中指定其他文件夹。
要运行它,我们
cd ./publish
./tempserver
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /home/pi/tempserver/publish
再次
现在您可以从 LAN 上的任何设备访问它,无需 VS Code,只需 SSH 登录到 Pi 启动,或者将其添加到启动程序中。
我的 CS 教授过去常说:“作为学生的练习”,添加一个文本框,允许用户选择数据刷新延迟时间。我为了测试将其设置为 2 秒。请自行尝试。
结论
在我们的 Pi 上安装 .NET 后,我们就可以使用 Blazor 和 C# 创建 Web 服务器了。Pi 的资源有限,但我发现性能是可以接受的。
关于 I2C 的一些注意事项。总线对电容敏感,比对电阻更敏感。这导致了距离和可连接设备数量的限制。您可以尝试通过改变上拉电阻来解决其中一些问题,但我更喜欢使用一个通过 Cat 5 (或以上) 电缆连接的 I2C 扩展器。对于较短的距离,您也可以使用多路复用器来增加地址并分离数据和时钟线。