PLC通讯

This commit is contained in:
quowingwang 2026-01-17 10:42:02 +08:00
parent 0fad00a363
commit 6db3bb3924
16 changed files with 861 additions and 9 deletions

View File

@ -25,7 +25,7 @@ namespace MDM.Controllers.Material
/// <summary>
/// BOM清单
/// </summary>
[Verify]
[AllowAnonymous]
[Route("MasterDataManagement/Material/MaterialBom")]
public class MaterialBomController : BaseController
{

View File

@ -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
{

View File

@ -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>

View File

@ -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; }

View File

@ -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; }

View File

@ -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(),

View 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; }
}
}

View 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}"));
}
}
}
}

View 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后台监听服务已释放资源");
}
}
}

View 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/DW16位整数
else if (address.Contains("DBW") || address.Contains("DW"))
{
if (short.TryParse(valueStr, out short s))
{
plc.Write(address, s);
return true;
}
}
// 字节DBB/DB8位整数
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);
}
}
}

View File

@ -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();

View File

@ -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" />

View File

@ -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 = 3PgSql = 4
"ConfigId": "0", //

View File

@ -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": "", //egD:\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 //
}
}

View File

@ -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>

View File

@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NPOI" Version="2.7.5" />
<PackageReference Include="S7netplus" Version="0.20.0" />
</ItemGroup>
</Project>