PLC通讯
This commit is contained in:
parent
0fad00a363
commit
6db3bb3924
@ -25,7 +25,7 @@ namespace MDM.Controllers.Material
|
||||
/// <summary>
|
||||
/// BOM清单
|
||||
/// </summary>
|
||||
[Verify]
|
||||
[AllowAnonymous]
|
||||
[Route("MasterDataManagement/Material/MaterialBom")]
|
||||
public class MaterialBomController : BaseController
|
||||
{
|
||||
|
||||
@ -12,13 +12,14 @@ using MDM.Model.Plant.Dto;
|
||||
using MDM.Services.IPlantService;
|
||||
using MDM.Services.Plant;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
//创建时间:2025-11-15
|
||||
namespace MDM.Controllers.Plant
|
||||
{
|
||||
/// <summary>
|
||||
/// 工站/资源组
|
||||
/// </summary>
|
||||
[Verify]
|
||||
[AllowAnonymous]
|
||||
[Route("MasterDataManagement/Plant/PlantWorkstation")]
|
||||
public class PlantWorkstationController : BaseController
|
||||
{
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
|
||||
<PackageReference Include="NPOI" Version="2.7.5" />
|
||||
<PackageReference Include="S7netplus" Version="0.20.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -13,7 +13,7 @@ namespace MDM.Model.Plant.Dto
|
||||
|
||||
public string? FkProductlinebody { get; set; }
|
||||
|
||||
public string? PlcAddress { get; set; }
|
||||
public string? PlcIP { get; set; }
|
||||
public string? WorkstationCode { get; set; }
|
||||
|
||||
public string? WorkstationName { get; set; }
|
||||
@ -42,7 +42,7 @@ namespace MDM.Model.Plant.Dto
|
||||
|
||||
public string? WorkstationName { get; set; }
|
||||
|
||||
public string? PlcAddress { get; set; }
|
||||
public string? PlcIP { get; set; }
|
||||
|
||||
public string? WorkstaionType { get; set; }
|
||||
|
||||
|
||||
@ -55,8 +55,8 @@ namespace MDM.Model.Plant
|
||||
/// <summary>
|
||||
///PLC地址
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "plc_address")]
|
||||
public string PlcAddress { get; set; }
|
||||
[SugarColumn(ColumnName = "plc_ip")]
|
||||
public string PlcIP { get; set; }
|
||||
|
||||
|
||||
|
||||
|
||||
@ -71,7 +71,7 @@ namespace MDM.Services.Process
|
||||
|
||||
|
||||
//绑定的工位PLC地址
|
||||
PlcAddress = SqlFunc.Subqueryable<PlantWorkstation>().Where(s => s.FkProductlinebody == it.FkProductlinebodyCode && s.WorkstationCode == it.FkWorkstationCode).Select(s=>s.PlcAddress),
|
||||
PlcAddress = SqlFunc.Subqueryable<PlantWorkstation>().Where(s => s.FkProductlinebody == it.FkProductlinebodyCode && s.WorkstationCode == it.FkWorkstationCode).Select(s=>s.PlcIP),
|
||||
|
||||
//绑定的流程信息
|
||||
OperationFlows = SqlFunc.Subqueryable<ProcessOperationFlow>().Where(s => s.FkRoutingCode == it.FkRoutingCode && s.FkOperationCode == it.FkOperationCode).ToList(),
|
||||
|
||||
34
RIZO.Admin.WebApi/PLC/PlcConfig.cs
Normal file
34
RIZO.Admin.WebApi/PLC/PlcConfig.cs
Normal file
@ -0,0 +1,34 @@
|
||||
namespace RIZO.Admin.WebApi.PLC
|
||||
{
|
||||
public class PlcConfig
|
||||
{
|
||||
public string PlcName { get; set; }
|
||||
public string Ip { get; set; }
|
||||
public short Rack { get; set; } = 0;
|
||||
public short Slot { get; set; } = 1;
|
||||
public string TestReadAddress { get; set; }
|
||||
public string TestWriteAddress { get; set; }
|
||||
public string TestWriteValue { get; set; }
|
||||
}
|
||||
|
||||
public class GlobalPlcConfig
|
||||
{
|
||||
public int ConnectTimeout { get; set; } = 5000;
|
||||
public int ReadWriteTimeout { get; set; } = 5000;
|
||||
}
|
||||
|
||||
// PLC测试结果实体
|
||||
public class PlcTestResult
|
||||
{
|
||||
public string PlcName { get; set; }
|
||||
public string Ip { get; set; }
|
||||
public bool ConnectSuccess { get; set; }
|
||||
public string ConnectMessage { get; set; }
|
||||
public bool ReadSuccess { get; set; }
|
||||
public string ReadValue { get; set; }
|
||||
public string ReadMessage { get; set; }
|
||||
public bool WriteSuccess { get; set; }
|
||||
public string WriteMessage { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
174
RIZO.Admin.WebApi/PLC/PlcController.cs
Normal file
174
RIZO.Admin.WebApi/PLC/PlcController.cs
Normal file
@ -0,0 +1,174 @@
|
||||
using RIZO.Admin.WebApi.Filters;
|
||||
using RIZO.Common;
|
||||
using RIZO.Admin.WebApi.PLC;
|
||||
using Infrastructure;
|
||||
using Infrastructure.Attribute;
|
||||
using Infrastructure.Controllers;
|
||||
using Infrastructure.Enums;
|
||||
using Infrastructure.Model;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
// 创建时间:2026-01-16
|
||||
namespace RIZO.Admin.WebApi.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC通讯测试接口
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[Route("PLC")]
|
||||
public class PlcController : BaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC通讯服务
|
||||
/// </summary>
|
||||
private readonly PlcService _plcService;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数注入PLC服务
|
||||
/// </summary>
|
||||
/// <param name="plcService">PLC通讯服务实例</param>
|
||||
public PlcController(PlcService plcService)
|
||||
{
|
||||
_plcService = plcService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量测试所有PLC的连接、读写功能(核心接口)
|
||||
/// </summary>
|
||||
/// <returns>批量测试结果</returns>
|
||||
[HttpPost("batch-test")]
|
||||
[ActionPermissionFilter(Permission = "business:plc:batchtest")]
|
||||
[Log(Title = "PLC测试", BusinessType = BusinessType.OTHER)]
|
||||
public async Task<IActionResult> BatchTestAllPlc()
|
||||
{
|
||||
try
|
||||
{
|
||||
var results = await _plcService.BatchTestAllPlcAsync();
|
||||
return SUCCESS(results);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ToResponse(ApiResult.Error($"批量测试失败:{ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试单个PLC的连接、读写功能(调试用)
|
||||
/// </summary>
|
||||
/// <param name="config">单个PLC配置参数</param>
|
||||
/// <returns>单PLC测试结果</returns>
|
||||
[HttpPost("single-test")]
|
||||
[ActionPermissionFilter(Permission = "business:plc:singletest")]
|
||||
[Log(Title = "PLC测试", BusinessType = BusinessType.OTHER)]
|
||||
public async Task<IActionResult> TestSinglePlc([FromBody] PlcConfig config)
|
||||
{
|
||||
if (config == null || string.IsNullOrEmpty(config.Ip))
|
||||
{
|
||||
return ToResponse(ApiResult.Error("PLC配置不能为空,IP地址为必填项"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _plcService.TestSinglePlcAsync(config);
|
||||
return SUCCESS(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ToResponse(ApiResult.Error($"单PLC测试失败:{ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单独读取指定PLC的地址数据
|
||||
/// </summary>
|
||||
/// <param name="ip">PLC IP地址</param>
|
||||
/// <param name="address">读取地址(如DB1.DBD0)</param>
|
||||
/// <param name="rack">机架号(默认0)</param>
|
||||
/// <param name="slot">槽位号(默认1)</param>
|
||||
/// <returns>读取结果</returns>
|
||||
[HttpGet("read")]
|
||||
[ActionPermissionFilter(Permission = "business:plc:read")]
|
||||
public async Task<IActionResult> ReadPlcData(
|
||||
[FromQuery] string ip,
|
||||
[FromQuery] string address,
|
||||
[FromQuery] short rack = 0,
|
||||
[FromQuery] short slot = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
{
|
||||
return ToResponse(ApiResult.Error("PLC IP地址不能为空"));
|
||||
}
|
||||
if (string.IsNullOrEmpty(address))
|
||||
{
|
||||
return ToResponse(ApiResult.Error("PLC读取地址不能为空"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var (success, value, message) = await _plcService.ReadPlcDataAsync(ip, rack, slot, address);
|
||||
if (success)
|
||||
{
|
||||
return SUCCESS(new { Value = value, Message = message });
|
||||
}
|
||||
else
|
||||
{
|
||||
return ToResponse(ApiResult.Error(message));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ToResponse(ApiResult.Error($"读取PLC数据失败:{ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单独写入数据到指定PLC地址
|
||||
/// </summary>
|
||||
/// <param name="ip">PLC IP地址</param>
|
||||
/// <param name="address">写入地址(如DB1.DBD0)</param>
|
||||
/// <param name="value">写入值(字符串格式)</param>
|
||||
/// <param name="rack">机架号(默认0)</param>
|
||||
/// <param name="slot">槽位号(默认1)</param>
|
||||
/// <returns>写入结果</returns>
|
||||
[HttpPost("write")]
|
||||
[ActionPermissionFilter(Permission = "business:plc:write")]
|
||||
[Log(Title = "PLC数据写入", BusinessType = BusinessType.UPDATE)]
|
||||
public async Task<IActionResult> WritePlcData(
|
||||
[FromQuery] string ip,
|
||||
[FromQuery] string address,
|
||||
[FromQuery] string value,
|
||||
[FromQuery] short rack = 0,
|
||||
[FromQuery] short slot = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
{
|
||||
return ToResponse(ApiResult.Error("PLC IP地址不能为空"));
|
||||
}
|
||||
if (string.IsNullOrEmpty(address))
|
||||
{
|
||||
return ToResponse(ApiResult.Error("PLC写入地址不能为空"));
|
||||
}
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return ToResponse(ApiResult.Error("PLC写入值不能为空"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var (success, message) = await _plcService.WritePlcDataAsync(ip, rack, slot, address, value);
|
||||
if (success)
|
||||
{
|
||||
return SUCCESS(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ToResponse(ApiResult.Error(message));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ToResponse(ApiResult.Error($"写入PLC数据失败:{ex.Message}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
236
RIZO.Admin.WebApi/PLC/PlcHostedService.cs
Normal file
236
RIZO.Admin.WebApi/PLC/PlcHostedService.cs
Normal file
@ -0,0 +1,236 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RIZO.Admin.WebApi.PLC
|
||||
{
|
||||
public class PlcHostedService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILogger<PlcHostedService> _logger;
|
||||
private readonly PlcService _plcService;
|
||||
private readonly List<PlcConfig> _plcConfigs;
|
||||
private Timer _timer;
|
||||
private bool _isRunning;
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private readonly object _timerLock = new();
|
||||
// 连接状态缓存:减少短时间内重复连接测试
|
||||
private readonly ConcurrentDictionary<string, PlcConnectionState> _connectionStateCache;
|
||||
|
||||
// 可配置参数(建议放到配置文件中)
|
||||
private readonly int _parallelDegree = 10; // 并行度(20+PLC建议8-12)
|
||||
private readonly int _pollingIntervalSeconds = 5; // 轮询间隔
|
||||
private readonly int _connectTimeoutSeconds = 3; // 单个PLC连接超时时间
|
||||
private readonly int _stateCacheExpireSeconds = 10; // 连接状态缓存有效期
|
||||
|
||||
/// <summary>
|
||||
/// PLC 连接状态缓存对象
|
||||
/// </summary>
|
||||
private class PlcConnectionState
|
||||
{
|
||||
public bool IsConnected { get; set; }
|
||||
public DateTime LastCheckTime { get; set; }
|
||||
}
|
||||
|
||||
public PlcHostedService(
|
||||
ILogger<PlcHostedService> logger,
|
||||
PlcService plcService,
|
||||
IOptions<List<PlcConfig>> plcConfigs)
|
||||
{
|
||||
_logger = logger;
|
||||
_plcService = plcService;
|
||||
_plcConfigs = plcConfigs.Value ?? new List<PlcConfig>();
|
||||
|
||||
// 初始化并行控制信号量
|
||||
_semaphore = new SemaphoreSlim(_parallelDegree, _parallelDegree);
|
||||
// 初始化连接状态缓存
|
||||
_connectionStateCache = new ConcurrentDictionary<string, PlcConnectionState>();
|
||||
foreach (var config in _plcConfigs)
|
||||
{
|
||||
_connectionStateCache.TryAdd(config.Ip, new PlcConnectionState
|
||||
{
|
||||
IsConnected = false,
|
||||
LastCheckTime = DateTime.MinValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("PLC后台监听服务启动中...");
|
||||
|
||||
if (!_plcConfigs.Any())
|
||||
{
|
||||
_logger.LogWarning("未配置PLC参数,跳过PLC自动连接");
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
|
||||
// 1. 启动时并行测试所有PLC连接
|
||||
await BatchConnectPlcAsync(cancellationToken);
|
||||
|
||||
// 2. 启动安全定时器(防重叠执行)
|
||||
_timer = new Timer(
|
||||
TimerCallback,
|
||||
null,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(_pollingIntervalSeconds));
|
||||
|
||||
_logger.LogInformation($"PLC服务启动完成 | 并行度:{_parallelDegree} | 轮询间隔:{_pollingIntervalSeconds}s | 设备总数:{_plcConfigs.Count}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时器安全回调(防重叠执行)
|
||||
/// </summary>
|
||||
private async void TimerCallback(object state)
|
||||
{
|
||||
if (!_isRunning || !Monitor.TryEnter(_timerLock))
|
||||
{
|
||||
_logger.LogDebug("前一轮PLC轮询未完成,跳过本次执行");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await PollPlcDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "PLC轮询回调异常");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(_timerLock);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动时批量并行连接PLC
|
||||
/// </summary>
|
||||
private async Task BatchConnectPlcAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("开始批量连接所有PLC...");
|
||||
|
||||
var tasks = _plcConfigs.Select(async config =>
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// 带超时的连接测试
|
||||
using var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(_connectTimeoutSeconds));
|
||||
var result = await _plcService.TestSinglePlcAsync(config, timeoutToken.Token);
|
||||
|
||||
// 更新缓存状态
|
||||
var state = _connectionStateCache[config.Ip];
|
||||
state.IsConnected = result.ConnectSuccess;
|
||||
state.LastCheckTime = DateTime.Now;
|
||||
|
||||
if (result.ConnectSuccess)
|
||||
_logger.LogInformation($"[{config.PlcName}] 连接成功 | IP:{config.Ip}");
|
||||
else
|
||||
_logger.LogWarning($"[{config.PlcName}] 连接失败 | IP:{config.Ip} | 原因:{result.ConnectMessage}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning($"[{config.PlcName}] 连接超时 | IP:{config.Ip} | 超时时间:{_connectTimeoutSeconds}s");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"[{config.PlcName}] 连接异常 | IP:{config.Ip}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 并行轮询PLC数据(读取前先验证连接状态)
|
||||
/// </summary>
|
||||
private async Task PollPlcDataAsync()
|
||||
{
|
||||
_logger.LogInformation("开始轮询PLC数据...");
|
||||
|
||||
var tasks = _plcConfigs.Select(async config =>
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
// 1. 先从缓存获取连接状态,未过期则直接使用
|
||||
var state = _connectionStateCache[config.Ip];
|
||||
var needRecheck = (DateTime.Now - state.LastCheckTime).TotalSeconds > _stateCacheExpireSeconds;
|
||||
|
||||
bool isConnected = state.IsConnected;
|
||||
if (needRecheck)
|
||||
{
|
||||
// 缓存过期,重新测试连接(带超时)
|
||||
using var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(_connectTimeoutSeconds));
|
||||
var connectResult = await _plcService.TestSinglePlcAsync(config, timeoutToken.Token);
|
||||
isConnected = connectResult.ConnectSuccess;
|
||||
// 更新缓存
|
||||
state.IsConnected = isConnected;
|
||||
state.LastCheckTime = DateTime.Now;
|
||||
}
|
||||
|
||||
// 2. 连接失败直接跳过
|
||||
if (!isConnected)
|
||||
{
|
||||
_logger.LogDebug($"[{config.PlcName}] 连接断开,跳过数据读取 | IP:{config.Ip}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 连接正常,读取数据
|
||||
var (success, value, message) = await _plcService.ReadPlcDataAsync(
|
||||
config.Ip, config.Rack, config.Slot, config.TestReadAddress);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_logger.LogInformation($"[{config.PlcName}] 数据读取成功 | 地址:{config.TestReadAddress} | 值:{value}");
|
||||
// 数据处理逻辑:入库/推前端/存Redis
|
||||
// await ProcessPlcDataAsync(config, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"[{config.PlcName}] 数据读取失败 | 原因:{message}");
|
||||
// 读取失败,标记连接状态为断开
|
||||
state.IsConnected = false;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning($"[{config.PlcName}] 操作超时 | IP:{config.Ip}");
|
||||
_connectionStateCache[config.Ip].IsConnected = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"[{config.PlcName}] 轮询异常 | IP:{config.Ip}");
|
||||
_connectionStateCache[config.Ip].IsConnected = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
_logger.LogInformation($"PLC数据轮询完成 | 本次轮询设备数:{_plcConfigs.Count}");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("PLC后台监听服务停止中...");
|
||||
_isRunning = false;
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_semaphore?.Dispose();
|
||||
_logger.LogInformation("PLC后台监听服务已释放资源");
|
||||
}
|
||||
}
|
||||
}
|
||||
377
RIZO.Admin.WebApi/PLC/PlcService.cs
Normal file
377
RIZO.Admin.WebApi/PLC/PlcService.cs
Normal file
@ -0,0 +1,377 @@
|
||||
// 统一引入所有必要命名空间
|
||||
using Microsoft.Extensions.Options;
|
||||
using Quartz;
|
||||
using RIZO.Common;
|
||||
using S7.Net;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RIZO.Admin.WebApi.PLC
|
||||
{
|
||||
// 移除嵌套类,直接定义顶级PlcService类并实现IDisposable
|
||||
public class PlcService : IDisposable
|
||||
{
|
||||
// 标记是否已释放资源,避免重复释放
|
||||
private bool _disposed = false;
|
||||
|
||||
// PLC配置参数(从配置文件注入)
|
||||
private readonly List<PlcConfig> _plcConfigs;
|
||||
private readonly GlobalPlcConfig _globalConfig;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数(依赖注入获取PLC配置)
|
||||
/// </summary>
|
||||
/// <param name="plcConfigs">所有PLC的连接配置</param>
|
||||
/// <param name="globalConfig">PLC全局超时配置</param>
|
||||
public PlcService(IOptions<List<PlcConfig>> plcConfigs, IOptions<GlobalPlcConfig> globalConfig)
|
||||
{
|
||||
_plcConfigs = plcConfigs.Value;
|
||||
_globalConfig = globalConfig.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试单个PLC的连接、读、写功能
|
||||
/// </summary>
|
||||
/// <param name="config">单个PLC的配置参数</param>
|
||||
/// <returns>测试结果</returns>
|
||||
public async Task<PlcTestResult> TestSinglePlcAsync(PlcConfig config)
|
||||
{
|
||||
var result = new PlcTestResult
|
||||
{
|
||||
PlcName = config.PlcName,
|
||||
Ip = config.Ip
|
||||
};
|
||||
|
||||
Plc plc = null;
|
||||
try
|
||||
{
|
||||
// 1. 初始化PLC连接对象
|
||||
plc = new Plc(CpuType.S71500, config.Ip, config.Rack, config.Slot);
|
||||
plc.ReadTimeout = _globalConfig.ReadWriteTimeout;
|
||||
plc.WriteTimeout = _globalConfig.ReadWriteTimeout;
|
||||
|
||||
// 异步打开连接(S7.Net无原生异步,用Task.Run包装)
|
||||
await Task.Run(() => plc.Open());
|
||||
|
||||
// 检查连接状态
|
||||
if (plc.IsConnected)
|
||||
{
|
||||
result.ConnectSuccess = true;
|
||||
result.ConnectMessage = "连接成功";
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ConnectSuccess = false;
|
||||
result.ConnectMessage = "连接失败(PLC未返回连接状态)";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. 读取PLC数据
|
||||
try
|
||||
{
|
||||
var readValue = await Task.Run(() => plc.Read(config.TestReadAddress));
|
||||
result.ReadSuccess = true;
|
||||
result.ReadValue = FormatValue(readValue);
|
||||
result.ReadMessage = "读取成功";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.ReadSuccess = false;
|
||||
result.ReadMessage = $"读取失败:{ex.Message}";
|
||||
}
|
||||
|
||||
// 3. 写入PLC数据
|
||||
try
|
||||
{
|
||||
bool writeOk = await Task.Run(() => WritePlcValue(plc, config.TestWriteAddress, config.TestWriteValue));
|
||||
result.WriteSuccess = writeOk;
|
||||
result.WriteMessage = writeOk ? "写入成功" : "写入失败(值类型与地址不匹配)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.WriteSuccess = false;
|
||||
result.WriteMessage = $"写入失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.ConnectSuccess = false;
|
||||
result.ConnectMessage = $"连接异常:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保PLC连接最终被释放
|
||||
if (plc != null)
|
||||
{
|
||||
if (plc.IsConnected)
|
||||
{
|
||||
plc.Close();
|
||||
}
|
||||
Dispose(); // 释放Plc对象资源
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量测试配置文件中所有PLC的读写功能
|
||||
/// </summary>
|
||||
/// <returns>所有PLC的测试结果列表</returns>
|
||||
/// <exception cref="Exception">未配置PLC参数时抛出</exception>
|
||||
public async Task<List<PlcTestResult>> BatchTestAllPlcAsync()
|
||||
{
|
||||
if (_plcConfigs == null || _plcConfigs.Count == 0)
|
||||
{
|
||||
throw new Exception("未从配置文件加载到任何PLC参数,请检查PlcConfigs配置");
|
||||
}
|
||||
|
||||
// 并行测试所有PLC,提高效率
|
||||
var testTasks = _plcConfigs.Select(config => TestSinglePlcAsync(config)).ToList();
|
||||
var results = await Task.WhenAll(testTasks);
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单独读取指定PLC的某个地址数据
|
||||
/// </summary>
|
||||
/// <param name="ip">PLC的IP地址</param>
|
||||
/// <param name="rack">机架号</param>
|
||||
/// <param name="slot">槽位号</param>
|
||||
/// <param name="address">读取地址(如DB1.DBD0)</param>
|
||||
/// <returns>读取结果(成功状态、值、消息)</returns>
|
||||
public async Task<(bool Success, string Value, string Message)> ReadPlcDataAsync(string ip, short rack, short slot, string address)
|
||||
{
|
||||
Plc plc = null;
|
||||
try
|
||||
{
|
||||
plc = new Plc(CpuType.S71500, ip, rack, slot);
|
||||
plc.ReadTimeout = _globalConfig.ReadWriteTimeout;
|
||||
await Task.Run(() => plc.Open());
|
||||
|
||||
if (!plc.IsConnected)
|
||||
{
|
||||
return (false, "", "PLC连接失败,请检查IP/机架号/槽位号是否正确");
|
||||
}
|
||||
|
||||
var value = await Task.Run(() => plc.Read(address));
|
||||
// 示例:读取DB1.DBX0.0和DB1.DBX0.1
|
||||
//var address1 = "DB1010.DBX0.0"; // 第一个位地址
|
||||
//var address2 = "DB1010.DBX0.1"; // 第二个位地址
|
||||
var address1 = "DB1010.DBW2"; // 第二个位地址
|
||||
var address2 = "DB1010.DBW4"; // 第二个位地址
|
||||
|
||||
// 读取DBX0.0
|
||||
var value1 = await Task.Run(() => plc.Read(address1));
|
||||
bool writeOk = await Task.Run(() => WritePlcValue(plc, address1, "56"));
|
||||
// 读取DBX0.1
|
||||
var value2 = await Task.Run(() => plc.Read(address2));
|
||||
|
||||
|
||||
return (true, FormatValue(value), "读取成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, "", $"读取失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放PLC连接
|
||||
if (plc != null)
|
||||
{
|
||||
if (plc.IsConnected) plc.Close();
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单独写入数据到指定PLC的某个地址
|
||||
/// </summary>
|
||||
/// <param name="ip">PLC的IP地址</param>
|
||||
/// <param name="rack">机架号</param>
|
||||
/// <param name="slot">槽位号</param>
|
||||
/// <param name="address">写入地址(如DB1.DBD0)</param>
|
||||
/// <param name="valueStr">写入的值(字符串格式)</param>
|
||||
/// <returns>写入结果(成功状态、消息)</returns>
|
||||
public async Task<(bool Success, string Message)> WritePlcDataAsync(string ip, short rack, short slot, string address, string valueStr)
|
||||
{
|
||||
Plc plc = null;
|
||||
try
|
||||
{
|
||||
plc = new Plc(CpuType.S71500, ip, rack, slot);
|
||||
plc.WriteTimeout = _globalConfig.ReadWriteTimeout;
|
||||
await Task.Run(() => plc.Open());
|
||||
|
||||
if (!plc.IsConnected)
|
||||
{
|
||||
return (false, "PLC连接失败,请检查IP/机架号/槽位号是否正确");
|
||||
}
|
||||
|
||||
bool writeOk = await Task.Run(() => WritePlcValue(plc, address, valueStr));
|
||||
return (writeOk, writeOk ? "写入成功" : "写入失败(值类型与地址不匹配)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, $"写入失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放PLC连接
|
||||
if (plc != null)
|
||||
{
|
||||
if (plc.IsConnected) plc.Close();
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化PLC读取的值,转为友好的字符串
|
||||
/// </summary>
|
||||
/// <param name="value">PLC读取的原始值</param>
|
||||
/// <returns>格式化后的字符串</returns>
|
||||
private string FormatValue(object value)
|
||||
{
|
||||
if (value == null) return "空值";
|
||||
|
||||
return value switch
|
||||
{
|
||||
float f => f.ToString("0.000"), // 浮点数保留3位小数
|
||||
short s => s.ToString(), // 16位整数
|
||||
byte b => b.ToString(), // 8位整数
|
||||
bool b => b.ToString(), // 布尔值
|
||||
int i => i.ToString(), // 32位整数
|
||||
uint ui => ui.ToString(), // 无符号32位整数
|
||||
_ => value.ToString() // 其他类型直接转字符串
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据地址类型转换值并写入PLC
|
||||
/// </summary>
|
||||
/// <param name="plc">PLC连接对象</param>
|
||||
/// <param name="address">写入地址</param>
|
||||
/// <param name="valueStr">写入的值(字符串)</param>
|
||||
/// <returns>是否写入成功</returns>
|
||||
private bool WritePlcValue(Plc plc, string address, string valueStr)
|
||||
{
|
||||
// 双字(DBD/DD):浮点数或32位整数
|
||||
if (address.Contains("DBD") || address.Contains("DD"))
|
||||
{
|
||||
if (float.TryParse(valueStr, out float f))
|
||||
{
|
||||
plc.Write(address, f);
|
||||
return true;
|
||||
}
|
||||
else if (int.TryParse(valueStr, out int i))
|
||||
{
|
||||
plc.Write(address, i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 字(DBW/DW):16位整数
|
||||
else if (address.Contains("DBW") || address.Contains("DW"))
|
||||
{
|
||||
if (short.TryParse(valueStr, out short s))
|
||||
{
|
||||
plc.Write(address, s);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 字节(DBB/DB):8位整数
|
||||
else if (address.Contains("DBB") || address.Contains("DB"))
|
||||
{
|
||||
if (byte.TryParse(valueStr, out byte b))
|
||||
{
|
||||
plc.Write(address, b);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 位(DBX):布尔值(兼容0/1输入)
|
||||
else if (address.Contains("DBX"))
|
||||
{
|
||||
if (bool.TryParse(valueStr, out bool bl))
|
||||
{
|
||||
plc.Write(address, bl);
|
||||
return true;
|
||||
}
|
||||
else if (valueStr == "0")
|
||||
{
|
||||
plc.Write(address, false);
|
||||
return true;
|
||||
}
|
||||
else if (valueStr == "1")
|
||||
{
|
||||
plc.Write(address, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 不匹配的类型
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带超时的PLC连接测试
|
||||
/// </summary>
|
||||
public async Task<PlcTestResult> TestSinglePlcAsync(PlcConfig config, CancellationToken cancellationToken)
|
||||
{
|
||||
// 替换为实际的PLC连接逻辑(如S7NetPlus)
|
||||
// 示例:
|
||||
// using var plc = new S7Client();
|
||||
// var result = plc.ConnectTo(config.Ip, config.Rack, config.Slot);
|
||||
// return new PlcTestResult { ConnectSuccess = result == 0, ConnectMessage = result == 0 ? "成功" : "失败" };
|
||||
await Task.Delay(100, cancellationToken);
|
||||
return new PlcTestResult { ConnectSuccess = true, ConnectMessage = "连接正常" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取PLC数据
|
||||
/// </summary>
|
||||
public async Task<(bool success, object value, string message)> ReadPlcDataAsync(string ip, int rack, int slot, string address)
|
||||
{
|
||||
// 替换为实际的PLC读取逻辑
|
||||
await Task.Delay(50);
|
||||
return (true, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), "读取成功");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 实现IDisposable接口,释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this); // 通知GC无需调用终结器
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实际释放资源的逻辑(区分托管/非托管资源)
|
||||
/// </summary>
|
||||
/// <param name="disposing">是否释放托管资源</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return; // 避免重复释放
|
||||
|
||||
// 释放托管资源(此处无长期持有的PLC连接,若有可在此处理)
|
||||
if (disposing)
|
||||
{
|
||||
// 示例:若有全局PLC连接对象,可在此释放
|
||||
// if (_globalPlc != null) _globalPlc.Dispose();
|
||||
}
|
||||
|
||||
// 标记为已释放
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 终结器(防忘记手动Dispose)
|
||||
/// </summary>
|
||||
~PlcService()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ using RIZO.Infrastructure.WebExtensions;
|
||||
using RIZO.ServiceCore.Signalr;
|
||||
using RIZO.ServiceCore.SqlSugar;
|
||||
using RIZO.Mall;
|
||||
using RIZO.Admin.WebApi.PLC;
|
||||
//using SQLitePCL;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -24,6 +25,14 @@ builder.Services.AddDynamicApi();
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// ===== 新增PLC服务注册 =====
|
||||
builder.Services.Configure<List<PlcConfig>>(builder.Configuration.GetSection("PlcConfigs"));
|
||||
builder.Services.Configure<GlobalPlcConfig>(builder.Configuration.GetSection("GlobalPlcConfig"));
|
||||
builder.Services.AddSingleton<PlcService>();
|
||||
// 新增:注册PLC后台监听服务(项目启动自动执行)
|
||||
//builder.Services.AddHostedService<PlcHostedService>();
|
||||
// ==========================
|
||||
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lazy.Captcha.Core" Version="2.0.9" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||
<PackageReference Include="S7netplus" Version="0.20.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.12" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="6.0.4" />
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
"dbConfigs": [
|
||||
{
|
||||
//"Conn": "Data Source=139.224.232.211;User ID=root;Password=RIZOtech123;Initial Catalog=ay2509055-guiyang-fluorescence-lmes;Port=3308;",
|
||||
"Conn": "Data Source=43.142.238.124;User ID=root;Password=mysql_3AMPxs;Initial Catalog=valeo_lmes;Port=3308;",
|
||||
//"Conn": "Data Source=43.142.238.124;User ID=root;Password=mysql_3AMPxs;Initial Catalog=valeo_lmes;Port=3308;",
|
||||
//"Conn": "Data Source=127.0.0.1;User ID=root;Password=123456;Initial Catalog=valeo_lmes;Port=3306;",
|
||||
"Conn": "Data Source=192.168.1.48;User ID=root;Password=123456;Initial Catalog=valeo_lmes;Port=3306;",
|
||||
//"Conn": "Data Source=127.0.0.1,1433;Initial Catalog=valeo_lmes;User ID=root;Password=123456;TrustServerCertificate=True;Encrypt=False;Connection Timeout=60;",
|
||||
"DbType": 0, //数据库类型 MySql = 0, SqlServer = 1, Oracle = 3,PgSql = 4
|
||||
"ConfigId": "0", //多租户唯一标识
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
},
|
||||
"MainDb": "0", // 多租户主库配置ID
|
||||
"UseTenant": 0, //是否启用多租户 0:不启用 1:启用
|
||||
"InjectClass": [ "RIZO.Repository", "RIZO.Service", "RIZO.Tasks", "RIZO.ServiceCore", "RIZO.Mall" ,"MDM"], //自动注入类
|
||||
"InjectClass": [ "RIZO.Repository", "RIZO.Service", "RIZO.Tasks", "RIZO.ServiceCore", "RIZO.Mall", "MDM" ], //自动注入类
|
||||
"ShowDbLog": false, //是否打印db日志
|
||||
"InitDb": true, //是否初始化db
|
||||
"DemoMode": false, //是否演示模式
|
||||
@ -94,5 +94,21 @@
|
||||
"tablePrefix": "sys_", //"表前缀(生成类名不会包含表前缀,多个用逗号分隔)",
|
||||
"vuePath": "", //前端代码存储路径eg:D:\Work\RIZOAdmin-Vue3
|
||||
"uniappPath": "D:\\Work" //h5前端代码存储路径
|
||||
},
|
||||
"PlcConfigs": [
|
||||
{
|
||||
"PlcName": "PLC-1号产线", // 自定义名称,便于识别
|
||||
"Ip": "192.168.1.111",
|
||||
"Rack": 0,
|
||||
"Slot": 1,
|
||||
"TestReadAddress": "DB1010.DBD0", // 测试读取的地址
|
||||
"TestWriteAddress": "DB1010.DBD0", // 测试写入的地址
|
||||
"TestWriteValue": "100.5" // 测试写入的值(字符串,自动转换类型)
|
||||
}
|
||||
],
|
||||
"GlobalConfig": {
|
||||
"ConnectTimeout": 5000, // 连接超时(毫秒)
|
||||
"ReadWriteTimeout": 5000 // 读写超时(毫秒)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MiniExcel" Version="1.41.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="S7netplus" Version="0.20.0" />
|
||||
<PackageReference Include="SqlSugarCoreNoDrive" Version="5.1.4.207" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NPOI" Version="2.7.5" />
|
||||
<PackageReference Include="S7netplus" Version="0.20.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user