.NET 8 跨平台自动化开发实战
基于真实中小型自动化项目经验(实验室温湿度监控、智能家居、小型产线测试台等),本文分享一套全部使用 .NET 8(跨平台)的解决方案。代码同时适用于 Windows 工控机/上位机和树莓派/工业迷你PC/Jetson Nano 等下位机运行环境。
用 C# 做下位机,正好能弥补传统方案的短板。尤其是.NET Core(现在的.NET 8)跨平台之后,C#不仅能跑在Windows上,还能流畅运行在树莓派、工业迷你PC等嵌入式设备上,这让'同一套语言写上下位机'成为可能。
一句话总结:在中小规模、非极端苛刻实时性的场景里,C#上下位机一体化开发是当前性价比最高、最容易维护的方案。
典型场景与技术选型对比
| 场景类型 | 传统方案 | C#上下位机一体化方案优势 | 适用性评分 |
|---|---|---|---|
| 实验室温湿度监控 | PLC + 组态王 / LabVIEW | C#统一开发,成本低,易集成数据库/云端,扩展性强 | ★★★★★ |
| 小型产线测试台 | STM32 + 上位机C# | 上下位机共享代码逻辑,调试效率翻倍,维护成本大幅降低 | ★★★★★ |
| 智能家居/小型设备控制 | ESP32 + App/小程序 | C#跨端(桌面+嵌入式+移动端MAUI),生态统一 | ★★★★☆ |
| 高实时运动控制 | 西门子1200 + C#上位机 | 下位机仍建议PLC,上位机C#,不适合C#做下位机 | ★★☆☆☆ |
结论:当实时性要求 < 10ms、需要极高抗干扰时,优先PLC/单片机;当实时性 50–500ms 可接受、需要快速迭代、数据分析、网络通信时,C#上下位机一体化是最佳选择。
上下位机一体化架构设计
[上位机(PC/工控机).NET 8 WinForms / MAUI]
├── UI层(实时曲线、参数设置、报警看板)
├── 业务层(数据分析、报表、云端同步)
└── 通信层(MQTT / TCP Socket / gRPC)
↑↓(同一协议,双向通信)
[下位机(树莓派 / 迷你PC / Jetson).NET 8 Console / Worker Service]
├── 采集层(串口 / I2C / GPIO / ADC)
├── 执行层(继电器 / PWM / DAC / 步进电机)
└── 通信层(MQTT / TCP Socket / gRPC)
↑↓
[物理层]
├── 传感器(DHT22、SHT30、PT100、压力变送器)
└── 执行器(继电器、电磁阀、步进电机、伺服)
核心通信协议选择
中小项目推荐排序如下:
- MQTT(首选):轻量、发布订阅、断网续传、跨端天然支持。
- TCP Socket:自定义协议,延迟最低,适合高频小包。
- gRPC:结构化、高性能、支持流式通信(未来趋势)。
- Modbus TCP:兼容老设备,但效率较低。
实战代码实现(温湿度监控 + 设备控制)
1. 下位机核心代码(Console + MQTT)
下位机项目:Worker Service(.NET 8)
这里我们使用 System.Device.Gpio 和 Iot.Device.Dht 库来驱动硬件。注意初始化时的引脚编号和 GPIO 控制器配置。
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MQTTnet;
using MQTTnet.Client;
using System.Device.Gpio;
using System.Device.I2c;
using Iot.Device.Dht;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private IMqttClient _mqttClient;
private Dht22 _dht22;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// 初始化 DHT22(GPIO 4)
_dht22 = new Dht22(4, PinNumberingScheme.Board);
// MQTT 客户端
var factory = new MqttFactory();
_mqttClient = factory.CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("192.168.1.100", 1883) // 上位机 IP
.WithClientId("RPi-Downlink-" + Guid.NewGuid().ToString("N").Substring(0, 8))
.Build();
await _mqttClient.ConnectAsync(options, stoppingToken);
// 订阅上位机控制指令
await _mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("device/control/#").Build());
_mqttClient.ApplicationMessageReceivedAsync += async e =>
{
string topic = e.ApplicationMessage.Topic;
string payload = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment);
if (topic == "device/control/relay1")
{
bool state = payload == "ON";
// 假设继电器接 GPIO 17
using var controller = new GpioController();
controller.OpenPin(17, PinMode.Output);
controller.Write(17, state ? PinValue.High : PinValue.Low);
_logger.LogInformation("继电器 1 {State}", state ? "开" : "关");
}
};
// 定时采集 & 上报
while (!stoppingToken.IsCancellationRequested)
{
try
{
var reading = _dht22.Read();
if (reading.IsValid)
{
var json = System.Text.Json.JsonSerializer.Serialize(new
{
temperature = reading.Temperature.DegreesCelsius,
humidity = reading.Humidity.Percent,
timestamp = DateTime.UtcNow
});
var msg = new MqttApplicationMessageBuilder()
.WithTopic("device/sensor/dht22")
.WithPayload(json)
.Build();
await _mqttClient.PublishAsync(msg, stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "传感器读取异常");
}
await Task.Delay(2000, stoppingToken);
}
}
}
2. 上位机接收与控制界面
WinForms 示例(实时曲线 + 控制按钮)
上位机负责数据展示和指令下发。这里使用了 ZedGraph 绘制实时曲线,并通过 MQTT 订阅传感器数据。
public partial class FormMain : Form
{
private IMqttClient _mqttClient;
private RollingPointPairList curveTemp = new RollingPointPairList(3600);
private RollingPointPairList curveHumid = new RollingPointPairList(3600);
public FormMain()
{
InitializeComponent();
SetupZedGraph();
// MQTT 连接
var factory = new MqttFactory();
_mqttClient = factory.CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("localhost", 1883) // 或树莓派 IP
.WithClientId("PC-Uplink")
.Build();
_mqttClient.ConnectAsync(options);
_mqttClient.ApplicationMessageReceivedAsync += e =>
{
if (e.ApplicationMessage.Topic == "device/sensor/dht22")
{
var json = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment);
var data = System.Text.Json.JsonSerializer.Deserialize<SensorData>(json);
this.Invoke((MethodInvoker)(() =>
{
curveTemp.Add(DateTime.Now.ToOADate(), data.temperature);
curveHumid.Add(DateTime.Now.ToOADate(), data.humidity);
zgc.Invalidate();
lblTemp.Text = $"温度:{data.temperature:F1} °C";
lblHumid.Text = $"湿度:{data.humidity:F1} %";
}));
}
return Task.CompletedTask;
};
// 订阅
_mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("device/sensor/#").Build());
}
private void btnRelayOn_Click(object sender, EventArgs e)
{
var msg = new MqttApplicationMessageBuilder()
.WithTopic("device/control/relay1")
.WithPayload("ON")
.Build();
_mqttClient.PublishAsync(msg);
}
private void btnRelayOff_Click(object sender, EventArgs e)
{
var msg = new MqttApplicationMessageBuilder()
.WithTopic("device/control/relay1")
.WithPayload("OFF")
.Build();
_mqttClient.PublishAsync(msg);
}
private void SetupZedGraph()
{
var pane = zgc.GraphPane;
pane.AddCurve("温度", curveTemp, Color.Red, SymbolType.None);
pane.AddCurve("湿度", curveHumid, Color.Blue, SymbolType.None);
}
}
public class SensorData
{
public double temperature { get; set; }
public double humidity { get; set; }
}
进阶案例:RRT* 路径规划算法
除了数据采集,自动化项目中常涉及 AGV、机械臂的路径规划。下面是一个基于 .NET 8 的 RRT* 算法实现版本,包含详细注释、异常处理及动态障碍支持。
1. 核心类定义
using System;
using System.Collections.Generic;
using System.Linq;
// 点结构(支持浮点坐标)
public readonly struct Point2D(double x, double y)
{
public double X { get; } = x;
public double Y { get; } = y;
public double DistanceTo(Point2D other) => Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
public override string ToString() => $"({X:F2}, {Y:F2})";
}
// RRT* 节点
public class RRTNode
{
public Point2D Position { get; }
public RRTNode Parent { get; set; }
public double Cost { get; set; } // 从起点到该节点的累计成本
public List<RRTNode> Children { get; } = new(); // 用于 rewire 时快速查找子节点
public RRTNode(Point2D pos, RRTNode parent = null, double cost = 0)
{
Position = pos;
Parent = parent;
Cost = cost;
if (parent != null) parent.Children.Add(this);
}
// 计算到目标的启发式距离(可换成欧氏、曼哈顿等)
public double HeuristicTo(Point2D goal) => Position.DistanceTo(goal);
}
// 障碍物(简单矩形表示)
public class Obstacle
{
public double MinX { get; }
public double MinY { get; }
public double MaxX { get; }
public double MaxY { get; }
public Obstacle(double minX, double minY, double maxX, double maxY)
{
MinX = minX; MinY = minY; MaxX = maxX; MaxY = maxY;
}
public bool Contains(Point2D p) => p.X >= MinX && p.X <= MaxX && p.Y >= MinY && p.Y <= MaxY;
public bool IntersectsLine(Point2D a, Point2D b)
{
// 线段与矩形相交检测(简化版,实际项目可使用更精确算法)
return !(b.X < MinX || a.X > MaxX || b.Y < MinY || a.Y > MaxY);
}
}
2. RRT* 主规划类
public class RRTStarPlanner
{
private readonly Point2D _start;
private readonly Point2D _goal;
private readonly double _maxStep; // 单次扩展最大步长
private readonly double _nearRadius; // 附近节点搜索半径
private readonly int _maxIterations; // 最大迭代次数
private readonly double _goalSampleRate; // 朝目标采样概率
private readonly double _goalTolerance; // 到达目标的容差半径
private readonly List<Obstacle> _obstacles; // 静态障碍物
private readonly List<RRTNode> _nodes = new();
private readonly Random _rand = new();
public RRTStarPlanner(Point2D start, Point2D goal, double maxStep = 30.0, double nearRadius = 50.0, int maxIterations = 5000, double goalSampleRate = 0.1, double goalTolerance = 10.0, List<Obstacle> obstacles = null)
{
_start = start;
_goal = goal;
_maxStep = maxStep;
_nearRadius = nearRadius;
_maxIterations = maxIterations;
_goalSampleRate = goalSampleRate;
_goalTolerance = goalTolerance;
_obstacles = obstacles ?? new List<Obstacle>();
// 初始化根节点
_nodes.Add(new RRTNode(start, null, 0));
}
/// <summary>
/// 执行 RRT* 规划,返回路径点列表(从起点到终点)
/// </summary>
/// <returns>成功返回路径点列表,失败返回 null</returns>
public List<Point2D> Plan()
{
for (int i = 0; i < _maxIterations; i++)
{
Point2D randPoint = Sample();
RRTNode nearest = Nearest(randPoint);
Point2D newPos = Steer(nearest.Position, randPoint);
if (!IsCollision(nearest.Position, newPos))
{
var nearNodes = Near(newPos);
var parent = ChooseParent(nearest, nearNodes, newPos);
var newNode = new RRTNode(newPos, parent, parent.Cost + parent.Position.DistanceTo(newPos));
_nodes.Add(newNode);
// RRT* 核心:rewire 附近节点
Rewire(nearNodes, newNode);
// 检查是否到达目标
if (newPos.DistanceTo(_goal) <= _goalTolerance)
{
return ReconstructPath(newNode);
}
}
}
// 找不到路径,返回最近的节点路径(渐进最优特性)
var closest = _nodes.OrderBy(n => n.Position.DistanceTo(_goal)).First();
return ReconstructPath(closest);
}
private Point2D Sample()
{
// 以一定概率直接采样目标点(加快收敛)
if (_rand.NextDouble() < _goalSampleRate) return _goal;
// 随机采样(地图范围可根据实际场景调整)
double x = _rand.NextDouble() * 1000;
double y = _rand.NextDouble() * 1000;
return new Point2D(x, y);
}
private RRTNode Nearest(Point2D point) => _nodes.OrderBy(n => n.Position.DistanceTo(point)).First();
private Point2D Steer(Point2D from, Point2D to)
{
double dist = from.DistanceTo(to);
if (dist <= _maxStep) return to;
double angle = Math.Atan2(to.Y - from.Y, to.X - from.X);
return new Point2D(from.X + _maxStep * Math.Cos(angle), from.Y + _maxStep * Math.Sin(angle));
}
private List<RRTNode> Near(Point2D point) => _nodes.Where(n => n.Position.DistanceTo(point) < _nearRadius).ToList();
private RRTNode ChooseParent(RRTNode nearest, List<RRTNode> nearNodes, Point2D newPos)
{
RRTNode best = nearest;
double bestCost = nearest.Cost + nearest.Position.DistanceTo(newPos);
foreach (var near in nearNodes)
{
double cost = near.Cost + near.Position.DistanceTo(newPos);
if (cost < bestCost && !IsCollision(near.Position, newPos))
{
bestCost = cost;
best = near;
}
}
return best;
}
private void Rewire(List<RRTNode> nearNodes, RRTNode newNode)
{
foreach (var near in nearNodes)
{
double newCost = newNode.Cost + newNode.Position.DistanceTo(near.Position);
if (newCost < near.Cost && !IsCollision(newNode.Position, near.Position))
{
// 断开旧父节点连接
near.Parent?.Children.Remove(near);
near.Parent = newNode;
near.Cost = newCost;
newNode.Children.Add(near);
}
}
}
private bool IsCollision(Point2D a, Point2D b)
{
foreach (var obs in _obstacles)
{
// 简单线段 - 矩形相交检测(实际可使用更精确算法)
if (LineIntersectsRect(a, b, obs)) return true;
}
return false;
}
private bool LineIntersectsRect(Point2D a, Point2D b, Obstacle rect)
{
// 实现略(可使用标准线段 - 矩形相交算法,如 Liang-Barsky 或 Cohen-Sutherland)
// 这里简化返回 false,实际项目中必须完整实现
return false;
}
private List<Point2D> ReconstructPath(RRTNode node)
{
var path = new List<Point2D>();
while (node != null)
{
path.Add(node.Position);
node = node.Parent;
}
path.Reverse();
return path;
}
}
使用示例
// 地图范围 1000×1000,障碍物示例
var obstacles = new List<Obstacle>
{
new Obstacle(200, 200, 300, 500), // 矩形障碍
new Obstacle(600, 400, 800, 550)
};
var planner = new RRTStarPlanner(
start: new Point2D(50, 50),
goal: new Point2D(950, 950),
maxStep: 25.0, // 步长(可调,越大越快但路径越粗糙)
nearRadius: 60.0, // 附近搜索半径(影响 rewire 效果)
maxIterations: 8000, // 迭代次数(越大越优,但耗时增加)
goalSampleRate: 0.15,// 偏向目标采样概率
goalTolerance: 15.0, // 到达目标的容差半径
obstacles: obstacles
);
// 规划路径
var path = planner.Plan();
if (path != null)
{
Console.WriteLine($"找到路径,共 {path.Count} 个点");
foreach (var p in path) Console.WriteLine(p);
}
else
{
Console.WriteLine("未找到路径");
}
工业现场踩坑经验与优化建议
- 实时性坑:MQTT 默认 QoS 0,容易丢包 → 改用 QoS 1(至少送达一次)。
- 下位机资源坑:树莓派 CPU 占用高 → 采集间隔调到 2–5s,推理任务放上位机。
- 断网续传:下位机本地缓存(SQLite),重连后批量上报。
- 跨端统一:用 .NET MAUI 做上位机 + 移动端监控,代码复用率 95%。
- 部署:下位机用 .NET 8 Worker Service + systemd(Linux)开机自启。
- RRT 优化*:初始规划用较少迭代(1000 次)快速出路径,后台持续优化(每秒再跑 2000 次迭代)。规划超时控制:超过 300ms 返回当前最优路径。
- 动态障碍集成:每帧从 YOLOv8 + DeepSORT 获取动态障碍位置,更新
_obstacles列表(临时添加圆形/矩形禁区)。当路径被阻挡时触发局部重规划(只重新规划前半段)。 - 路径平滑:RRT* 路径通常锯齿状,可后处理使用贝塞尔曲线或样条插值(例如 MathNet.Numerics 库)。
- 部署建议:.NET 8 AOT 单文件发布(体积 <100MB),工控机可用 Windows IoT Enterprise / 麒麟 ARM64,地图数据 SQLite 持久化 + Redis 缓存动态障碍。

