diff --git a/MDM/Controllers/Material/MaterialBomController.cs b/MDM/Controllers/Material/MaterialBomController.cs
index 19d4d0b..a4c251f 100644
--- a/MDM/Controllers/Material/MaterialBomController.cs
+++ b/MDM/Controllers/Material/MaterialBomController.cs
@@ -25,7 +25,7 @@ namespace MDM.Controllers.Material
///
/// BOM清单
///
- [Verify]
+ [AllowAnonymous]
[Route("MasterDataManagement/Material/MaterialBom")]
public class MaterialBomController : BaseController
{
diff --git a/MDM/Controllers/Plant/PlantWorkstationController.cs b/MDM/Controllers/Plant/PlantWorkstationController.cs
index c1d55e1..0518650 100644
--- a/MDM/Controllers/Plant/PlantWorkstationController.cs
+++ b/MDM/Controllers/Plant/PlantWorkstationController.cs
@@ -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
{
///
/// 工站/资源组
///
- [Verify]
+ [AllowAnonymous]
[Route("MasterDataManagement/Plant/PlantWorkstation")]
public class PlantWorkstationController : BaseController
{
diff --git a/MDM/MDM.csproj b/MDM/MDM.csproj
index 4aa2f65..784add9 100644
--- a/MDM/MDM.csproj
+++ b/MDM/MDM.csproj
@@ -30,6 +30,7 @@
+
diff --git a/MDM/Models/Plant/Dto/PlantWorkstationDto.cs b/MDM/Models/Plant/Dto/PlantWorkstationDto.cs
index d4d4452..f9fd9a1 100644
--- a/MDM/Models/Plant/Dto/PlantWorkstationDto.cs
+++ b/MDM/Models/Plant/Dto/PlantWorkstationDto.cs
@@ -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; }
diff --git a/MDM/Models/Plant/PlantWorkstation.cs b/MDM/Models/Plant/PlantWorkstation.cs
index 30d492e..173b0a6 100644
--- a/MDM/Models/Plant/PlantWorkstation.cs
+++ b/MDM/Models/Plant/PlantWorkstation.cs
@@ -55,8 +55,8 @@ namespace MDM.Model.Plant
///
///PLC地址
///
- [SugarColumn(ColumnName = "plc_address")]
- public string PlcAddress { get; set; }
+ [SugarColumn(ColumnName = "plc_ip")]
+ public string PlcIP { get; set; }
diff --git a/MDM/Services/Process/ProcessOperationWorkstationMappingService.cs b/MDM/Services/Process/ProcessOperationWorkstationMappingService.cs
index 150c6ee..e340bf2 100644
--- a/MDM/Services/Process/ProcessOperationWorkstationMappingService.cs
+++ b/MDM/Services/Process/ProcessOperationWorkstationMappingService.cs
@@ -71,7 +71,7 @@ namespace MDM.Services.Process
//绑定的工位PLC地址
- PlcAddress = SqlFunc.Subqueryable().Where(s => s.FkProductlinebody == it.FkProductlinebodyCode && s.WorkstationCode == it.FkWorkstationCode).Select(s=>s.PlcAddress),
+ PlcAddress = SqlFunc.Subqueryable().Where(s => s.FkProductlinebody == it.FkProductlinebodyCode && s.WorkstationCode == it.FkWorkstationCode).Select(s=>s.PlcIP),
//绑定的流程信息
OperationFlows = SqlFunc.Subqueryable().Where(s => s.FkRoutingCode == it.FkRoutingCode && s.FkOperationCode == it.FkOperationCode).ToList(),
diff --git a/RIZO.Admin.WebApi/PLC/PlcConfig.cs b/RIZO.Admin.WebApi/PLC/PlcConfig.cs
new file mode 100644
index 0000000..b6c6e75
--- /dev/null
+++ b/RIZO.Admin.WebApi/PLC/PlcConfig.cs
@@ -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; }
+ }
+
+}
diff --git a/RIZO.Admin.WebApi/PLC/PlcController.cs b/RIZO.Admin.WebApi/PLC/PlcController.cs
new file mode 100644
index 0000000..5ab993a
--- /dev/null
+++ b/RIZO.Admin.WebApi/PLC/PlcController.cs
@@ -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
+{
+ ///
+ /// PLC通讯测试接口
+ ///
+ [AllowAnonymous]
+ [Route("PLC")]
+ public class PlcController : BaseController
+ {
+ ///
+ /// PLC通讯服务
+ ///
+ private readonly PlcService _plcService;
+
+ ///
+ /// 构造函数注入PLC服务
+ ///
+ /// PLC通讯服务实例
+ public PlcController(PlcService plcService)
+ {
+ _plcService = plcService;
+ }
+
+ ///
+ /// 批量测试所有PLC的连接、读写功能(核心接口)
+ ///
+ /// 批量测试结果
+ [HttpPost("batch-test")]
+ [ActionPermissionFilter(Permission = "business:plc:batchtest")]
+ [Log(Title = "PLC测试", BusinessType = BusinessType.OTHER)]
+ public async Task BatchTestAllPlc()
+ {
+ try
+ {
+ var results = await _plcService.BatchTestAllPlcAsync();
+ return SUCCESS(results);
+ }
+ catch (Exception ex)
+ {
+ return ToResponse(ApiResult.Error($"批量测试失败:{ex.Message}"));
+ }
+ }
+
+ ///
+ /// 测试单个PLC的连接、读写功能(调试用)
+ ///
+ /// 单个PLC配置参数
+ /// 单PLC测试结果
+ [HttpPost("single-test")]
+ [ActionPermissionFilter(Permission = "business:plc:singletest")]
+ [Log(Title = "PLC测试", BusinessType = BusinessType.OTHER)]
+ public async Task 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}"));
+ }
+ }
+
+ ///
+ /// 单独读取指定PLC的地址数据
+ ///
+ /// PLC IP地址
+ /// 读取地址(如DB1.DBD0)
+ /// 机架号(默认0)
+ /// 槽位号(默认1)
+ /// 读取结果
+ [HttpGet("read")]
+ [ActionPermissionFilter(Permission = "business:plc:read")]
+ public async Task 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}"));
+ }
+ }
+
+ ///
+ /// 单独写入数据到指定PLC地址
+ ///
+ /// PLC IP地址
+ /// 写入地址(如DB1.DBD0)
+ /// 写入值(字符串格式)
+ /// 机架号(默认0)
+ /// 槽位号(默认1)
+ /// 写入结果
+ [HttpPost("write")]
+ [ActionPermissionFilter(Permission = "business:plc:write")]
+ [Log(Title = "PLC数据写入", BusinessType = BusinessType.UPDATE)]
+ public async Task 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}"));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/RIZO.Admin.WebApi/PLC/PlcHostedService.cs b/RIZO.Admin.WebApi/PLC/PlcHostedService.cs
new file mode 100644
index 0000000..7b46016
--- /dev/null
+++ b/RIZO.Admin.WebApi/PLC/PlcHostedService.cs
@@ -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 _logger;
+ private readonly PlcService _plcService;
+ private readonly List _plcConfigs;
+ private Timer _timer;
+ private bool _isRunning;
+ private readonly SemaphoreSlim _semaphore;
+ private readonly object _timerLock = new();
+ // 连接状态缓存:减少短时间内重复连接测试
+ private readonly ConcurrentDictionary _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; // 连接状态缓存有效期
+
+ ///
+ /// PLC 连接状态缓存对象
+ ///
+ private class PlcConnectionState
+ {
+ public bool IsConnected { get; set; }
+ public DateTime LastCheckTime { get; set; }
+ }
+
+ public PlcHostedService(
+ ILogger logger,
+ PlcService plcService,
+ IOptions> plcConfigs)
+ {
+ _logger = logger;
+ _plcService = plcService;
+ _plcConfigs = plcConfigs.Value ?? new List();
+
+ // 初始化并行控制信号量
+ _semaphore = new SemaphoreSlim(_parallelDegree, _parallelDegree);
+ // 初始化连接状态缓存
+ _connectionStateCache = new ConcurrentDictionary();
+ 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}");
+ }
+
+ ///
+ /// 定时器安全回调(防重叠执行)
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 启动时批量并行连接PLC
+ ///
+ 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);
+ }
+
+ ///
+ /// 并行轮询PLC数据(读取前先验证连接状态)
+ ///
+ 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后台监听服务已释放资源");
+ }
+ }
+}
\ No newline at end of file
diff --git a/RIZO.Admin.WebApi/PLC/PlcService.cs b/RIZO.Admin.WebApi/PLC/PlcService.cs
new file mode 100644
index 0000000..70f424d
--- /dev/null
+++ b/RIZO.Admin.WebApi/PLC/PlcService.cs
@@ -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 _plcConfigs;
+ private readonly GlobalPlcConfig _globalConfig;
+
+ ///
+ /// 构造函数(依赖注入获取PLC配置)
+ ///
+ /// 所有PLC的连接配置
+ /// PLC全局超时配置
+ public PlcService(IOptions> plcConfigs, IOptions globalConfig)
+ {
+ _plcConfigs = plcConfigs.Value;
+ _globalConfig = globalConfig.Value;
+ }
+
+ ///
+ /// 测试单个PLC的连接、读、写功能
+ ///
+ /// 单个PLC的配置参数
+ /// 测试结果
+ public async Task 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;
+ }
+
+ ///
+ /// 批量测试配置文件中所有PLC的读写功能
+ ///
+ /// 所有PLC的测试结果列表
+ /// 未配置PLC参数时抛出
+ public async Task> 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();
+ }
+
+ ///
+ /// 单独读取指定PLC的某个地址数据
+ ///
+ /// PLC的IP地址
+ /// 机架号
+ /// 槽位号
+ /// 读取地址(如DB1.DBD0)
+ /// 读取结果(成功状态、值、消息)
+ 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();
+ }
+ }
+ }
+
+ ///
+ /// 单独写入数据到指定PLC的某个地址
+ ///
+ /// PLC的IP地址
+ /// 机架号
+ /// 槽位号
+ /// 写入地址(如DB1.DBD0)
+ /// 写入的值(字符串格式)
+ /// 写入结果(成功状态、消息)
+ 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();
+ }
+ }
+ }
+
+ ///
+ /// 格式化PLC读取的值,转为友好的字符串
+ ///
+ /// PLC读取的原始值
+ /// 格式化后的字符串
+ 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() // 其他类型直接转字符串
+ };
+ }
+
+ ///
+ /// 根据地址类型转换值并写入PLC
+ ///
+ /// PLC连接对象
+ /// 写入地址
+ /// 写入的值(字符串)
+ /// 是否写入成功
+ 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;
+ }
+
+ ///
+ /// 带超时的PLC连接测试
+ ///
+ public async Task 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 = "连接正常" };
+ }
+
+ ///
+ /// 读取PLC数据
+ ///
+ 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"), "读取成功");
+ }
+
+
+ ///
+ /// 实现IDisposable接口,释放资源
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this); // 通知GC无需调用终结器
+ }
+
+ ///
+ /// 实际释放资源的逻辑(区分托管/非托管资源)
+ ///
+ /// 是否释放托管资源
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return; // 避免重复释放
+
+ // 释放托管资源(此处无长期持有的PLC连接,若有可在此处理)
+ if (disposing)
+ {
+ // 示例:若有全局PLC连接对象,可在此释放
+ // if (_globalPlc != null) _globalPlc.Dispose();
+ }
+
+ // 标记为已释放
+ _disposed = true;
+ }
+
+ ///
+ /// 终结器(防忘记手动Dispose)
+ ///
+ ~PlcService()
+ {
+ Dispose(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/RIZO.Admin.WebApi/Program.cs b/RIZO.Admin.WebApi/Program.cs
index 0a0c8fb..2323e4f 100644
--- a/RIZO.Admin.WebApi/Program.cs
+++ b/RIZO.Admin.WebApi/Program.cs
@@ -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>(builder.Configuration.GetSection("PlcConfigs"));
+builder.Services.Configure(builder.Configuration.GetSection("GlobalPlcConfig"));
+builder.Services.AddSingleton();
+// 新增:注册PLC后台监听服务(项目启动自动执行)
+//builder.Services.AddHostedService();
+// ==========================
+
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
diff --git a/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj b/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj
index 35138f6..cefc6c9 100644
--- a/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj
+++ b/RIZO.Admin.WebApi/RIZO.Admin.WebApi.csproj
@@ -27,6 +27,7 @@
+
diff --git a/RIZO.Admin.WebApi/appsettings.Development.json b/RIZO.Admin.WebApi/appsettings.Development.json
index 9847591..b8f36df 100644
--- a/RIZO.Admin.WebApi/appsettings.Development.json
+++ b/RIZO.Admin.WebApi/appsettings.Development.json
@@ -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", //多租户唯一标识
diff --git a/RIZO.Admin.WebApi/appsettings.json b/RIZO.Admin.WebApi/appsettings.json
index f8055de..6b006e5 100644
--- a/RIZO.Admin.WebApi/appsettings.json
+++ b/RIZO.Admin.WebApi/appsettings.json
@@ -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 // 读写超时(毫秒)
}
+
}
diff --git a/RIZO.Model/RIZO.Model.csproj b/RIZO.Model/RIZO.Model.csproj
index 4b1c26b..8dae0f3 100644
--- a/RIZO.Model/RIZO.Model.csproj
+++ b/RIZO.Model/RIZO.Model.csproj
@@ -9,6 +9,7 @@
+
diff --git a/RIZO.Service/RIZO.Service.csproj b/RIZO.Service/RIZO.Service.csproj
index 07e491f..41a52b3 100644
--- a/RIZO.Service/RIZO.Service.csproj
+++ b/RIZO.Service/RIZO.Service.csproj
@@ -13,6 +13,7 @@
+