1. 项目概述:C#全栈物联网开发实战
作为一名在工业自动化领域摸爬滚打多年的老工程师,我见证了太多项目因为技术栈割裂导致的维护噩梦。直到.NET IoT库的出现,终于让我们C#开发者能够用统一的语言搞定从边缘设备到上位机的全链路开发。这次要分享的正是基于树莓派4B和.NET IoT库的完整解决方案,包含GPIO控制、摄像头采集、传感器数据融合等工业级功能实现。
这个方案最吸引人的地方在于它的"全C#"特性。传统方案中,树莓派端用Python写数据采集,上位机用C#开发界面,中间还得折腾各种通信协议转换。现在只需要一套C#代码,从设备驱动到云端通信全部打通。实测在工业环境下,这种架构的稳定性比Python方案提升至少30%,特别是在7x24小时连续运行的场景中。
技术选型关键点:选择树莓派4B而非3B+,主要考虑其4GB内存和USB 3.0接口,在处理视频流和多个传感器数据时更加游刃有余。工业场景建议加装金属外壳和散热片。
2. 环境搭建与基础配置
2.1 树莓派系统准备
首先需要为树莓派准备64位操作系统。虽然官方Raspbian是32位的,但在处理视频流和大数据包时,64位系统能更好地发挥树莓派4B的性能。我推荐使用Ubuntu Server 20.04 LTS for Raspberry Pi:
bash复制# 刷写系统到SD卡(Windows下用Rufus工具)
dd if=ubuntu-20.04-preinstalled-server-arm64+raspi.img of=/dev/sdX bs=4M conv=fsync
# 首次启动后必备配置
sudo apt update && sudo apt upgrade -y
sudo apt install -y openssh-server vim git
特别注意:工业环境下务必配置好看门狗和自动恢复机制。我在/etc/rc.local中添加了以下内容来监控系统健康状态:
bash复制# 启用硬件看门狗(bcm2835_wdt)
sudo modprobe bcm2835_wdt
echo "bcm2835_wdt" | sudo tee -a /etc/modules
sudo apt install watchdog -y
sudo systemctl enable watchdog
2.2 .NET运行时安装
树莓派上需要安装.NET 6.0或更高版本运行时。微软官方提供了ARM64的安装包:
bash复制# 注册微软密钥和源
wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
# 安装.NET运行时
sudo apt-get update && \
sudo apt-get install -y apt-transport-https && \
sudo apt-get update && \
sudo apt-get install -y dotnet-runtime-6.0
验证安装是否成功:
bash复制dotnet --info
3. GPIO控制实战:从LED到继电器
3.1 硬件连接规范
在工业场景中,GPIO连接必须考虑电气隔离和抗干扰。我的经验法则是:
- 数字输出(如控制继电器)必须使用光耦隔离
- 长距离信号传输要加装RS485转换器
- 每个GPIO口都要有适当的保护电路
典型连接示意图:
code复制树莓派 GPIO17 ──[1kΩ电阻]──[PC817光耦]──继电器线圈
│
[10kΩ下拉电阻]
3.2 .NET IoT库基础操作
创建控制台项目并添加NuGet包:
bash复制dotnet new console -n GpioDemo
cd GpioDemo
dotnet add package System.Device.Gpio --version 2.2.0
基础GPIO操作代码框架:
csharp复制using System.Device.Gpio;
// 使用BCM编号模式(与WiringPi相同)
const int ledPin = 17;
using var controller = new GpioController(PinNumberingScheme.Board);
// 初始化引脚
controller.OpenPin(ledPin, PinMode.Output);
// 工业级操作:添加重试机制和状态验证
try {
for (int i = 0; i < 10; i++) {
controller.Write(ledPin, PinValue.High);
Thread.Sleep(500);
controller.Write(ledPin, PinValue.Low);
Thread.Sleep(500);
// 状态回读验证
if (controller.Read(ledPin) != PinValue.Low) {
throw new Exception("GPIO状态异常");
}
}
}
finally {
controller.ClosePin(ledPin);
}
3.3 工业级继电器控制
实际工业场景中,继电器控制需要更严谨的逻辑:
csharp复制public class RelayController : IDisposable {
private readonly GpioController _gpio;
private readonly int _pin;
private bool _isOn;
public RelayController(int pin) {
_gpio = new GpioController(PinNumberingScheme.Board);
_pin = pin;
_gpio.OpenPin(_pin, PinMode.Output);
TurnOff(); // 初始化为关闭状态
}
public void TurnOn() {
_gpio.Write(_pin, PinValue.High);
_isOn = true;
LogState();
}
public void TurnOff() {
_gpio.Write(_pin, PinValue.Low);
_isOn = false;
LogState();
}
private void LogState() {
string state = _isOn ? "ON" : "OFF";
Console.WriteLine($"[{DateTime.Now}] Relay {_pin} 状态: {state}");
}
public void Dispose() {
TurnOff();
_gpio.ClosePin(_pin);
_gpio.Dispose();
}
}
关键细节:工业设备控制必须实现IDisposable接口,确保资源释放;所有状态变更都要有日志记录;重要操作需要加入硬件状态回读验证。
4. 摄像头采集与视频流处理
4.1 摄像头选型与配置
推荐使用官方CSI接口摄像头或USB3.0工业摄像头。测试过最稳定的配置是:
- Raspberry Pi Camera Module 3(带自动对焦)
- Arducam 16MP Autofocus(工业级)
启用摄像头接口:
bash复制sudo raspi-config nonint do_camera 0
sudo reboot
安装视频处理依赖:
bash复制sudo apt install -y ffmpeg libcamera-dev
4.2 .NET视频采集实现
使用FFmpeg进行视频采集是最可靠的方式:
csharp复制using System.Diagnostics;
public class CameraCapture {
private readonly string _outputPath;
public CameraCapture(string outputDir) {
_outputPath = Path.Combine(outputDir, $"capture_{DateTime.Now:yyyyMMdd_HHmmss}.h264");
}
public async Task CaptureVideoAsync(int durationMs, CancellationToken ct) {
var psi = new ProcessStartInfo {
FileName = "ffmpeg",
Arguments = $"-f libcamera -t {durationMs/1000} -o {_outputPath}",
RedirectStandardError = true,
UseShellExecute = false
};
using var process = new Process { StartInfo = psi };
process.Start();
// 工业级处理:监控ffmpeg输出
var errorOutput = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync(ct);
if (process.ExitCode != 0) {
throw new Exception($"视频采集失败: {errorOutput}");
}
}
}
4.3 视频流实时传输
工业场景常用RTSP协议传输实时视频:
csharp复制public class RtspStreamer : IDisposable {
private Process _ffmpegProcess;
public void StartStreaming(string rtspUrl, int width = 1280, int height = 720) {
var psi = new ProcessStartInfo {
FileName = "ffmpeg",
Arguments = $"-f libcamera -video_size {width}x{height} " +
$"-i /dev/video0 -c:v h264_omx -f rtsp {rtspUrl}",
RedirectStandardError = true,
UseShellExecute = false
};
_ffmpegProcess = new Process { StartInfo = psi };
_ffmpegProcess.Start();
}
public void Dispose() {
if (_ffmpegProcess?.HasExited == false) {
_ffmpegProcess.Kill();
_ffmpegProcess.Dispose();
}
}
}
5. 传感器数据采集与融合
5.1 温湿度传感器(DHT22)接入
使用I2C接口的DHT22模块更稳定:
csharp复制public class Dht22Reader : IDisposable {
private readonly I2cDevice _device;
public Dht22Reader(int busId = 1, int deviceAddress = 0x38) {
_device = I2cDevice.Create(new I2cConnectionSettings(busId, deviceAddress));
}
public (double Temperature, double Humidity) ReadData() {
Span<byte> writeBuf = stackalloc byte[] { 0xAC, 0x33, 0x00 };
Span<byte> readBuf = stackalloc byte[6];
_device.Write(writeBuf);
Thread.Sleep(500); // DHT22需要测量时间
_device.Read(readBuf);
// 数据解析(根据传感器协议文档)
var humidity = (readBuf[1] << 8 | readBuf[2]) / 10.0;
var temperature = ((readBuf[3] & 0x7F) << 8 | readBuf[4]) / 10.0;
if ((readBuf[3] & 0x80) != 0) temperature = -temperature;
return (temperature, humidity);
}
public void Dispose() => _device?.Dispose();
}
5.2 光照传感器(BH1750)接入
csharp复制public class Bh1750Reader : IDisposable {
private readonly I2cDevice _device;
public Bh1750Reader(int busId = 1, int deviceAddress = 0x23) {
_device = I2cDevice.Create(new I2cConnectionSettings(busId, deviceAddress));
}
public double ReadLux() {
// 发送测量命令
_device.WriteByte(0x10); // 1lx分辨率模式
Thread.Sleep(180); // 等待测量完成
// 读取测量结果
Span<byte> buffer = stackalloc byte[2];
_device.Read(buffer);
return (buffer[0] << 8 | buffer[1]) / 1.2;
}
public void Dispose() => _device?.Dispose();
}
5.3 多传感器数据融合
工业场景下需要对多源数据进行时间对齐和滤波处理:
csharp复制public class SensorFusionService {
private readonly Dht22Reader _dht22;
private readonly Bh1750Reader _bh1750;
private readonly Queue<SensorData> _dataBuffer = new();
public SensorFusionService() {
_dht22 = new Dht22Reader();
_bh1750 = new Bh1750Reader();
}
public async Task<FusedData> GetFusedDataAsync() {
// 同步采集(误差<100ms)
var (temp, humidity) = _dht22.ReadData();
var lux = _bh1750.ReadLux();
// 工业级滤波:滑动平均
var newData = new SensorData {
Timestamp = DateTime.UtcNow,
Temperature = temp,
Humidity = humidity,
Illuminance = lux
};
_dataBuffer.Enqueue(newData);
if (_dataBuffer.Count > 10) _dataBuffer.Dequeue();
return new FusedData {
AvgTemperature = _dataBuffer.Average(d => d.Temperature),
AvgHumidity = _dataBuffer.Average(d => d.Humidity),
AvgIlluminance = _dataBuffer.Average(d => d.Illuminance),
SampleCount = _dataBuffer.Count
};
}
}
public record SensorData {
public DateTime Timestamp { get; init; }
public double Temperature { get; init; }
public double Humidity { get; init; }
public double Illuminance { get; init; }
}
public record FusedData {
public double AvgTemperature { get; init; }
public double AvgHumidity { get; init; }
public double AvgIlluminance { get; init; }
public int SampleCount { get; init; }
}
6. 上位机通信与系统集成
6.1 TCP长连接实现
工业级TCP通信需要包含心跳机制和断线重连:
csharp复制public class IndustrialTcpClient : IDisposable {
private TcpClient _client;
private NetworkStream _stream;
private readonly string _host;
private readonly int _port;
private CancellationTokenSource _cts;
public IndustrialTcpClient(string host, int port) {
_host = host;
_port = port;
}
public async Task ConnectAsync() {
_client = new TcpClient();
await _client.ConnectAsync(_host, _port);
_stream = _client.GetStream();
_cts = new CancellationTokenSource();
// 启动心跳线程
_ = Task.Run(HeartbeatLoop);
}
private async Task HeartbeatLoop() {
var buffer = new byte[] { 0xAA }; // 心跳包标识
while (!_cts.IsCancellationRequested) {
try {
await _stream.WriteAsync(buffer, 0, 1, _cts.Token);
await Task.Delay(5000, _cts.Token); // 5秒一次心跳
} catch {
// 自动重连逻辑
await ReconnectAsync();
}
}
}
private async Task ReconnectAsync(int maxRetry = 5) {
int retryCount = 0;
while (retryCount++ < maxRetry && !_cts.IsCancellationRequested) {
try {
_client.Dispose();
await Task.Delay(1000 * retryCount, _cts.Token);
await ConnectAsync();
return;
} catch { /* 记录日志 */ }
}
throw new Exception("TCP连接重试次数超过限制");
}
public void Dispose() {
_cts?.Cancel();
_stream?.Dispose();
_client?.Dispose();
}
}
6.2 数据协议设计
工业场景推荐使用二进制协议提高传输效率:
csharp复制public static class IndustrialProtocol {
// 协议格式:[HEADER][LENGTH][PAYLOAD][CHECKSUM]
public const byte Header = 0x55;
public static byte[] PackData(SensorData data) {
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms);
writer.Write(Header);
writer.Write(0); // 占位长度
// 写入有效载荷
writer.Write(data.Timestamp.ToBinary());
writer.Write(data.Temperature);
writer.Write(data.Humidity);
writer.Write(data.Illuminance);
// 计算校验和
byte[] payload = ms.ToArray();
byte checksum = CalculateChecksum(payload);
writer.Write(checksum);
// 回写实际长度
payload = ms.ToArray();
BitConverter.GetBytes(payload.Length - 5).CopyTo(payload, 1);
return payload;
}
private static byte CalculateChecksum(byte[] data) {
byte sum = 0;
for (int i = 0; i < data.Length; i++) {
sum ^= data[i]; // 简单异或校验
}
return sum;
}
}
7. 系统部署与运维
7.1 systemd服务配置
创建/etc/systemd/system/iot-gateway.service:
ini复制[Unit]
Description=Industrial IoT Gateway Service
After=network.target
[Service]
Type=notify
WorkingDirectory=/opt/iot-gateway
ExecStart=/usr/bin/dotnet /opt/iot-gateway/IotGateway.dll
Restart=always
RestartSec=10
KillSignal=SIGINT
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
启用服务:
bash复制sudo systemctl daemon-reload
sudo systemctl enable iot-gateway
sudo systemctl start iot-gateway
7.2 工业级监控脚本
创建监控脚本/usr/local/bin/monitor_iot.sh:
bash复制#!/bin/bash
SERVICE="iot-gateway"
MAX_RESTARTS=3
LOG_FILE="/var/log/iot-monitor.log"
count=$(systemctl show -p NRestarts $SERVICE | cut -d= -f2)
if [ $count -ge $MAX_RESTARTS ]; then
echo "$(date) - 服务 $SERVICE 重启次数超过 $MAX_RESTARTS 次" >> $LOG_FILE
# 触发告警邮件/短信
/usr/local/bin/send_alert.sh "IoT服务异常"
fi
添加到crontab:
bash复制*/5 * * * * /usr/local/bin/monitor_iot.sh
8. 实战经验与避坑指南
-
GPIO抖动问题:工业环境下电磁干扰严重,解决方法是:
- 所有数字输入口加装RC滤波(100Ω电阻+0.1μF电容)
- 软件端实现消抖逻辑(连续读取3次相同值才确认)
-
I2C设备冲突:多个I2C设备地址冲突时:
- 使用PCA9548A等I2C多路复用器
- 修改设备硬件地址(部分传感器支持)
-
视频流卡顿优化:
bash复制# 提高GPU内存分配(/boot/config.txt) gpu_mem=256 # 禁用桌面环境(仅服务器模式) systemctl set-default multi-user.target -
长时间运行内存泄漏:
- 定期重启关键服务(通过systemd定时器)
- 使用Valgrind检测.NET Core应用内存问题
-
工业环境温度控制:
- 避免在超过60℃环境连续运行
- 使用金属外壳辅助散热
- 监控SoC温度:
csharp复制public double GetCpuTemperature() { string tempStr = File.ReadAllText("/sys/class/thermal/thermal_zone0/temp"); return double.Parse(tempStr) / 1000; }
这套方案已经在多个工业现场稳定运行超过1年,最长的连续运行时间达到287天。关键是要做好异常处理和自动恢复机制,毕竟工业现场很难随时进行人工干预。