C#/.NET산업 자동화개발完全指南:构建企业级工业적용
서론:C#/.NET在산업 자동화中的优势
C#/.NET平台凭借其强大的유형系统、丰富的类库、卓越的性能和微软的全面支持,在企业级산업 자동화领域占据重要地位。从传统的Windows桌面적용到现代的云原生微服务,C#/.NET提供了完整的솔루션栈,特别适合需要高可靠性、高性能和长期维护的工业적용。
本文将深入探讨C#/.NET在산업 자동화中的적용,重点介绍NModbus库的使用、WPF/WinForms界面개발、企业级架构设计,并结合实际案例展示如何构建专业级的산업 자동화系统。
一、C#/.NET工业개발환경설정
1.1 개발환경要求
<!-- .NET SDK버전要求 -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
1.2 项目结构和依赖설정
<!-- Industrial자동화.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Modbus통신 -->
<PackageReference Include="NModbus" Version="3.0.73" />
<PackageReference Include="NModbus.Serial" Version="3.0.73" />
<!-- 시리얼 포트통신 -->
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
<!-- 로그记录 -->
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<!-- 依赖注入 -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<!-- 설정管理 -->
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<!-- 데이터验证 -->
<PackageReference Include="FluentValidation" Version="11.8.0" />
<!-- 单元测试 -->
<PackageReference Include="xunit" Version="2.6.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5" />
<PackageReference Include="Moq" Version="4.20.69" />
</ItemGroup>
</Project>
1.3 项目目录结构
Industrial자동화/
├── src/
│ ├── Industrial자동화.Core/ # 核心业务逻辑
│ │ ├── Models/ # 데이터模型
│ │ ├── Services/ # 业务服务
│ │ ├── Interfaces/ # 인터페이스定义
│ │ └── Exceptions/ # 自定义异常
│ │
│ ├── Industrial자동화.Modbus/ # Modbus통신模块
│ │ ├── Clients/ # Modbus클라이언트
│ │ ├── Servers/ # Modbus服务器
│ │ └── 컨버터/ # 데이터컨버터
│ │
│ ├── Industrial자동화.UI/ # 用户界面
│ │ ├── Views/ # WPF视图
│ │ ├── ViewModels/ # MVVM视图模型
│ │ ├── 컨버터/ # 值컨버터
│ │ └── Behaviors/ # 行为
│ │
│ └── Industrial자동화.Host/ # 적용程序宿主
│
├── tests/
│ ├── Industrial자동화.Core.Tests/
│ ├── Industrial자동화.Modbus.Tests/
│ └── Industrial자동화.Integration.Tests/
│
├── config/
│ ├── appsettings.json # 설정 적용
│ ├── appsettings.Development.json # 개발환경설정
│ └── devices.json # 设备설정
│
└── docs/ # 项目문서
二、NModbus库深度使用
2.1 基础Modbus TCP클라이언트
using NModbus;
using NModbus.Device;
using System.Net.Sockets;
namespace Industrial자동화.Modbus.Clients
{
/// <summary>
/// Modbus TCP클라이언트
/// </summary>
public class ModbusTcpClient : IModbusClient, IDisposable
{
private readonly ILogger<ModbusTcpClient> _logger;
private readonly TcpClient _tcpClient;
private IModbusMaster? _modbusMaster;
private bool _disposed;
/// <summary>
/// 设备연결状态
/// </summary>
public bool IsConnected => _tcpClient?.Connected ?? false;
/// <summary>
/// 设备IP주소
/// </summary>
public string Ip주소 { get; }
/// <summary>
/// 设备포트
/// </summary>
public int Port { get; }
/// <summary>
/// 初始化Modbus TCP클라이언트
/// </summary>
public ModbusTcpClient(
string ip주소,
int port = 502,
ILogger<ModbusTcpClient>? logger = null)
{
Ip주소 = ip주소 ?? throw new ArgumentNullException(nameof(ip주소));
Port = port;
_logger = logger ?? NullLogger<ModbusTcpClient>.Instance;
_tcpClient = new TcpClient();
}
/// <summary>
/// 연결到设备
/// </summary>
public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("正在연결到设备 {Ip주소}:{Port}", Ip주소, Port);
await _tcpClient.ConnectAsync(Ip주소, Port, cancellationToken);
var factory = new ModbusFactory();
_modbusMaster = factory.CreateMaster(_tcpClient);
// 设置超时时间
_modbusMaster.Transport.ReadTimeout = 3000;
_modbusMaster.Transport.WriteTimeout = 3000;
_logger.LogInformation("成功연결到设备 {Ip주소}:{Port}", Ip주소, Port);
return true;
}
catch (Exception ex)
{
_logger.Log오답(ex, "연결设备 {Ip주소}:{Port} 失败", Ip주소, Port);
return false;
}
}
/// <summary>
/// 유지 레지스터 읽기 (FC03)
/// </summary>
public async Task<ushort[]> ReadHolding회원가입sAsync(
byte slave주소,
ushort 시작주소,
ushort numberOfPoints,
CancellationToken cancellationToken = default)
{
ValidateConnection();
try
{
_logger.LogDebug(
"유지 레지스터 읽기 (FC03) - 슬레이브: {Slave주소}, 주소: {Start주소}, 数量: {NumberOfPoints}",
slave주소, 시작주소, numberOfPoints);
var result = await Task.Run(() =>
_modbusMaster!.ReadHolding회원가입s(slave주소, 시작주소, numberOfPoints),
cancellationToken);
_logger.LogDebug(
"유지 레지스터 읽기 (FC03)成功 - 슬레이브: {Slave주소}, 주소: {Start주소}, 데이터: {데이터}",
slave주소, 시작주소, result);
return result;
}
catch (Exception ex)
{
_logger.Log오답(ex,
"유지 레지스터 읽기 (FC03)失败 - 슬레이브: {Slave주소}, 주소: {Start주소}, 数量: {NumberOfPoints}",
slave주소, 시작주소, numberOfPoints);
throw new ModbusCommunicationException(
$"유지 레지스터 읽기 (FC03)失败: {ex.Message}", ex);
}
}
/// <summary>
/// 쓰기单个유지 레지스터
/// </summary>
public async Task WriteSingle회원가입Async(
byte slave주소,
ushort register주소,
ushort value,
CancellationToken cancellationToken = default)
{
ValidateConnection();
try
{
_logger.LogDebug(
"단일 레지스터 쓰기 (FC06) - 슬레이브: {Slave주소}, 주소: {회원가입주소}, 值: {Value}",
slave주소, register주소, value);
await Task.Run(() =>
_modbusMaster!.WriteSingle회원가입(slave주소, register주소, value),
cancellationToken);
_logger.LogDebug(
"단일 레지스터 쓰기 (FC06)成功 - 슬레이브: {Slave주소}, 주소: {회원가입주소}",
slave주소, register주소);
}
catch (Exception ex)
{
_logger.Log오답(ex,
"단일 레지스터 쓰기 (FC06)失败 - 슬레이브: {Slave주소}, 주소: {회원가입주소}, 值: {Value}",
slave주소, register주소, value);
throw new ModbusCommunicationException(
$"단일 레지스터 쓰기 (FC06)失败: {ex.Message}", ex);
}
}
/// <summary>
/// 쓰기多个유지 레지스터
/// </summary>
public async Task WriteMultiple회원가입sAsync(
byte slave주소,
ushort 시작주소,
ushort[] 데이터,
CancellationToken cancellationToken = default)
{
ValidateConnection();
try
{
_logger.LogDebug(
"다중 레지스터 쓰기 (FC16) - 슬레이브: {Slave주소}, 주소: {Start주소}, 데이터길이: {Length}",
slave주소, 시작주소, 데이터.Length);
await Task.Run(() =>
_modbusMaster!.WriteMultiple회원가입s(slave주소, 시작주소, 데이터),
cancellationToken);
_logger.LogDebug(
"다중 레지스터 쓰기 (FC16)成功 - 슬레이브: {Slave주소}, 주소: {Start주소}",
slave주소, 시작주소);
}
catch (Exception ex)
{
_logger.Log오답(ex,
"다중 레지스터 쓰기 (FC16)失败 - 슬레이브: {Slave주소}, 주소: {Start주소}, 데이터길이: {Length}",
slave주소, 시작주소, 데이터.Length);
throw new ModbusCommunicationException(
$"다중 레지스터 쓰기 (FC16)失败: {ex.Message}", ex);
}
}
/// <summary>
/// 코일 읽기 (FC01)
/// </summary>
public async Task<bool[]> ReadCoilsAsync(
byte slave주소,
ushort 시작주소,
ushort numberOfPoints,
CancellationToken cancellationToken = default)
{
ValidateConnection();
try
{
_logger.LogDebug(
"읽기코일 - 슬레이브: {Slave주소}, 주소: {Start주소}, 数量: {NumberOfPoints}",
slave주소, 시작주소, numberOfPoints);
var result = await Task.Run(() =>
_modbusMaster!.ReadCoils(slave주소, 시작주소, numberOfPoints),
cancellationToken);
_logger.LogDebug(
"읽기코일成功 - 슬레이브: {Slave주소}, 주소: {Start주소}, 데이터: {데이터}",
slave주소, 시작주소, result);
return result;
}
catch (Exception ex)
{
_logger.Log오답(ex,
"읽기코일失败 - 슬레이브: {Slave주소}, 주소: {Start주소}, 数量: {NumberOfPoints}",
slave주소, 시작주소, numberOfPoints);
throw new ModbusCommunicationException(
$"읽기코일失败: {ex.Message}", ex);
}
}
/// <summary>
/// 批量읽기优化
/// </summary>
public async Task<Dictionary<ushort, ushort[]>> BatchRead회원가입sAsync(
byte slave주소,
IEnumerable<회원가입ReadRequest> requests,
CancellationToken cancellationToken = default)
{
ValidateConnection();
var results = new Dictionary<ushort, ushort[]>();
foreach (var request in requests)
{
try
{
var 데이터 = await ReadHolding회원가입sAsync(
slave주소,
request.Start주소,
request.NumberOfPoints,
cancellationToken);
results[request.Start주소] = 데이터;
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"批量읽기失败 - 주소: {Start주소}, 数量: {NumberOfPoints}",
request.Start주소, request.NumberOfPoints);
results[request.Start주소] = Array.Empty<ushort>();
}
}
return results;
}
/// <summary>
/// 断开연결
/// </summary>
public void Disconnect()
{
try
{
_modbusMaster?.Dispose();
_modbusMaster = null;
if (_tcpClient.Connected)
{
_tcpClient.닫기();
}
_logger.LogInformation("已断开与设备 {Ip주소}:{Port} 的연결", Ip주소, Port);
}
catch (Exception ex)
{
_logger.Log오답(ex, "断开연결时发生错误");
}
}
/// <summary>
/// 验证연결状态
/// </summary>
private void ValidateConnection()
{
if (!IsConnected || _modbusMaster == null)
{
throw new Invalid작업Exception("设备연결되지 않음");
}
}
/// <summary>
/// 释放자료
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
Disconnect();
_tcpClient.Dispose();
}
_disposed = true;
}
}
}
/// <summary>
/// 레지스터읽기请求
/// </summary>
public record 회원가입ReadRequest(
ushort Start주소,
ushort NumberOfPoints);
}
2.2 Modbus RTU시리얼 포트클라이언트
```csharp
using NModbus;
using NModbus.Serial;
using System.IO.Ports;
namespace Industrial자동화.Modbus.Clients
{
///
/// Modbus RTU시리얼 포트클라이언트
///
public class ModbusRtuClient : IModbusClient, IDisposable
{
private readonly ILogger
_logger;
private readonly SerialPort _serialPort;
private IModbusMaster? _modbusMaster;
private bool _disposed;
/// <summary>
/// 시리얼 포트연결状态
/// </summary>
public bool IsConnected => _serialPort?.IsOpen ?? false;
/// <summary>
/// 시리얼 포트名称
/// </summary>
public string Port이름 { get; }
/// <summary>
/// 통신 속도
/// </summary>
public int BaudRate { get; }
/// <summary>
/// 初始化Modbus RTU클라이언트
/// </summary>
public ModbusRtuClient(
string port이름,
int baudRate = 9600,
ILogger<ModbusRtuClient>? logger = null)
{
Port이름 = port이름 ?? throw new ArgumentNullException(nameof(port이름));
BaudRate = baudRate;
_logger = logger ?? NullLogger<ModbusRtuClient>.Instance;
_serialPort = new SerialPort(port이름)
{
BaudRate = baudRate,
데이터Bits = 8,
Parity = Parity.None,
StopBits = StopBits.One,
ReadTimeout = 1000,
WriteTimeout = 1000
};
}
/// <summary>
/// 연결到시리얼 포트设备
/// </summary>
public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("正在시리얼 포트 열기 {Port이름} (통신 속도: {BaudRate})", Port이름, BaudRate);
await Task.Run(() =>
{
if (!_serialPort.IsOpen)
{
_serialPort.Open();
}
}, cancellationToken);
var factory = new ModbusFactory();
var adapter = new SerialPortAdapter(_serialPort);
_modbusMaster = factory.CreateRtuMaster(adapter);
// 설정Modbus매개변수
_modbusMaster.Transport.ReadTimeout = 1000;
_modbusMaster.Transport.WriteTimeout = 1000;
_modbusMaster.Transport.Retries = 3;
_modbusMaster.Transport.WaitToRetryMilliseconds = 100;
_logger.LogInformation("成功시리얼 포트 열기 {Port이름}", Port이름);
return true;
}
catch (Exception ex)
{
_logger.Log오답(ex, "시리얼 포트 열기 {Port이름} 失败", Port이름);
return false;
}
}
/// <summary>
/// 扫描시리얼 포트设备
/// </summary>
public async Task<List<byte>> ScanDevicesAsync(
byte 시작주소 = 1,
byte end주소 = 247,
CancellationToken cancellationToken = default)
{
var foundDevices = new List<byte>();
_logger.LogInformation("开始扫描设备 (주소范围: {Start주소}-{End주소})", 시작주소, end주소);
for (byte slave주소 = 시작주소; slave주소 <= end주소; slave주소++)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
try
{
// 尝试읽기设备标识
var result = await Task.Run(() =>
_modbusMaster!.ReadDeviceIdentification(slave주소, 0x01),
cancellationToken);
if (result != null)
{
foundDevices.Add(slave주소);
_logger.LogInformation("
답글 작성