工业上位机开发技术手册
C# WPF + PLC 通信 + MES 对接 完整学习指南
目录
第一章 Modbus 通信
1.1 重点知识
- Modbus RTU 走串口(RS485),Modbus TCP 走网口(以太网)
- 主从架构:主站(上位机)发请求,从站(PLC/仪表)回复
- 核心功能码:03读保持寄存器、06写单个寄存器、16写多个寄存器
- 报文结构:从站地址 + 功能码 + 数据 + CRC校验(RTU)或MBAP头(TCP)
- 一个寄存器 = 16位(2字节),32位数据需要2个寄存器拼接
1.2 注意事项
- 字节序问题:不同PLC的高低字节顺序不同(大端/小端),必须确认
- 从站地址范围 1~247,0是广播地址
- 串口参数必须匹配:波特率、数据位、停止位、校验位
- RS485 是半双工,同一时刻只能一个设备发送
- 响应超时必须处理,一般设 1~3 秒
1.3 常见问题
- CRC校验失败 → 字节序不对或数据被截断
- 收到异常码 02(非法数据地址)→ 寄存器地址超出范围
- 读到的值全是0 → 从站地址错误或接线问题
- 通信不稳定 → 检查屏蔽线、终端电阻、波特率匹配
1.4 Demo: Modbus RTU 读取保持寄存器
using System.IO.Ports;
public class ModbusRtuClient
{
private SerialPort _port;
public void Connect(string portName, int baudRate)
{
_port = new SerialPort(portName, baudRate,
Parity.None, 8, StopBits.One);
_port.ReadTimeout = 3000;
_port.Open();
}
// 读保持寄存器(功能码03)
public ushort[] ReadHoldingRegisters(
byte slaveId, ushort startAddr, ushort count)
{
// 构造请求报文
byte[] request = new byte[8];
request[0] = slaveId; // 从站地址
request[1] = 0x03; // 功能码
request[2] = (byte)(startAddr >> 8); // 起始地址高字节
request[3] = (byte)(startAddr & 0xFF); // 起始地址低字节
request[4] = (byte)(count >> 8);
request[5] = (byte)(count & 0xFF);
// CRC校验
var crc = CalcCRC16(request, 6);
request[6] = crc[0];
request[7] = crc[1];
_port.DiscardInBuffer();
_port.Write(request, 0, request.Length);
// 读取响应
int dataLen = 3 + count * 2 + 2; // 地址+功能+字节数+数据+CRC
byte[] response = new byte[dataLen];
int read = 0;
while (read < dataLen)
read += _port.Read(response, read, dataLen - read);
// 校验CRC
var checkCrc = CalcCRC16(response, dataLen - 2);
if (checkCrc[0] != response[dataLen-2]
|| checkCrc[1] != response[dataLen-1])
throw new Exception("CRC校验失败");
// 解析寄存器值
ushort[] values = new ushort[count];
for (int i = 0; i < count; i++)
{
int offset = 3 + i * 2;
values[i] = (ushort)((response[offset] << 8)
| response[offset + 1]);
}
return values;
}
// CRC16 计算(Modbus标准)
private byte[] CalcCRC16(byte[] data, int length)
{
ushort crc = 0xFFFF;
for (int i = 0; i < length; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) != 0)
crc = (ushort)((crc >> 1) ^ 0xA001);
else
crc = (ushort)(crc >> 1);
}
}
return new byte[] { (byte)(crc & 0xFF),
(byte)(crc >> 8) };
}
public void Close() => _port?.Close();
}
1.5 Demo: 两个寄存器拼成 Float
// 两个16位寄存器拼成32位浮点数
// 寄存器 D100=0x4228, D101=0x4148 → 42.13
public float RegistersToFloat(ushort high, ushort low)
{
// 大端模式(ABCD):高寄存器高字节在前
byte[] bytes = new byte[4];
bytes[0] = (byte)(high >> 8);
bytes[1] = (byte)(high & 0xFF);
bytes[2] = (byte)(low >> 8);
bytes[3] = (byte)(low & 0xFF);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return BitConverter.ToSingle(bytes, 0);
}
// 小端模式(CDAB):字节序交换
public float RegistersToFloatCDAB(ushort high, ushort low)
{
byte[] bytes = new byte[4];
bytes[0] = (byte)(low & 0xFF);
bytes[1] = (byte)(low >> 8);
bytes[2] = (byte)(high & 0xFF);
bytes[3] = (byte)(high >> 8);
return BitConverter.ToSingle(bytes, 0);
}
第二章 串口通信编程
2.1 重点知识
- SerialPort 类的核心属性:PortName、BaudRate、DataBits、StopBits、Parity
- DataReceived 事件在后台线程触发,更新UI必须用 Dispatcher
- 粘包问题:多次发送的数据粘在一起收到
- 断包问题:一条完整数据分多次收到
- 关闭程序前必须 Close() 串口,否则下次打不开
2.2 注意事项
- 串口被占用时 Open() 会抛异常,必须 try-catch
- 关闭窗体时要在 Closing 事件中关闭串口
- DataReceived 可能一次收到多条数据,也可能一条分多次
- BytesToRead 属性可以判断缓冲区有多少字节
- RS485 通信需要控制收发切换(有些USB转485需要延时)
2.3 常见问题
- 串口打开失败 → 设备管理器确认COM口号,其他程序是否占用
- 收到乱码 → 波特率不匹配或编码方式不对
- DataReceived不触发 → 检查是否调用了Open(),事件是否绑定
- 关闭程序后串口还是占用 → 没有正确Close(),用Dispose()
2.4 Demo: 完整串口通信类
public class SerialComm : IDisposable
{
private SerialPort _port;
private readonly object _lock = new();
public event Action<byte[]> DataReceived;
public event Action<string> ErrorOccurred;
public bool IsOpen => _port?.IsOpen ?? false;
public void Open(string portName, int baudRate)
{
_port = new SerialPort(portName, baudRate,
Parity.None, 8, StopBits.One);
_port.ReadTimeout = 3000;
_port.WriteTimeout = 3000;
_port.DataReceived += OnDataReceived;
_port.ErrorReceived += OnErrorReceived;
_port.Open();
}
private void OnDataReceived(object sender,
SerialDataReceivedEventArgs e)
{
try
{
int count = _port.BytesToRead;
byte[] buffer = new byte[count];
_port.Read(buffer, 0, count);
DataReceived?.Invoke(buffer);
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(ex.Message);
}
}
private void OnErrorReceived(object sender,
SerialErrorReceivedEventArgs e)
{
ErrorOccurred?.Invoke($"串口错误: {e.EventType}");
}
public void Send(byte[] data)
{
lock (_lock)
{
if (_port?.IsOpen == true)
_port.Write(data, 0, data.Length);
}
}
public void Dispose()
{
if (_port != null)
{
_port.DataReceived -= OnDataReceived;
_port.ErrorReceived -= OnErrorReceived;
if (_port.IsOpen) _port.Close();
_port.Dispose();
}
}
}
2.5 Demo: 帧解析(处理粘包断包)
// 帧格式:帧头(0xAA) + 长度(1字节) + 数据 + 校验(1字节)
private byte[] _buffer = new byte[0];
private void OnDataReceived(byte[] newData)
{
// 拼接数据到缓冲区
byte[] combined = new byte[_buffer.Length + newData.Length];
Array.Copy(_buffer, 0, combined, 0, _buffer.Length);
Array.Copy(newData, 0, combined, _buffer.Length,
newData.Length);
_buffer = combined;
// 循环解析完整帧
while (true)
{
// 找帧头
int headerIdx = Array.IndexOf(_buffer, (byte)0xAA);
if (headerIdx < 0) { _buffer = new byte[0]; break; }
if (headerIdx > 0) // 丢弃帧头前的垃圾数据
{
_buffer = _buffer[headerIdx..];
}
// 长度不够,等下次数据
if (_buffer.Length < 3) break;
int dataLen = _buffer[1];
int frameLen = 2 + dataLen + 1; // 头+长度+数据+校验
if (_buffer.Length < frameLen) break;
// 取出完整帧
byte[] frame = new byte[frameLen];
Array.Copy(_buffer, 0, frame, 0, frameLen);
// 校验
byte checkSum = 0;
for (int i = 1; i < frameLen - 1; i++)
checkSum ^= frame[i];
if (checkSum == frame[frameLen - 1])
ProcessFrame(frame); // 处理有效帧
// 移除已处理的帧
byte[] remaining = new byte[_buffer.Length - frameLen];
Array.Copy(_buffer, frameLen, remaining, 0,
remaining.Length);
_buffer = remaining;
}
}
第三章 TCP/IP 网络通信
3.1 重点知识
- TcpClient 用于客户端连接,TcpListener 用于服务端监听
- TCP 是流式协议,没有消息边界,必须自己处理粘包
- 心跳机制检测连接是否断开
- 断线自动重连是工业场景的刚需
- 异步编程(async/await)避免阻塞UI
3.2 注意事项
- NetworkStream.Read 可能一次读不到完整消息
- 必须处理 SocketException(断线、超时等)
- 服务端要处理多客户端并发(每个客户端一个Task)
- 发送和接收要在不同的Task中
- 程序退出时必须正确关闭所有连接
3.3 Demo: TCP 客户端(带心跳和重连)
public class TcpClientWithReconnect
{
private TcpClient? _client;
private NetworkStream? _stream;
private CancellationTokenSource _cts = new();
private readonly string _ip;
private readonly int _port;
public event Action<byte[]> DataReceived;
public event Action<bool> ConnectionChanged;
public TcpClientWithReconnect(string ip, int port)
{
_ip = ip;
_port = port;
}
public async Task StartAsync()
{
while (!_cts.Token.IsCancellationRequested)
{
try
{
_client = new TcpClient();
await _client.ConnectAsync(_ip, _port);
_stream = _client.GetStream();
ConnectionChanged?.Invoke(true);
// 接收和心跳并行
var receiveTask = ReceiveLoopAsync();
var heartbeatTask = HeartbeatLoopAsync();
await Task.WhenAny(receiveTask, heartbeatTask);
}
catch (Exception)
{
ConnectionChanged?.Invoke(false);
}
finally
{
_stream?.Dispose();
_client?.Dispose();
}
// 5秒后重连
await Task.Delay(5000, _cts.Token);
}
}
private async Task ReceiveLoopAsync()
{
byte[] buf = new byte[4096];
while (!_cts.Token.IsCancellationRequested)
{
int len = await _stream!.ReadAsync(buf, _cts.Token);
if (len == 0) break; // 连接断开
byte[] data = new byte[len];
Array.Copy(buf, data, len);
DataReceived?.Invoke(data);
}
}
private async Task HeartbeatLoopAsync()
{
while (!_cts.Token.IsCancellationRequested)
{
await Task.Delay(3000, _cts.Token);
byte[] heartbeat = new byte[] { 0xFF };
await _stream!.WriteAsync(heartbeat, _cts.Token);
}
}
public void Stop() => _cts.Cancel();
}
第四章 OPC UA 通信
4.1 重点知识
- OPC UA 是工业通信的新标准,跨平台、安全、支持复杂数据模型
- 核心概念:节点(Node)、地址空间(Address Space)、订阅(Subscription)
- 每个数据点有 NodeId(如
ns=2;s=Temperature)
- 支持读/写/订阅三种操作模式
- NuGet 包:
OPCFoundation.NetStandard.Opc.Ua
4.2 注意事项
- 连接字符串格式:
opc.tcp://192.168.1.100:4840
- 安全策略:开发时可用 None,生产环境建议 SignAndEncrypt
- 订阅比轮询效率高,数据变化时服务器主动推送
- Session 需要定期 KeepAlive 防止超时断开
4.3 Demo: OPC UA 读取和订阅
using Opc.Ua;
using Opc.Ua.Client;
public class OpcUaHelper
{
private Session? _session;
public async Task ConnectAsync(string serverUrl)
{
var config = ApplicationConfiguration.Create()
.AsClient().Build();
var endpoint = new ConfiguredEndpoint(null,
new EndpointDescription(serverUrl));
_session = await Session.Create(config, endpoint, true);
}
// 读取单个节点值
public object? ReadValue(string nodeId)
{
var node = new NodeId(nodeId);
var value = _session!.ReadValue(0, 0, node);
return value.Value;
}
// 订阅数据变化(推荐,比轮询高效)
public void Subscribe(string nodeId,
Action<object?> onDataChange)
{
var subscription = new Subscription
{
PublishingInterval = 500 // 500ms发布一次
};
var item = new MonitoredItem
{
StartNodeId = new NodeId(nodeId),
DisplayName = nodeId
};
item.Notification += (mon, args) =>
{
var value = args.NotificationValues.First().Value;
onDataChange(value);
};
subscription.AddItem(item);
_session!.AddSubscription(subscription);
subscription.Create();
}
}
第五章 多线程与异步编程
5.1 重点知识
- async/await 是C#异步编程的核心,不阻塞调用线程
- Task 代表一个异步操作,Task\<T\> 有返回值
- CancellationToken 用于优雅取消长时间运行的任务
- ConcurrentDictionary 是线程安全的字典
- lock 保证同一时刻只有一个线程执行临界区代码
- UI线程只有一个,后台线程不能直接操作UI控件
5.2 注意事项
- async void 只用于事件处理,其他情况用 async Task
- 避免 async void,异常无法被 catch
- Task.Run 把同步代码丢到线程池执行
- 多个异步操作用 Task.WhenAll 并行执行
- Dispatcher.Invoke 同步等待UI线程,BeginInvoke 不等待
5.3 常见问题
- 死锁 → UI线程用 .Result 或 .Wait() 等待异步任务
- 竞态条件 → 多线程同时修改共享数据,用 lock 或 ConcurrentDictionary
- 内存泄漏 → CancellationToken 没传,Task 无法取消
- UI卡死 → 在UI线程执行了耗时操作(数据库查询、网络请求)
5.4 Demo: 生产者-消费者模式
using System.Threading.Channels;
// 生产者:串口收到数据放入Channel
// 消费者:从Channel取数据解析后更新UI
public class DataPipeline
{
private readonly Channel<byte[]> _channel
= Channel.CreateBounded<byte[]>(1000);
// 生产者调用
public async Task ProduceAsync(byte[] data)
{
await _channel.Writer.WriteAsync(data);
}
// 消费者:后台线程持续处理
public async Task ConsumeAsync(
Action<byte[]> processData,
CancellationToken ct)
{
await foreach (var data
in _channel.Reader.ReadAllAsync(ct))
{
processData(data); // 解析数据
}
}
}
// 使用示例
var pipeline = new DataPipeline();
var cts = new CancellationTokenSource();
// 启动消费者
_ = Task.Run(() => pipeline.ConsumeAsync(
data =>
{
var result = ParseData(data);
Application.Current.Dispatcher.BeginInvoke(() =>
{
Temperature = result.Temp;
Pressure = result.Press;
});
}, cts.Token));
// 串口收到数据时调用生产者
private void OnDataReceived(byte[] data)
{
_ = pipeline.ProduceAsync(data);
}
5.5 Demo: CancellationToken 优雅退出
private CancellationTokenSource _cts;
public async Task StartPollingAsync()
{
_cts = new CancellationTokenSource();
try
{
while (!_cts.Token.IsCancellationRequested)
{
var values = await ReadPlcAsync();
UpdateUI(values);
await Task.Delay(200, _cts.Token);
}
}
catch (OperationCanceledException)
{
// 正常取消,不报错
}
}
public void StopPolling()
{
_cts?.Cancel(); // 通知任务停止
_cts?.Dispose();
}
第六章 数据库操作
6.1 重点知识
- SQLite 适合本地存储(配置、缓存、日志),无需安装数据库服务
- SQL Server 适合共享数据(多台电脑访问同一数据库)
- Dapper 是轻量 ORM,SQL 手写但自动映射对象,性能极好
- EF Core 是完整 ORM,适合复杂业务模型
- 批量插入比逐条插入快 100 倍以上
6.2 注意事项
- 数据库连接用完必须关闭(用 using 语句)
- SQL 拼接必须用参数化查询,防 SQL 注入
- 高频采集数据建议用时序数据库(InfluxDB/TDengine)
- 数据库文件定期备份,防止数据丢失
- 连接字符串不要硬编码,放配置文件
6.3 Demo: SQLite + Dapper
using System.Data.SQLite;
using Dapper;
// NuGet: Install-Package System.Data.SQLite
// NuGet: Install-Package Dapper
public class ProductionRepository
{
private readonly string _connStr;
public ProductionRepository(string dbPath)
{
_connStr = $"Data Source={dbPath};Version=3;";
InitDatabase();
}
private void InitDatabase()
{
using var conn = new SQLiteConnection(_connStr);
conn.Execute(@"
CREATE TABLE IF NOT EXISTS ProductionRecord (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
DeviceId TEXT NOT NULL,
Temperature REAL,
Pressure REAL,
ProductCount INTEGER,
CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP
)");
}
// 单条插入
public void Insert(ProductionRecord record)
{
using var conn = new SQLiteConnection(_connStr);
conn.Execute(@"
INSERT INTO ProductionRecord
(DeviceId, Temperature, Pressure, ProductCount)
VALUES
(@DeviceId, @Temperature, @Pressure, @ProductCount)",
record);
}
// 批量插入(比逐条快100倍)
public void BatchInsert(IEnumerable<ProductionRecord> records)
{
using var conn = new SQLiteConnection(_connStr);
conn.Execute(@"
INSERT INTO ProductionRecord
(DeviceId, Temperature, Pressure, ProductCount)
VALUES
(@DeviceId, @Temperature, @Pressure, @ProductCount)",
records);
}
// 查询
public List<ProductionRecord> GetTodayRecords(string deviceId)
{
using var conn = new SQLiteConnection(_connStr);
return conn.Query<ProductionRecord>(@"
SELECT * FROM ProductionRecord
WHERE DeviceId = @DeviceId
AND date(CreateTime) = date('now')
ORDER BY CreateTime DESC",
new { DeviceId = deviceId }).ToList();
}
}
public class ProductionRecord
{
public int Id { get; set; }
public string DeviceId { get; set; } = "";
public double Temperature { get; set; }
public double Pressure { get; set; }
public int ProductCount { get; set; }
public DateTime CreateTime { get; set; }
}
第七章 WPF 进阶技术
7.1 重点知识
- UserControl 做可复用组件(设备卡片、仪表盘、指示灯)
- LiveCharts / OxyPlot 做实时数据曲线图
- DataGrid 做数据表格,支持排序、筛选
- CommunityToolkit.Mvvm 简化 MVVM 开发
- 打印功能用 PrintDialog + FlowDocument 或 FixedDocument
7.2 Demo: 自定义指示灯控件
<!-- StatusLight.xaml - 可复用的设备状态灯 -->
<UserControl x:Class="SCEAST_MES.Views.StatusLight">
<Grid>
<Ellipse Width="20" Height="20">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Setter Property="Fill" Value="Gray"/>
<Style.Triggers>
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource
AncestorType=UserControl},
Path=Status}" Value="Running">
<Setter Property="Fill" Value="#4CAF50"/>
</DataTrigger>
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource
AncestorType=UserControl},
Path=Status}" Value="Error">
<Setter Property="Fill" Value="#F44336"/>
</DataTrigger>
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource
AncestorType=UserControl},
Path=Status}" Value="Warning">
<Setter Property="Fill" Value="#FF9800"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</Grid>
</UserControl>
<!-- 使用 -->
<local:StatusLight Status="{Binding DeviceStatus}" />
7.3 Demo: 导出 Excel 报表
using ClosedXML.Excel;
// NuGet: Install-Package ClosedXML
public void ExportProductionReport(
List<ProductionRecord> records, string filePath)
{
using var workbook = new XLWorkbook();
var ws = workbook.Worksheets.Add("生产报表");
// 表头
ws.Cell(1, 1).Value = "设备编号";
ws.Cell(1, 2).Value = "温度";
ws.Cell(1, 3).Value = "压力";
ws.Cell(1, 4).Value = "产量";
ws.Cell(1, 5).Value = "时间";
ws.Row(1).Style.Font.Bold = true;
// 数据
for (int i = 0; i < records.Count; i++)
{
var r = records[i];
ws.Cell(i + 2, 1).Value = r.DeviceId;
ws.Cell(i + 2, 2).Value = r.Temperature;
ws.Cell(i + 2, 3).Value = r.Pressure;
ws.Cell(i + 2, 4).Value = r.ProductCount;
ws.Cell(i + 2, 5).Value = r.CreateTime;
}
ws.Columns().AdjustToContents();
workbook.SaveAs(filePath);
}
第八章 日志与异常处理
8.1 重点知识
- Serilog 是目前最流行的 C# 日志框架
- 日志分级:Verbose < Debug < Information < Warning < Error < Fatal
- 日志按天滚动,自动清理过期日志
- 全局异常捕获防止程序崩溃白屏
- 通信异常必须有重试机制
8.2 Demo: Serilog 日志配置
// NuGet: Install-Package Serilog
// Install-Package Serilog.Sinks.File
using Serilog;
// App.xaml.cs 中初始化
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.File("logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30, // 保留30天
outputTemplate:
"{Timestamp:HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}")
.WriteTo.Console()
.CreateLogger();
// 使用
Log.Information("串口 {Port} 已连接", "COM1");
Log.Warning("设备 {Id} 通信超时", "DEV001");
Log.Error(ex, "读取PLC数据失败");
// 全局异常捕获(App.xaml.cs)
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
AppDomain.CurrentDomain.UnhandledException
+= (s, args) =>
{
Log.Fatal(args.ExceptionObject as Exception,
"未处理的异常");
};
DispatcherUnhandledException
+= (s, args) =>
{
Log.Fatal(args.Exception, "UI线程异常");
args.Handled = true; // 防止程序崩溃
};
}
第九章 MES 系统对接
9.1 重点知识
- 常见对接方式:Web API(最主流)、数据库中间表、消息队列
- Web API 用 HttpClient 或 RestSharp 调用
- 工单下发:MES 下发生产任务到上位机
- 生产报工:上位机把产量、良率上报 MES
- 物料追溯:扫码绑定产品与物料批次
9.2 Demo: Web API 对接 MES
using System.Net.Http.Json;
public class MesApiClient
{
private readonly HttpClient _http;
public MesApiClient(string baseUrl)
{
_http = new HttpClient { BaseAddress =
new Uri(baseUrl) };
_http.Timeout = TimeSpan.FromSeconds(10);
}
// 获取工单
public async Task<WorkOrder?> GetWorkOrderAsync(
string orderNo)
{
return await _http.GetFromJsonAsync<WorkOrder>(
$"/api/orders/{orderNo}");
}
// 报工
public async Task<bool> ReportProductionAsync(
ProductionReport report)
{
var resp = await _http.PostAsJsonAsync(
"/api/production/report", report);
return resp.IsSuccessStatusCode;
}
}
public class WorkOrder
{
public string OrderNo { get; set; } = "";
public string ProductCode { get; set; } = "";
public int TargetQty { get; set; }
}
public class ProductionReport
{
public string OrderNo { get; set; } = "";
public string DeviceId { get; set; } = "";
public int GoodQty { get; set; }
public int BadQty { get; set; }
public DateTime ReportTime { get; set; }
}
第十章 配置文件管理
10.1 重点知识
- appsettings.json 是 .NET 推荐的配置方式
- 设备参数(IP、端口、地址)不要硬编码,放配置文件
- 配置文件修改后不用重新编译程序
- 敏感信息(密码)不要明文存储
10.2 Demo: JSON 配置文件读写
using System.Text.Json;
// appsettings.json
// {
// "Devices": [
// { "Id": "DEV001", "Type": "ModbusTcp",
// "Ip": "192.168.1.10", "Port": 502 },
// { "Id": "DEV002", "Type": "Serial",
// "ComPort": "COM3", "BaudRate": 9600 }
// ],
// "PollInterval": 200,
// "DatabasePath": "data/production.db"
// }
public class AppConfig
{
public List<DeviceConfig> Devices { get; set; } = new();
public int PollInterval { get; set; } = 200;
public string DatabasePath { get; set; } = "";
public static AppConfig Load(string path = "appsettings.json")
{
if (!File.Exists(path))
return new AppConfig();
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<AppConfig>(json)
?? new AppConfig();
}
public void Save(string path = "appsettings.json")
{
var options = new JsonSerializerOptions
{
WriteIndented = true // 格式化输出
};
var json = JsonSerializer.Serialize(this, options);
File.WriteAllText(path, json);
}
}
public class DeviceConfig
{
public string Id { get; set; } = "";
public string Type { get; set; } = "";
public string? Ip { get; set; }
public int Port { get; set; }
public string? ComPort { get; set; }
public int BaudRate { get; set; }
}
附录:学习路线图
第一阶段(1-2个月)
- Modbus RTU/TCP 通信 → 能读写PLC寄存器
- 串口编程 → 完整收发、粘包处理
- SQLite + Dapper → 数据存取
- 多线程基础 → async/await、Task、Dispatcher
第二阶段(3-4个月)
- OPC UA → 连接主流设备
- TCP/IP Socket → 自定义协议通信
- WPF 进阶 → 自定义控件、实时图表
- 日志框架 → Serilog 集成
第三阶段(5-6个月)
- MES 对接 → Web API、工单报工
- 报表导出 → Excel/PDF 生成
- 多设备管理 → 架构设计、性能优化
- 项目实战 → 独立完成完整上位机项目
核心建议: 通信协议 + 多线程 + 数据库 是上位机开发的三根支柱。先把这三样吃透,其他技术都是在这些基础上叠加。每个技术点都要动手写Demo,光看不练等于没学。