Microsoft Build 2019 为 .NET 开发人员带来了令人激动的消息:.NET Core 3.0 现在支持 C# 8.0、Windows 桌面和 IoT,因此,可以使用现有的 .NET 技能为智能设备开发跨平台应用。在本文中,我将向你演示如何使用 Sense HAT 附加板为 Raspberry Pi 2/3 创建一个 .NET Core 应用。该应用将获得各种传感器读数,并可通过 ASP.NET Core Web API 服务获取最新读数。我将使用 Swagger(图 1)为此服务创建简单的 UI,这样,你可以轻松地与 IoT 设备进行交互。除了从设备获取数据外,还可以远程更改 Sense HAT LED 阵列的颜色(图 2)。可通过我的 GitHub 页面 bit.ly/2WCj0G2 获得随附的代码。
图 1 通过 Web API 从运行 .NET Core 3.0 应用的 IoT 设备获取传感器读数
图 2 IoT 设备的远程控制(带有 Sense HAT 附加板的 Raspberry Pi 2)
我的设备
首先,设置 IoT 设备,包括 Raspberry Pi 2(或简称 RPi2)和 Sense HAT 附加板(参阅图 2 右侧)。RPi2 是一款流行的单板机,可以运行 Linux 或 Windows 10 IoT 核心版操作系统。例如,可以从 adafruit.com 获得该设备。Sense HAT 附加板配有多个传感器,包括温度计、气压计、磁力仪、陀螺仪和加速度计。此外,Sense HAT 有 64 块 RGB LED,可以将其用作指示器或低分辨率屏幕。Sense HAT 可以轻松连接到 RPi2,因此,可以快速获取传感器读数,而无需任何焊接。为了启动 RPi2,我使用了 USB 硬件保护装置,然后使用 USB 适配器连接到本地 Wi-Fi 网络(因为 RPi2 与 RPi3 不同,它没有内置 Wi-Fi 模块)。
设置好硬件之后,我安装了 Windows 10 IoT 核心版。可以在 bit.ly/2Ic1Ew1 中找到完整说明。简单地说,你可将操作系统闪存到 microSD 卡上。这可以通过 Windows 10 核心板仪表板 (bit.ly/2VvXm76) 轻松实现。安装仪表板后,转到“设置新设备”选项卡,选择可用于 PC 的设备类型、操作系统内部版本、设备名称、管理员密码和 Wi-Fi 连接。接受软件许可条款并单击“下载并安装”。完成这一过程后,将 microSD 卡插入 IoT 设备并启动。你很快就会在仪表板的“我的设备”选项卡下看到该设备显示为一个条目。
公用库
在开始实际实现之前,我安装了 .Net Core 3.0 Preview 5。然后,我打开 Visual Studio 2019 并使用类库 (.NET Core) 模板创建了一个新项目。我分别将项目和解决方案名称设置为 SenseHat.DotNetCore.Common 和 SenseHat.DotNetCore。然后,我通过在包管理器控制台 (bit.ly/2KRvCHj) 中调用以下命令来安装 Iot.Device.Bindings NuGet 包 (github.com/dotnet/iot):
代码语言:javascript复制Install-Package IoT.Device.Bindings -PreRelease
-Source https://dotnetfeed.blob.core.windows.net/dotnet-iot/index.json
IoT.Device.Bindings 是一个适用于热门 IoT 硬件组件的开放源代码 .NET Core 实现,可让用户为 IoT 设备快速实现 .NET Core 应用。可在 src/devices/SenseHat 子文件夹下找到与此处关联度最高的 Sense HAT 绑定。快速浏览一下这段代码,就会发现 IoT.Device.Bindings 使用 I2C 总线来访问 Sense HAT 组件。在展示如何使用 Sense HAT 的 IoT.Device.Bindings 之前,我首先实现了简单的 POCO 类 SensorReadings (SenseHat.DotNetCore.Common/Sensors/SensorReadings.cs):
代码语言:javascript复制public class SensorReadings
{
public Temperature Temperature { get; set; }
public Temperature Temperature2 { get; set; }
public float Humidity { get; set; }
public float Pressure { get; set; }
public DateTime TimeStamp { get; } = DateTime.UtcNow;
}
此类有五个公共成员。四个成员存储实际传感器读数、两个温度读数以及湿度和压力。存在两个温度,因为 Sense HAT 有两个温度传感器,一个嵌入在 LPS25H 压力传感器中 (bit.ly/2MiYZEI),另一个嵌入在 HTS221 湿度传感器中 (bit.ly/2HMP9GO)。温度由 Iot.Device.Bindings 中的 Iot.Units.Temperature 结构表示。此结构没有任何公共构造函数,但可以使用以下静态方法之一进行实例化:FromCelsius、FromFahrenheit 或 FromKelvin。给定其中一个标度的温度,结构将一个值转换为其他单位。然后,可以通过读取相应的属性获得所选单位的温度:摄氏、华氏或开尔文。
SensorReadings 类的第五个成员 TimeStamp 包含记录传感器读数的时间点。在将数据流式传输到云,然后使用 Azure 流分析或时序见解等专用服务执行时间序列分析时,此功能非常有用。
此外,SensorReadings 类会覆盖 ToString 方法,以在控制台中正确显示传感器值:
代码语言:javascript复制public override string ToString()
{
return $"Temperature: {Temperature.Celsius,5:F2} °C"
$" Temperature2: {Temperature2.Celsius,5:F2} °C"
$" Humidity: {Humidity,4:F1} %"
$" Pressure: {Pressure,6:F1} hPa";
}
下一步是实现 SenseHatService 类来访问选定的 Sense HAT 组件。出于测试目的,我还决定实现另一个用作模拟器的 SenseHatEmulationService 类。我想快速测试 Web API 和代码的其他元素,而无需连接硬件。为了在两个具体实现之间轻松切换,我使用了依赖关系注入软件设计模式。为此,我首先声明了一个接口:
代码语言:javascript复制public interface ISenseHatService
{
public SensorReadings SensorReadings { get; }
public void Fill(Color color);
public bool EmulationMode { get; }
}
此接口定义了两个具体实现的公共成员:
- SensorReadings:调用方可使用此属性获取从传感器中得到的值(如果使用 SenseHatService,则得到实际值),以及通过 SenseHatEmulationService 随机生成的值。
- Fill:此方法将用于统一设置所有 LED 的颜色。
- EmulationMode:此属性表示是否模拟 Sense HAT(true 或 false)。
接下来,我实现了 SenseHatService 类。让我们先来看一下类构造函数,如图 3**** 所示。此构造函数实例化三个私有只读字段:ledMatrix、pressureAndTemperatureSensor 和 temperatureAndHumiditySensor。为此,我使用了 IoT.Device.Bindings 包中的相应类:分别为 SenseHatLedMatrixI2c、SenseHatPressureAndTemperature 和 SenseHatTemperatureAndHumidity。每个类都有一个公共构造函数,它接受一个参数,即抽象类型 System.Device.I2c.I2cDevice 的 i2cDevice。此参数的默认值为 null。在这种情况下,IoT.Device.Bindings 将在内部使用 System.Device.I2c.Drivers.UnixI2cDevice 或 System.Device.I2c.Drivers.Windows10I2cDevice 的实例,具体取决于操作系统。
SenseHatService 类的选定成员
代码语言:javascript复制public class SenseHatService : ISenseHatService
{
// Other members of the SenseHatService (refer to the companion code)
private readonly SenseHatLedMatrix ledMatrix;
private readonly SenseHatPressureAndTemperature pressureAndTemperatureSensor;
private readonly SenseHatTemperatureAndHumidity temperatureAndHumiditySensor;
public SenseHatService()
{
ledMatrix = new SenseHatLedMatrixI2c();
pressureAndTemperatureSensor = new SenseHatPressureAndTemperature();
temperatureAndHumiditySensor = new SenseHatTemperatureAndHumidity();
}
}
之后,可以获取传感器值(在随附的代码中,请参阅 SenseHat.DotNetCore.Common/Services/SenseHatService.cs):
public SensorReadings SensorReadings => GetReadings();
private SensorReadings GetReadings()
{
return new SensorReadings
{
Temperature = temperatureAndHumiditySensor.Temperature,
Humidity = temperatureAndHumiditySensor.Humidity,
Temperature2 = pressureAndTemperatureSensor.Temperature,
Pressure = pressureAndTemperatureSensor.Pressure
};
}
并设置 LED 阵列的颜色:public void Fill(Color color) => ledMatrix.Fill(color);
不过,在尝试之前,让我们首先实现模拟器 SenseHatEmulationService。该类的完整代码可在随附代码 (SenseHat.DotNetCore.Common/Services/SenseHatEmulationService.cs) 中找到。该类派生自 ISenseHatService 接口,因此它必须实现前面描述的三个公共成员:SensorReadings、Fill 和 EmulationMode。
我首先开始合成传感器读数。为此,我实现了以下帮助程序方法:
代码语言:javascript复制
private double GetRandomValue(SensorReadingRange sensorReadingRange)
{
var randomValueRescaled = randomNumberGenerator.NextDouble()
* sensorReadingRange.ValueRange();
return sensorReadingRange.Min randomValueRescaled;
}
private readonly Random randomNumberGenerator = new Random();
GetRandomValue 使用 System.Random 类生成 double,其值在指定范围内。此范围由 SensorReadingRange (SenseHat.DotNetCore.Common/Sensors/SensorReadingRange.cs) 的实例表示,该实例具有两个公共属性 Min 和 Max,它们指定模拟传感器读数的最小值和最大值。此外,SensorReadingRange 类实现一个实例方法 ValueRange,该方法返回 Max 和 Min 之间的差值。
GetRandomValue 用于 GetSensorReadings 私有方法,以合成传感器读数,如图 4 所示。
SenseHatEmulationService
代码语言:javascript复制
private SensorReadings GetSensorReadings()
{
return new SensorReadings
{
Temperature =
Temperature.FromCelsius(GetRandomValue(temperatureRange)),
Humidity = (float)GetRandomValue(humidityRange),
Temperature2 =
Temperature.FromCelsius(GetRandomValue(temperatureRange)),
Pressure = (float)GetRandomValue(pressureRange)
};
}
private readonly SensorReadingRange temperatureRange = new SensorReadingRange { Min = 20, Max = 40 };
private readonly SensorReadingRange humidityRange = new SensorReadingRange { Min = 0, Max = 100 };
private readonly SensorReadingRange pressureRange = new SensorReadingRange { Min = 1000, Max = 1050 };
代码语言:javascript复制最后,我实现了公共成员:
代码语言:javascript复制public SensorReadings SensorReadings => GetSensorReadings();
public void Fill(Color color) {/* Intentionally do nothing*/}
public bool EmulationMode => true;
我还创建了 SenseHatServiceHelper 类,稍后我将使用该类 (SenseHat.DotNetCore.Common/Helpers/SenseHatServiceHelper.cs)。SenseHatServiceHelper 类有一个公共方法,该方法返回 SenseHatService 或 SenseHatEmulationService 的一个实例,具体取决于布尔参数 emulationMode:
代码语言:javascript复制public static ISenseHatService GetService(bool emulationMode = false)
{
if (emulationMode)
{
return new SenseHatEmulationService();
}
else
{
return new SenseHatService();
}
}
控制台应用
现在,我可以使用控制台应用测试代码。可在开发电脑或 IoT 设备上使用此应用。在电脑上运行时,应用可以使用模拟器。要在模拟和非模拟模式之间切换,我将使用一个命令行参数,它将是一个包含 Y 或 N 字母的字符串。控制台应用将解析这个参数,然后使用 SenseHatEmulationService (Y) 或 SenseHatService (N)。在模拟模式下,应用仅显示合成的传感器读数。在非模拟模式下,应用将显示从实际传感器获得的值,并且还将按顺序更改 LED 阵列颜色。
为了创建控制台应用,我使用一个使用控制台应用 (.NET Core) 项目模板创建的新项目 SenseHat.DotNetCore.ConsoleApp 补充了 SenseHat.DotNetCore 解决方案。然后,我引用了 SenseHat.DotNetCore.Common 并开始实现 Program 类。我定义了三个私有成员:
代码语言:javascript复制private static readonly Color[] ledColors =
{ Color.Red, Color.Blue, Color.Green };
private static readonly int msDelayTime = 1000;
private static int ledColorIndex = 0;
第一个成员 ledColors 是一组颜色,将按顺序使用以统一更改 LED 阵列。第二个成员 msDelayTime 指定访问连续传感器读数和更改 LED 阵列之间的持续时间。最后一个成员 ledColorIndex 存储 ledColors 集合中当前显示的颜色的值。
接下来,我编写了两个帮助程序方法:
代码语言:javascript复制private static bool ParseInputArgumentsToSetEmulationMode(string[] args)
{
return args.Length == 1 && string.Equals(
args[0], "Y", StringComparison.OrdinalIgnoreCase);
}
和:
代码语言:javascript复制private static void ChangeFillColor(ISenseHatService senseHatService)
{
if (!senseHatService.EmulationMode)
{
senseHatService.Fill(ledColors[ledColorIndex]);
ledColorIndex = ledColorIndex % ledColors.Length;
}
}
第一种方法 ParseInputArgumentsToSetEmulationMode 分析输入参数的集合(传递给 Main 方法)以确定是否应该使用模拟模式。仅当集合具有等于 y 或 Y 的一个元素时,该方法才返回 true。
第二种方法 ChangeFillColor 仅在模拟模式关闭时有效。如果是,该方法将从 ledColors 集合中获取一个颜色并将其传递给 SenseHatService 具体实现的 Fill 方法;然后递增 ledColorIndex。在此特定实现中,可以省略 ChangeFillColor 中的 if 语句,因为 SenseHatEmulationService 的 Fill 方法不执行任何操作。但是,通常应包含 if 语句。
最后,我实现了 Main 方法,如图 5**** 所示,该方法将所有内容连接在一起。首先,解析输入参数,并根据结果调用 SenseHatServiceHelper 的 GetService 静态方法。其次,我显示字符串以通知用户应用是否在模拟模式下工作。第三,我开始无限循环,可从中获取传感器读数,并最终更改 LED 阵列颜色。循环使用 msDelayTime 暂停应用执行。
控制台应用的入口点
代码语言:javascript复制static void Main(string[] args)
{
// Parse input arguments, and set emulation mode accordingly
var emulationMode = ParseInputArgumentsToSetEmulationMode(args);
// Instantiate service
var senseHatService = SenseHatServiceHelper.GetService(emulationMode);
// Display the mode
Console.WriteLine($"Emulation mode: {senseHatService.EmulationMode}");
// Infinite loop
while (true)
{
// Display sensor readings
Console.WriteLine(senseHatService.SensorReadings);
// Change the LED array color
ChangeFillColor(senseHatService);
// Delay
Task.Delay(msDelayTime).Wait();
}
}
将应用部署到设备
要在 RPi 中运行应用,可以将 .NET Core 3.0 SDK 下载到设备中,在设备中复制代码,生成应用,最后使用 dotnet 运行 .NET Core CLI 命令来执行应用。或者,可以使用开发电脑发布应用,然后将二进制文件复制到设备。在这里,我将选择第二个选项。
首先生成 SenseHat.DotNetCore 解决方案,然后在解决方案文件夹中调用以下命令:
代码语言:javascript复制dotnet publish -r win-arm
如果项目文件包含以下属性,则可以省略参数 -r win-arm:<RuntimeIdentifier>win-arm</RuntimeIdentifier>。可以使用所选的命令行界面或 Visual Studio 中的包管理器控制台。如果使用的是 Visual Studio 2019,则还可以使用 UI 工具发布应用。为此,请转到“解决方案资源管理器”,右键单击 ConsoleApp 项目,然后从上下文菜单中选择“发布”。Visual Studio 将显示一个对话框,可以在其中选择“文件夹”作为发布目标。然后,在发布配置文件设置下,将“部署模式”设置为“自包含”,并将“目标运行时”设置为“win-arm”。
无论选择哪种方法,.NET Core SDK 都将准备二进制文件以进行部署。默认情况下,可以在 bin(Configuration)netcoreapp3.0win-armpublish 输出文件夹中找到它们。将此文件夹复制到设备。复制这些文件最直接的方法是使用 Windows 文件资源管理器 (bit.ly/2WYtnrT)。打开文件资源管理器,在地址栏中输入设备的 IP 地址,然后加上双反斜杠,后跟 c。我的 RPi 的 IP 为 192.168.0.109,因此,我键入了 \192.168.0.109c
若要实际运行该应用,可以使用 PowerShell。最简单的方法是使用 IoT 仪表板,如图 6 所示。只需右键单击“我的设备”选项卡下的设备,然后选择“PowerShell”。出现提示时,需要再次键入管理员密码。然后转到包含二进制文件的文件夹,并通过调用以下内容执行应用:
代码语言:javascript复制.SenseHat.DotNetCore.ConsoleApp N
图 6 使用 Windows 10 IoT 仪表板启动 PowerShell
你将看到实际传感器读数(如图 7 中所示),SenseHat LED 阵列将更改颜色。可以看到两个传感器报告的温度几乎相同。湿度和压力也在预期范围内。为了进一步确认一切正常,让我们引入一些更改。为此,请用手遮盖设备。如你所见,湿度将上升。在我的案例中,湿度从 37% 增加到 51%。
图 7 使用 Raspberry Pi 2 上执行的控制台应用获取传感器读数
Web API
使用 .NET Core,可以进一步执行操作,通过 Web API 服务公开传感器读数。我将使用 Swagger UI (bit.ly/2IEnXXV) 创建一个简单的 UI。借助此 UI,最终用户可向 IoT 设备发送 HTTP 请求,因为他会将这些请求发送到常规 Web 应用!我们来看一下这是如何工作的。
我首先通过另一个 ASP.NET Core Web 应用程序项目 SenseHat.DotNetCore.WebApp 扩展 SenseHat.DotNetCore 解决方案,使用 API 模板创建项目。然后,我引用了 SenseHat.DotNetCore.Common 项目并安装了 Swashbuckle NuGet 包 (Install-Package Swashbuckle.AspNetCore)。有关在 ASP.NET Core Web 应用程序中设置 Swagger 的详细说明,请参阅 bit.ly/2BpFzWC,因此,我将省略所有详细信息,并仅显示在我的应用中设置 Swagger UI 所需的说明。所有这些更改都将在 SenseHat.DotNetCore.WebApp 项目的 Startup.cs 文件中实现。
首先,我实现了只读字段 openApiInfo:
代码语言:javascript复制private readonly OpenApiInfo openApiInfo = new OpenApiInfo
{
Title = "Sense HAT API",
Version = "v1"
};
然后,我通过添加以下语句修改了 ConfigureServices 方法:
代码语言:javascript复制services.AddSwaggerGen(o =>
{
o.SwaggerDoc(openApiInfo.Version, openApiInfo);
});
这些语句负责设置 Swagger 生成器。接下来,我为依赖关系注入注册了 ISenseHatService 接口的具体实现:
代码语言:javascript复制var emulationMode = string.Equals(Configuration["Emulation"], "Y",
StringComparison.OrdinalIgnoreCase);
var senseHatService = SenseHatServiceHelper.GetService(emulationMode);
services.AddSingleton(senseHatService);
此处,SenseHat 服务的具体实现是根据配置文件中的 Emulation 设置来设置的。模拟键在 appsettings.Development.json 中设置为 N,在 appsettings.json 中设置为 Y。因此,Web 应用将在开发环境中使用模拟器,在生产环境中使用真正的 Sense HAT 硬件。与任何其他 ASP.NET Core Web 应用一样,默认情况下为版本生成配置启用生产环境。
最后一个修改是通过设置 Swagger 终结点的说明来扩展 Configure 方法:
代码语言:javascript复制app.UseSwagger();
app.UseSwaggerUI(o =>
{
o.SwaggerEndpoint($"/swagger/
{openApiInfo.Version}/swagger.
json",
openApiInfo.Title);
});
然后,我实现了 Web API 控制器的实际类。在 Controllers 文件夹中,我创建了新文件 SenseHatController.cs,我对其进行了修改,如图 8 所示。SenseHatController 有一个公共构造函数,用于依赖关系注入以获取 ISenseHatService 的实例。对此实例的引用存储在 senseHatService 字段中。
SenseHatController Web API 服务的完整定义
代码语言:javascript复制[Route("api/[controller]")]
[ApiController]
public class SenseHatController : ControllerBase
{
private readonly ISenseHatService senseHatService;
public SenseHatController(ISenseHatService senseHatService)
{
this.senseHatService = senseHatService;
}
[HttpGet]
[ProducesResponseType(typeof(SensorReadings), (int)HttpStatusCode.OK)]
public ActionResult<SensorReadings> Get()
{
return senseHatService.SensorReadings;
}
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Accepted)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.InternalServerError)]
public ActionResult SetColor(string colorName)
{
var color = Color.FromName(colorName);
if (color.IsKnownColor)
{
senseHatService.Fill(color);
return Accepted();
}
else
{
return BadRequest();
}
}
}
SenseHatController 有两个公共方法,Get 和 SetColor。第一个方法处理 HTTP GET 请求,并从 Sense HAT 附加板返回传感器读数。第二个方法 SetColor 处理 HTTP POST 请求。SetColor 有一个字符串参数 colorName。客户端应用使用此参数选择颜色,然后使用该颜色统一更改 LED 阵列颜色。
我现在可以测试该应用的最终版本。同样,我可以使用模拟器或真正的硬件来实现这一目的。让我们从开发电脑开始,使用 Debug 生成配置来运行应用。执行应用后,在浏览器地址栏中键入 localhost:5000/swagger。请注意,如果使用随附的代码并使用 Kestrel Web 服务器执行应用,浏览器将自动打开,你将被重定向到 swagger 终结点。我使用 launchSettings.json 的 launchUrl 对其进行了配置。
在 Swagger UI 中,将看到一个包含 Sense HAT API 标头的页面。此标头正下方是带有 GET 和 POST 标签的两个行。如果单击其中一行,则会显示更详细的视图。借助此视图,可以向 SenseHatController 发送请求,查看响应并查阅 API 文档(如前面的图 1**** 左侧所示)。若要显示传感器读数,请展开 GET 行,然后单击“试一试”和“执行”按钮。将请求发送到 Web API 控制器并进行处理,传感器读数将显示在 Response 主体下(如前面的图 1 右侧所示)。以类似的方式发送 POST 请求,只需在单击“执行”按钮之前设置 colorName 参数。
为了在设备上测试应用,我使用“发布”配置发布了应用,然后将生成的二进制文件部署到 Raspberry Pi(与使用控制台应用一样)。然后,我必须打开端口 5000(通过在 RPi2 上从 PowerShell 调用以下命令来完成该操作):
代码语言:javascript复制netsh advfirewall firewall add rule name="ASP.NET Core Web Server port"
dir=in action=allow protocol=TCP localport=5000
最后,我使用以下命令从 PowerShell 执行了应用:
代码语言:javascript复制.SenseHat.DotNetCore.WebApp.exe --urls http://*:5000
附加的命令行参数 (urls) 用于将默认 Web 服务器终结点从 localhost 更改为本地 IP 地址 (bit.ly/2VEUIji),以便可以从网络中的其他设备访问 Web 服务器。完成此操作后,我在开发电脑上打开浏览器,键入 192.168.0.109:5000/swagger,随即显示 Swagger UI(当然,你将需要使用设备的 IP)。然后,我开始发送 HTTP 请求以获取传感器读数并更改 LED 阵列颜色,如前面的图 1**** 和图 2 所示。
总结
在本文中,我演示了如何使用 .NET Core 3.0 实现跨平台的 IoT 应用。该应用在 Raspberry Pi 2/3 上运行,并与 Sense HAT 附加板的组件进行交互。使用该应用,我通过 Iot.Device.Bindings 库获得了各种传感器读数(温度、湿度和大气压力)。然后,我实现了 ASP.NET Core Web API 服务并使用 Swagger 创建了一个简单的 UI。现在,只需单击几下鼠标,任何人都可以访问这些传感器读数并远程控制设备。代码可以运行,而不会对其他系统进行任何更改,包括 Raspbian。此示例演示了 .NET 开发人员如何利用现有的技能和代码库来编程各种物联网设备。
Dawid Borycki是一名软件工程师和生物医学研究员,并在 Microsoft 技术方面拥有丰富的经验。他完成了一系列具有挑战性的项目,包括开发设备原型软件(主要是医疗设备)、嵌入式设备接口以及桌面和移动编程。Borycki 是 Microsoft Press 出版的以下两本书籍的作者:“Programming for Mixed Reality (2018)”《混合现实编程 (2018)》和“Programming for the Internet of Things (2017)”《物联网编程 (2017)》。
衷心感谢以下 Microsoft 技术专家对本文的审阅:Krzysztof Wicher Krzysztof Wicher 是 IoT 存储库中 Sense HAT 设备绑定的作者,同时也是 .NET Core 团队(包括 .NET IoT 团队)的软件工程师。