首次提交:本地项目同步到Gitea
This commit is contained in:
185
Plugins/Driver/Cowain.Driver/Abstractions/DriverBase.cs
Normal file
185
Plugins/Driver/Cowain.Driver/Abstractions/DriverBase.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.Models;
|
||||
using HslCommunication.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System.Diagnostics;
|
||||
using static NpgsqlTypes.NpgsqlTsQuery;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Abstractions;
|
||||
|
||||
public abstract class DriverBase : IDriver
|
||||
{
|
||||
private readonly ILogger<DriverBase> _logger;
|
||||
public DriverBase()
|
||||
{
|
||||
_logger = ServiceLocator.GetRequiredService<ILogger<DriverBase>>();
|
||||
}
|
||||
|
||||
public abstract string DeviceName { get; set; }
|
||||
|
||||
public bool IsConnected { get; protected set; }
|
||||
|
||||
public DeviceViewModel? DeviceModel { get; private set; }
|
||||
|
||||
public abstract bool Close();
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract IReadWriteDevice? GetReadWrite();
|
||||
|
||||
public abstract Task<bool> OpenAsync();
|
||||
//public abstract Task ReadThreadAsync(DeviceViewModel device, CancellationToken token);
|
||||
|
||||
public virtual async Task ReadThreadAsync(DeviceViewModel device, CancellationToken token)
|
||||
{
|
||||
Stopwatch sw = new Stopwatch();
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
device.IsConnected = IsConnected;
|
||||
if (!IsConnected)
|
||||
{
|
||||
if (device.Variables != null)
|
||||
{
|
||||
foreach (var item in device.Variables)
|
||||
{
|
||||
item.IsSuccess = false;
|
||||
item.Message = "更新失败";
|
||||
}
|
||||
}
|
||||
var connectTask = OpenAsync();
|
||||
var timeouttask = Task.Delay(5000, token);
|
||||
var completedTask = await Task.WhenAny(connectTask, timeouttask);
|
||||
if (completedTask == timeouttask)
|
||||
{
|
||||
// 情况1:超时→连接失败,取消Connect任务
|
||||
_logger.LogError($"ReadThreadAsync重连失败:{device.DeviceName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await connectTask; // 确保连接任务完成
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sw.Restart();
|
||||
IsConnected = await ReadVariablesAsync(device, token);
|
||||
await Task.Delay(device.MinPeriod, token);
|
||||
sw.Stop();
|
||||
device.ReadUseTime = (int)sw.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
IsConnected = false;
|
||||
_logger.LogInformation($"ReadThreadAsync任务异常:{device.DeviceName}");
|
||||
if (device.Variables != null)
|
||||
{
|
||||
foreach (var item in device.Variables)
|
||||
{
|
||||
item.IsSuccess = false;
|
||||
item.Message = "更新失败";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsConnected = false;
|
||||
_logger.LogError(ex, $"ReadThreadAsync任务异常:{device.DeviceName}");
|
||||
if (device.Variables != null)
|
||||
{
|
||||
foreach (var item in device.Variables)
|
||||
{
|
||||
item.IsSuccess = false;
|
||||
item.Message = "更新失败";
|
||||
}
|
||||
}
|
||||
}
|
||||
await Task.Delay(device.MinPeriod, token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public abstract Task<bool> ReadVariablesAsync(DeviceViewModel device, CancellationToken token);
|
||||
|
||||
public abstract void SetParam(string param);
|
||||
|
||||
public async Task<ResultModel<T>> ReadAsync<T>(string address, DataTypeEnum dataType, ushort arrayCount, int retryCount = 5) where T : notnull
|
||||
{
|
||||
int baseDelay = 100; // 基础延迟100ms
|
||||
var random = new Random();
|
||||
ResultModel<T>? result = null;
|
||||
for (int retry = 0; retry < retryCount; retry++)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = await ReadAsync<T>(address, dataType, arrayCount);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError($"读PLC地址:{address} 数据失败:{result.ErrorMessage},第{retry + 1}次尝试");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"读PLC地址:{address} 数据失败:{ex.Message},第{retry + 1}次尝试");
|
||||
result = ResultModel<T>.Error(ex.Message);
|
||||
}
|
||||
// 计算指数退避+抖动
|
||||
int jitter = random.Next(0, 100); // 0~100ms
|
||||
int delay = baseDelay * (int)Math.Pow(2, retry) + jitter;
|
||||
await Task.Delay(delay);
|
||||
}
|
||||
return result ?? ResultModel<T>.Error("未知错误");
|
||||
}
|
||||
public abstract Task<ResultModel<T>> ReadAsync<T>(string address, DataTypeEnum dataType, ushort arrayCount) where T : notnull;
|
||||
|
||||
|
||||
|
||||
public async Task<ResultModel> WriteAsync<T>(string address, DataTypeEnum dataType, T value, int retryCount = 5) where T : struct
|
||||
{
|
||||
int baseDelay = 100; // 基础延迟100ms
|
||||
var random = new Random();
|
||||
ResultModel? result = null;
|
||||
for (int retry = 0; retry < retryCount; retry++)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = await WriteAsync<T>(address, dataType, value);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError($"写PLC地址:{address} 数据失败:{result.ErrorMessage},第{retry + 1}次尝试");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"写PLC地址:{address} 数据失败:{ex.Message},第{retry + 1}次尝试");
|
||||
result = ResultModel.Error(ex.Message);
|
||||
}
|
||||
// 计算指数退避+抖动
|
||||
int jitter = random.Next(0, 100); // 0~100ms
|
||||
int delay = baseDelay * (int)Math.Pow(2, retry) + jitter;
|
||||
await Task.Delay(delay);
|
||||
}
|
||||
return result ?? ResultModel.Error("未知错误");
|
||||
}
|
||||
|
||||
public abstract Task<ResultModel> WriteAsync<T>(string address, DataTypeEnum dataType, T value) where T : struct;
|
||||
|
||||
public void SetDeviceModel(DeviceViewModel device)
|
||||
{
|
||||
DeviceModel = device;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Abstractions;
|
||||
|
||||
public interface IActionCondition
|
||||
{
|
||||
bool IsMatch(VariableViewModel variable, string actionValue);
|
||||
}
|
||||
19
Plugins/Driver/Cowain.Driver/Abstractions/IDeviceMonitor.cs
Normal file
19
Plugins/Driver/Cowain.Driver/Abstractions/IDeviceMonitor.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Abstractions;
|
||||
|
||||
public interface IDeviceMonitor
|
||||
{
|
||||
List<DeviceThread> DeviceThreads { get; }
|
||||
List<DeviceViewModel> Devices { get; }
|
||||
List<AlarmViewModel> Alarms { get; }
|
||||
List<DeviceViewModel> GetDeviceViewModels();
|
||||
void AddDevice(IDriver driver, IActionPluginService actionPluginService, DeviceViewModel device);
|
||||
ResultModel<IDriver> GetDriver(string name);
|
||||
ResultModel<DeviceViewModel> GetDevice(string name);
|
||||
ResultModel<VariableViewModel> GetVariable(string plc, string address);
|
||||
ResultModel<string> GetActionParam(string plc, string address);
|
||||
}
|
||||
30
Plugins/Driver/Cowain.Driver/Abstractions/IDriver.cs
Normal file
30
Plugins/Driver/Cowain.Driver/Abstractions/IDriver.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.Models;
|
||||
using HslCommunication.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Abstractions;
|
||||
|
||||
public interface IDriver : IDisposable
|
||||
{
|
||||
public string DeviceName { get; set; }
|
||||
public bool IsConnected { get; }
|
||||
|
||||
public IReadWriteDevice? GetReadWrite();
|
||||
public Task<bool> OpenAsync();
|
||||
public bool Close();
|
||||
|
||||
public Task ReadThreadAsync(DeviceViewModel device, CancellationToken token);
|
||||
|
||||
public void SetParam(string param);
|
||||
|
||||
public void SetDeviceModel(DeviceViewModel device);
|
||||
|
||||
public Task<ResultModel<T>> ReadAsync<T>(string address, DataTypeEnum dataType, ushort arrayCount) where T : notnull;
|
||||
|
||||
public Task<ResultModel> WriteAsync<T>(string address, DataTypeEnum dataType, T value) where T : struct;
|
||||
|
||||
|
||||
}
|
||||
16
Plugins/Driver/Cowain.Driver/Abstractions/IVariableAction.cs
Normal file
16
Plugins/Driver/Cowain.Driver/Abstractions/IVariableAction.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Org.BouncyCastle.Asn1.Pkcs;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Abstractions;
|
||||
|
||||
public interface IVariableAction
|
||||
{
|
||||
Task<ResultModel> ExecuteAsync(VariableAction variableAction, CancellationToken cancellationToken);
|
||||
}
|
||||
25
Plugins/Driver/Cowain.Driver/Actions/TestAction.cs
Normal file
25
Plugins/Driver/Cowain.Driver/Actions/TestAction.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.Attributes;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Actions;
|
||||
|
||||
|
||||
[Action("Test", "测试事件")]
|
||||
public class TestAction : IVariableAction
|
||||
{
|
||||
private readonly ILogger<TestAction> _logger;
|
||||
public TestAction(ILogger<TestAction> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<ResultModel> ExecuteAsync(VariableAction variableAction, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"执行测试事件:{variableAction.Variable.Name}-{variableAction.Variable.Address},参数:{variableAction.Param},旧值:{variableAction.Variable.OldValue},新值:{variableAction.Variable.Value}");
|
||||
return Task.FromResult(ResultModel.Success());
|
||||
}
|
||||
}
|
||||
429
Plugins/Driver/Cowain.Driver/AlarmHostedService.cs
Normal file
429
Plugins/Driver/Cowain.Driver/AlarmHostedService.cs
Normal file
@@ -0,0 +1,429 @@
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plugin.Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks.Dataflow;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
public class AlarmHostedService : BackgroundHostedService
|
||||
{
|
||||
private readonly IMessenger _messenger; // MVVM工具包的消息器
|
||||
private readonly IAlarmService _alarmService;
|
||||
private readonly ILogger<AlarmHostedService> _logger;
|
||||
private IDeviceMonitor _deviceMonitor;
|
||||
private readonly IAlarmGroupService _alarmGroupService;
|
||||
private readonly IAlarmLevelService _alarmLevelService;
|
||||
// 缓存系统报警组ID和错误报警级别ID(避免重复查询数据库)
|
||||
private int _systemAlarmGroupId = 1;
|
||||
private int _errorAlarmLevelId = 1;
|
||||
|
||||
private List<AlarmGroupViewModel>? alarmGroups;
|
||||
private List<AlarmLevelViewModel>? alarmLevels;
|
||||
|
||||
private readonly SemaphoreSlim _alarmsSemaphore = new(1, 1);
|
||||
private readonly SemaphoreSlim _cacheSemaphore = new(1, 1); // 保护缓存初始化的信号量
|
||||
|
||||
public AlarmHostedService(IDeviceMonitor deviceMonitor, IAlarmService alarmService, IAlarmGroupService alarmGroupService, IMessenger messenger,
|
||||
IAlarmLevelService alarmLevelService, ILogger<AlarmHostedService> logger)
|
||||
{
|
||||
_deviceMonitor = deviceMonitor;
|
||||
_alarmService = alarmService;
|
||||
_alarmGroupService = alarmGroupService;
|
||||
_alarmLevelService = alarmLevelService;
|
||||
_messenger = messenger;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化报警配置缓存(查询"系统"组ID和"错误"级别ID)
|
||||
/// </summary>
|
||||
private async Task InitAlarmConfigCacheAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 新增:处理初始化时的取消异常
|
||||
if (cancellationToken.IsCancellationRequested) return;
|
||||
|
||||
await _cacheSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// 查询名称为"系统"的报警组ID
|
||||
alarmGroups = await _alarmGroupService.GetAllAsync().ConfigureAwait(false);
|
||||
var systemGroup = alarmGroups.FirstOrDefault(g => string.Equals(g.Name, "系统", StringComparison.Ordinal));
|
||||
if (systemGroup != null)
|
||||
{
|
||||
_systemAlarmGroupId = systemGroup.Id;
|
||||
_logger.LogInformation($"成功获取「系统」报警组ID:{_systemAlarmGroupId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("未查询到名称为「系统」的报警组,将使用默认ID:1");
|
||||
}
|
||||
|
||||
// 查询名称为"错误"的报警级别ID
|
||||
alarmLevels = await _alarmLevelService.GetAllAsync().ConfigureAwait(false);
|
||||
var errorLevel = alarmLevels.FirstOrDefault(l => string.Equals(l.Name, "错误", StringComparison.Ordinal));
|
||||
if (errorLevel != null)
|
||||
{
|
||||
_errorAlarmLevelId = errorLevel.Id;
|
||||
_logger.LogInformation($"成功获取「错误」报警级别ID:{_errorAlarmLevelId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("未查询到名称为「错误」的报警级别,将使用默认ID:1");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消,仅日志记录,不抛异常
|
||||
_logger.LogInformation("报警配置缓存初始化被取消");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "初始化报警配置缓存失败,将使用默认ID:1");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 新增:确保信号量释放(即使取消也释放)
|
||||
if (_cacheSemaphore.CurrentCount == 0)
|
||||
{
|
||||
_cacheSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[LogAndSwallow]
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var t = Task.Factory.StartNew(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 初始化报警配置缓存(仅执行一次)
|
||||
await InitAlarmConfigCacheAsync(stoppingToken).ConfigureAwait(false);
|
||||
// 延时5秒,等待PLC连接完成
|
||||
await Task.Delay(5000, stoppingToken).ConfigureAwait(false);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try //内层try捕获单次循环的取消异常
|
||||
{
|
||||
// 并行遍历设备集合
|
||||
// 修改1:内层用ct而非stoppingToken,避免令牌混用
|
||||
await Parallel.ForEachAsync(_deviceMonitor.Devices, stoppingToken, async (dev, ct) =>
|
||||
{
|
||||
// 检查内层令牌是否取消
|
||||
if (ct.IsCancellationRequested) return;
|
||||
|
||||
if (!dev.IsConnected)
|
||||
{
|
||||
// 通信故障发生 - 使用查询到的Group和LevelID(替代硬编码的1)
|
||||
var newAlarm = new AlarmViewModel
|
||||
{
|
||||
TagId = 100000 + dev.Id,
|
||||
StartTime = DateTime.Now,
|
||||
Status = true,
|
||||
Group = _systemAlarmGroupId, // 从缓存获取"系统"组ID
|
||||
Level = _errorAlarmLevelId, // 从缓存获取"错误"级别ID
|
||||
GroupName = alarmGroups?.FirstOrDefault(g => g.Id == _systemAlarmGroupId)?.Name ?? "系统",
|
||||
LevelName = alarmLevels?.FirstOrDefault(l => l.Id == _errorAlarmLevelId)?.Name ?? "错误",
|
||||
Color = alarmLevels?.FirstOrDefault(l => l.Id == _errorAlarmLevelId)?.Color ?? "Red",
|
||||
Desc = $"{dev.DeviceName}:设备通信故障",
|
||||
};
|
||||
// 修改2:传递内层ct而非stoppingToken
|
||||
await AddAlarmAsync(newAlarm, ct).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 通信恢复,清除通信报警
|
||||
var clearAlarm = new AlarmViewModel
|
||||
{
|
||||
TagId = 100000 + dev.Id,
|
||||
};
|
||||
// 修改2:传递内层ct而非stoppingToken
|
||||
await RemoveAlarmAsync(clearAlarm, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 快照变量列表并筛选启用报警的变量,避免其他线程修改集合
|
||||
var alarmVariables = (dev.Variables ?? Enumerable.Empty<VariableViewModel>()).Where(v => v.AlarmEnable).ToList();
|
||||
|
||||
// 并行处理每个变量的报警逻辑
|
||||
// 修改3:内层Parallel用ctInner,传递ctInner到异步方法
|
||||
await Parallel.ForEachAsync(alarmVariables, ct, async (item, ctInner) =>
|
||||
{
|
||||
if (ctInner.IsCancellationRequested) return;
|
||||
|
||||
if (!item.IsSuccess || string.IsNullOrEmpty(item.Value))
|
||||
{
|
||||
var clearAlarm = new AlarmViewModel
|
||||
{
|
||||
TagId = item.Id,
|
||||
};
|
||||
await RemoveAlarmAsync(clearAlarm, ctInner).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.Value == item.AlarmValue)
|
||||
{
|
||||
var newAlarm = new AlarmViewModel
|
||||
{
|
||||
TagId = item.Id,
|
||||
StartTime = DateTime.Now,
|
||||
Status = true,
|
||||
Group = item.AlarmGroup,
|
||||
Level = item.AlarmLevel,
|
||||
GroupName = alarmGroups?.FirstOrDefault(g => g.Id == item.AlarmGroup)?.Name ?? string.Empty,
|
||||
LevelName = alarmLevels?.FirstOrDefault(l => l.Id == item.AlarmLevel)?.Name ?? string.Empty,
|
||||
Color = alarmLevels?.FirstOrDefault(l => l.Id == item.AlarmLevel)?.Color ?? "Red",
|
||||
Desc = $"{item.Address}-{item.AlarmMsg}",
|
||||
};
|
||||
await AddAlarmAsync(newAlarm, ctInner).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var clearAlarm = new AlarmViewModel
|
||||
{
|
||||
TagId = item.Id,
|
||||
};
|
||||
await RemoveAlarmAsync(clearAlarm, ctInner).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
// 检查取消状态,避免无效操作
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
var alarms = await GetAlarmsAsync(stoppingToken).ConfigureAwait(false);
|
||||
_messenger.Send(new AlarmChangedMessage(alarms));
|
||||
|
||||
// 修改4:Task.Delay添加try-catch,处理取消异常
|
||||
try
|
||||
{
|
||||
await Task.Delay(500, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Delay被取消,跳出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 单次循环被取消,跳出外层循环
|
||||
_logger.LogInformation("报警服务循环执行被取消");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 非取消异常,记录日志并继续循环
|
||||
_logger.LogError(ex, "报警服务单次循环执行异常,将继续下一次循环");
|
||||
await Task.Delay(500, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常关闭时的取消异常,仅记录日志,不抛异常
|
||||
_logger.LogInformation("AlarmHostedService 执行线程已正常取消");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "AlarmHostedService 执行线程发生未预期异常");
|
||||
}
|
||||
|
||||
}, stoppingToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
|
||||
return t;
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("AlarmHostedService StopAsync 开始执行");
|
||||
|
||||
try
|
||||
{
|
||||
if (ExecuteTask != null && !ExecuteTask.IsCompleted)
|
||||
{
|
||||
// 修改5:优化取消令牌逻辑,避免重复取消
|
||||
using var stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
try
|
||||
{
|
||||
// 正确使用异步操作并传递取消令牌
|
||||
var stopTasks = _deviceMonitor.DeviceThreads
|
||||
.Select(hostTask => hostTask.StopReadAsync(stoppingCts.Token))
|
||||
.ToList();
|
||||
|
||||
// 新增:超时保护,避免等待过久
|
||||
await Task.WhenAll(stopTasks).WaitAsync(TimeSpan.FromSeconds(5), stoppingCts.Token).ConfigureAwait(false);
|
||||
_logger.LogInformation("AlarmHostedService 所有设备线程已停止");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_logger.LogInformation("设备线程停止操作被取消");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.LogWarning("设备线程停止操作超时");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 修改6:优雅等待执行任务结束,SuppressThrowing避免抛出取消异常
|
||||
await ExecuteTask.WaitAsync(stoppingCts.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略等待时的所有异常
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "AlarmHostedService StopAsync 执行异常");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("AlarmHostedService StopAsync 执行完成");
|
||||
// 确保调用基类StopAsync,释放HostedService资源
|
||||
await base.StopAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加故障
|
||||
/// </summary>
|
||||
public async Task AddAlarmAsync(AlarmViewModel alarm, CancellationToken cancellationToken)
|
||||
{
|
||||
// 新增:检查取消状态,避免无效操作
|
||||
if (cancellationToken.IsCancellationRequested) return;
|
||||
|
||||
await _alarmsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var exist = _deviceMonitor.Alarms.FirstOrDefault(a => a.TagId == alarm.TagId);
|
||||
if (exist == null)
|
||||
{
|
||||
_deviceMonitor.Alarms.Add(alarm);
|
||||
if (_alarmService != null)
|
||||
{
|
||||
var inAlarm = await _alarmService.GetInAlarmAsync(alarm.TagId).ConfigureAwait(false);
|
||||
if (inAlarm == null)
|
||||
{
|
||||
var addResult = await _alarmService.AddAsync(alarm).ConfigureAwait(false);
|
||||
if (!addResult.IsSuccess)
|
||||
{
|
||||
_logger.LogError($"添加历史报警异常:{JsonSerializer.Serialize(alarm)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("添加报警操作被取消");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"添加报警异常:{alarm.TagId}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 新增:确保信号量释放(即使取消/异常)
|
||||
if (_alarmsSemaphore.CurrentCount == 0)
|
||||
{
|
||||
_alarmsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除故障
|
||||
/// </summary>
|
||||
public async Task RemoveAlarmAsync(AlarmViewModel alarm, CancellationToken cancellationToken)
|
||||
{
|
||||
// 新增:检查取消状态,避免无效操作
|
||||
if (cancellationToken.IsCancellationRequested) return;
|
||||
|
||||
await _alarmsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var exist = _deviceMonitor.Alarms.FirstOrDefault(a => a.TagId == alarm.TagId);
|
||||
if (exist != null)
|
||||
{
|
||||
//实时报警列表中有此报警
|
||||
_deviceMonitor.Alarms.Remove(exist);
|
||||
if (_alarmService != null)
|
||||
{
|
||||
var inAlarm = await _alarmService.GetInAlarmAsync(alarm.TagId).ConfigureAwait(false);
|
||||
if (inAlarm != null)
|
||||
{
|
||||
//数据中有此报警,添加解除记录
|
||||
var removeResult = await _alarmService.CancelAsync(inAlarm).ConfigureAwait(false);
|
||||
if (!removeResult.IsSuccess)
|
||||
{
|
||||
_logger.LogError($"取消历史报警异常:{JsonSerializer.Serialize(alarm)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//实时报警列表中无此报警,还需要查询历史报警记录
|
||||
var inAlarm = await _alarmService.GetInAlarmAsync(alarm.TagId).ConfigureAwait(false);
|
||||
if (inAlarm != null)
|
||||
{
|
||||
//数据中有此报警,添加解除记录
|
||||
var removeResult = await _alarmService.CancelAsync(alarm).ConfigureAwait(false);
|
||||
if (!removeResult.IsSuccess)
|
||||
{
|
||||
_logger.LogError($"取消历史报警异常:{JsonSerializer.Serialize(alarm)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("移除报警操作被取消");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"移除报警异常:{alarm.TagId}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 新增:确保信号量释放(即使取消/异常)
|
||||
if (_alarmsSemaphore.CurrentCount == 0)
|
||||
{
|
||||
_alarmsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<AlarmViewModel>> GetAlarmsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 新增:检查取消状态,避免无效操作
|
||||
if (cancellationToken.IsCancellationRequested) return new List<AlarmViewModel>();
|
||||
|
||||
await _alarmsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
return [.. _deviceMonitor.Alarms];
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("获取报警列表操作被取消");
|
||||
return new List<AlarmViewModel>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 新增:确保信号量释放(即使取消/异常)
|
||||
if (_alarmsSemaphore.CurrentCount == 0)
|
||||
{
|
||||
_alarmsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Plugins/Driver/Cowain.Driver/Attributes/ActionAttribute.cs
Normal file
19
Plugins/Driver/Cowain.Driver/Attributes/ActionAttribute.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public class ActionAttribute : Attribute
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Desc { get; }
|
||||
public ActionAttribute(string name, string desc)
|
||||
{
|
||||
Name = name;
|
||||
Desc = desc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public class ConditionAttribute : Attribute
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Desc { get; }
|
||||
public ConditionAttribute(string name, string desc)
|
||||
{
|
||||
Name = name;
|
||||
Desc = desc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
|
||||
public class ConfigParameterAttribute : Attribute
|
||||
{
|
||||
public string Description { get; }
|
||||
public ConfigParameterAttribute(string description)
|
||||
{
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public class DeviceParamAttribute : Attribute
|
||||
{
|
||||
public Type Param { get; }
|
||||
public Type Control { get; }
|
||||
public Type DialogViewModel { get; }
|
||||
public DeviceParamAttribute(Type paramType, Type control, Type dialog)
|
||||
{
|
||||
Param = paramType;
|
||||
Control = control;
|
||||
DialogViewModel = dialog;
|
||||
}
|
||||
}
|
||||
23
Plugins/Driver/Cowain.Driver/Attributes/DriverAttribute.cs
Normal file
23
Plugins/Driver/Cowain.Driver/Attributes/DriverAttribute.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public class DriverAttribute : Attribute
|
||||
{
|
||||
public string DriverName { get; }
|
||||
public string DeviceType { get; }
|
||||
public string? Desc { get; }
|
||||
public string Group { get; }
|
||||
public DriverAttribute(string driverName, string deviceType, string group, string desc)
|
||||
{
|
||||
DriverName = driverName;
|
||||
DeviceType = deviceType;
|
||||
Group = group;
|
||||
Desc = desc;
|
||||
}
|
||||
}
|
||||
15
Plugins/Driver/Cowain.Driver/Conditions/ChangeCondition.cs
Normal file
15
Plugins/Driver/Cowain.Driver/Conditions/ChangeCondition.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Plugin.Cowain.Driver.Attributes;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Conditions;
|
||||
|
||||
[Condition("ValueChange", "值改变事件")]
|
||||
public class ChangeCondition : IActionCondition
|
||||
{
|
||||
public bool IsMatch(VariableViewModel variable, string actionValue)
|
||||
{
|
||||
//值改变永远返回true
|
||||
return !string.IsNullOrEmpty(variable.OldValue);
|
||||
}
|
||||
}
|
||||
16
Plugins/Driver/Cowain.Driver/Conditions/EqualCondition.cs
Normal file
16
Plugins/Driver/Cowain.Driver/Conditions/EqualCondition.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Plugin.Cowain.Driver.Attributes;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Conditions;
|
||||
|
||||
[Condition("IsEqual", "值相等")]
|
||||
public class EqualCondition : IActionCondition
|
||||
{
|
||||
public bool IsMatch(VariableViewModel variable, string actionValue)
|
||||
{
|
||||
//这里旧值不能为空,为空代表第一次采集到数据
|
||||
return actionValue.Equals(variable.Value) && !string.IsNullOrEmpty(variable.OldValue);
|
||||
}
|
||||
}
|
||||
69
Plugins/Driver/Cowain.Driver/Configs/menus.json
Normal file
69
Plugins/Driver/Cowain.Driver/Configs/menus.json
Normal file
@@ -0,0 +1,69 @@
|
||||
[
|
||||
{
|
||||
"Key": "Device",
|
||||
"Icon": "M273.10411269 145.4163357H80.44613906c-23.83030935 0-43.1553861 19.32507674-43.15538609 43.15538609v651.48034777c0 23.83030935 19.32507674 43.1553861 43.15538609 43.1553861h192.65797363c23.83030935 0 43.1553861-19.32507674 43.15538609-43.1553861V188.57172179c0-23.83030935-19.32507674-43.1553861-43.15538609-43.15538609z m-7.94343644 194.7920312H88.38957551v-100.4192638h176.77110074v100.4192638zM977.10598804 260.06264986H408.97244364c-17.07246044 0-30.94383453 13.87137409-30.94383454 30.94383453v376.06836454c0 17.07246044 13.87137409 30.94383453 30.94383454 30.94383453h568.01498565c17.07246044 0 30.94383453-13.87137409 30.94383453-30.94383453V290.88792564c0-17.07246044-13.87137409-30.82527578-30.82527578-30.82527578z m0.11855876 404.16678901H408.73532613v-374.64565951h568.37066191v374.64565951zM535.59319185 741.64830457h322.00557316c9.01046522 0 17.30957793 2.60829256 21.81481055 6.87640767l26.08292566 24.54166188c9.95893526 9.36614149-2.25261631 21.22201679-21.81481055 21.22201679H504.05656355c-20.62922303 0-32.60365707-12.92290408-20.62922303-22.17048682l31.5366283-24.54166187c4.74235011-3.67532134 12.44866906-5.92793764 20.62922303-5.92793765z",
|
||||
"LocaleKey": "Menu.Toolbar.DeviceManagement",
|
||||
"Group": "Toolbar",
|
||||
"CommandType": "Active",
|
||||
"Items": [
|
||||
{
|
||||
"Key": "DeviceManagement",
|
||||
"Icon": "M869 119.3H156.8c-36.4 0-66 29.6-66 66v657.9c0 36.4 29.6 66 66 66H869c36.4 0 66-29.6 66-66V185.3c0-36.4-29.6-66-66-66z m-4 70v176.6H160.8V189.3H865z m0 246.6V602H160.8V435.9H865zM160.8 839.2V672H865v167.2H160.8z M723.5 317.8h43c19.3 0 35-15.7 35-35s-15.7-35-35-35h-43c-19.3 0-35 15.7-35 35s15.7 35 35 35zM723.5 553.9h43c19.3 0 35-15.7 35-35s-15.7-35-35-35h-43c-19.3 0-35 15.7-35 35 0 19.4 15.7 35 35 35zM766.5 720.1h-43c-19.3 0-35 15.7-35 35s15.7 35 35 35h43c19.3 0 35-15.7 35-35 0-19.4-15.7-35-35-35z",
|
||||
"LocaleKey": "Menu.Sidebar.DeviceManagement",
|
||||
"CommandType": "Navigate",
|
||||
"CommandParameter": "DeviceManagement",
|
||||
"PageActions": [ "add", "edit", "delete" ]
|
||||
},
|
||||
{
|
||||
"Key": "TagManagement",
|
||||
"Icon": "M308.958295 375.934247c-53.30411 0-84.164384-44.887671-84.164383-84.164384 0-36.471233 30.860274-84.164384 84.164383-84.164384s84.164384 44.887671 84.164384 84.164384c0 42.082192-30.860274 84.164384-84.164384 84.164384z m-2.805479-117.830137c-16.832877 0-30.860274 14.027397-30.860274 30.860274 0 16.832877 14.027397 30.860274 30.860274 30.860274 16.832877 0 30.860274-14.027397 30.860274-30.860274 0-16.832877-14.027397-30.860274-30.860274-30.860274z m684.536986 384.350685L631.588432 1004.361644c-11.221918 11.221918-28.054795 19.638356-44.887671 19.638356-16.832877 0-33.665753-5.610959-44.887671-19.638356L19.993912 479.736986c-11.221918-11.221918-19.638356-28.054795-19.638356-44.887671V75.747945C0.355556 39.276712 28.41035 11.221918 64.881583 11.221918h359.10137c16.832877 0 33.665753 5.610959 44.887671 19.638356l521.819178 521.819178c25.249315 25.249315 25.249315 64.526027 0 89.775343zM421.177473 67.331507H56.465145v364.712329l533.041095 533.041096 364.712329-364.712329-533.041096-533.041096z",
|
||||
"LocaleKey": "Menu.Sidebar.TagManagement",
|
||||
"CommandType": "Navigate",
|
||||
"CommandParameter": "TagManagement",
|
||||
"PageActions": [ "add", "edit", "save", "import", "export", "delete" ]
|
||||
},
|
||||
{
|
||||
"Key": "ActionManagement",
|
||||
"Icon": "M662.117195 60.235896a30.120659 30.120659 0 0 0-20.823943 8.823266l-240.941177 240.941177a30.120659 30.120659 0 0 0 0 42.588762l240.941177 240.941177a30.120659 30.120659 0 0 0 21.764819 8.823266 30.120659 30.120659 0 0 0 20.823943-8.823266l240.941177-240.941177a30.120659 30.120659 0 0 0 0-42.588762l-10.941139-10.941139-230.000038-230.000038a30.120659 30.120659 0 0 0-21.764819-8.823266z m0.470438 72.705808l198.353016 198.353016-198.353016 198.353016-198.353017-198.353016z m-512 168.235369v60.235294h60.235294v-60.235294z m120.470588 0v60.235294h60.235294v-60.235294z m30.117647 240.941176c-7.706504 0-15.413007 2.945506-21.294381 8.823266l-180.705882 180.705883c-11.757327 11.762146-11.757327 30.826616 0 42.588762l180.705882 180.705882a30.126682 30.126682 0 0 0 21.764819 8.823266 30.102588 30.102588 0 0 0 20.823943-8.823266l180.705883-180.705882c11.757327-11.762146 11.757327-30.826616 0-42.588762l-180.705883-180.705883c-5.881374-5.878362-13.587878-8.823266-21.294381-8.823266z m271.058824 180.705883v60.235294h60.235294v-60.235294z m120.470588 0v60.235294h60.235294v-60.235294z m120.470588 0v60.235294h60.235294v-60.235294z",
|
||||
"LocaleKey": "Menu.Sidebar.ActionManagement",
|
||||
"CommandType": "Navigate",
|
||||
"CommandParameter": "ActionManagement",
|
||||
"PageActions": [ "add", "edit", "save", "import", "export", "delete" ]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"Key": "RealTime",
|
||||
"Group": "Toolbar",
|
||||
"Items": [
|
||||
{
|
||||
"Key": "VariableMonitor",
|
||||
"Icon": "M920.48 86.88H113.28A60.32 60.32 0 0 0 52.8 147.2v594.56a60.48 60.48 0 0 0 60.48 60.48h807.2a60.32 60.32 0 0 0 60.32-60.48V147.2a60.32 60.32 0 0 0-60.32-60.32z m0 623.04a30.24 30.24 0 0 1-30.24 30.08H144a30.24 30.24 0 0 1-30.24-30.08V177.44A30.24 30.24 0 0 1 144 147.2h746.24a30.24 30.24 0 0 1 30.24 30.24z M826.24 547.04h-150.88A29.44 29.44 0 0 1 656 539.2l-139.2-143.04a32 32 0 0 0-46.4 3.84l-100.48 131.52a33.76 33.76 0 0 1-23.2 11.68H208A33.28 33.28 0 0 1 176 512a30.4 30.4 0 0 1 32-30.72h108.32a27.36 27.36 0 0 0 23.2-11.68l123.2-162.4a32 32 0 0 1 46.4-3.84L679.2 473.6a30.08 30.08 0 0 0 19.36 7.68h128A33.28 33.28 0 0 1 857.12 512a34.24 34.24 0 0 1-30.88 34.88zM168.8 871.84h692.16a33.28 33.28 0 0 1 31.04 30.88 30.56 30.56 0 0 1-31.04 30.88H168.8a30.88 30.88 0 0 1 0-61.76z",
|
||||
"LocaleKey": "Menu.Sidebar.VariableMonitor",
|
||||
"CommandType": "Navigate",
|
||||
"CommandParameter": "VariableMonitor",
|
||||
"PageActions": [ "add", "edit", "save", "import", "export", "delete" ]
|
||||
},
|
||||
{
|
||||
"Key": "AlarmRealTime",
|
||||
"Icon": "M193.408 441.536v64.64H64V441.6h129.408z m-81.28-157.76l110.528 61.44-31.36 56.576-110.72-61.44 31.488-56.576z m106.304-121.6L299.328 260.48l-50.048 41.152-80.896-98.496 50.048-41.088zM829.184 441.536v64.64h129.472V441.6h-129.472z m81.344-157.76l-110.592 61.44 31.36 56.576 110.72-61.44-31.488-56.576z m-106.368-121.6L723.264 260.48l50.048 41.152 80.896-98.496-50.048-41.088zM511.36 162.112l447.296 756.992H64L511.36 162.112zM511.232 288l-335.168 567.168h670.272L511.296 287.936z M543.36 482.752v137.984h-64V482.752zM543.36 680.704v74.944h-64v-74.944z",
|
||||
"LocaleKey": "Menu.Sidebar.AlarmRealTime",
|
||||
"CommandType": "Navigate",
|
||||
"CommandParameter": "AlarmRealTime",
|
||||
"PageActions": [ "import", "export"]
|
||||
},
|
||||
{
|
||||
"Key": "AlarmHistory",
|
||||
"Icon": "M666.006 771.98c0 1.1-0.9 2-2 2H252.571c-1.1 0-2-0.9-2-2v-69.195c0-1.1 0.9-2 2-2h411.435c1.1 0 2 0.9 2 2v69.195zM647.707 567.693c0 1.1-0.9 2-2 2H252.571c-1.1 0-2-0.9-2-2V498.5c0-1.1 0.9-2 2-2h393.136c1.1 0 2 0.9 2 2v69.193zM501.319 363.425c0 1.1-0.9 2-2 2H252.571c-1.1 0-2-0.9-2-2v-69.194c0-1.1 0.9-2 2-2h246.748c1.1 0 2 0.9 2 2v69.194z M742.186 879.808c0 1.1-0.9 2-2 2H167.154c-1.1 0-2-0.9-2-2v-700.55c0-1.1 0.9-2 2-2h340.117c1.1 0 2.145-0.888 2.322-1.974l15.808-69.305c0.317-1.053-0.323-1.915-1.423-1.915H91.119c-1.1 0-2 0.9-2 2V953c0 1.1 0.9 2 2 2h725.12c1.1 0 2-0.9 2-2V522.856c0-1.1-0.898-2.061-1.995-2.136l-72.093-8.91c-1.08-0.205-1.965 0.528-1.965 1.628v366.37z M596.512 418.784c-25.77 0-46.748-20.961-46.748-46.747 0-24.946 19.657-45.389 44.281-46.675V209.691c0-62.061 45.782-111.472 109.667-119.423 8.272-14.295 23.569-23.266 40.402-23.266s32.13 8.971 40.423 23.266c63.865 7.952 109.646 57.362 109.646 119.423v115.671c24.625 1.286 44.282 21.729 44.282 46.675 0 25.786-20.979 46.747-46.747 46.747H596.512z m274.3-49.213c-15.297-7.666-25.841-23.534-25.841-41.815V209.691c0-41.333-33.111-71.335-78.716-71.335h-44.281c-45.603 0-78.715 30.003-78.715 71.335v118.064c0 18.282-10.544 34.15-25.84 41.815h253.393z M891.719 408.956H596.512c-20.337 0-36.884-16.565-36.884-36.919 0-20.335 16.547-36.883 36.884-36.883 4.074 0 7.398-3.324 7.398-7.398V209.691c0-59.345 44.853-104.949 106.181-110.203 5.575-13.295 18.728-22.641 34.023-22.641 15.297 0 28.449 9.346 34.042 22.641 61.312 5.253 106.165 50.857 106.165 110.203v118.064c0 4.075 3.323 7.398 7.397 7.398 20.335 0 36.882 16.548 36.882 36.883 0.001 20.355-16.546 36.92-36.881 36.92zM721.974 128.509c-51.321 0-88.563 34.13-88.563 81.182v118.064c0 20.354-16.565 36.92-36.899 36.92-4.058 0-7.381 3.306-7.381 7.362 0 4.074 3.323 7.398 7.381 7.398h295.207c4.057 0 7.38-3.324 7.38-7.398 0-4.056-3.323-7.362-7.38-7.362-20.336 0-36.901-16.565-36.901-36.92V209.691c0-47.051-37.241-81.182-88.563-81.182h-14.777v-14.761c0-4.074-3.289-7.38-7.363-7.38s-7.363 3.306-7.363 7.38v14.761h-14.778z M744.114 455.704c-40.547 0-68.869-25.304-68.869-61.508v-24.625h137.738v24.625c0 36.204-28.304 61.508-68.869 61.508z M744.114 445.839c-32.558 0-59.041-19.103-59.041-51.643v-14.761h118.083v14.761c0 32.54-26.482 51.643-59.042 51.643z m-25.553-36.883c5.11 8.792 14.654 14.742 25.554 14.742 10.9 0 20.443-5.95 25.554-14.742h-51.108z",
|
||||
"LocaleKey": "Menu.Sidebar.AlarmHistory",
|
||||
"CommandType": "Navigate",
|
||||
"CommandParameter": "AlarmHistory",
|
||||
"PageActions": [ "import", "export"]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Converters;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将多选集合拼接为字符串(如:"系统, 设备")
|
||||
/// </summary>
|
||||
public class ListToJoinedStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
|
||||
{
|
||||
// 参数:分隔符(默认逗号+空格)
|
||||
string separator = parameter as string ?? ", ";
|
||||
|
||||
// 空值处理
|
||||
if (value is not IEnumerable<object> list || !list.Any())
|
||||
{
|
||||
return "请选择"; // 占位提示
|
||||
}
|
||||
// 拼接选中项的Name属性
|
||||
return string.Join(separator, list
|
||||
.Where(item => item != null)
|
||||
.Select(item => item.GetType().GetProperty("Name")?.GetValue(item)?.ToString() ?? string.Empty)
|
||||
.Where(name => !string.IsNullOrEmpty(name)));
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
|
||||
{
|
||||
// 无需反向转换
|
||||
return Avalonia.Data.BindingOperations.DoNothing;
|
||||
}
|
||||
}
|
||||
|
||||
101
Plugins/Driver/Cowain.Driver/DeviceHostedService.cs
Normal file
101
Plugins/Driver/Cowain.Driver/DeviceHostedService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Cowain.Base.Helpers;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
public class DeviceHostedService : BackgroundHostedService
|
||||
{
|
||||
private readonly IDeviceMonitor _deviceMonitor;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IDriverPluginService _driverPlugin;
|
||||
private readonly IActionPluginService _actionPlugin;
|
||||
private readonly ITagService _tagService;
|
||||
private readonly IActionService _actionService;
|
||||
private readonly ILogger<DeviceHostedService> _logger;
|
||||
|
||||
public DeviceHostedService(IDeviceMonitor deviceMonitor, IDeviceService deviceService, IDriverPluginService driverPlugin, IActionPluginService actionPlugin, ITagService tagService, IActionService actionService, ILogger<DeviceHostedService> logger)
|
||||
{
|
||||
_deviceMonitor = deviceMonitor;
|
||||
_deviceService = deviceService;
|
||||
_driverPlugin = driverPlugin;
|
||||
_actionPlugin = actionPlugin;
|
||||
_tagService = tagService;
|
||||
_actionService = actionService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("DeviceHostedService StopAsync 开始执行");
|
||||
if (ExecuteTask != null)
|
||||
{
|
||||
var stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
stoppingCts.Cancel();
|
||||
try
|
||||
{
|
||||
|
||||
// 正确使用异步操作并传递取消令牌
|
||||
var stopTasks = _deviceMonitor.DeviceThreads
|
||||
.Select(hostTask => hostTask.StopReadAsync(stoppingCts.Token))
|
||||
.ToList();
|
||||
await Task.WhenAll(stopTasks);
|
||||
_logger.LogInformation("DeviceHostedService 所有设备线程已停止");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteTask.WaitAsync(stoppingCts.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
_logger.LogInformation("DeviceHostedService StopAsync 执行完成");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[LogAndSwallow]
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
|
||||
var devices = await _deviceService.GetAllAsync();
|
||||
foreach (var device in devices)
|
||||
{
|
||||
var driver = _driverPlugin.GetDriver(device.DriverName);
|
||||
if (driver != null)
|
||||
{
|
||||
driver.DeviceName = device.DeviceName;
|
||||
//需要获取变量表
|
||||
var tags = await _tagService.GetDeviceTagsAsync(device.Id);
|
||||
var variables = tags.Select(tag => new VariableViewModel
|
||||
{
|
||||
Id = tag.Id,
|
||||
DeviceId = tag.DeviceId,
|
||||
Name = tag.Name,
|
||||
Address = tag.Address,
|
||||
Desc = tag.Desc,
|
||||
DataType = Enum.Parse<DataTypeEnum>(tag.DataType),
|
||||
OperMode = Enum.Parse<OperModeEnum>(tag.OperMode),
|
||||
AlarmEnable = tag.AlarmEnable,
|
||||
AlarmValue = tag.AlarmValue,
|
||||
AlarmMsg = tag.AlarmMsg,
|
||||
AlarmGroup = tag.AlarmGroup,
|
||||
AlarmLevel = tag.AlarmLevel,
|
||||
Json = tag.Json,
|
||||
ArrayCount = tag.ArrayCount
|
||||
}).ToList();
|
||||
device.Variables = new(variables);
|
||||
var actions = await _actionService.GetDeviceActionsAsync(device.Id);
|
||||
device.VarActions = new(actions);
|
||||
_deviceMonitor.AddDevice(driver, _actionPlugin, device);
|
||||
}
|
||||
}
|
||||
// 正确使用异步操作并传递取消令牌
|
||||
var startTasks = _deviceMonitor.DeviceThreads
|
||||
.Select(hostTask => hostTask.StartReadAsync(stoppingToken))
|
||||
.ToList();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
100
Plugins/Driver/Cowain.Driver/DeviceMonitor.cs
Normal file
100
Plugins/Driver/Cowain.Driver/DeviceMonitor.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
public class DeviceMonitor : IDeviceMonitor
|
||||
{
|
||||
|
||||
private IServiceProvider _serviceProvider;
|
||||
private List<DeviceThread> _deviceThreads = new List<DeviceThread>();
|
||||
public List<DeviceViewModel> Devices => GetDeviceViewModels();
|
||||
public List<DeviceThread> DeviceThreads => _deviceThreads;
|
||||
|
||||
private readonly List<AlarmViewModel> alarms = new();
|
||||
public List<AlarmViewModel> Alarms => alarms;
|
||||
|
||||
private readonly ILogger<DeviceMonitor> _logger;
|
||||
public DeviceMonitor(IServiceProvider serviceProvider, ILogger<DeviceMonitor> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ResultModel<IDriver> GetDriver(string name)
|
||||
{
|
||||
var dev = _deviceThreads.FirstOrDefault(x => x.Device.DeviceName == name);
|
||||
if (dev == null)
|
||||
{
|
||||
return ResultModel<IDriver>.Error("device is null");
|
||||
}
|
||||
return ResultModel<IDriver>.Success(dev.Driver);
|
||||
}
|
||||
|
||||
public ResultModel<DeviceViewModel> GetDevice(string name)
|
||||
{
|
||||
var dev = _deviceThreads.FirstOrDefault(x => x.Device.DeviceName == name);
|
||||
if (dev == null)
|
||||
{
|
||||
return ResultModel<DeviceViewModel>.Error("device is null");
|
||||
}
|
||||
return ResultModel<DeviceViewModel>.Success(dev.Device);
|
||||
}
|
||||
public ResultModel<VariableViewModel> GetVariable(string plc, string address)
|
||||
{
|
||||
var dev = GetDevice(plc);
|
||||
if (!dev.IsSuccess)
|
||||
{
|
||||
return ResultModel<VariableViewModel>.Error("device is null");
|
||||
}
|
||||
var variable = dev.Data?.Variables?.FirstOrDefault(x => x.Address == address);
|
||||
if (variable == null) return ResultModel<VariableViewModel>.Error("variable is null");
|
||||
return ResultModel<VariableViewModel>.Success(variable);
|
||||
}
|
||||
|
||||
public ResultModel<string> GetActionParam(string plc, string address)
|
||||
{
|
||||
var dev = GetDevice(plc);
|
||||
if (!dev.IsSuccess)
|
||||
{
|
||||
return ResultModel<string>.Error("device is null");
|
||||
}
|
||||
if (dev.Data == null)
|
||||
{
|
||||
return ResultModel<string>.Error("device is null");
|
||||
}
|
||||
var device = dev.Data;
|
||||
if (device.VarActions == null)
|
||||
{
|
||||
return ResultModel<string>.Error($"设备 {device.DeviceName} 的动作列表为空");
|
||||
}
|
||||
|
||||
var q = from v in device.Variables
|
||||
join va in device.VarActions on v.Id equals va.TagId
|
||||
where v.Address == address
|
||||
select va.Param;
|
||||
var param = q.FirstOrDefault();
|
||||
return string.IsNullOrEmpty(param) ? ResultModel<string>.Error("action param is null") : ResultModel<string>.Success(param);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void AddDevice(IDriver driver, IActionPluginService actionPluginService, DeviceViewModel device)
|
||||
{
|
||||
var deviceThread = new DeviceThread(driver, actionPluginService, _serviceProvider, device);
|
||||
_deviceThreads.Add(deviceThread);
|
||||
}
|
||||
|
||||
public List<DeviceViewModel> GetDeviceViewModels()
|
||||
{
|
||||
return _deviceThreads.Select(x => x.Device).ToList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
106
Plugins/Driver/Cowain.Driver/DeviceThread.cs
Normal file
106
Plugins/Driver/Cowain.Driver/DeviceThread.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Cowain.Base.Helpers;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设备数据采集,不注入到容器,一个设备一个线程
|
||||
/// </summary>
|
||||
public class DeviceThread
|
||||
{
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // 初始化计数为1,最大计数也为1,实现互斥
|
||||
private Task? _mainTask;
|
||||
private Task? _consumeVariablesTask;
|
||||
private readonly IDriver _driver;
|
||||
private readonly DeviceViewModel _device;
|
||||
private readonly IActionPluginService _actionPluginService;
|
||||
private IServiceProvider _serviceProvider;
|
||||
private IVariableChannelService? _variableChannelService;
|
||||
private readonly ILogger<DeviceThread> _logger;
|
||||
public DeviceViewModel Device { get => _device; }
|
||||
|
||||
public IDriver Driver { get => _driver; }
|
||||
|
||||
public DeviceThread(IDriver driver, IActionPluginService actionPluginService, IServiceProvider serviceProvider, DeviceViewModel device)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_driver = driver;
|
||||
_actionPluginService = actionPluginService;
|
||||
_device = device;
|
||||
_variableChannelService = _serviceProvider.GetService<IVariableChannelService>();
|
||||
_logger = ServiceLocator.GetRequiredService<ILogger<DeviceThread>>();
|
||||
}
|
||||
|
||||
|
||||
public async Task StartReadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_device.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_logger.LogInformation($"DeviceThread线程自动启动:{_device.DeviceName}");
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
_variableChannelService?.RegisterDeviceActions(_device);
|
||||
//消费变量,但不等待结果
|
||||
_consumeVariablesTask = _variableChannelService?.ConsumeVariablesAsync();
|
||||
_mainTask = Task.Factory.StartNew(async () =>
|
||||
{
|
||||
_driver.SetParam(_device.Param);
|
||||
_driver.SetDeviceModel(_device);
|
||||
var connect = await _driver.OpenAsync();
|
||||
if (!connect)
|
||||
{
|
||||
//建立过链接后就不用再连接了
|
||||
_logger.LogInformation($"DeviceThread设备连接失败:{_device.DeviceName}");
|
||||
}
|
||||
await _driver.ReadThreadAsync(_device, cancellationToken);
|
||||
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
|
||||
//释放信号量,允许其他线程进入
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
public async Task StopReadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"DeviceThread线程停止开始:{_device.DeviceName}");
|
||||
_variableChannelService?.StopConsumeVariablesAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// 等待消费变量任务完成
|
||||
_logger.LogInformation($"DeviceThread 消费变量任务判断:{_device.DeviceName}");
|
||||
if (_consumeVariablesTask != null && !_consumeVariablesTask.IsCompleted)
|
||||
{
|
||||
_logger.LogInformation($"DeviceThread 消费变量任务await:{_device.DeviceName}");
|
||||
await Task.WhenAny(_consumeVariablesTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
||||
}
|
||||
// 等待主任务完成
|
||||
_logger.LogInformation($"DeviceThread 主任务判断:{_device.DeviceName}");
|
||||
if (_mainTask != null && !_mainTask.IsCompleted)
|
||||
{
|
||||
_logger.LogInformation($"DeviceThread 主任务await:{_device.DeviceName}");
|
||||
await Task.WhenAny(_mainTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
_logger.LogInformation($"DeviceThread-StopReadAsync任务取消:{_device.DeviceName}->{ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"DeviceThread-StopReadAsync任务异常:{_device.DeviceName}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保资源释放
|
||||
_driver.Close();
|
||||
_logger.LogInformation($"DeviceThread线程停止完成:{_device.DeviceName}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
6
Plugins/Driver/Cowain.Driver/DriverConsts.cs
Normal file
6
Plugins/Driver/Cowain.Driver/DriverConsts.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
public class DriverConsts
|
||||
{
|
||||
public const string PluginName = "Driver";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Cowain.Base.Abstractions.Localization;
|
||||
using Cowain.Base.Attributes;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
internal class DriverLocalizationResourceContributor(IOptions<AppSettings> appSettings) :
|
||||
LocalizationResourceContributorBase(appSettings, DriverConsts.PluginName)
|
||||
{
|
||||
}
|
||||
40
Plugins/Driver/Cowain.Driver/DriverPlugin.cs
Normal file
40
Plugins/Driver/Cowain.Driver/DriverPlugin.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Cowain.Base.Abstractions.Plugin;
|
||||
using Ke.Bee.Localization.Providers.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Services;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
public class DriverPlugin : PluginBase
|
||||
{
|
||||
|
||||
public override string PluginName => DriverConsts.PluginName;
|
||||
|
||||
public override R? Execute<T, R>(string methodName, T? parameters)
|
||||
where T : default
|
||||
where R : class
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
}
|
||||
|
||||
public override void RegisterServices(IServiceCollection services, List<Assembly>? _assemblies)
|
||||
{
|
||||
HslCommunication.Authorization.SetAuthorizationCode("ac963114-3a46-4444-9a16-080a0ce99535");
|
||||
services.AddSingleton<ILocalizationResourceContributor, DriverLocalizationResourceContributor>();
|
||||
services.AddSingleton<IDeviceMonitor, DeviceMonitor>();
|
||||
//将所有驱动注册到容器
|
||||
services.AddDrivers(_assemblies);
|
||||
services.AddVariables(_assemblies);
|
||||
services.AddActionConditions(_assemblies);
|
||||
services.AddTransient<IDriverPluginService, DriverPluginService>();
|
||||
services.AddSingleton<IActionPluginService, ActionPluginService>();
|
||||
services.AddTransient<IVariableChannelService, VariableChannelService>();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
122
Plugins/Driver/Cowain.Driver/DriverServiceExtensions.cs
Normal file
122
Plugins/Driver/Cowain.Driver/DriverServiceExtensions.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Cowain.Base.Abstractions.Plugin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.Attributes;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
public static class DriverServiceExtensions
|
||||
{
|
||||
public static List<Type>? DriverTypes { get; private set; }
|
||||
/// <summary>
|
||||
/// 注册插件服务
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddDrivers(this IServiceCollection services, List<Assembly>? assemblies)
|
||||
{
|
||||
if (assemblies == null)
|
||||
{
|
||||
return services;
|
||||
}
|
||||
var driverTypes = assemblies.SelectMany(a => a.GetTypes())
|
||||
.Where(t => typeof(DriverBase).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
|
||||
if (DriverTypes == null)
|
||||
{
|
||||
DriverTypes = driverTypes.ToList();
|
||||
}
|
||||
// 注册时只注册类型,不注册IDriver
|
||||
foreach (var driver in driverTypes)
|
||||
{
|
||||
services.AddTransient(driver); // 只注册类型
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 注册变量动作
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddVariables(this IServiceCollection services, List<Assembly>? assemblies)
|
||||
{
|
||||
if (assemblies == null)
|
||||
{
|
||||
return services;
|
||||
}
|
||||
var driverTypes = assemblies.SelectMany(a => a.GetTypes())
|
||||
.Where(t => typeof(IVariableAction).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
|
||||
Dictionary<string, Type> drivers = new Dictionary<string, Type>();
|
||||
foreach (var driver in driverTypes)
|
||||
{
|
||||
//检查是否已经注册了同名的动作
|
||||
var attribute = driver.GetCustomAttributes(typeof(ActionAttribute), false).FirstOrDefault() as ActionAttribute;
|
||||
if (attribute != null)
|
||||
{
|
||||
var driverInfo = new ActionInfo
|
||||
{
|
||||
Name = attribute.Name,
|
||||
Desc = attribute.Desc,
|
||||
};
|
||||
if (!drivers.ContainsKey(driverInfo.Name))
|
||||
{
|
||||
services.AddTransient(typeof(IVariableAction), driver);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Variable action with name '{driverInfo.Name}' is already registered.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册动作条件
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddActionConditions(this IServiceCollection services, List<Assembly>? assemblies)
|
||||
{
|
||||
if (assemblies == null)
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
var conditionTypes = assemblies.SelectMany(a => a.GetTypes())
|
||||
.Where(t => typeof(IActionCondition).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
|
||||
Dictionary<string, Type> conditions = new Dictionary<string, Type>();
|
||||
foreach (var cond in conditionTypes)
|
||||
{
|
||||
//检查是否已经注册了同名的条件
|
||||
var attribute = cond.GetCustomAttributes(typeof(ConditionAttribute), false).FirstOrDefault() as ConditionAttribute;
|
||||
if (attribute != null)
|
||||
{
|
||||
var driverInfo = new ConditionInfo
|
||||
{
|
||||
Name = attribute.Name,
|
||||
Desc = attribute.Desc,
|
||||
};
|
||||
if (!conditions.ContainsKey(driverInfo.Name))
|
||||
{
|
||||
services.AddTransient(typeof(IActionCondition), cond);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"condition with name '{driverInfo.Name}' is already registered.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface IActionPluginService
|
||||
{
|
||||
List<ActionInfo>? GetActions();
|
||||
List<ConditionInfo>? GetConditions();
|
||||
IActionCondition? GetCondition(string name);
|
||||
IVariableAction? GetAction(string name);
|
||||
|
||||
}
|
||||
15
Plugins/Driver/Cowain.Driver/IServices/IActionService.cs
Normal file
15
Plugins/Driver/Cowain.Driver/IServices/IActionService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Cowain.Base.IServices;
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface IActionService : IBaseService
|
||||
{
|
||||
Task<List<VarActionViewModel>> GetDeviceActionsAsync(int deviceId);
|
||||
Task<List<DeviceViewModel>> GetDeviceAsync();
|
||||
|
||||
Task<ResultModel> AddActionAsync(VarActionViewModel varAction);
|
||||
Task<ResultModel> DeleteActionAsync(int id);
|
||||
Task<ResultModel> UpdateActionAsync(VarActionViewModel varAction);
|
||||
}
|
||||
12
Plugins/Driver/Cowain.Driver/IServices/IAlarmGroupService.cs
Normal file
12
Plugins/Driver/Cowain.Driver/IServices/IAlarmGroupService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Cowain.Base.IServices;
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface IAlarmGroupService : IBaseService
|
||||
{
|
||||
Task<List<AlarmGroupViewModel>> GetAllAsync();
|
||||
Task<ResultModel> AddAsync(AlarmGroupViewModel model);
|
||||
Task<ResultModel> UpdateAsync(AlarmGroupViewModel model);
|
||||
}
|
||||
12
Plugins/Driver/Cowain.Driver/IServices/IAlarmLevelService.cs
Normal file
12
Plugins/Driver/Cowain.Driver/IServices/IAlarmLevelService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Cowain.Base.IServices;
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface IAlarmLevelService : IBaseService
|
||||
{
|
||||
Task<List<AlarmLevelViewModel>> GetAllAsync();
|
||||
Task<ResultModel> AddAsync(AlarmLevelViewModel model);
|
||||
Task<ResultModel> UpdateAsync(AlarmLevelViewModel model);
|
||||
}
|
||||
18
Plugins/Driver/Cowain.Driver/IServices/IAlarmService.cs
Normal file
18
Plugins/Driver/Cowain.Driver/IServices/IAlarmService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Cowain.Base.IServices;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface IAlarmService : IBaseService
|
||||
{
|
||||
Task<(List<AlarmViewModel>, int totals)> GetAlarmAsync(int pageIndex, int pageSize, DateTime? startTime, DateTime? endTime);
|
||||
|
||||
Task<(List<AlarmViewModel>, int totals)> GetAlarmAsync(int pageIndex, int pageSize, DateTime? startTime, DateTime? endTime, List<int>? groups = default, List<int>? levels = default);
|
||||
|
||||
|
||||
Task<ResultModel> AddAsync(AlarmViewModel model);
|
||||
Task<ResultModel> CancelAsync(AlarmViewModel model);
|
||||
|
||||
Task<AlarmViewModel?> GetInAlarmAsync(int tagId);
|
||||
}
|
||||
17
Plugins/Driver/Cowain.Driver/IServices/IDeviceService.cs
Normal file
17
Plugins/Driver/Cowain.Driver/IServices/IDeviceService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Cowain.Base.IServices;
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface IDeviceService : IBaseService
|
||||
{
|
||||
Task<List<DeviceViewModel>> GetAllAsync();
|
||||
Task<(List<DeviceViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize);
|
||||
Task<ResultModel> AddDeviceAsync(DeviceViewModel? device);
|
||||
Task<ResultModel> UpdateDeviceAsync(DeviceViewModel? device);
|
||||
Task<ResultModel> DeleteDeviceAsync(int id);
|
||||
|
||||
Task<ResultModel> EnableDeviceAsync(int id);
|
||||
Task<ResultModel> DisableDeviceAsync(int id);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface IDriverPluginService
|
||||
{
|
||||
List<DriverInfo> GetDriverInfos();
|
||||
IDriver? GetDriver(string driverName);
|
||||
Task ParamEditDialogAsync(DeviceViewModel deviceViewModel);
|
||||
}
|
||||
17
Plugins/Driver/Cowain.Driver/IServices/ITagService.cs
Normal file
17
Plugins/Driver/Cowain.Driver/IServices/ITagService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Cowain.Base.IServices;
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices;
|
||||
|
||||
public interface ITagService : IBaseService
|
||||
{
|
||||
Task<List<TagViewModel>> GetAllAsync();
|
||||
Task<(List<TagViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize);
|
||||
|
||||
Task<List<TagViewModel>> GetDeviceTagsAsync(int deviceId);
|
||||
Task<ResultModel> AddTagAsync(TagViewModel tag);
|
||||
Task<ResultModel> UpdateTagAsync(TagViewModel tag);
|
||||
Task<ResultModel> DeleteTagAsync(int id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.IServices
|
||||
{
|
||||
public interface IVariableChannelService
|
||||
{
|
||||
void RegisterDeviceActions(DeviceViewModel device);
|
||||
Task ConsumeVariablesAsync();
|
||||
Task StopConsumeVariablesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
10
Plugins/Driver/Cowain.Driver/Models/ActionInfo.cs
Normal file
10
Plugins/Driver/Cowain.Driver/Models/ActionInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models;
|
||||
|
||||
public class ActionInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Desc { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
10
Plugins/Driver/Cowain.Driver/Models/ConditionInfo.cs
Normal file
10
Plugins/Driver/Cowain.Driver/Models/ConditionInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models;
|
||||
|
||||
public class ConditionInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Desc { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
13
Plugins/Driver/Cowain.Driver/Models/DeviceTypeInfo.cs
Normal file
13
Plugins/Driver/Cowain.Driver/Models/DeviceTypeInfo.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models;
|
||||
|
||||
public class DeviceTypeInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<DriverInfoGroup>? Groups { get; set; }
|
||||
}
|
||||
12
Plugins/Driver/Cowain.Driver/Models/DriverInfo.cs
Normal file
12
Plugins/Driver/Cowain.Driver/Models/DriverInfo.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models;
|
||||
|
||||
public class DriverInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DeviceType { get; set; } = string.Empty;
|
||||
public string? Desc { get; set; }
|
||||
public string Group { get; set; } = "未分类";
|
||||
|
||||
}
|
||||
13
Plugins/Driver/Cowain.Driver/Models/DriverInfoGroup.cs
Normal file
13
Plugins/Driver/Cowain.Driver/Models/DriverInfoGroup.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models;
|
||||
|
||||
public class DriverInfoGroup
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<DriverInfo>? Drivers { get; set; }
|
||||
}
|
||||
47
Plugins/Driver/Cowain.Driver/Models/Dto/AlarmGroupDto.cs
Normal file
47
Plugins/Driver/Cowain.Driver/Models/Dto/AlarmGroupDto.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Dto;
|
||||
|
||||
[Table("alarm_group")]
|
||||
public class AlarmGroupDto : BaseModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 报警组名称
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class AlarmGroupSeed : IDataSeeding
|
||||
{
|
||||
public void DataSeeding(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<AlarmGroupDto>().HasData(
|
||||
new AlarmGroupDto
|
||||
{
|
||||
Id = 1,
|
||||
Name = "系统"
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
49
Plugins/Driver/Cowain.Driver/Models/Dto/AlarmHistoryDto.cs
Normal file
49
Plugins/Driver/Cowain.Driver/Models/Dto/AlarmHistoryDto.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Dto;
|
||||
|
||||
[Table("alarm_history")]
|
||||
public class AlarmHistoryDto : BaseModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public int TagId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 报警详情
|
||||
/// </summary>
|
||||
public string Desc { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 报警组
|
||||
/// </summary>
|
||||
public int Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 报警等级
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
public bool Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生时间
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
public Nullable<DateTime> StopTime { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
61
Plugins/Driver/Cowain.Driver/Models/Dto/AlarmLevelDto.cs
Normal file
61
Plugins/Driver/Cowain.Driver/Models/Dto/AlarmLevelDto.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Avalonia.Media;
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Dto;
|
||||
|
||||
[Table("alarm_level")]
|
||||
public class AlarmLevelDto : BaseModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 颜色
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Color { get; set; } = string.Empty;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class AlarmLevelSeed : IDataSeeding
|
||||
{
|
||||
public void DataSeeding(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<AlarmLevelDto>().HasData(
|
||||
new AlarmLevelDto
|
||||
{
|
||||
Id = 1,
|
||||
Name = "报警",
|
||||
Color = Colors.Red.ToString()
|
||||
},
|
||||
new AlarmLevelDto
|
||||
{
|
||||
Id = 2,
|
||||
Name = "警告",
|
||||
Color = Colors.Yellow.ToString()
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
77
Plugins/Driver/Cowain.Driver/Models/Dto/DeviceDto.cs
Normal file
77
Plugins/Driver/Cowain.Driver/Models/Dto/DeviceDto.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Dto;
|
||||
|
||||
|
||||
[Table("device")]
|
||||
public class DeviceDto : BaseModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// 设备名称
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 驱动名称
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string DriverName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 设备类型
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(20)]
|
||||
public string DeviceType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 连接参数
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Param { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Desc { get; set; }
|
||||
|
||||
public bool Enable { get; set; }
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 最后一次更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdateTime { get; set; } = DateTime.MinValue;
|
||||
}
|
||||
|
||||
|
||||
//public class DeviceSeed : IDataSeeding
|
||||
//{
|
||||
// public void DataSeeding(ModelBuilder modelBuilder)
|
||||
// {
|
||||
// modelBuilder.Entity<DeviceDto>().HasData(
|
||||
// new DeviceDto
|
||||
// {
|
||||
// Id = 1,
|
||||
// DeviceName = "plc1",
|
||||
// DeviceType = "PLC",
|
||||
// DriverName = "SiemensS7Tcp",
|
||||
// Param = "{\r\n \"IpAddress\": \"127.0.0.1\",\r\n \"Port\": 102,\r\n \"Slot\": 0,\r\n \"Rack\": 0,\r\n \"DataFormat\": \"ABCD\",\r\n \"SiemensPLCS\": \"S1500\"\r\n}",
|
||||
// Enable = true,
|
||||
// Desc = "测试PLC"
|
||||
// }
|
||||
// );
|
||||
|
||||
// }
|
||||
//}
|
||||
|
||||
81
Plugins/Driver/Cowain.Driver/Models/Dto/TagAddressDto.cs
Normal file
81
Plugins/Driver/Cowain.Driver/Models/Dto/TagAddressDto.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Dto;
|
||||
|
||||
[Table("tag_address")]
|
||||
public class TagAddressDto : BaseModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
[Required]
|
||||
public int DeviceId { get; set; }
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Address { get; set; } = string.Empty;
|
||||
public string Desc { get; set; } = string.Empty;
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string DataType { get; set; } = DataTypeEnum.Int16.ToString();
|
||||
[DefaultValue(1)]
|
||||
public int ArrayCount { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 操作模式 0:循环读取 1:订阅模式
|
||||
/// </summary>
|
||||
public string OperMode { get; set; } = OperModeEnum.Read.ToString();
|
||||
|
||||
public bool AlarmEnable { get; set; }
|
||||
|
||||
public string AlarmValue { get; set; } = string.Empty;
|
||||
|
||||
public int AlarmGroup { get; set; }
|
||||
|
||||
public int AlarmLevel { get; set; }
|
||||
|
||||
[MaxLength(1000)]
|
||||
public string AlarmMsg { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(1000)]
|
||||
public string Json { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class TagSeed : IDataSeeding
|
||||
{
|
||||
public void DataSeeding(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<TagAddressDto>().HasData(
|
||||
new TagAddressDto
|
||||
{
|
||||
Id = 1,
|
||||
DeviceId = 1,
|
||||
Name = "Tag1",
|
||||
Address = "ns=4;s=L1RSTemp_Output1[0]",
|
||||
ArrayCount = 1,
|
||||
DataType = DataTypeEnum.Int16.ToString(),
|
||||
AlarmEnable = false,
|
||||
Desc = "Tag1",
|
||||
},
|
||||
new TagAddressDto
|
||||
{
|
||||
Id = 2,
|
||||
DeviceId = 1,
|
||||
Name = "Tag2",
|
||||
Address = "ns=4;s=L1RSTemp_Output1[1]",
|
||||
ArrayCount = 1,
|
||||
DataType = DataTypeEnum.Int16.ToString(),
|
||||
Desc = "Tag2",
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
63
Plugins/Driver/Cowain.Driver/Models/Dto/VarActionDto.cs
Normal file
63
Plugins/Driver/Cowain.Driver/Models/Dto/VarActionDto.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Dto;
|
||||
|
||||
[Table("var_action")]
|
||||
public class VarActionDto : BaseModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// 设备ID
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int DeviceId { get; set; }
|
||||
/// <summary>
|
||||
/// 变量Id
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int TagId { get; set; }
|
||||
/// <summary>
|
||||
/// 方法名称
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string ActionName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 方法参数
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(1000)]
|
||||
public string Param { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(500)]
|
||||
public string Desc { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 触发参数
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string ActionValue { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 条件
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string Condition { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
|
||||
21
Plugins/Driver/Cowain.Driver/Models/Enum/DataTypeEnum.cs
Normal file
21
Plugins/Driver/Cowain.Driver/Models/Enum/DataTypeEnum.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Enum;
|
||||
|
||||
public enum DataTypeEnum
|
||||
{
|
||||
Bit,
|
||||
Byte,
|
||||
Int16,
|
||||
Uint16,
|
||||
Int32,
|
||||
Uint32,
|
||||
Real,
|
||||
String,
|
||||
Wstring,
|
||||
DateTime
|
||||
}
|
||||
17
Plugins/Driver/Cowain.Driver/Models/Enum/OperModeEnum.cs
Normal file
17
Plugins/Driver/Cowain.Driver/Models/Enum/OperModeEnum.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Enum
|
||||
{
|
||||
public enum OperModeEnum
|
||||
{
|
||||
[Description("主动读")]
|
||||
Read,
|
||||
[Description("订阅")]
|
||||
Subscribe
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models.Enum;
|
||||
|
||||
public enum VariableStatusEnum
|
||||
{
|
||||
Good,
|
||||
AddressError,
|
||||
DataTypeError,
|
||||
ArroryCountError,
|
||||
ConnectError,
|
||||
Bad,
|
||||
UnKnow,
|
||||
}
|
||||
15
Plugins/Driver/Cowain.Driver/Models/TagJson.cs
Normal file
15
Plugins/Driver/Cowain.Driver/Models/TagJson.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models;
|
||||
|
||||
public class TagJson
|
||||
{
|
||||
/// <summary>
|
||||
/// 缩放比例
|
||||
/// </summary>
|
||||
public double Scale { get; set; } = 1.0;
|
||||
}
|
||||
16
Plugins/Driver/Cowain.Driver/Models/VariableAction.cs
Normal file
16
Plugins/Driver/Cowain.Driver/Models/VariableAction.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Models;
|
||||
|
||||
public class VariableAction
|
||||
{
|
||||
public VariableAction(string action, string param, VariableViewModel variable)
|
||||
{
|
||||
Action = action;
|
||||
Param = param;
|
||||
Variable = variable;
|
||||
}
|
||||
public string Action { get; set; }
|
||||
public string Param { get; set; }
|
||||
public VariableViewModel Variable { get; set; }
|
||||
}
|
||||
79
Plugins/Driver/Cowain.Driver/Plugin.Cowain.Driver.csproj
Normal file
79
Plugins/Driver/Cowain.Driver/Plugin.Cowain.Driver.csproj
Normal file
@@ -0,0 +1,79 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!--<Import Project="../../../Directory.Version.props" />-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<!-- 确保 NuGet 包的 DLL 被复制 -->
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="i18n\en-US.json" />
|
||||
<None Remove="i18n\zh-CN.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="i18n\en-US.json" />
|
||||
<AvaloniaResource Include="i18n\zh-CN.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HslCommunication" />
|
||||
<PackageReference Include="Ke.Bee.Localization" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Semi.Avalonia" />
|
||||
<PackageReference Include="Semi.Avalonia.ColorPicker" />
|
||||
<PackageReference Include="Semi.Avalonia.DataGrid" />
|
||||
<PackageReference Include="Irihi.Ursa" />
|
||||
<PackageReference Include="Irihi.Ursa.Themes.Semi" />
|
||||
<PackageReference Include="Xaml.Behaviors.Avalonia" />
|
||||
<PackageReference Include="Xaml.Behaviors.Interactivity" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Cowain.Base\Cowain.Base.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Views\ActionManagementView.axaml.cs">
|
||||
<DependentUpon>ActionManagementView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\VariableMonitorView.axaml.cs">
|
||||
<DependentUpon>VariableMonitorView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\TagManagementView.axaml.cs">
|
||||
<DependentUpon>TagManagementView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\DriverSelectedDialog.axaml.cs">
|
||||
<DependentUpon>DriverSelectedDialog.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyFilesToDestination" AfterTargets="AfterBuild" Condition="true">
|
||||
<!-- 确定相对路径到目标目录 -->
|
||||
<PropertyGroup>
|
||||
<!-- 这里使用 MSBuild 的内置属性来构造正确的路径 -->
|
||||
<TargetDirectory>../../../Cowain.TestProject/Plugins/Driver</TargetDirectory>
|
||||
</PropertyGroup>
|
||||
<!-- 确保目标目录存在 -->
|
||||
<MakeDir Directories="$(TargetDirectory)" Condition="!Exists('$(TargetDirectory)')" />
|
||||
|
||||
<ItemGroup>
|
||||
<Source1 Include="i18n/*.*" />
|
||||
<Source2 Include="Configs/*.*" />
|
||||
<FilesToCopy Include="$(OutputPath)**/Plugin.Cowain.Driver.dll" />
|
||||
<FilesToCopy Include="$(OutputPath)**/Plugin.Cowain.Driver.pdb" />
|
||||
<FilesToCopy Include="$(OutputPath)**/Plugin.Cowain.Driver.deps.json" />
|
||||
<FilesToCopy Include="$(OutputPath)**/HslCommunication.dll" />
|
||||
<FilesToCopy Include="$(OutputPath)**/Newtonsoft.Json.dll" />
|
||||
<FilesToCopy Include="$(OutputPath)**/System.IO.Ports.dll" />
|
||||
</ItemGroup>
|
||||
<!-- 复制文件到目标目录 -->
|
||||
<Copy SourceFiles="@(FilesToCopy)" DestinationFolder="$(TargetDirectory)/%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(Source1)" DestinationFolder="$(TargetDirectory)/i18n/" />
|
||||
<Copy SourceFiles="@(Source2)" DestinationFolder="$(TargetDirectory)/Configs/" />
|
||||
<!-- 输出TargetDirectory -->
|
||||
<Message Text="TargetDirectory: $(TargetDirectory)" Importance="high" />
|
||||
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
197
Plugins/Driver/Cowain.Driver/ReadWriteExtensions.cs
Normal file
197
Plugins/Driver/Cowain.Driver/ReadWriteExtensions.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.Models;
|
||||
using HslCommunication;
|
||||
using HslCommunication.BasicFramework;
|
||||
using HslCommunication.Core;
|
||||
using HslCommunication.Profinet.Siemens;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
|
||||
namespace Plugin.Cowain.Driver;
|
||||
|
||||
public static class ReadWriteExtensions
|
||||
{
|
||||
private static readonly ILogger _logger = ServiceLocator.GetRequiredService<ILogger>();
|
||||
public static ResultModel<T1> ToResultModel<T1, T2>(OperateResult<T2> read) where T1 : notnull where T2 : notnull
|
||||
{
|
||||
if (!read.IsSuccess) return ResultModel<T1>.Error(read.Message);
|
||||
return ResultModel<T1>.Success((T1)(object)read.Content);
|
||||
}
|
||||
public static ResultModel ToResultModel(this OperateResult operate)
|
||||
{
|
||||
if (!operate.IsSuccess) return ResultModel.Error(operate.Message);
|
||||
return ResultModel.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 如果S7读取的字符串不对,使用这个方法读取
|
||||
/// </summary>
|
||||
public static async Task<OperateResult<string>> ReadS7StringAsync(SiemensS7Net plc, string address, ushort length)
|
||||
{
|
||||
var readBytes = await plc.ReadAsync(address, length);
|
||||
if (!readBytes.IsSuccess)
|
||||
{
|
||||
return new OperateResult<string>(readBytes.Message);
|
||||
}
|
||||
byte[] data = readBytes.Content;
|
||||
if (data.Length > 2)
|
||||
{
|
||||
byte[] strData = new byte[data[1]];
|
||||
Array.Copy(data, 2, strData, 0, strData.Length);
|
||||
return OperateResult.CreateSuccessResult<string>(Encoding.ASCII.GetString(strData));
|
||||
}
|
||||
else
|
||||
{
|
||||
//读取错误
|
||||
return new OperateResult<string>("数据长度错误");
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetS7String(this byte[] data)
|
||||
{
|
||||
if (data.Length > 2)
|
||||
{
|
||||
if (data.Length < data[1] + 2)
|
||||
{
|
||||
return string.Empty; // 数据长度不足
|
||||
}
|
||||
byte[] strData = new byte[data[1]];
|
||||
Array.Copy(data, 2, strData, 0, strData.Length);
|
||||
return Encoding.ASCII.GetString(strData);
|
||||
}
|
||||
else
|
||||
{
|
||||
//读取错误
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetS7WString(this byte[] data)
|
||||
{
|
||||
if (data.Length > 4)
|
||||
{
|
||||
ushort len = (ushort)(4 + (data[2] * 256 + data[3]) * 2);
|
||||
if (data.Length < len + 4)
|
||||
{
|
||||
return string.Empty; // 数据长度不足
|
||||
}
|
||||
byte[] strData = new byte[len];
|
||||
Array.Copy(data, 4, strData, 0, strData.Length);
|
||||
return Encoding.Unicode.GetString(SoftBasic.BytesReverseByWord(strData));
|
||||
}
|
||||
else
|
||||
{
|
||||
//读取错误
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ResultModel> WriteValuesAysnc(this IReadWriteDevice plc, string address, short[] value, int retryCount = 5)
|
||||
{
|
||||
byte[] bytes = new byte[value.Length * 2];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
plc.ByteTransform.TransByte(value[i]).CopyTo(bytes, i * 2);
|
||||
}
|
||||
return await plc.WriteValuesAysnc(address, bytes, retryCount);
|
||||
}
|
||||
|
||||
public static async Task<ResultModel> WriteValuesAysnc(this IReadWriteDevice plc, string address, byte[] value, int retryCount = 5)
|
||||
{
|
||||
int baseDelay = 100; // 基础延迟100ms
|
||||
var random = new Random();
|
||||
ResultModel? result = null;
|
||||
for (int retry = 0; retry < retryCount; retry++)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = ToResultModel(await plc.WriteAsync(address, value));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
if (retry > 1)
|
||||
{
|
||||
_logger.LogInformation($"ReadWriteExtensions写PLC地址:{address} 数据成功,第{retry + 1}次尝试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"ReadWriteExtensions写PLC地址:{address} 数据失败:{result.ErrorMessage},第{retry + 1}次尝试");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result = ResultModel.Error(ex.Message);
|
||||
_logger.LogError(ex, $"ReadWriteExtensions写PLC地址:{address} 数据失败:{ex.Message},第{retry + 1}次尝试");
|
||||
}
|
||||
// 计算指数退避+抖动
|
||||
int jitter = random.Next(0, 100); // 0~100ms
|
||||
int delay = baseDelay * (int)Math.Pow(2, retry) + jitter;
|
||||
await Task.Delay(delay);
|
||||
}
|
||||
return result ?? ResultModel.Error("未知错误");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只支持常规数据类型,不支持字符串
|
||||
/// </summary>
|
||||
public static async Task<ResultModel<T>> ReadValuesAsync<T>(this IReadWriteDevice plc, string address, ushort count = 1) where T : notnull
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
// 处理数组类型
|
||||
if (count > 1)
|
||||
{
|
||||
return await ReadArrayAsync<T>(plc, address, count);
|
||||
}
|
||||
|
||||
// 处理单个值类型
|
||||
return type switch
|
||||
{
|
||||
Type t when t == typeof(bool) => ToResultModel<T, bool>(await plc.ReadBoolAsync(address)),
|
||||
Type t when t == typeof(byte) => await ReadByteAsync<T>(plc, address),
|
||||
Type t when t == typeof(short) => ToResultModel<T, short>(await plc.ReadInt16Async(address)),
|
||||
Type t when t == typeof(ushort) => ToResultModel<T, ushort>(await plc.ReadUInt16Async(address)),
|
||||
Type t when t == typeof(int) => ToResultModel<T, int>(await plc.ReadInt32Async(address)),
|
||||
Type t when t == typeof(uint) => ToResultModel<T, uint>(await plc.ReadUInt32Async(address)),
|
||||
Type t when t == typeof(long) => ToResultModel<T, long>(await plc.ReadInt64Async(address)),
|
||||
Type t when t == typeof(ulong) => ToResultModel<T, ulong>(await plc.ReadUInt64Async(address)),
|
||||
Type t when t == typeof(float) => ToResultModel<T, float>(await plc.ReadFloatAsync(address)),
|
||||
Type t when t == typeof(double) => ToResultModel<T, double>(await plc.ReadDoubleAsync(address)),
|
||||
_ => ResultModel<T>.Error("不支持的类型: " + type.Name)
|
||||
};
|
||||
}
|
||||
|
||||
// 处理byte类型的特殊读取逻辑
|
||||
private static async Task<ResultModel<T>> ReadByteAsync<T>(IReadWriteDevice plc, string address) where T : notnull
|
||||
{
|
||||
var result = await plc.ReadAsync(address, 1);
|
||||
if (!result.IsSuccess) return ResultModel<T>.Error(result.Message);
|
||||
return ResultModel<T>.Success((T)(object)result.Content[0]);
|
||||
}
|
||||
|
||||
// 处理数组读取的辅助方法
|
||||
private static async Task<ResultModel<T>> ReadArrayAsync<T>(IReadWriteDevice plc, string address, ushort count) where T : notnull
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
// 处理数组类型
|
||||
return type switch
|
||||
{
|
||||
Type t when t == typeof(bool[]) => ToResultModel<T, bool[]>(await plc.ReadBoolAsync(address, (ushort)count)),
|
||||
Type t when t == typeof(byte[]) => ToResultModel<T, byte[]>(await plc.ReadAsync(address, (ushort)count)),
|
||||
Type t when t == typeof(short[]) => ToResultModel<T, short[]>(await plc.ReadInt16Async(address, (ushort)count)),
|
||||
Type t when t == typeof(ushort[]) => ToResultModel<T, ushort[]>(await plc.ReadUInt16Async(address, (ushort)count)),
|
||||
Type t when t == typeof(int[]) => ToResultModel<T, int[]>(await plc.ReadInt32Async(address, (ushort)count)),
|
||||
Type t when t == typeof(uint[]) => ToResultModel<T, uint[]>(await plc.ReadUInt32Async(address, (ushort)count)),
|
||||
Type t when t == typeof(long[]) => ToResultModel<T, long[]>(await plc.ReadInt64Async(address, (ushort)count)),
|
||||
Type t when t == typeof(ulong[]) => ToResultModel<T, ulong[]>(await plc.ReadUInt64Async(address, (ushort)count)),
|
||||
Type t when t == typeof(float[]) => ToResultModel<T, float[]>(await plc.ReadFloatAsync(address, (ushort)count)),
|
||||
Type t when t == typeof(double[]) => ToResultModel<T, double[]>(await plc.ReadDoubleAsync(address, (ushort)count)),
|
||||
_ => ResultModel<T>.Error("不支持的数组类型: " + type.Name)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
73
Plugins/Driver/Cowain.Driver/Services/ActionPluginService.cs
Normal file
73
Plugins/Driver/Cowain.Driver/Services/ActionPluginService.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.Attributes;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public class ActionPluginService : IActionPluginService
|
||||
{
|
||||
private readonly IEnumerable<IActionCondition> _actionConditions;
|
||||
private readonly IEnumerable<IVariableAction> _variableActions;
|
||||
public ActionPluginService(IEnumerable<IActionCondition> actionConditions, IEnumerable<IVariableAction> variableActions)
|
||||
{
|
||||
_actionConditions = actionConditions;
|
||||
_variableActions = variableActions;
|
||||
}
|
||||
|
||||
public List<ActionInfo>? GetActions()
|
||||
{
|
||||
var infos = new List<ActionInfo>();
|
||||
foreach (var item in _variableActions)
|
||||
{
|
||||
var action = item.GetType();
|
||||
var attribute = action.GetCustomAttributes(typeof(ActionAttribute), false).FirstOrDefault() as ActionAttribute;
|
||||
if (attribute != null)
|
||||
{
|
||||
var driverInfo = new ActionInfo
|
||||
{
|
||||
Name = attribute.Name,
|
||||
Desc = attribute.Desc,
|
||||
};
|
||||
infos.Add(driverInfo);
|
||||
}
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
public List<ConditionInfo>? GetConditions()
|
||||
{
|
||||
var infos = new List<ConditionInfo>();
|
||||
foreach (var item in _actionConditions)
|
||||
{
|
||||
var action = item.GetType();
|
||||
var attribute = action.GetCustomAttributes(typeof(ConditionAttribute), false).FirstOrDefault() as ConditionAttribute;
|
||||
if (attribute != null)
|
||||
{
|
||||
var driverInfo = new ConditionInfo
|
||||
{
|
||||
Name = attribute.Name,
|
||||
Desc = attribute.Desc,
|
||||
};
|
||||
infos.Add(driverInfo);
|
||||
}
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
|
||||
public IActionCondition? GetCondition(string name)
|
||||
{
|
||||
var condition = _actionConditions.FirstOrDefault(d => d.GetType().GetCustomAttributes(typeof(ConditionAttribute), false).FirstOrDefault() is ConditionAttribute attribute && attribute.Name == name);
|
||||
return condition;
|
||||
}
|
||||
|
||||
public IVariableAction? GetAction(string name)
|
||||
{
|
||||
var action = _variableActions.FirstOrDefault(d => d.GetType().GetCustomAttributes(typeof(ActionAttribute), false).FirstOrDefault() is ActionAttribute attribute && attribute.Name == name);
|
||||
return action;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
164
Plugins/Driver/Cowain.Driver/Services/ActionService.cs
Normal file
164
Plugins/Driver/Cowain.Driver/Services/ActionService.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Dto;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public class ActionService : BaseService, IActionService
|
||||
{
|
||||
public ActionService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<List<VarActionViewModel>> GetDeviceActionsAsync(int deviceId)
|
||||
{
|
||||
var actions = await QueryAsync<VarActionDto>(x => x.DeviceId == deviceId);
|
||||
var result = actions.Select(a => new VarActionViewModel
|
||||
{
|
||||
Id = a.Id,
|
||||
DeviceId = a.DeviceId,
|
||||
TagId = a.TagId,
|
||||
ActionName = a.ActionName,
|
||||
Param = a.Param,
|
||||
ActionValue = a.ActionValue,
|
||||
Desc = a.Desc,
|
||||
Condition = a.Condition
|
||||
}).ToList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<DeviceViewModel>> GetDeviceAsync()
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var devices = await dbContext.Set<DeviceDto>().ToListAsync();
|
||||
var tagAddresses = await dbContext.Set<TagAddressDto>().ToListAsync();
|
||||
var varActions = await dbContext.Set<VarActionDto>().ToListAsync();
|
||||
|
||||
var deviceViewModels = devices.Select(device => new DeviceViewModel
|
||||
{
|
||||
Id = device.Id,
|
||||
DeviceName = device.DeviceName,
|
||||
DriverName = device.DriverName,
|
||||
DeviceType = device.DeviceType,
|
||||
Variables = new ObservableCollection<VariableViewModel>(
|
||||
tagAddresses.Where(tag => tag.DeviceId == device.Id).Select(tag => new VariableViewModel
|
||||
{
|
||||
Id = tag.Id,
|
||||
DeviceId = tag.DeviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
Name = tag.Name,
|
||||
Address = tag.Address,
|
||||
Desc = tag.Desc,
|
||||
}).ToList()
|
||||
),
|
||||
VarActions = new ObservableCollection<VarActionViewModel>(
|
||||
varActions.Where(action => action.DeviceId == device.Id).Select(action => new VarActionViewModel
|
||||
{
|
||||
Id = action.Id,
|
||||
DeviceId = action.DeviceId,
|
||||
TagId = action.TagId,
|
||||
ActionName = action.ActionName,
|
||||
Param = action.Param,
|
||||
ActionValue = action.ActionValue,
|
||||
Desc = action.Desc,
|
||||
Condition = action.Condition
|
||||
}).ToList()
|
||||
)
|
||||
}).ToList();
|
||||
|
||||
return deviceViewModels;
|
||||
}
|
||||
|
||||
public async Task<ResultModel> AddActionAsync(VarActionViewModel varAction)
|
||||
{
|
||||
//using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
//var existingAction = await dbContext.Set<VarActionDto>()
|
||||
// .FirstOrDefaultAsync(a => a.DeviceId == varAction.DeviceId && a.TagId == varAction.TagId);
|
||||
var existingAction = await FirstOrDefaultAsync<VarActionDto>(x => x.DeviceId == varAction.DeviceId && x.TagId == varAction.TagId);
|
||||
if (existingAction != null)
|
||||
{
|
||||
return ResultModel.Error("Action with the same DeviceId and TagId already exists.");
|
||||
}
|
||||
|
||||
var newAction = new VarActionDto
|
||||
{
|
||||
DeviceId = varAction.DeviceId,
|
||||
TagId = varAction.TagId,
|
||||
ActionName = varAction.ActionName,
|
||||
Param = varAction.Param,
|
||||
ActionValue = varAction.ActionValue,
|
||||
Desc = varAction.Desc,
|
||||
Condition = varAction.Condition
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await InsertAsync<VarActionDto>(newAction);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("Action added successfully");
|
||||
}
|
||||
return ResultModel.Error("Failed to add Action.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while adding the Action: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultModel> DeleteActionAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await DeleteAsync<VarActionDto>(id);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("Action delete successfully");
|
||||
}
|
||||
return ResultModel.Error("Failed to delete Action.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while deleting the Action: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultModel> UpdateActionAsync(VarActionViewModel varAction)
|
||||
{
|
||||
var existingAction = await FirstOrDefaultAsync<VarActionDto>(a => a.Id == varAction.Id);
|
||||
if (existingAction == null)
|
||||
{
|
||||
return ResultModel.Error("Action not found", 404);
|
||||
}
|
||||
|
||||
existingAction.DeviceId = varAction.DeviceId;
|
||||
existingAction.TagId = varAction.TagId;
|
||||
existingAction.ActionName = varAction.ActionName;
|
||||
existingAction.Param = varAction.Param;
|
||||
existingAction.ActionValue = varAction.ActionValue;
|
||||
existingAction.Desc = varAction.Desc;
|
||||
existingAction.Condition = varAction.Condition;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await UpdateAsync<VarActionDto>(existingAction);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("Action Update successfully");
|
||||
}
|
||||
return ResultModel.Error("Failed to Update Action.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while updating the Action: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
98
Plugins/Driver/Cowain.Driver/Services/AlarmGroupService.cs
Normal file
98
Plugins/Driver/Cowain.Driver/Services/AlarmGroupService.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Dto;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public class AlarmGroupService : BaseService, IAlarmGroupService
|
||||
{
|
||||
public AlarmGroupService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<ResultModel> AddAsync(AlarmGroupViewModel model)
|
||||
{
|
||||
AlarmGroupDto alarm = new AlarmGroupDto
|
||||
{
|
||||
Name = model.Name,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await InsertAsync<AlarmGroupDto>(alarm);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("alarm added successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel.Error("failed to add alarm", 500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while adding the alarm: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<AlarmGroupViewModel>> GetAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = await FindAsync<AlarmGroupDto>();
|
||||
if (entities == null || entities.Count == 0)
|
||||
{
|
||||
return new List<AlarmGroupViewModel>();
|
||||
}
|
||||
|
||||
var viewModels = entities
|
||||
.Select(e => new AlarmGroupViewModel
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return viewModels;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 遇到异常时返回空列表;具体日志记录由调用方或框架处理(此处保持方法简洁)
|
||||
return new List<AlarmGroupViewModel>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultModel> UpdateAsync(AlarmGroupViewModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await FindAsync<AlarmGroupDto>(model.Id);
|
||||
if (existing == null)
|
||||
{
|
||||
return ResultModel.Error("alarm not found", 404);
|
||||
}
|
||||
var result = await UpdateAsync<AlarmGroupDto>(existing);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("alarm updated successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel.Error("failed to update alarm", 500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while updating the alarm: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Plugins/Driver/Cowain.Driver/Services/AlarmLevelService.cs
Normal file
100
Plugins/Driver/Cowain.Driver/Services/AlarmLevelService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Dto;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public class AlarmLevelService : BaseService, IAlarmLevelService
|
||||
{
|
||||
public AlarmLevelService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<ResultModel> AddAsync(AlarmLevelViewModel model)
|
||||
{
|
||||
AlarmLevelDto alarm = new AlarmLevelDto
|
||||
{
|
||||
Color = model.Color,
|
||||
Name = model.Name,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await InsertAsync<AlarmLevelDto>(alarm);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("alarm added successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel.Error("failed to add alarm", 500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while adding the alarm: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<AlarmLevelViewModel>> GetAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = await FindAsync<AlarmLevelDto>();
|
||||
if (entities == null || entities.Count == 0)
|
||||
{
|
||||
return new List<AlarmLevelViewModel>();
|
||||
}
|
||||
|
||||
var viewModels = entities
|
||||
.Select(e => new AlarmLevelViewModel
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name,
|
||||
Color = e.Color
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return viewModels;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 遇到异常时返回空列表;具体日志记录由调用方或框架处理(此处保持方法简洁)
|
||||
return new List<AlarmLevelViewModel>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultModel> UpdateAsync(AlarmLevelViewModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await FindAsync<AlarmLevelDto>(model.Id);
|
||||
if (existing == null)
|
||||
{
|
||||
return ResultModel.Error("alarm not found", 404);
|
||||
}
|
||||
var result = await UpdateAsync<AlarmLevelDto>(existing);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("alarm updated successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel.Error("failed to update alarm", 500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while updating the alarm: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
181
Plugins/Driver/Cowain.Driver/Services/AlarmService.cs
Normal file
181
Plugins/Driver/Cowain.Driver/Services/AlarmService.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.Services;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Dto;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public class AlarmService : BaseService, IAlarmService
|
||||
{
|
||||
public AlarmService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<ResultModel> AddAsync(AlarmViewModel model)
|
||||
{
|
||||
AlarmHistoryDto alarm = new AlarmHistoryDto
|
||||
{
|
||||
TagId = model.TagId,
|
||||
Status = true,
|
||||
StartTime = DateTime.Now,
|
||||
Group = model.Group,
|
||||
Level = model.Level,
|
||||
Desc = model.Desc
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await InsertAsync<AlarmHistoryDto>(alarm);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("alarm added successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel.Error("failed to add alarm", 500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while adding the alarm: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultModel> CancelAsync(AlarmViewModel model)
|
||||
{
|
||||
|
||||
var existing = await FindAsync<AlarmHistoryDto>(model.Id);
|
||||
if (existing == null)
|
||||
{
|
||||
return ResultModel.Error("alarm not found", 404);
|
||||
}
|
||||
existing.Status = false;
|
||||
existing.StopTime = DateTime.Now;
|
||||
try
|
||||
{
|
||||
var result = await UpdateAsync<AlarmHistoryDto>(existing);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("alarm updated successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel.Error("failed to update alarm", 500);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while updating the alarm: {ex.Message}", 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<(List<AlarmViewModel>, int totals)> GetAlarmAsync(int pageIndex, int pageSize, DateTime? startTime, DateTime? endTime)
|
||||
{
|
||||
// 1. 构造开始时间:有值则取日期+00:00:01,无值则取最小时间(不限制开始)
|
||||
DateTime startDateTime = startTime.HasValue
|
||||
? startTime.Value.Date.AddSeconds(1) // 日期部分 + 00:00:01
|
||||
: DateTime.MinValue;
|
||||
|
||||
// 2. 构造结束时间:有值则取日期+23:59:59,无值则取最大时间(不限制结束)
|
||||
DateTime endDateTime = endTime.HasValue
|
||||
? endTime.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59) // 日期部分 + 23:59:59
|
||||
: DateTime.MaxValue;
|
||||
|
||||
var query = await QueryAsync<AlarmHistoryDto>(x => x.StartTime >= startDateTime && x.StartTime <= endDateTime);
|
||||
|
||||
var result = query.Select(alarm => new AlarmViewModel
|
||||
{
|
||||
Id = alarm.Id,
|
||||
TagId = alarm.TagId,
|
||||
Group = alarm.Group,
|
||||
Status = alarm.Status,
|
||||
StartTime = alarm.StartTime,
|
||||
StopTime = alarm.StopTime,
|
||||
Level = alarm.Level,
|
||||
Desc = alarm.Desc
|
||||
});
|
||||
|
||||
var total = query.Count();
|
||||
var tagViewModels = result.OrderByDescending(a => a.StartTime).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
|
||||
return (tagViewModels, total);
|
||||
}
|
||||
|
||||
public async Task<(List<AlarmViewModel>, int totals)> GetAlarmAsync(
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
List<int>? groups = null,
|
||||
List<int>? levels = null)
|
||||
{
|
||||
// 1. 时间范围构造(保留原有逻辑,增加空值注释)
|
||||
DateTime startDateTime = startTime.HasValue
|
||||
? startTime.Value.Date.AddSeconds(1) // 开始日期 00:00:01
|
||||
: DateTime.MinValue; // 无开始时间则不限制
|
||||
|
||||
DateTime endDateTime = endTime.HasValue
|
||||
? endTime.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59) // 结束日期 23:59:59
|
||||
: DateTime.MaxValue; // 无结束时间则不限制
|
||||
|
||||
// 2. 构建核心查询条件(整合时间+分组+级别筛选,查询层面过滤,性能最优)
|
||||
var query = await QueryAsync<AlarmHistoryDto>(x =>
|
||||
// 时间范围筛选(原有)
|
||||
x.StartTime >= startDateTime && x.StartTime <= endDateTime &&
|
||||
// 分组筛选:groups不为null且有元素时才过滤,否则不限制
|
||||
(groups == null || !groups.Any() || groups.Contains(x.Group)) &&
|
||||
// 级别筛选:levels不为null且有元素时才过滤,否则不限制
|
||||
(levels == null || !levels.Any() || levels.Contains(x.Level)));
|
||||
|
||||
// 3. 转换为ViewModel(保留原有逻辑)
|
||||
var result = query.Select(alarm => new AlarmViewModel
|
||||
{
|
||||
Id = alarm.Id,
|
||||
TagId = alarm.TagId,
|
||||
Group = alarm.Group,
|
||||
Status = alarm.Status,
|
||||
StartTime = alarm.StartTime,
|
||||
StopTime = alarm.StopTime,
|
||||
Level = alarm.Level,
|
||||
Desc = alarm.Desc
|
||||
});
|
||||
|
||||
// 4. 分页优化:处理pageIndex<=0的边界情况,避免负数Skip
|
||||
int validPageIndex = Math.Max(pageIndex, 1);
|
||||
int validPageSize = Math.Max(pageSize, 1); // 避免pageSize<=0导致的异常
|
||||
|
||||
// 5. 计算筛选后的总数 + 分页查询(总数必须基于筛选后的结果)
|
||||
int total = query.Count();
|
||||
List<AlarmViewModel> tagViewModels = result
|
||||
.OrderByDescending(a => a.StartTime)
|
||||
.Skip((validPageIndex - 1) * validPageSize)
|
||||
.Take(validPageSize)
|
||||
|
||||
.ToList();
|
||||
|
||||
return (tagViewModels, total);
|
||||
}
|
||||
|
||||
public async Task<AlarmViewModel?> GetInAlarmAsync(int tagId)
|
||||
{
|
||||
var query = await QueryAsync<AlarmHistoryDto>(x => x.TagId == tagId && x.StopTime == null);
|
||||
var result = query.Select(alarm => new AlarmViewModel
|
||||
{
|
||||
Id = alarm.Id,
|
||||
TagId = alarm.TagId,
|
||||
Group = alarm.Group,
|
||||
Status = alarm.Status,
|
||||
StartTime = alarm.StartTime,
|
||||
StopTime = alarm.StopTime,
|
||||
Level = alarm.Level,
|
||||
Desc = alarm.Desc
|
||||
}).FirstOrDefault();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
229
Plugins/Driver/Cowain.Driver/Services/DeviceService.cs
Normal file
229
Plugins/Driver/Cowain.Driver/Services/DeviceService.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Dto;
|
||||
using Cowain.Base.Services;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public class DeviceService : BaseService, IDeviceService
|
||||
{
|
||||
public DeviceService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
|
||||
{
|
||||
}
|
||||
public async Task<ResultModel> AddDeviceAsync(DeviceViewModel? device)
|
||||
{
|
||||
if (device == null)
|
||||
{
|
||||
return ResultModel.Error("device cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.DeviceName))
|
||||
{
|
||||
return ResultModel.Error("deviceName cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.DeviceType))
|
||||
{
|
||||
return ResultModel.Error("deviceType cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.DriverName))
|
||||
{
|
||||
return ResultModel.Error("driverName cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.Param))
|
||||
{
|
||||
return ResultModel.Error("param cannot be null");
|
||||
}
|
||||
var existingDevice = await FirstOrDefaultAsync<DeviceDto>(x => x.DeviceName == device.DeviceName);
|
||||
if (existingDevice != null)
|
||||
{
|
||||
return ResultModel.Error("deviceName already exists");
|
||||
}
|
||||
|
||||
var entity = new DeviceDto
|
||||
{
|
||||
Id = device.Id,
|
||||
DeviceName = device.DeviceName,
|
||||
DeviceType = device.DeviceType,
|
||||
DriverName = device.DriverName,
|
||||
Param = device.Param,
|
||||
Desc = device.Desc,
|
||||
Enable = device.Enable
|
||||
};
|
||||
var result = await InsertAsync<DeviceDto>(entity);
|
||||
if (result > 0)
|
||||
{
|
||||
return ResultModel.Success("device added successfully");
|
||||
}
|
||||
return ResultModel.Error("device added error");
|
||||
}
|
||||
public async Task<ResultModel> UpdateDeviceAsync(DeviceViewModel? device)
|
||||
{
|
||||
if (device == null)
|
||||
{
|
||||
return ResultModel.Error("device cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.DeviceName))
|
||||
{
|
||||
return ResultModel.Error("deviceName cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.DeviceType))
|
||||
{
|
||||
return ResultModel.Error("deviceType cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.DriverName))
|
||||
{
|
||||
return ResultModel.Error("driverName cannot be null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(device.Param))
|
||||
{
|
||||
return ResultModel.Error("param cannot be null");
|
||||
}
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var DbSet = dbContext.GetDbSet<DeviceDto>();
|
||||
var existingDevice = await DbSet.FirstOrDefaultAsync(x => x.Id == device.Id);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
return ResultModel.Error("device no exists");
|
||||
}
|
||||
var duplicateDevice = await DbSet.FirstOrDefaultAsync(x => x.DeviceName == device.DeviceName && x.Id != device.Id);
|
||||
if (duplicateDevice != null)
|
||||
{
|
||||
return ResultModel.Error("deviceName already exists");
|
||||
}
|
||||
existingDevice.DeviceName = device.DeviceName;
|
||||
existingDevice.DeviceType = device.DeviceType;
|
||||
existingDevice.Param = device.Param;
|
||||
existingDevice.DriverName = device.DriverName;
|
||||
existingDevice.Desc = device.Desc;
|
||||
existingDevice.Enable = device.Enable;
|
||||
existingDevice.UpdateTime = DateTime.Now;
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ResultModel.Success("device updated successfully");
|
||||
}
|
||||
public async Task<ResultModel> DeleteDeviceAsync(int id)
|
||||
{
|
||||
if (id <= 0)
|
||||
{
|
||||
return ResultModel.Error("id cannot be null");
|
||||
}
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var deviceSet = dbContext.GetDbSet<DeviceDto>();
|
||||
var tagSet = dbContext.GetDbSet<TagAddressDto>();
|
||||
|
||||
// 开启数据库事务,保证删除设备及其关联变量的原子性
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var existingDevice = await deviceSet.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return ResultModel.Error("device no exists");
|
||||
}
|
||||
// 删除与设备关联的 TagAddressDto(变量)
|
||||
var relatedTags = await tagSet.Where(t => t.DeviceId == id).ToListAsync();
|
||||
if (relatedTags.Count > 0)
|
||||
{
|
||||
tagSet.RemoveRange(relatedTags);
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
deviceSet.Remove(existingDevice);
|
||||
|
||||
// 一次性保存所有变更
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// 提交事务
|
||||
await transaction.CommitAsync();
|
||||
return ResultModel.Success("device deleted successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略回滚失败时的异常,返回主要异常信息
|
||||
}
|
||||
return ResultModel.Error($"delete failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultModel> DisableDeviceAsync(int id)
|
||||
{
|
||||
if (id <= 0)
|
||||
{
|
||||
return ResultModel.Error("id cannot be null");
|
||||
}
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var DbSet = dbContext.GetDbSet<DeviceDto>();
|
||||
var existingDevice = await DbSet.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
return ResultModel.Error("device no exists");
|
||||
}
|
||||
existingDevice.Enable = false;
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ResultModel.Success("device disabled successfully");
|
||||
}
|
||||
|
||||
public async Task<ResultModel> EnableDeviceAsync(int id)
|
||||
{
|
||||
if (id <= 0)
|
||||
{
|
||||
return ResultModel.Error("id cannot be null");
|
||||
}
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var DbSet = dbContext.GetDbSet<DeviceDto>();
|
||||
var existingDevice = await DbSet.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
return ResultModel.Error("device no exists");
|
||||
}
|
||||
existingDevice.Enable = true;
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ResultModel.Success("device enabled successfully");
|
||||
}
|
||||
|
||||
public async Task<List<DeviceViewModel>> GetAllAsync()
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var DbSet = dbContext.GetDbSet<DeviceDto>();
|
||||
var data = await DbSet.ToListAsync();
|
||||
return new List<DeviceViewModel>(data.Select(x => new DeviceViewModel
|
||||
{
|
||||
Id = x.Id,
|
||||
DeviceName = x.DeviceName,
|
||||
DeviceType = x.DeviceType,
|
||||
DriverName = x.DriverName,
|
||||
Enable = x.Enable,
|
||||
Param = x.Param,
|
||||
Desc = x.Desc,
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
public async Task<(List<DeviceViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize)
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var DbSet = dbContext.GetDbSet<DeviceDto>();
|
||||
var data = await DbSet.ToListAsync();
|
||||
var list = data.Skip((pageIndex - 1) * pageSize).Take(pageSize);
|
||||
return (new List<DeviceViewModel>(list.Select(x => new DeviceViewModel
|
||||
{
|
||||
Id = x.Id,
|
||||
DeviceName = x.DeviceName,
|
||||
DeviceType = x.DeviceType,
|
||||
DriverName = x.DriverName,
|
||||
Enable = x.Enable,
|
||||
Param = x.Param,
|
||||
Desc = x.Desc,
|
||||
})), data.Count());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
109
Plugins/Driver/Cowain.Driver/Services/DriverPluginService.cs
Normal file
109
Plugins/Driver/Cowain.Driver/Services/DriverPluginService.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Avalonia.Controls;
|
||||
using Cowain.Base.Helpers;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.Attributes;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
using System.Text.Json;
|
||||
using Ursa.Controls;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public partial class DriverPluginService : IDriverPluginService
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private readonly ILocalizer _l;
|
||||
public DriverPluginService(ILocalizer localizer, IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_l = localizer;
|
||||
}
|
||||
|
||||
public List<DriverInfo> GetDriverInfos()
|
||||
{
|
||||
var driverInfos = new List<DriverInfo>();
|
||||
if (DriverServiceExtensions.DriverTypes == null || DriverServiceExtensions.DriverTypes.Count == 0)
|
||||
{
|
||||
return driverInfos;
|
||||
}
|
||||
foreach (var driver in DriverServiceExtensions.DriverTypes)
|
||||
{
|
||||
var driverAttribute = driver.GetCustomAttributes(typeof(DriverAttribute), false).FirstOrDefault() as DriverAttribute;
|
||||
if (driverAttribute != null)
|
||||
{
|
||||
var driverInfo = new DriverInfo
|
||||
{
|
||||
Name = driverAttribute.DriverName,
|
||||
DeviceType = driverAttribute.DeviceType,
|
||||
Desc = driverAttribute.Desc,
|
||||
Group = driverAttribute.Group ?? "未分类"
|
||||
};
|
||||
driverInfos.Add(driverInfo);
|
||||
}
|
||||
}
|
||||
return driverInfos;
|
||||
}
|
||||
|
||||
|
||||
public IDriver? GetDriver(string driverName)
|
||||
{
|
||||
//var drivers = _serviceProvider.GetServices<IDriver>();
|
||||
var driver = DriverServiceExtensions.DriverTypes?.FirstOrDefault(d => d.GetCustomAttributes(typeof(DriverAttribute), false).FirstOrDefault() is DriverAttribute attribute && attribute.DriverName == driverName);
|
||||
if (driver == null) return null;
|
||||
return ActivatorUtilities.CreateInstance(_serviceProvider, driver) as IDriver;
|
||||
}
|
||||
|
||||
[LogAndSwallow]
|
||||
public async Task ParamEditDialogAsync(DeviceViewModel deviceViewModel)
|
||||
{
|
||||
//var drivers = _serviceProvider.GetServices<IDriver>();
|
||||
var driver = DriverServiceExtensions.DriverTypes?.FirstOrDefault(d => d.GetCustomAttributes(typeof(DriverAttribute), false).FirstOrDefault() is DriverAttribute attribute && attribute.DriverName == deviceViewModel.DriverName);
|
||||
|
||||
if (driver != null)
|
||||
{
|
||||
var paramAttribute = driver.GetCustomAttributes(typeof(DeviceParamAttribute), false).FirstOrDefault() as DeviceParamAttribute;
|
||||
if (paramAttribute != null)
|
||||
{
|
||||
var paramType = paramAttribute.Param;
|
||||
var controlType = paramAttribute.Control;
|
||||
var viewModelType = paramAttribute.DialogViewModel;
|
||||
var param = string.IsNullOrEmpty(deviceViewModel.Param) ? Activator.CreateInstance(paramType) : JsonSerializer.Deserialize(deviceViewModel.Param, paramType);
|
||||
if (param is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var view = Activator.CreateInstance(controlType) as Control;
|
||||
if (view is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var viewModel = Activator.CreateInstance(viewModelType, new object[] { param });
|
||||
if (view is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var options = new DialogOptions()
|
||||
{
|
||||
Title = _l["DeviceParam.Dialog.Title"],
|
||||
ShowInTaskBar = false,
|
||||
IsCloseButtonVisible = false,
|
||||
StartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Button = DialogButton.OKCancel,
|
||||
CanDragMove = true,
|
||||
CanResize = false,
|
||||
};
|
||||
|
||||
var ret = await Dialog.ShowCustomModal<bool>(view, viewModel, options: options);
|
||||
if (ret)
|
||||
{
|
||||
deviceViewModel.Param = JsonSerializer.Serialize(param, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
208
Plugins/Driver/Cowain.Driver/Services/TagService.cs
Normal file
208
Plugins/Driver/Cowain.Driver/Services/TagService.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using Cowain.Base.DBContext;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Dto;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services;
|
||||
|
||||
public class TagService : BaseService, ITagService
|
||||
{
|
||||
public TagService(IDbContextFactory<SqlDbContext> dbContextFactory) : base(dbContextFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<ResultModel> AddTagAsync(TagViewModel tag)
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var existingTag = await dbContext.Set<TagAddressDto>()
|
||||
.FirstOrDefaultAsync(t => t.DeviceId == tag.DeviceId && (t.Name == tag.Name || t.Address == tag.Address));
|
||||
|
||||
if (existingTag != null)
|
||||
{
|
||||
return ResultModel.Error("Tag with the same Name or Address already exists for the given DeviceId", 400);
|
||||
}
|
||||
|
||||
var newTag = new TagAddressDto
|
||||
{
|
||||
DeviceId = tag.DeviceId,
|
||||
Name = tag.Name,
|
||||
Address = tag.Address,
|
||||
Desc = tag.Desc,
|
||||
DataType = tag.DataType.ToString(),
|
||||
OperMode = tag.OperMode.ToString(),
|
||||
AlarmEnable = tag.AlarmEnable,
|
||||
AlarmValue = tag.AlarmValue,
|
||||
AlarmMsg = tag.AlarmMsg,
|
||||
AlarmGroup = tag.AlarmGroup,
|
||||
AlarmLevel= tag.AlarmLevel,
|
||||
Json = tag.Json,
|
||||
ArrayCount = tag.ArrayCount
|
||||
};
|
||||
|
||||
await dbContext.Set<TagAddressDto>().AddAsync(newTag);
|
||||
|
||||
try
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ResultModel.Success("Tag added successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while adding the tag: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResultModel> DeleteTagAsync(int id)
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var tag = await dbContext.Set<TagAddressDto>().FindAsync(id);
|
||||
if (tag == null)
|
||||
{
|
||||
return ResultModel.Error("Tag not found", 404);
|
||||
}
|
||||
|
||||
dbContext.Set<TagAddressDto>().Remove(tag);
|
||||
|
||||
try
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ResultModel.Success("Tag deleted successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while deleting the tag: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TagViewModel>> GetAllAsync()
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var tags = await (from tag in dbContext.Set<TagAddressDto>()
|
||||
join device in dbContext.Set<DeviceDto>() on tag.DeviceId equals device.Id
|
||||
select new
|
||||
{
|
||||
tag,
|
||||
device.DeviceName
|
||||
}).ToListAsync();
|
||||
|
||||
var tagViewModels = tags.Select(t => new TagViewModel
|
||||
{
|
||||
Id = t.tag.Id,
|
||||
DeviceId = t.tag.DeviceId,
|
||||
Name = t.tag.Name,
|
||||
Address = t.tag.Address,
|
||||
Desc = t.tag.Desc,
|
||||
DataType = t.tag.DataType,
|
||||
OperMode = t.tag.OperMode,
|
||||
AlarmEnable = t.tag.AlarmEnable,
|
||||
AlarmValue = t.tag.AlarmValue,
|
||||
AlarmMsg = t.tag.AlarmMsg,
|
||||
AlarmGroup=t.tag.AlarmGroup,
|
||||
AlarmLevel=t.tag.AlarmLevel,
|
||||
Json = t.tag.Json,
|
||||
ArrayCount = t.tag.ArrayCount
|
||||
}).ToList();
|
||||
|
||||
return tagViewModels ?? new List<TagViewModel>();
|
||||
}
|
||||
|
||||
public async Task<(List<TagViewModel>, int totals)> GetAllAsync(int pageIndex, int pageSize)
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var query = from tag in dbContext.Set<TagAddressDto>()
|
||||
join device in dbContext.Set<DeviceDto>() on tag.DeviceId equals device.Id
|
||||
select new
|
||||
{
|
||||
tag,
|
||||
device.DeviceName
|
||||
};
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var tagViewModels = await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
|
||||
var result = tagViewModels.Select(t => new TagViewModel
|
||||
{
|
||||
Id = t.tag.Id,
|
||||
DeviceId = t.tag.DeviceId,
|
||||
Name = t.tag.Name,
|
||||
Address = t.tag.Address,
|
||||
Desc = t.tag.Desc,
|
||||
DataType = t.tag.DataType,
|
||||
OperMode = t.tag.OperMode,
|
||||
AlarmEnable = t.tag.AlarmEnable,
|
||||
AlarmValue = t.tag.AlarmValue,
|
||||
AlarmMsg = t.tag.AlarmMsg,
|
||||
AlarmGroup = t.tag.AlarmGroup,
|
||||
AlarmLevel = t.tag.AlarmLevel,
|
||||
Json = t.tag.Json,
|
||||
ArrayCount = t.tag.ArrayCount
|
||||
}).ToList();
|
||||
|
||||
return (result, total);
|
||||
}
|
||||
|
||||
public async Task<List<TagViewModel>> GetDeviceTagsAsync(int deviceId)
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var tags = await (from tag in dbContext.Set<TagAddressDto>()
|
||||
where tag.DeviceId == deviceId
|
||||
select tag).ToListAsync();
|
||||
|
||||
var tagViewModels = tags.Select(t => new TagViewModel
|
||||
{
|
||||
Id = t.Id,
|
||||
DeviceId = t.DeviceId,
|
||||
Name = t.Name,
|
||||
Address = t.Address,
|
||||
Desc = t.Desc,
|
||||
DataType = t.DataType,
|
||||
OperMode = t.OperMode,
|
||||
AlarmEnable = t.AlarmEnable,
|
||||
AlarmValue = t.AlarmValue,
|
||||
AlarmMsg = t.AlarmMsg,
|
||||
AlarmGroup = t.AlarmGroup,
|
||||
AlarmLevel = t.AlarmLevel,
|
||||
Json = t.Json,
|
||||
ArrayCount = t.ArrayCount
|
||||
}).ToList();
|
||||
|
||||
return tagViewModels ?? new List<TagViewModel>();
|
||||
}
|
||||
|
||||
public async Task<ResultModel> UpdateTagAsync(TagViewModel tag)
|
||||
{
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
var existingTag = await dbContext.Set<TagAddressDto>().FindAsync(tag.Id);
|
||||
if (existingTag == null)
|
||||
{
|
||||
return ResultModel.Error("Tag not found", 404);
|
||||
}
|
||||
|
||||
existingTag.DeviceId = tag.DeviceId;
|
||||
existingTag.Name = tag.Name;
|
||||
existingTag.Address = tag.Address;
|
||||
existingTag.Desc = tag.Desc;
|
||||
existingTag.DataType = tag.DataType.ToString();
|
||||
existingTag.Json = tag.Json;
|
||||
existingTag.ArrayCount = tag.ArrayCount;
|
||||
existingTag.AlarmEnable = tag.AlarmEnable;
|
||||
existingTag.AlarmValue = tag.AlarmValue;
|
||||
existingTag.AlarmMsg = tag.AlarmMsg;
|
||||
existingTag.AlarmGroup = tag.AlarmGroup;
|
||||
existingTag.AlarmLevel = tag.AlarmLevel;
|
||||
existingTag.OperMode = tag.OperMode.ToString();
|
||||
|
||||
try
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ResultModel.Success("Tag updated successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ResultModel.Error($"An error occurred while updating the tag: {ex.Message}", 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
200
Plugins/Driver/Cowain.Driver/Services/VariableChannelService.cs
Normal file
200
Plugins/Driver/Cowain.Driver/Services/VariableChannelService.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Services
|
||||
{
|
||||
public class VariableChannelService : IVariableChannelService
|
||||
{
|
||||
// 核心:标记是否已关闭,初始为 false
|
||||
private bool _isChannelClosed;
|
||||
private SemaphoreSlim _semaphores = new SemaphoreSlim(1000);
|
||||
private readonly Channel<VariableAction> _channel;
|
||||
private readonly CancellationTokenSource _internalCts = new();
|
||||
private readonly IActionPluginService _actionPluginService;
|
||||
private readonly ILogger<VariableChannelService> _logger;
|
||||
public VariableChannelService(IActionPluginService actionPluginService, ILogger<VariableChannelService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_actionPluginService = actionPluginService;
|
||||
_channel = Channel.CreateUnbounded<VariableAction>();
|
||||
}
|
||||
|
||||
public void RegisterDeviceActions(DeviceViewModel device)
|
||||
{
|
||||
if (device.Variables == null)
|
||||
{
|
||||
_logger.LogError($"设备 {device.DeviceName} 的变量为空");
|
||||
return;
|
||||
}
|
||||
if (device.VarActions == null)
|
||||
{
|
||||
_logger.LogError($"设备 {device.DeviceName} 的动作列表为空");
|
||||
return;
|
||||
}
|
||||
var q = from v in device.Variables
|
||||
join va in device.VarActions on v.Id equals va.TagId
|
||||
select new { v, va };
|
||||
foreach (var item in q)
|
||||
{
|
||||
item.v.Register(x =>
|
||||
{
|
||||
RegisterAction(x, item.va);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void RegisterAction(VariableViewModel variable, VarActionViewModel varAction)
|
||||
{
|
||||
var condition = _actionPluginService.GetCondition(varAction.Condition);
|
||||
if (condition == null)
|
||||
{
|
||||
_logger.LogError($"条件插件未注册:{varAction.Condition}");
|
||||
return;
|
||||
}
|
||||
if (variable.Value == null)
|
||||
{
|
||||
_logger.LogError($"变量值为空:{variable.DeviceName}->{variable.Name}");
|
||||
return;
|
||||
}
|
||||
// 创建变量的副本,避免引用问题
|
||||
var variableCopy = new VariableViewModel
|
||||
{
|
||||
Id = variable.Id,
|
||||
DeviceId = variable.DeviceId,
|
||||
DeviceName = variable.DeviceName,
|
||||
Name = variable.Name,
|
||||
Address = variable.Address,
|
||||
Desc = variable.Desc,
|
||||
DataType = variable.DataType,
|
||||
ArrayCount = variable.ArrayCount,
|
||||
Value = variable.Value,
|
||||
OldValue = variable.OldValue,
|
||||
UpdateTime = variable.UpdateTime,
|
||||
Message = variable.Message,
|
||||
IsSuccess = variable.IsSuccess
|
||||
};
|
||||
//需要再设置一次,因为new的时候调用了OnDataChange方法,导致IsManualTrig被设未false
|
||||
if (condition.IsMatch(variableCopy, varAction.ActionValue))
|
||||
{
|
||||
// 条件匹配,入队列
|
||||
var va = new VariableAction(varAction.ActionName, varAction.Param, variableCopy);
|
||||
try
|
||||
{
|
||||
_ = _channel.Writer.WriteAsync(va).AsTask().ContinueWith(task =>
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
_logger.LogError(task.Exception, $"入队列发生错误:{variable.DeviceName}->{variable.Name}");
|
||||
}
|
||||
else if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
_logger.LogDebug($"入队列成功:{variable.DeviceName}->{variable.Name}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"入队列错误:{variable.DeviceName}->{variable.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 消费
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task ConsumeVariablesAsync()
|
||||
{
|
||||
var combinedToken = _internalCts.Token;
|
||||
//最大并行任务数量20
|
||||
|
||||
await foreach (var item in _channel.Reader.ReadAllAsync(combinedToken))
|
||||
{
|
||||
await _semaphores.WaitAsync(combinedToken);
|
||||
if (string.IsNullOrEmpty(item.Param))
|
||||
{
|
||||
_logger.LogError($"参数不能未空:{item.Action}");
|
||||
continue;
|
||||
}
|
||||
var action = _actionPluginService.GetAction(item.Action);
|
||||
if (action == null)
|
||||
{
|
||||
_logger.LogError($"变量动作未注册:{item.Action}");
|
||||
continue;
|
||||
}
|
||||
//执行,但不等待结果
|
||||
try
|
||||
{
|
||||
_ = action.ExecuteAsync(item, combinedToken).ContinueWith(task =>
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
_logger.LogError(task.Exception, $"执行动作时发生错误:{item.Action}");
|
||||
}
|
||||
else if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
_logger.LogDebug($"动作执行成功:{item.Action}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
_logger.LogError(ex, $"执行动作时发生错误:{item.Action}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphores.Release();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopConsumeVariablesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _semaphores.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_isChannelClosed)
|
||||
return;
|
||||
_logger.LogInformation($"软件关闭,触发取消消费");
|
||||
_internalCts.Cancel();
|
||||
Exception error = new();
|
||||
if (_channel.Writer.TryComplete(error))
|
||||
{
|
||||
_isChannelClosed = true;
|
||||
if (error != null)
|
||||
{
|
||||
_logger.LogError(error, "ChannelWriter 关闭时携带异常");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("ChannelWriter 已无法完成(可能已被关闭)");
|
||||
_isChannelClosed = true;
|
||||
}
|
||||
}
|
||||
catch (ChannelClosedException ex)
|
||||
{
|
||||
//捕获通道已关闭的异常,标记状态并记录日志
|
||||
_isChannelClosed = true;
|
||||
_logger.LogError(ex, "Channel 已关闭,调用 Complete 失败");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphores.Release();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Plugins/Driver/Cowain.Driver/UPGRADING.md
Normal file
3
Plugins/Driver/Cowain.Driver/UPGRADING.md
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
## 首次发布 1.0.0
|
||||
* 第一版
|
||||
@@ -0,0 +1,214 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using Plugin.Cowain.Driver.Services;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
using SixLabors.Fonts.Tables.AdvancedTypographic;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Ursa.Controls;
|
||||
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class ActionManagementViewModel : PageViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<DeviceViewModel>? _devices;
|
||||
|
||||
private DeviceViewModel? _selectedDevice;
|
||||
|
||||
public DeviceViewModel? SelectedDevice
|
||||
{
|
||||
get { return _selectedDevice; }
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedDevice, value);
|
||||
if (value != null && value.Variables != null)
|
||||
{
|
||||
Variables = new(value.Variables);
|
||||
OnPropertyChanged(nameof(Variables));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private readonly ILocalizer _l;
|
||||
private IActionService _actionService;
|
||||
private IActionPluginService _actionPlugin;
|
||||
|
||||
public static ObservableCollection<VariableViewModel>? Variables { get; set; }
|
||||
/// <summary>
|
||||
/// 动作列表
|
||||
/// </summary>
|
||||
public static List<ActionInfo>? Actions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 条件列表
|
||||
/// </summary>
|
||||
public static List<ConditionInfo>? Conditions { get; set; }
|
||||
public ActionManagementViewModel(ILocalizer localizer, IActionService actionService, IActionPluginService actionPlugin)
|
||||
{
|
||||
_l = localizer;
|
||||
_actionService = actionService;
|
||||
_actionPlugin = actionPlugin;
|
||||
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadedAsync()
|
||||
{
|
||||
//获取所有设备列表
|
||||
Actions = _actionPlugin.GetActions();
|
||||
Conditions = _actionPlugin.GetConditions();
|
||||
await RefreshAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Add()
|
||||
{
|
||||
SelectedDevice?.VarActions?.Add(new VarActionViewModel()
|
||||
{
|
||||
DeviceId = SelectedDevice.Id,
|
||||
ActionName = Actions?.FirstOrDefault()?.Name ?? string.Empty,
|
||||
Param = string.Empty,
|
||||
Desc = "New Action",
|
||||
ActionValue = "1",
|
||||
TagId = SelectedDevice.Variables?.FirstOrDefault()?.Id ?? 0,
|
||||
Condition = Conditions?.FirstOrDefault()?.Name ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteAsync(VarActionViewModel? varAction)
|
||||
{
|
||||
if (varAction == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (varAction.Id == 0)
|
||||
{
|
||||
SelectedDevice?.VarActions?.Remove(varAction);
|
||||
return;
|
||||
}
|
||||
var result = await MessageBox.ShowOverlayAsync(_l["DeleteDialog"], _l["Message.Info.Title"], button: MessageBoxButton.YesNo);
|
||||
if (result != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var deleteTag = await _actionService.DeleteActionAsync(varAction.Id);
|
||||
if (deleteTag.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["ActionManagement.Delete.Success"]);
|
||||
SelectedDevice?.VarActions?.Remove(varAction);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["ActionManagement.Delete.Error"] + ":" + deleteTag.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (SelectedDevice == null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["ActionManagement.Save.SelectedDeviceNull"]);
|
||||
return;
|
||||
}
|
||||
if (SelectedDevice.VarActions == null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["ActionManagement.Save.VarActionsNull"]);
|
||||
return;
|
||||
}
|
||||
List<ResultModel> tasks = new List<ResultModel>();
|
||||
foreach (var action in SelectedDevice.VarActions)
|
||||
{
|
||||
if (action.Id == 0)
|
||||
{
|
||||
var add = await _actionService.AddActionAsync(action);
|
||||
tasks.Add(add);
|
||||
}
|
||||
else
|
||||
{
|
||||
var update = await _actionService.UpdateActionAsync(action);
|
||||
tasks.Add(update);
|
||||
}
|
||||
}
|
||||
await RefreshAsync();
|
||||
if (tasks.All(x => x.IsSuccess))
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["ActionManagement.Save.Success"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["ActionManagement.Save.Error"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
var devices = await _actionService.GetDeviceAsync();
|
||||
// 使用Dispatcher确保UI线程更新
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Devices = new ObservableCollection<DeviceViewModel>(devices);
|
||||
OnPropertyChanged(nameof(Devices));
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
|
||||
var saveDialog = await FileDialogHelper.SaveFileDialogAsync(GetFileTypes());
|
||||
|
||||
if (!saveDialog.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (SelectedDevice == null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["ActionManagement.Save.SelectedDeviceNull"]);
|
||||
return;
|
||||
}
|
||||
if (SelectedDevice.VarActions == null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["ActionManagement.Save.VarActionsNull"]);
|
||||
return;
|
||||
}
|
||||
var result = await ExcelHelper<VarActionViewModel>.ExportExcelAsync(SelectedDevice.VarActions.ToList(), saveDialog.Data!.Path.LocalPath);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["ActionManagement.Export.Success"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["ActionManagement.Export.Error"] + ":" + result.ErrorMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
List<FilePickerFileType>? GetFileTypes()
|
||||
{
|
||||
return
|
||||
[
|
||||
new FilePickerFileType("Excel"){ Patterns=["*.xlsx"]}
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
23
Plugins/Driver/Cowain.Driver/ViewModels/AddressViewModel.cs
Normal file
23
Plugins/Driver/Cowain.Driver/ViewModels/AddressViewModel.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class AddressViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string? _name;
|
||||
[ObservableProperty]
|
||||
private string? _address;
|
||||
[ObservableProperty]
|
||||
private string? _desc;
|
||||
[ObservableProperty]
|
||||
private DataTypeEnum _dataType;
|
||||
[ObservableProperty]
|
||||
private int _arrayCount;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class AlarmGroupViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int _id;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _name = string.Empty;
|
||||
|
||||
//[ObservableProperty]
|
||||
//private bool _isSelected;
|
||||
//public override string ToString()
|
||||
//{
|
||||
// return Name; // 这样默认会显示Name
|
||||
//}
|
||||
}
|
||||
139
Plugins/Driver/Cowain.Driver/ViewModels/AlarmHistoryViewModel.cs
Normal file
139
Plugins/Driver/Cowain.Driver/ViewModels/AlarmHistoryViewModel.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Services;
|
||||
using SixLabors.Fonts.Tables.AdvancedTypographic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class AlarmHistoryViewModel : PageViewModelBase
|
||||
{
|
||||
private readonly ILocalizer _l;
|
||||
private readonly IAlarmService _alarmService;
|
||||
private readonly IAlarmGroupService _alarmGroupService;
|
||||
private readonly IAlarmLevelService _levelService;
|
||||
public AlarmHistoryViewModel(ILocalizer localizer, IAlarmService alarmService, IAlarmGroupService alarmGroupService, IAlarmLevelService alarmLevelService)
|
||||
|
||||
{
|
||||
PageSize = 40;
|
||||
_l = localizer;
|
||||
_alarmService = alarmService;
|
||||
_alarmGroupService = alarmGroupService;
|
||||
_levelService = alarmLevelService;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totals;
|
||||
[ObservableProperty]
|
||||
private int _pageSize;
|
||||
[ObservableProperty]
|
||||
private int _pageIndex;
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<AlarmViewModel>? _alarms = new();
|
||||
|
||||
[ObservableProperty] private DateTime? _startDate;
|
||||
[ObservableProperty] private DateTime? _endDate;
|
||||
[ObservableProperty] private IList<AlarmGroupViewModel>? _selectedGroups = new AvaloniaList<AlarmGroupViewModel>();
|
||||
[ObservableProperty] private IList<AlarmLevelViewModel>? _selectedLevels = new AvaloniaList<AlarmLevelViewModel>();
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<AlarmLevelViewModel>? _alarmLevels;
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<AlarmGroupViewModel>? _alarmGroups;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadedAsync()
|
||||
{
|
||||
//获取所有设备列表
|
||||
var alarmLevels = await _levelService.GetAllAsync();
|
||||
AlarmLevels = new ObservableCollection<AlarmLevelViewModel>(alarmLevels);
|
||||
var alarmGroups = await _alarmGroupService.GetAllAsync();
|
||||
AlarmGroups = new ObservableCollection<AlarmGroupViewModel>(alarmGroups);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
if (StartDate == null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Warning, _l["AlarmHistory.Warning.StartDateIsNull"]);
|
||||
return;
|
||||
}
|
||||
if (EndDate == null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Warning, _l["AlarmHistory.Warning.EndDateIsNull"]);
|
||||
return;
|
||||
}
|
||||
List<int>? groups = SelectedGroups?.Select(g => g.Id).ToList();
|
||||
List<int>? levels = SelectedLevels?.Select(g => g.Id).ToList();
|
||||
var (data, count) = await _alarmService.GetAlarmAsync(PageIndex, PageSize, StartDate, EndDate, groups, levels);
|
||||
Alarms?.Clear();
|
||||
Totals = count;
|
||||
if (count > 0)
|
||||
{
|
||||
foreach (var item in data)
|
||||
{
|
||||
// 设置报警组名称和等级名称
|
||||
item.GroupName = AlarmGroups?.FirstOrDefault(g => g.Id == item.Group)?.Name ?? string.Empty;
|
||||
item.LevelName = AlarmLevels?.FirstOrDefault(g => g.Id == item.Level)?.Name ?? string.Empty;
|
||||
//设置颜色
|
||||
if (item.Status)
|
||||
{
|
||||
item.Color = AlarmLevels?.FirstOrDefault(g => g.Id == item.Level)?.Color ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Color = Colors.Gray.ToString();
|
||||
}
|
||||
|
||||
Alarms?.Add(item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
|
||||
var saveDialog = await FileDialogHelper.SaveFileDialogAsync(GetFileTypes());
|
||||
|
||||
if (!saveDialog.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Alarms == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var result = await ExcelHelper<AlarmViewModel>.ExportExcelAsync(Alarms.ToList(), saveDialog.Data!.Path.LocalPath);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["AlarmHistory.Export.Success"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["AlarmHistory.Export.Error"] + ":" + result.ErrorMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
List<FilePickerFileType>? GetFileTypes()
|
||||
{
|
||||
return
|
||||
[
|
||||
new FilePickerFileType("Excel"){ Patterns=["*.xlsx"]}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class AlarmLevelViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int _id;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private string _name = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _color = string.Empty;
|
||||
//[ObservableProperty]
|
||||
//private bool _isSelected;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Base.Models;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class AlarmRealTimeViewModel : PageViewModelBase, IRecipient<AlarmChangedMessage>
|
||||
{
|
||||
private readonly ILocalizer _l;
|
||||
private readonly IMessenger _messenger;
|
||||
private readonly IAlarmLevelService _levelService;
|
||||
public AlarmRealTimeViewModel(ILocalizer localizer, IMessenger messenger, IAlarmLevelService levelService)
|
||||
{
|
||||
_l = localizer;
|
||||
_levelService = levelService;
|
||||
_messenger = messenger;
|
||||
_messenger.RegisterAll(this);
|
||||
}
|
||||
|
||||
//[ObservableProperty]
|
||||
//private ObservableCollection<AlarmLevelViewModel>? _alarmLevels = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<AlarmViewModel> _alarms = new();
|
||||
|
||||
//[RelayCommand]
|
||||
//private async Task LoadedAsync()
|
||||
//{
|
||||
// //获取所有设备列表
|
||||
// var alarmLevels = await _levelService.GetAllAsync();
|
||||
// AlarmLevels?.Clear();
|
||||
// alarmLevels.ForEach(level =>
|
||||
// {
|
||||
// AlarmLevels?.Add(level);
|
||||
// });
|
||||
|
||||
//}
|
||||
|
||||
public void Receive(AlarmChangedMessage message)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
// 1. 获取最新的报警列表(空值保护)
|
||||
var latestAlarms = message.Value ?? new List<AlarmViewModel>();
|
||||
// 2. 提取新旧列表的TagId(唯一标识),用HashSet提升查询效率
|
||||
var currentTagIds = Alarms.Select(a => a.TagId).ToHashSet();
|
||||
var latestTagIds = latestAlarms.Select(a => a.TagId).ToHashSet();
|
||||
// 3. 增量添加:最新列表有、当前列表没有的报警
|
||||
var alarmsToAdd = latestAlarms.Where(alarm => !currentTagIds.Contains(alarm.TagId)).ToList();
|
||||
foreach (var alarm in alarmsToAdd)
|
||||
{
|
||||
Alarms.Add(alarm);
|
||||
}
|
||||
// 4. 增量删除:当前列表有、最新列表没有的报警
|
||||
var tagIdsToRemove = currentTagIds.Where(tagId => !latestTagIds.Contains(tagId)).ToList();
|
||||
foreach (var tagId in tagIdsToRemove)
|
||||
{
|
||||
var alarmToRemove = Alarms.FirstOrDefault(a => a.TagId == tagId);
|
||||
if (alarmToRemove != null)
|
||||
{
|
||||
Alarms.Remove(alarmToRemove);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 销毁时取消注册,避免内存泄漏
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) _messenger.UnregisterAll(this);
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Avalonia.Controls.Notifications;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Cowain.Base.Helpers;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class DeviceEditDialogViewModel : ObservableObject, IDialogContext
|
||||
{
|
||||
public event EventHandler<object?>? RequestClose;
|
||||
|
||||
[ObservableProperty]
|
||||
private DeviceViewModel? _device;
|
||||
|
||||
private readonly ILocalizer _l;
|
||||
private IDriverPluginService _driverService;
|
||||
|
||||
public DeviceEditDialogViewModel(ILocalizer l, DeviceViewModel deviceModel, IDriverPluginService driverService)
|
||||
{
|
||||
_l = l;
|
||||
_device = deviceModel;
|
||||
_driverService = driverService;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
RequestClose?.Invoke(this, false);
|
||||
}
|
||||
[RelayCommand]
|
||||
private void EditParam()
|
||||
{
|
||||
if (Device == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_driverService.ParamEditDialogAsync(Device);
|
||||
|
||||
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Ok()
|
||||
{
|
||||
if (Device == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(Device.DeviceName))
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["DeviceEditDilog.Error.DeviceNameNull"]);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(Device.DriverName))
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["DeviceEditDilog.Error.DriverNameNull"]);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(Device.DeviceType))
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["DeviceEditDilog.Error.DeviceTypeNull"]);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(Device.Param))
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["DeviceEditDilog.Error.ParamNull"]);
|
||||
return;
|
||||
}
|
||||
RequestClose?.Invoke(this, true);
|
||||
}
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
RequestClose?.Invoke(this, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Plugin.Cowain.Driver.Services;
|
||||
using Plugin.Cowain.Driver.Views;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using System.Collections.ObjectModel;
|
||||
using Ursa.Controls;
|
||||
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class DeviceManagementViewModel : PageViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<DeviceViewModel>? _devices;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totals;
|
||||
[ObservableProperty]
|
||||
private int _pageSize;
|
||||
[ObservableProperty]
|
||||
private int _pageIndex;
|
||||
|
||||
private readonly ILocalizer _l;
|
||||
private IDeviceService _deviceService;
|
||||
private IDriverPluginService _driverService;
|
||||
|
||||
public DeviceManagementViewModel(ILocalizer localizer, IDeviceService deviceService, IDriverPluginService driverService)
|
||||
{
|
||||
|
||||
PageSize = 20;
|
||||
_l = localizer;
|
||||
_deviceService = deviceService;
|
||||
_driverService = driverService;
|
||||
Devices = new ObservableCollection<DeviceViewModel>();
|
||||
// 异步调用刷新
|
||||
RefreshCommand.ExecuteAsync(1);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RefreshAsync(int pageIndex)
|
||||
{
|
||||
var (data, count) = await _deviceService.GetAllAsync(pageIndex, PageSize);
|
||||
Totals = count;
|
||||
if (count > 0)
|
||||
{
|
||||
Devices?.Clear();
|
||||
foreach (var item in data)
|
||||
{
|
||||
Devices?.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddDeviceAsync()
|
||||
{
|
||||
var options = new DialogOptions()
|
||||
{
|
||||
Title = _l["DriverSelect.Dialog.Title"],
|
||||
ShowInTaskBar = false,
|
||||
IsCloseButtonVisible = false,
|
||||
StartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Button = DialogButton.OKCancel,
|
||||
CanDragMove = true,
|
||||
CanResize = true,
|
||||
};
|
||||
|
||||
DriverSelectedDialogViewModel model = new DriverSelectedDialogViewModel(_l, _driverService);
|
||||
var ret = await Dialog.ShowModal<DriverSelectedDialog, DriverSelectedDialogViewModel>(model, options: options);
|
||||
if (ret != DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (model.Driver == null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Information, _l["DeviceAddDilog.Error.DriverNull"]);
|
||||
return;
|
||||
}
|
||||
options.Title = _l["DeviceManagement.Dialog.Title"];
|
||||
|
||||
DeviceViewModel deviceViewModel = new DeviceViewModel()
|
||||
{
|
||||
DriverName = model.Driver.Name,
|
||||
DeviceType = model.Driver.DeviceType,
|
||||
};
|
||||
DeviceEditDialogViewModel deviceModel = new DeviceEditDialogViewModel(_l, deviceViewModel, _driverService);
|
||||
var deviceEditDialog = await Dialog.ShowCustomModal<DeviceEditDialog, DeviceEditDialogViewModel, bool>(deviceModel, options: options);
|
||||
if (deviceEditDialog)
|
||||
{
|
||||
|
||||
var add = await _deviceService.AddDeviceAsync(deviceViewModel);
|
||||
if (add.IsSuccess)
|
||||
{
|
||||
await RefreshAsync(1);
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["DeviceEditDilog.Add.Success"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["DeviceEditDilog.Add.Error"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task EditDeviceAsync(DeviceViewModel? device)
|
||||
{
|
||||
if (device == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var options = new DialogOptions()
|
||||
{
|
||||
Title = _l["DeviceManagement.Dialog.Title"],
|
||||
ShowInTaskBar = false,
|
||||
IsCloseButtonVisible = false,
|
||||
StartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Button = DialogButton.OKCancel,
|
||||
CanDragMove = true,
|
||||
CanResize = false,
|
||||
};
|
||||
|
||||
DeviceEditDialogViewModel deviceModel = new DeviceEditDialogViewModel(_l, device, _driverService);
|
||||
var deviceEditDialog = await Dialog.ShowCustomModal<DeviceEditDialog, DeviceEditDialogViewModel, bool>(deviceModel, options: options);
|
||||
if (deviceEditDialog)
|
||||
{
|
||||
var add = await _deviceService.UpdateDeviceAsync(device);
|
||||
if (add.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["DeviceEditDilog.Edit.Success"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
await RefreshAsync(1);
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["DeviceEditDilog.Edit.Error"] + ":" + add.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteDeviceAsync(DeviceViewModel? device)
|
||||
{
|
||||
if (device == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var result = await MessageBox.ShowOverlayAsync(_l["DeleteDialog"], _l["Message.Info.Title"], button: MessageBoxButton.YesNo);
|
||||
if (result != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var deleteDevice = await _deviceService.DeleteDeviceAsync(device.Id);
|
||||
if (deleteDevice.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["DeviceEditDilog.Delete.Success"]);
|
||||
Devices?.Remove(device);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["DeviceEditDilog.Delete.Error"] + ":" + deleteDevice.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
40
Plugins/Driver/Cowain.Driver/ViewModels/DeviceViewModel.cs
Normal file
40
Plugins/Driver/Cowain.Driver/ViewModels/DeviceViewModel.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 设备信息模型
|
||||
/// </summary>
|
||||
public partial class DeviceViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int _id;
|
||||
[ObservableProperty]
|
||||
private string _deviceName = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _driverName = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _deviceType = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _param = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string? _desc;
|
||||
[ObservableProperty]
|
||||
private bool _enable;
|
||||
[ObservableProperty]
|
||||
private int _minPeriod = 200;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _readUseTime;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isConnected;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<VariableViewModel>? _variables;
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<VarActionViewModel>? _varActions;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Avalonia.Controls.Notifications;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Cowain.Base.Helpers;
|
||||
using Irihi.Avalonia.Shared.Contracts;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class DriverParamDialogBase : ObservableObject, IDialogContext
|
||||
{
|
||||
public DriverParamDialogBase(object param)
|
||||
{
|
||||
_l = ServiceLocator.GetRequiredService<ILocalizer>();
|
||||
}
|
||||
protected readonly ILocalizer _l;
|
||||
public event EventHandler<object?>? RequestClose;
|
||||
|
||||
public virtual void Close()
|
||||
{
|
||||
RequestClose?.Invoke(this, false);
|
||||
}
|
||||
[RelayCommand]
|
||||
public virtual void Ok()
|
||||
{
|
||||
RequestClose?.Invoke(this, true);
|
||||
}
|
||||
[RelayCommand]
|
||||
public virtual void Cancel()
|
||||
{
|
||||
RequestClose?.Invoke(this, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class DriverSelectedDialogViewModel : ObservableObject
|
||||
{
|
||||
|
||||
[ObservableProperty]
|
||||
private DriverInfo? _driver;
|
||||
[ObservableProperty]
|
||||
private List<DeviceTypeInfo>? _deviceTypes;
|
||||
|
||||
private readonly ILocalizer _l;
|
||||
private readonly IDriverPluginService _driverService;
|
||||
|
||||
public DriverSelectedDialogViewModel(ILocalizer l, IDriverPluginService driverService)
|
||||
{
|
||||
_l = l;
|
||||
_driverService = driverService;
|
||||
DeviceTypes = GetDrivers(_driverService.GetDriverInfos());
|
||||
}
|
||||
|
||||
public List<DeviceTypeInfo> GetDrivers(List<DriverInfo> driverInfoList)
|
||||
{
|
||||
var multiLevelDataSource = driverInfoList
|
||||
.GroupBy(d => d.DeviceType)
|
||||
.Select(deviceTypeGroup => new DeviceTypeInfo
|
||||
{
|
||||
Name = deviceTypeGroup.Key,
|
||||
Groups = deviceTypeGroup
|
||||
.GroupBy(d => d.Group)
|
||||
.Select(groupGroup => new DriverInfoGroup
|
||||
{
|
||||
Name = groupGroup.Key,
|
||||
Drivers = groupGroup
|
||||
.Select(d => new DriverInfo
|
||||
{
|
||||
Name = d.Name,
|
||||
DeviceType = d.DeviceType,
|
||||
Group = d.Group,
|
||||
Desc = d.Desc,
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return multiLevelDataSource;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.Models;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.IServices;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using Plugin.Cowain.Driver.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using Ursa.Controls;
|
||||
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class TagManagementViewModel : PageViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<TagViewModel>? _tags;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totals;
|
||||
[ObservableProperty]
|
||||
private int _pageSize;
|
||||
[ObservableProperty]
|
||||
private int _pageIndex;
|
||||
|
||||
private readonly ILocalizer _l;
|
||||
private readonly ITagService _tagService;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IDriverPluginService _driverService;
|
||||
private readonly IAlarmGroupService _alarmGroupService;
|
||||
private readonly IAlarmLevelService _alarmLevelService;
|
||||
|
||||
public static ObservableCollection<DeviceViewModel>? DeviceList { get; set; }
|
||||
public static ObservableCollection<AlarmGroupViewModel>? AlarmGroups { get; set; }
|
||||
public static ObservableCollection<AlarmLevelViewModel>? AlarmLevels { get; set; }
|
||||
|
||||
public static List<string> DataTypes =>
|
||||
Enum.GetNames(typeof(DataTypeEnum)).ToList(); // 直接获取枚举名称字符串列表
|
||||
public static List<string> OperModes =>
|
||||
Enum.GetNames(typeof(OperModeEnum)).ToList(); // 直接获取枚举名称字符串列表
|
||||
public TagManagementViewModel(ILocalizer localizer, ITagService tagService, IDeviceService deviceService, IDriverPluginService driverService, IAlarmGroupService alarmGroupService, IAlarmLevelService alarmLevelService)
|
||||
{
|
||||
PageSize = 20;
|
||||
_l = localizer;
|
||||
_tagService = tagService;
|
||||
_deviceService = deviceService;
|
||||
_driverService = driverService;
|
||||
_alarmGroupService = alarmGroupService;
|
||||
_alarmLevelService = alarmLevelService;
|
||||
Tags = new ObservableCollection<TagViewModel>();
|
||||
// 异步调用刷新
|
||||
//RefreshCommand.ExecuteAsync(1);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadedAsync()
|
||||
{
|
||||
//获取所有设备列表
|
||||
var devices = await _deviceService.GetAllAsync();
|
||||
DeviceList = new ObservableCollection<DeviceViewModel>(devices);
|
||||
var groups = await _alarmGroupService.GetAllAsync();
|
||||
AlarmGroups = new ObservableCollection<AlarmGroupViewModel>(groups);
|
||||
var levels = await _alarmLevelService.GetAllAsync();
|
||||
AlarmLevels = new ObservableCollection<AlarmLevelViewModel>(levels);
|
||||
await RefreshAsync(1);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddTag()
|
||||
{
|
||||
Tags?.Add(new TagViewModel()
|
||||
{
|
||||
Name = "Tag" + (Tags.Count + 1),
|
||||
DataType = DataTypeEnum.Int16.ToString(),
|
||||
DeviceId = DeviceList?.FirstOrDefault()?.Id ?? 1, // 处理可空类型
|
||||
Address = "0",
|
||||
ArrayCount = 1,
|
||||
Desc = "Tag" + (Tags.Count + 1),
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveTagAsync()
|
||||
{
|
||||
if (Tags == null || !Tags.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<ResultModel> tasks = new List<ResultModel>();
|
||||
foreach (var tag in Tags)
|
||||
{
|
||||
if (tag.Id == 0)
|
||||
{
|
||||
var add = await _tagService.AddTagAsync(tag);
|
||||
tasks.Add(add);
|
||||
}
|
||||
else
|
||||
{
|
||||
var update = await _tagService.UpdateTagAsync(tag);
|
||||
tasks.Add(update);
|
||||
}
|
||||
}
|
||||
await RefreshAsync(1);
|
||||
if (tasks.All(x => x.IsSuccess))
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["TagManagement.Save.Success"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = tasks.FirstOrDefault(x => !x.IsSuccess);
|
||||
if (error != null)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["TagManagement.Save.Error"]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
[RelayCommand]
|
||||
private async Task DeleteTagAsync(TagViewModel? tag)
|
||||
{
|
||||
if (tag == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (tag.Id == 0)
|
||||
{
|
||||
Tags?.Remove(tag);
|
||||
return;
|
||||
}
|
||||
var result = await MessageBox.ShowOverlayAsync(_l["DeleteDialog"], _l["Message.Info.Title"], button: MessageBoxButton.YesNo);
|
||||
if (result != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var deleteTag = await _tagService.DeleteTagAsync(tag.Id);
|
||||
if (deleteTag.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["TagManagement.Delete.Success"]);
|
||||
Tags?.Remove(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["TagManagement.Delete.Error"] + ":" + deleteTag.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RefreshAsync(int pageIndex)
|
||||
{
|
||||
var (data, count) = await _tagService.GetAllAsync(pageIndex, PageSize);
|
||||
Totals = count;
|
||||
if (count > 0)
|
||||
{
|
||||
// 使用Dispatcher确保UI线程更新
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Tags?.Clear();
|
||||
foreach (var item in data)
|
||||
{
|
||||
Tags?.Add(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
|
||||
var saveDialog = await FileDialogHelper.SaveFileDialogAsync(GetFileTypes());
|
||||
|
||||
if (!saveDialog.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Tags == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var result = await ExcelHelper<TagViewModel>.ExportExcelAsync(Tags.ToList(), saveDialog.Data!.Path.LocalPath);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["TagManagement.Export.Success"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["TagManagement.Export.Error"] + ":" + result.ErrorMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ImportAsync()
|
||||
{
|
||||
var openDialog = await FileDialogHelper.OpenFileDialogAsync(GetFileTypes());
|
||||
if (!openDialog.IsSuccess) return;
|
||||
var result = ExcelHelper<TagViewModel>.ImportExcel(openDialog.Data!.Path.LocalPath);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
NotificationHelper.ShowNormal(NotificationType.Error, _l["TagManagement.Import.Error"] + ":" + result.ErrorMessage);
|
||||
return;
|
||||
}
|
||||
// 使用Dispatcher确保UI线程更新
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Tags = new ObservableCollection<TagViewModel>(result.Data!);
|
||||
OnPropertyChanged(nameof(Tags));
|
||||
|
||||
});
|
||||
NotificationHelper.ShowNormal(NotificationType.Success, _l["TagManagement.Import.Success"]);
|
||||
}
|
||||
|
||||
List<FilePickerFileType>? GetFileTypes()
|
||||
{
|
||||
return
|
||||
[
|
||||
new FilePickerFileType("Excel"){ Patterns=["*.xlsx"]}
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
45
Plugins/Driver/Cowain.Driver/ViewModels/TagViewModel.cs
Normal file
45
Plugins/Driver/Cowain.Driver/ViewModels/TagViewModel.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class TagViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int _id;
|
||||
[ObservableProperty]
|
||||
private int _deviceId;
|
||||
[ObservableProperty]
|
||||
private string _name = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _address = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _desc = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _dataType = DataTypeEnum.Int16.ToString();
|
||||
[ObservableProperty]
|
||||
private int _arrayCount;
|
||||
[ObservableProperty]
|
||||
private string _operMode = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _json = string.Empty;
|
||||
[ObservableProperty]
|
||||
private bool _alarmEnable;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _alarmValue = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _alarmGroup;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _alarmLevel;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _alarmMsg = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class VarActionViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int _id;
|
||||
[ObservableProperty]
|
||||
private int _deviceId;
|
||||
[ObservableProperty]
|
||||
private int _tagId;
|
||||
[ObservableProperty]
|
||||
private string _actionName = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _param = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _desc = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _actionValue = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _condition = string.Empty;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Cowain.Base.ViewModels;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class VariableMonitorViewModel : PageViewModelBase
|
||||
{
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<DeviceViewModel>? _devices;
|
||||
|
||||
[ObservableProperty]
|
||||
private DeviceViewModel? _selectedDevice;
|
||||
|
||||
|
||||
private readonly IDeviceMonitor _deviceMonitor;
|
||||
private readonly ILocalizer _l;
|
||||
public VariableMonitorViewModel(ILocalizer localizer, IDeviceMonitor deviceMonitor)
|
||||
{
|
||||
_l = localizer;
|
||||
_deviceMonitor = deviceMonitor;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Loaded()
|
||||
{
|
||||
//获取所有设备
|
||||
var devices = _deviceMonitor.Devices;
|
||||
Devices = new(devices);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
112
Plugins/Driver/Cowain.Driver/ViewModels/VariableViewModel.cs
Normal file
112
Plugins/Driver/Cowain.Driver/ViewModels/VariableViewModel.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
|
||||
namespace Plugin.Cowain.Driver.ViewModels;
|
||||
|
||||
public partial class VariableViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int _id;
|
||||
[ObservableProperty]
|
||||
private int _deviceId;
|
||||
[ObservableProperty]
|
||||
private string _deviceName = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _name = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _address = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string _desc = string.Empty;
|
||||
[ObservableProperty]
|
||||
private DataTypeEnum _dataType;
|
||||
[ObservableProperty]
|
||||
private int _arrayCount;
|
||||
[ObservableProperty]
|
||||
private string _json = string.Empty;
|
||||
[ObservableProperty]
|
||||
private OperModeEnum _operMode;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _alarmEnable;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _alarmValue = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _alarmMsg = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _alarmGroup;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _alarmLevel;
|
||||
|
||||
private string? _value;
|
||||
|
||||
public string? Value
|
||||
{
|
||||
get { return _value; }
|
||||
set
|
||||
{
|
||||
SetProperty(ref _value, value);
|
||||
if (value != null)
|
||||
{
|
||||
UpdateTime = DateTime.Now;
|
||||
}
|
||||
if (OldValue != _value)
|
||||
{
|
||||
//触发数据改变事件
|
||||
OnDataChange();
|
||||
}
|
||||
//if (!string.IsNullOrEmpty(OldValue) && !OldValue.Equals(_value))
|
||||
//{
|
||||
// //OldValue不为空且不等于新值,触发数据改变事件
|
||||
// OnDataChange();
|
||||
//}
|
||||
OldValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _oldValue;
|
||||
[ObservableProperty]
|
||||
private DateTime _updateTime = DateTime.MinValue;
|
||||
[ObservableProperty]
|
||||
private string? _message;
|
||||
[ObservableProperty]
|
||||
private bool _isSuccess;
|
||||
|
||||
private Action<VariableViewModel>? dataChanged;//数据改变事件
|
||||
|
||||
public void Register(Action<VariableViewModel> action)
|
||||
{
|
||||
dataChanged += action;
|
||||
}
|
||||
|
||||
public void UnRegister(Action<VariableViewModel> action)
|
||||
{
|
||||
if (dataChanged == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (action == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
dataChanged -= action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据改变方法
|
||||
/// </summary>
|
||||
/// <param name="var"></param>
|
||||
private void OnDataChange()
|
||||
{
|
||||
if (dataChanged != null)
|
||||
{
|
||||
dataChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
240
Plugins/Driver/Cowain.Driver/Views/ActionManagementView.axaml
Normal file
240
Plugins/Driver/Cowain.Driver/Views/ActionManagementView.axaml
Normal file
@@ -0,0 +1,240 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:i="using:Avalonia.Xaml.Interactivity"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Core"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
xmlns:model="using:Plugin.Cowain.Driver.Models"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
xmlns:conv="using:Cowain.Base.Converters"
|
||||
xmlns:extensions="using:Cowain.Base.Extensions"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:DataType="vm:ActionManagementViewModel"
|
||||
x:Name="MainControl"
|
||||
x:Class="Plugin.Cowain.Driver.Views.ActionManagementView">
|
||||
<Interaction.Behaviors>
|
||||
<ia:EventTriggerBehavior EventName="Loaded">
|
||||
<ia:InvokeCommandAction Command="{Binding LoadedCommand}"/>
|
||||
</ia:EventTriggerBehavior>
|
||||
</Interaction.Behaviors>
|
||||
|
||||
<Grid RowDefinitions="Auto, *">
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="10" Margin="10 8">
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize ActionManagement.Tooltip.Add}"
|
||||
IsEnabled="{extensions:MenuEnable ActionManagementView,add}"
|
||||
Command="{Binding AddCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconPlusStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize ActionManagement.Tooltip.Refresh}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconRedoStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize ActionManagement.Tooltip.Save}"
|
||||
IsEnabled="{extensions:MenuEnable ActionManagementView,save}"
|
||||
Command="{Binding SaveCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconSave}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize ActionManagement.Tooltip.Export}"
|
||||
Command="{Binding ExportCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconExternalOpenStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto *">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{i18n:Localize ActionManagement.DataGrid.DeviceName}" />
|
||||
<u:SelectionList Name="roleMenu" Width="150"
|
||||
ItemsSource="{Binding Devices}"
|
||||
SelectedItem="{Binding SelectedDevice}">
|
||||
<u:SelectionList.Indicator>
|
||||
<Border Background="Transparent" CornerRadius="4">
|
||||
<Border
|
||||
Width="4"
|
||||
Margin="0,8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{DynamicResource SemiBlue6}"
|
||||
CornerRadius="4" />
|
||||
</Border>
|
||||
</u:SelectionList.Indicator>
|
||||
<u:SelectionList.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Panel Height="30">
|
||||
<TextBlock
|
||||
Classes.Active="{Binding $parent[u:SelectionListItem].IsSelected, Mode=OneWay}"
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding DeviceName}">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="TextBlock.Active">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SemiOrange6}" />
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
</TextBlock>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</u:SelectionList.ItemTemplate>
|
||||
</u:SelectionList>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Grid Grid.Column="1" Margin="8">
|
||||
<DataGrid FrozenColumnCount="2"
|
||||
Margin="8"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HeadersVisibility="All"
|
||||
ItemsSource="{Binding SelectedDevice.VarActions}"
|
||||
Name="LeftDataGrid">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Width="80"
|
||||
x:DataType="vm:VarActionViewModel"
|
||||
Binding="{Binding Id}"
|
||||
Header="{i18n:Localize ActionManagement.DataGrid.Id}" />
|
||||
<DataGridTemplateColumn Header="{i18n:Localize ActionManagement.DataGrid.Edit}" Width="100">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize ActionManagement.Tooltip.Delete}"
|
||||
x:CompileBindings="False"
|
||||
IsEnabled="{extensions:MenuEnable ActionManagementView,delete}"
|
||||
Command="{Binding $parent[DataGrid].DataContext.DeleteCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon Width="16" Height="16" Data="{StaticResource SemiIconDeleteStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="100"
|
||||
Header="{i18n:Localize ActionManagement.DataGrid.TagName}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox ItemsSource="{x:Static vm:ActionManagementViewModel.Variables}"
|
||||
SelectedValue="{Binding TagId, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValueBinding="{Binding Id}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VariableViewModel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="10 8">
|
||||
<TextBlock Text="{Binding Name}" ToolTip.Tip="{Binding Address}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Width="100"
|
||||
Header="{i18n:Localize ActionManagement.DataGrid.ActionName}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox ItemsSource="{x:Static vm:ActionManagementViewModel.Actions}"
|
||||
SelectedValue="{Binding ActionName, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValueBinding="{Binding Name}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ActionInfo">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="10 8">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
ToolTip.Tip="{Binding Desc}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Width="120"
|
||||
Header="{i18n:Localize ActionManagement.DataGrid.Condition}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox ItemsSource="{x:Static vm:ActionManagementViewModel.Conditions}"
|
||||
SelectedValue="{Binding Condition, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValueBinding="{Binding Name}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ConditionInfo">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="10 8">
|
||||
<TextBlock Text="{Binding Name}" ToolTip.Tip="{Binding Desc}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn Width="200"
|
||||
x:DataType="vm:VarActionViewModel"
|
||||
Binding="{Binding ActionValue}"
|
||||
Header="{i18n:Localize ActionManagement.DataGrid.ActionValue}" />
|
||||
|
||||
<DataGridTemplateColumn Header="{i18n:Localize ActionManagement.DataGrid.Param}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:VarActionViewModel">
|
||||
<TextBlock Text="{i18n:Localize ActionManagement.DataGrid.Edit}" Width="200"
|
||||
ToolTip.Tip="{Binding Param}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.CellEditingTemplate>
|
||||
<DataTemplate x:DataType="vm:VarActionViewModel">
|
||||
<TextBox Text="{Binding Param}" TextWrapping="Wrap" AcceptsReturn="True" HorizontalAlignment="Stretch" Height="400"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellEditingTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn Width="200"
|
||||
x:DataType="vm:VarActionViewModel"
|
||||
Binding="{Binding Desc}"
|
||||
Header="{i18n:Localize ActionManagement.DataGrid.Desc}" />
|
||||
|
||||
|
||||
</DataGrid.Columns>
|
||||
|
||||
|
||||
</DataGrid>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class ActionManagementView : UserControl
|
||||
{
|
||||
public ActionManagementView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
164
Plugins/Driver/Cowain.Driver/Views/AlarmHistoryView.axaml
Normal file
164
Plugins/Driver/Cowain.Driver/Views/AlarmHistoryView.axaml
Normal file
@@ -0,0 +1,164 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Core"
|
||||
xmlns:converters="using:Cowain.Base.Converters"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
xmlns:bvm="using:Cowain.Base.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Plugin.Cowain.Driver.Views.AlarmHistoryView">
|
||||
<Interaction.Behaviors>
|
||||
<ia:EventTriggerBehavior EventName="Loaded">
|
||||
<ia:InvokeCommandAction Command="{Binding LoadedCommand}"/>
|
||||
</ia:EventTriggerBehavior>
|
||||
</Interaction.Behaviors>
|
||||
|
||||
<UserControl.Resources>
|
||||
<converters:StringToBrushConverter x:Key="StringToBrushConverter" />
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Foreground"
|
||||
Value="{Binding Color, Converter={StaticResource StringToBrushConverter}, ConverterParameter=Foreground}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="2" />
|
||||
</Style>
|
||||
|
||||
</UserControl.Styles>
|
||||
<Grid RowDefinitions="Auto * Auto">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="6 2">
|
||||
<u:DateRangePicker
|
||||
Width="360"
|
||||
Classes="ClearButton"
|
||||
DisplayFormat="yyyy-MM-dd"
|
||||
SelectedEndDate="{Binding EndDate}"
|
||||
SelectedStartDate="{Binding StartDate}"/>
|
||||
<TextBlock Text="{i18n:Localize AlarmHistory.SelectGroup}" />
|
||||
<u:MultiComboBox
|
||||
Width="200"
|
||||
MaxHeight="200"
|
||||
SelectedItems="{Binding SelectedGroups}"
|
||||
ItemsSource="{Binding AlarmGroups}" >
|
||||
<u:MultiComboBox.SelectedItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AlarmGroupViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:MultiComboBox.SelectedItemTemplate>
|
||||
<u:MultiComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AlarmGroupViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:MultiComboBox.ItemTemplate>
|
||||
</u:MultiComboBox>
|
||||
<TextBlock Text="{i18n:Localize AlarmHistory.SelectLevel}" />
|
||||
<u:MultiComboBox
|
||||
Width="200"
|
||||
MaxHeight="200"
|
||||
SelectedItems="{Binding SelectedLevels}"
|
||||
ItemsSource="{Binding AlarmLevels}" >
|
||||
<u:MultiComboBox.SelectedItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AlarmLevelViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:MultiComboBox.SelectedItemTemplate>
|
||||
<u:MultiComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AlarmLevelViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:MultiComboBox.ItemTemplate>
|
||||
</u:MultiComboBox>
|
||||
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize Button.Tooltip.Refresh}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconRedoStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<!--µ¼³ö-->
|
||||
<u:IconButton
|
||||
Command="{Binding ExportCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}"
|
||||
ToolTip.Tip="{i18n:Localize Button.Tooptip.Export}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconExternalOpenStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<DataGrid
|
||||
Margin="8"
|
||||
Grid.Row="1"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HeadersVisibility="All"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding Alarms}">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding Desc}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.Desc}" />
|
||||
<DataGridTextColumn
|
||||
Width="180"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding StartTime}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.StartTime}" />
|
||||
<DataGridTextColumn
|
||||
Width="180"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding StopTime}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.StopTime}" />
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding GroupName}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.GroupName}" />
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding LevelName}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.LevelName}" />
|
||||
|
||||
</DataGrid.Columns>
|
||||
|
||||
</DataGrid>
|
||||
|
||||
<u:Pagination
|
||||
Name="page"
|
||||
Grid.Row="2"
|
||||
Command="{Binding RefreshCommand}"
|
||||
CommandParameter="{Binding $self.CurrentPage}"
|
||||
CurrentPage="{Binding PageIndex, Mode=TwoWay}"
|
||||
PageSize="{Binding PageSize, Mode=TwoWay}"
|
||||
PageSizeOptions="10, 20, 50, 100"
|
||||
ShowPageSizeSelector="True"
|
||||
ShowQuickJump="True"
|
||||
TotalCount="{Binding Totals}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
13
Plugins/Driver/Cowain.Driver/Views/AlarmHistoryView.axaml.cs
Normal file
13
Plugins/Driver/Cowain.Driver/Views/AlarmHistoryView.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class AlarmHistoryView : UserControl
|
||||
{
|
||||
public AlarmHistoryView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
60
Plugins/Driver/Cowain.Driver/Views/AlarmRealTimeView.axaml
Normal file
60
Plugins/Driver/Cowain.Driver/Views/AlarmRealTimeView.axaml
Normal file
@@ -0,0 +1,60 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Core"
|
||||
xmlns:converters="using:Cowain.Base.Converters"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
xmlns:bvm="using:Cowain.Base.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Plugin.Cowain.Driver.Views.AlarmRealTimeView">
|
||||
<UserControl.Resources>
|
||||
<converters:StringToBrushConverter x:Key="StringToBrushConverter" />
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Foreground"
|
||||
Value="{Binding Color, Converter={StaticResource StringToBrushConverter}, ConverterParameter=Foreground}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<Grid >
|
||||
<DataGrid
|
||||
Margin="8"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HeadersVisibility="All"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding Alarms}">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding Desc}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.Desc}" />
|
||||
<DataGridTextColumn
|
||||
Width="180"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding StartTime}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.StartTime}" />
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding GroupName}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.GroupName}" />
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
x:DataType="bvm:AlarmViewModel"
|
||||
Binding="{Binding LevelName}"
|
||||
Header="{i18n:Localize AlarmRealTimeView.DataGrid.LevelName}" />
|
||||
|
||||
</DataGrid.Columns>
|
||||
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class AlarmRealTimeView : UserControl
|
||||
{
|
||||
public AlarmRealTimeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
70
Plugins/Driver/Cowain.Driver/Views/DeviceEditDialog.axaml
Normal file
70
Plugins/Driver/Cowain.Driver/Views/DeviceEditDialog.axaml
Normal file
@@ -0,0 +1,70 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:conv="using:Cowain.Base.Converters"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
Width="350" Height="700"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Plugin.Cowain.Driver.Views.DeviceEditDialog">
|
||||
<UserControl.Resources>
|
||||
<conv:I18nLocalizeConverter x:Key="i18nConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid RowDefinitions="* Auto" Margin="8 40 8 8">
|
||||
<ScrollViewer>
|
||||
<u:Form Margin="20 20" HorizontalAlignment="Stretch" LabelPosition="Top">
|
||||
<u:Form.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="10"/>
|
||||
</ItemsPanelTemplate>
|
||||
</u:Form.ItemsPanel>
|
||||
<u:FormItem u:FormItem.Label="{i18n:Localize DeviceManagement.DataGrid.DeviceType}">
|
||||
<TextBlock u:FormItem.Label="Owner" Text="{Binding Device.DeviceType}" />
|
||||
</u:FormItem>
|
||||
<u:FormItem u:FormItem.Label="{i18n:Localize DeviceManagement.DataGrid.DriverName}">
|
||||
<TextBlock u:FormItem.Label="Owner" Text="{Binding Device.DriverName}" />
|
||||
</u:FormItem>
|
||||
<u:FormItem u:FormItem.Label="{i18n:Localize DeviceManagement.DataGrid.DeviceName}">
|
||||
<TextBox u:FormItem.Label="Owner" Text="{Binding Device.DeviceName}" />
|
||||
</u:FormItem>
|
||||
<u:FormItem u:FormItem.Label="{i18n:Localize DeviceManagement.DataGrid.Desc}">
|
||||
<TextBox u:FormItem.Label="Owner" Text="{Binding Device.Desc}" />
|
||||
</u:FormItem>
|
||||
<u:FormItem u:FormItem.Label="{i18n:Localize DeviceManagement.DataGrid.Param}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBox u:FormItem.Label="Owner"
|
||||
Classes="TextArea"
|
||||
VerticalAlignment="Center"
|
||||
Width="220"
|
||||
Text="{Binding Device.Param}" />
|
||||
<Button Content="{i18n:Localize DeviceManagement.DataGrid.Edit}"
|
||||
Command="{Binding EditParamCommand}"
|
||||
Theme="{DynamicResource SolidButton}" />
|
||||
</StackPanel>
|
||||
</u:FormItem>
|
||||
<u:FormItem
|
||||
u:FormItem.Label="{i18n:Localize DeviceManagement.DataGrid.Enable}">
|
||||
<ToggleSwitch u:FormItem.Label="Owner"
|
||||
IsChecked="{Binding Device.Enable}"
|
||||
OnContent="{i18n:Localize Yes}"
|
||||
OffContent="{i18n:Localize No}"/>
|
||||
</u:FormItem>
|
||||
</u:Form>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal"
|
||||
HorizontalAlignment="Right" Spacing="12"
|
||||
Margin="8">
|
||||
<Button
|
||||
Command="{Binding OkCommand}"
|
||||
Content="{i18n:Localize OK}"
|
||||
Theme="{DynamicResource SolidButton}" />
|
||||
<Button
|
||||
Command="{Binding CancelCommand}"
|
||||
Content="{i18n:Localize Cancel}"
|
||||
Theme="{DynamicResource SolidButton}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Plugins/Driver/Cowain.Driver/Views/DeviceEditDialog.axaml.cs
Normal file
11
Plugins/Driver/Cowain.Driver/Views/DeviceEditDialog.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class DeviceEditDialog : UserControl
|
||||
{
|
||||
public DeviceEditDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
153
Plugins/Driver/Cowain.Driver/Views/DeviceManagementView.axaml
Normal file
153
Plugins/Driver/Cowain.Driver/Views/DeviceManagementView.axaml
Normal file
@@ -0,0 +1,153 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
xmlns:conv="using:Cowain.Base.Converters"
|
||||
xmlns:extensions="using:Cowain.Base.Extensions"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:DataType="vm:DeviceManagementViewModel"
|
||||
x:Class="Plugin.Cowain.Driver.Views.DeviceManagementView">
|
||||
<UserControl.Resources>
|
||||
<conv:I18nLocalizeConverter x:Key="i18nConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid RowDefinitions="Auto * Auto">
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="10" Margin="10 8">
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize DeviceManagement.Tooltip.Add}"
|
||||
IsEnabled="{extensions:MenuEnable DeviceManagementView,add}"
|
||||
Command="{Binding AddDeviceCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconPlusStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize DeviceManagement.Tooltip.Refresh}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
CommandParameter="{Binding #page.CurrentPage}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconRedoStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize DeviceManagement.Tooltip.Export}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconExternalOpenStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid Grid.Row="1" FrozenColumnCount="2"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HeadersVisibility="All"
|
||||
RowHeight="30"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding Devices}"
|
||||
HorizontalScrollBarVisibility="Auto">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Width="80"
|
||||
x:DataType="vm:DeviceViewModel"
|
||||
Binding="{Binding Id}"
|
||||
IsReadOnly="True"
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.Id}" />
|
||||
<DataGridTemplateColumn
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.Edit}"
|
||||
Width="100">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize DeviceManagement.Tooltip.Edit}"
|
||||
x:CompileBindings="False"
|
||||
IsEnabled="{extensions:MenuEnable DeviceManagementView,edit}"
|
||||
Command="{Binding $parent[DataGrid].DataContext.EditDeviceCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon Width="16" Height="16" Data="{StaticResource SemiIconEdit2Stroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
ToolTip.Tip="{i18n:Localize DeviceManagement.Tooltip.Delete}"
|
||||
x:CompileBindings="False"
|
||||
IsEnabled="{extensions:MenuEnable DeviceManagementView,delete}"
|
||||
Command="{Binding $parent[DataGrid].DataContext.DeleteDeviceCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Theme="{DynamicResource BorderlessIconButton}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon Width="16" Height="16" Data="{StaticResource SemiIconDeleteStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn Width="140"
|
||||
x:DataType="vm:DeviceViewModel"
|
||||
Binding="{Binding DeviceName}"
|
||||
IsReadOnly="True"
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.DeviceName}" />
|
||||
<DataGridTextColumn Width="140"
|
||||
x:DataType="vm:DeviceViewModel"
|
||||
Binding="{Binding DriverName}"
|
||||
IsReadOnly="True"
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.DriverName}" />
|
||||
<DataGridTextColumn Width="120"
|
||||
x:DataType="vm:DeviceViewModel"
|
||||
Binding="{Binding DeviceType}"
|
||||
IsReadOnly="True"
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.DeviceType}" />
|
||||
<DataGridTemplateColumn
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.Param}"
|
||||
Width="300">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{i18n:Localize DeviceManagement.DataGrid.Param.Placeholder}"
|
||||
IsReadOnly="True"
|
||||
ToolTip.Tip="{Binding Param}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Width="200"
|
||||
x:DataType="vm:DeviceViewModel"
|
||||
Binding="{Binding Desc}"
|
||||
IsReadOnly="True"
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.Desc}" />
|
||||
<DataGridTextColumn Width="60"
|
||||
x:DataType="vm:DeviceViewModel"
|
||||
Binding="{Binding Enable}"
|
||||
IsReadOnly="True"
|
||||
Header="{i18n:Localize DeviceManagement.DataGrid.Enable}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<u:Pagination Grid.Row="2"
|
||||
Name="page"
|
||||
PageSizeOptions="10, 20, 50, 100"
|
||||
ShowQuickJump="True"
|
||||
ShowPageSizeSelector="True"
|
||||
PageSize="{Binding PageSize,Mode=TwoWay}"
|
||||
CurrentPage="{Binding PageIndex,Mode=TwoWay}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
CommandParameter="{Binding $self.CurrentPage}"
|
||||
TotalCount="{Binding Totals}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class DeviceManagementView : UserControl
|
||||
{
|
||||
public DeviceManagementView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:conv="using:Cowain.Base.Converters"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
xmlns:models="using:Plugin.Cowain.Driver.Models"
|
||||
Width="400" Height="560"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:DataType="vm:DriverSelectedDialogViewModel"
|
||||
x:Class="Plugin.Cowain.Driver.Views.DriverSelectedDialog">
|
||||
<Grid RowDefinitions="* Auto" Margin="8 40 8 8">
|
||||
<Border Theme="{StaticResource CardBorder}">
|
||||
<TreeView Margin="0,10"
|
||||
Name="tree"
|
||||
ItemsSource="{Binding DeviceTypes}"
|
||||
SelectedItem="{Binding Driver}">
|
||||
<TreeView.Styles>
|
||||
<!-- 设置 TreeViewItem 默认展开 -->
|
||||
<Style Selector="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="True" />
|
||||
</Style>
|
||||
</TreeView.Styles>
|
||||
<TreeView.DataTemplates>
|
||||
<TreeDataTemplate DataType="models:DeviceTypeInfo" ItemsSource="{Binding Groups}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
<TreeDataTemplate DataType="models:DriverInfoGroup" ItemsSource="{Binding Drivers}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
<TreeDataTemplate DataType="models:DriverInfo" >
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" ToolTip.Tip="{Binding Desc}" >
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<TextBlock Text=":" />
|
||||
<TextBlock Text="{Binding Desc}" />
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
</TreeView.DataTemplates>
|
||||
</TreeView>
|
||||
</Border>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal"
|
||||
HorizontalAlignment="Left" Spacing="12"
|
||||
Margin="8">
|
||||
<TextBlock Text="{Binding Driver.Desc}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class DriverSelectedDialog : UserControl
|
||||
{
|
||||
public DriverSelectedDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
279
Plugins/Driver/Cowain.Driver/Views/TagManagementView.axaml
Normal file
279
Plugins/Driver/Cowain.Driver/Views/TagManagementView.axaml
Normal file
@@ -0,0 +1,279 @@
|
||||
<UserControl
|
||||
x:Class="Plugin.Cowain.Driver.Views.TagManagementView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="using:Cowain.Base.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:extensions="using:Cowain.Base.Extensions"
|
||||
xmlns:i="using:Avalonia.Xaml.Interactivity"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Core"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:DataType="vm:TagManagementViewModel"
|
||||
mc:Ignorable="d">
|
||||
<Interaction.Behaviors>
|
||||
<ia:EventTriggerBehavior EventName="Loaded">
|
||||
<ia:InvokeCommandAction Command="{Binding LoadedCommand}" />
|
||||
</ia:EventTriggerBehavior>
|
||||
</Interaction.Behaviors>
|
||||
|
||||
<UserControl.Resources>
|
||||
<conv:I18nLocalizeConverter x:Key="i18nConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid RowDefinitions="Auto * Auto">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="10,8"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<u:IconButton
|
||||
Command="{Binding AddTagCommand}"
|
||||
IsEnabled="{extensions:MenuEnable TagManagementView,
|
||||
add}"
|
||||
Theme="{DynamicResource BorderlessIconButton}"
|
||||
ToolTip.Tip="{i18n:Localize TagManagement.Tooltip.Add}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconPlusStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
|
||||
<u:IconButton
|
||||
Command="{Binding RefreshCommand}"
|
||||
CommandParameter="{Binding #page.CurrentPage}"
|
||||
Theme="{DynamicResource BorderlessIconButton}"
|
||||
ToolTip.Tip="{i18n:Localize TagManagement.Tooltip.Refresh}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconRedoStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
Command="{Binding SaveTagCommand}"
|
||||
IsEnabled="{extensions:MenuEnable TagManagementView,
|
||||
save}"
|
||||
Theme="{DynamicResource BorderlessIconButton}"
|
||||
ToolTip.Tip="{i18n:Localize TagManagement.Tooltip.Save}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconSave}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
Command="{Binding ImportCommand}"
|
||||
IsEnabled="{extensions:MenuEnable TagManagementView,
|
||||
import}"
|
||||
Theme="{DynamicResource BorderlessIconButton}"
|
||||
ToolTip.Tip="{i18n:Localize TagManagement.Tooltip.Import}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconImport}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
<u:IconButton
|
||||
Command="{Binding ExportCommand}"
|
||||
Theme="{DynamicResource BorderlessIconButton}"
|
||||
ToolTip.Tip="{i18n:Localize TagManagement.Tooltip.Export}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconExternalOpenStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="1"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
FrozenColumnCount="2"
|
||||
HeadersVisibility="All"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
ItemsSource="{Binding Tags}"
|
||||
RowHeight="30">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
Width="80"
|
||||
x:DataType="vm:TagViewModel"
|
||||
Binding="{Binding Id}"
|
||||
Header="{i18n:Localize TagManagement.DataGrid.Id}" />
|
||||
|
||||
<DataGridTemplateColumn Width="100" Header="{i18n:Localize TagManagement.DataGrid.Edit}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<u:IconButton
|
||||
x:CompileBindings="False"
|
||||
Command="{Binding $parent[DataGrid].DataContext.DeleteTagCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
IsEnabled="{extensions:MenuEnable TagManagementView,
|
||||
delete}"
|
||||
Theme="{DynamicResource BorderlessIconButton}"
|
||||
ToolTip.Tip="{i18n:Localize TagManagement.Tooltip.Delete}">
|
||||
<u:IconButton.Icon>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{StaticResource SemiIconDeleteStroked}" />
|
||||
</u:IconButton.Icon>
|
||||
</u:IconButton>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="140" Header="{i18n:Localize TagManagement.DataGrid.DeviceName}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
DisplayMemberBinding="{Binding DeviceName}"
|
||||
ItemsSource="{x:Static vm:TagManagementViewModel.DeviceList}"
|
||||
SelectedValue="{Binding DeviceId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValueBinding="{Binding Id}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="140"
|
||||
x:DataType="vm:TagViewModel"
|
||||
Binding="{Binding Name}"
|
||||
Header="{i18n:Localize TagManagement.DataGrid.Name}" />
|
||||
<DataGridTextColumn
|
||||
Width="140"
|
||||
x:DataType="vm:TagViewModel"
|
||||
Binding="{Binding Address}"
|
||||
Header="{i18n:Localize TagManagement.DataGrid.Address}" />
|
||||
<DataGridTemplateColumn Width="120" Header="{i18n:Localize TagManagement.DataGrid.DataType}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{x:Static vm:TagManagementViewModel.DataTypes}"
|
||||
SelectedValue="{Binding DataType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Width="120" Header="{i18n:Localize TagManagement.DataGrid.OperMode}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{x:Static vm:TagManagementViewModel.OperModes}"
|
||||
SelectedValue="{Binding OperMode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Width="80" Header="{i18n:Localize TagManagement.DataGrid.ArrayCount}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:TagViewModel">
|
||||
<NumericUpDown
|
||||
HorizontalAlignment="Stretch"
|
||||
FormatString="N0"
|
||||
Minimum="1"
|
||||
Value="{Binding ArrayCount}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="140"
|
||||
x:DataType="vm:TagViewModel"
|
||||
Binding="{Binding Json}"
|
||||
Header="{i18n:Localize TagManagement.DataGrid.Json}" />
|
||||
<DataGridTemplateColumn Width="110" Header="{i18n:Localize TagManagement.DataGrid.AlarmEnable}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ToggleSwitch
|
||||
IsChecked="{Binding AlarmEnable}"
|
||||
OffContent="{i18n:Localize No}"
|
||||
OnContent="{i18n:Localize Yes}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Width="120" Header="{i18n:Localize TagManagement.DataGrid.AlarmLevel}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{x:Static vm:TagManagementViewModel.AlarmLevels}"
|
||||
SelectedValueBinding="{Binding Id}"
|
||||
SelectedValue="{Binding AlarmLevel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AlarmLevelViewModel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="10 8">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Width="120" Header="{i18n:Localize TagManagement.DataGrid.AlarmGroup}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{x:Static vm:TagManagementViewModel.AlarmGroups}"
|
||||
SelectedValueBinding="{Binding Id}"
|
||||
SelectedValue="{Binding AlarmGroup, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AlarmGroupViewModel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="10 8">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="140"
|
||||
x:DataType="vm:TagViewModel"
|
||||
Binding="{Binding AlarmValue}"
|
||||
Header="{i18n:Localize TagManagement.DataGrid.AlarmValue}" />
|
||||
<DataGridTextColumn
|
||||
Width="200"
|
||||
x:DataType="vm:TagViewModel"
|
||||
Binding="{Binding AlarmMsg}"
|
||||
Header="{i18n:Localize TagManagement.DataGrid.AlarmMsg}" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
MinWidth="200"
|
||||
x:DataType="vm:TagViewModel"
|
||||
Binding="{Binding Desc}"
|
||||
Header="{i18n:Localize TagManagement.DataGrid.Desc}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<u:Pagination
|
||||
Name="page"
|
||||
Grid.Row="2"
|
||||
Command="{Binding RefreshCommand}"
|
||||
CommandParameter="{Binding $self.CurrentPage}"
|
||||
CurrentPage="{Binding PageIndex, Mode=TwoWay}"
|
||||
PageSize="{Binding PageSize, Mode=TwoWay}"
|
||||
PageSizeOptions="10, 20, 50, 100"
|
||||
ShowPageSizeSelector="True"
|
||||
ShowQuickJump="True"
|
||||
TotalCount="{Binding Totals}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class TagManagementView : UserControl
|
||||
{
|
||||
public TagManagementView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
136
Plugins/Driver/Cowain.Driver/Views/VariableMonitorView.axaml
Normal file
136
Plugins/Driver/Cowain.Driver/Views/VariableMonitorView.axaml
Normal file
@@ -0,0 +1,136 @@
|
||||
<UserControl
|
||||
x:Class="Plugin.Cowain.Driver.Views.VariableMonitorView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="using:Cowain.Base.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:extensions="using:Cowain.Base.Extensions"
|
||||
xmlns:i="using:Avalonia.Xaml.Interactivity"
|
||||
xmlns:i18n="clr-namespace:Ke.Bee.Localization.Extensions;assembly=Ke.Bee.Localization"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Core"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:vm="using:Plugin.Cowain.Driver.ViewModels"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:DataType="vm:VariableMonitorViewModel"
|
||||
mc:Ignorable="d">
|
||||
<Interaction.Behaviors>
|
||||
<ia:EventTriggerBehavior EventName="Loaded">
|
||||
<ia:InvokeCommandAction Command="{Binding LoadedCommand}" />
|
||||
</ia:EventTriggerBehavior>
|
||||
</Interaction.Behaviors>
|
||||
|
||||
<Grid ColumnDefinitions="Auto *">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{i18n:Localize VariableMonitorView.DataGrid.DeviceName}" />
|
||||
<u:SelectionList
|
||||
Name="roleMenu"
|
||||
Width="150"
|
||||
ItemsSource="{Binding Devices}"
|
||||
SelectedItem="{Binding SelectedDevice}">
|
||||
<u:SelectionList.Indicator>
|
||||
<Border Background="Transparent" CornerRadius="4">
|
||||
<Border
|
||||
Width="4"
|
||||
Margin="0,8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{DynamicResource SemiBlue6}"
|
||||
CornerRadius="4" />
|
||||
</Border>
|
||||
</u:SelectionList.Indicator>
|
||||
<u:SelectionList.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Panel Height="30">
|
||||
<TextBlock
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center"
|
||||
Classes.Active="{Binding $parent[u:SelectionListItem].IsSelected, Mode=OneWay}"
|
||||
Text="{Binding DeviceName}">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="TextBlock.Active">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SemiOrange6}" />
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
</TextBlock>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</u:SelectionList.ItemTemplate>
|
||||
</u:SelectionList>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Grid Grid.Column="1" RowDefinitions="* Auto ">
|
||||
<DataGrid
|
||||
Margin="8"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HeadersVisibility="All"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding SelectedDevice.Variables}">
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding Id}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.Id}" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding Name}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.Name}" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding Address}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.Address}" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding Value}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.Value}" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding IsSuccess}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.IsSuccess}" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding ArrayCount}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.ArrayCount}" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding DataType}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.DataType}" />
|
||||
<DataGridTextColumn
|
||||
MinWidth="200"
|
||||
x:DataType="vm:VariableViewModel"
|
||||
Binding="{Binding Desc}"
|
||||
Header="{i18n:Localize VariableMonitorView.DataGrid.Desc}" />
|
||||
|
||||
|
||||
</DataGrid.Columns>
|
||||
|
||||
</DataGrid>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<TextBlock Text="{i18n:Localize VariableMonitorView.Connected}" />
|
||||
<TextBlock Text=":" />
|
||||
<TextBlock Text="{Binding SelectedDevice.IsConnected}" />
|
||||
<TextBlock Text="{i18n:Localize VariableMonitorView.ReadUseTime}" />
|
||||
<TextBlock Text=":" />
|
||||
<TextBlock Text="{Binding SelectedDevice.ReadUseTime}" />
|
||||
<TextBlock Text="ms" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Plugin.Cowain.Driver.Views;
|
||||
|
||||
public partial class VariableMonitorView : UserControl
|
||||
{
|
||||
public VariableMonitorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
135
Plugins/Driver/Cowain.Driver/i18n/en-US.json
Normal file
135
Plugins/Driver/Cowain.Driver/i18n/en-US.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"Menu.Toolbar.DeviceManagement": "Device",
|
||||
|
||||
"Menu.Sidebar.DeviceManagement": "DeviceManagement",
|
||||
"Menu.Sidebar.TagManagement": "TagManagement",
|
||||
"Menu.Sidebar.ActionManagement": "ActionManagement",
|
||||
"Menu.Sidebar.VariableMonitor": "VariableMonitor",
|
||||
"Menu.Sidebar.AlarmRealTime": "AlarmNow",
|
||||
"Menu.Sidebar.AlarmHistory": "AlarmHistory",
|
||||
|
||||
"Button.Tooptip.Refresh": "Refresh",
|
||||
"Button.Tooptip.Export": "Export",
|
||||
|
||||
"DeviceManagement.Dialog.Title": "DeviceEdit",
|
||||
|
||||
"DeviceManagement.DataGrid.Id": "Id",
|
||||
"DeviceManagement.DataGrid.DeviceName": "DeviceName",
|
||||
"DeviceManagement.DataGrid.DriverName": "DriverName",
|
||||
"DeviceManagement.DataGrid.DeviceType": "DeviceType",
|
||||
"DeviceManagement.DataGrid.Param": "Param",
|
||||
"DeviceManagement.DataGrid.Enable": "Enable",
|
||||
"DeviceManagement.DataGrid.Desc": "Desc",
|
||||
"DeviceManagement.DataGrid.Edit": "Edit",
|
||||
"DeviceManagement.DataGrid.Param.Placeholder": "Hover the Mouse to View",
|
||||
|
||||
"DeviceManagement.Tooltip.Add": "Add Device",
|
||||
"DeviceManagement.Tooltip.Refresh": "Refresh",
|
||||
"DeviceManagement.Tooltip.Export": "Export",
|
||||
"DeviceManagement.Tooltip.Edit": "Edit",
|
||||
"DeviceManagement.Tooltip.Delete": "Delete",
|
||||
|
||||
"DeviceEditDilog.Error.DeviceNameNull": "DeviceName is Null",
|
||||
"DeviceEditDilog.Error.DriverNameNull": "DriverName is Null",
|
||||
"DeviceEditDilog.Error.DeviceTypeNull": "DeviceType is Null",
|
||||
"DeviceEditDilog.Error.ParamNull": "Param is Null",
|
||||
"DeviceParam.Dialog.Title": "Device Param Edit",
|
||||
"DriverSelect.Dialog.Title": "DriverSelect",
|
||||
"DeviceAddDilog.Error.DriverNull": "Driver is Null",
|
||||
"DeviceEditDilog.Add.Success": "Add Device Success",
|
||||
"DeviceEditDilog.Add.Error": "Add Device Error",
|
||||
"DeviceEditDilog.Delete.Success": "Delete Device Success",
|
||||
"DeviceEditDilog.Delete.Error": "Delete Device Error",
|
||||
"DeviceEditDilog.Edit.Success": "Device Edit Success",
|
||||
"DeviceEditDilog.Edit.Error": "Device Edit Error",
|
||||
|
||||
"Cowain.Plugin.Driver.DeviceManagement": "DeviceManagement",
|
||||
|
||||
"TagManagement.DataGrid.Id": "Id",
|
||||
"TagManagement.DataGrid.DeviceName": "DeviceName",
|
||||
"TagManagement.DataGrid.Name": "Name",
|
||||
"TagManagement.DataGrid.Address": "Address",
|
||||
"TagManagement.DataGrid.DataType": "DataType",
|
||||
"TagManagement.DataGrid.Desc": "Desc",
|
||||
"TagManagement.DataGrid.ArrayCount": "ArrayCount",
|
||||
"TagManagement.DataGrid.Edit": "Edit",
|
||||
"TagManagement.DataGrid.OperMode": "OperMode",
|
||||
"TagManagement.DataGrid.AlarmEnable": "AlarmEnble",
|
||||
"TagManagement.DataGrid.AlarmValue": "AlarmValue",
|
||||
"TagManagement.DataGrid.AlarmMsg": "AlarmMsg",
|
||||
"TagManagement.DataGrid.AlarmLevel": "AlarmLevel",
|
||||
"TagManagement.DataGrid.AlarmGroup": "AlarmGroup",
|
||||
"TagManagement.DataGrid.Json": "Json",
|
||||
|
||||
"TagManagement.Tooltip.Add": "Add",
|
||||
"TagManagement.Tooltip.Refresh": "Refresh",
|
||||
"TagManagement.Tooltip.Export": "Export",
|
||||
"TagManagement.Tooltip.Import": "Import",
|
||||
"TagManagement.Tooltip.Save": "Save",
|
||||
"TagManagement.Tooltip.Edit": "Edit",
|
||||
"TagManagement.Tooltip.Delete": "Delete",
|
||||
|
||||
"TagManagement.Save.Success": "Save Tag Success",
|
||||
"TagManagement.Save.Error": "Save Tag Error",
|
||||
"TagManagement.Delete.Success": "Delete Tag Success",
|
||||
"TagManagement.Delete.Error": "Delete Tag Error",
|
||||
"TagManagement.Export.Success": "Export Tag Success",
|
||||
"TagManagement.Export.Error": "Export Tag Error",
|
||||
"TagManagement.Import.Success": "Import Tag Success",
|
||||
"TagManagement.Import.Error": "Import Tag Error",
|
||||
|
||||
"ActionManagement.DataGrid.Id": "Id",
|
||||
"ActionManagement.DataGrid.DeviceName": "DeviceName",
|
||||
"ActionManagement.DataGrid.ActionName": "ActionName",
|
||||
"ActionManagement.DataGrid.Desc": "Desc",
|
||||
"ActionManagement.DataGrid.TagName": "TagName",
|
||||
"ActionManagement.DataGrid.Condition": "Condition",
|
||||
"ActionManagement.DataGrid.ActionValue": "ActionValue",
|
||||
"ActionManagement.DataGrid.Edit": "Edit",
|
||||
"ActionManagement.DataGrid.Param": "Param",
|
||||
|
||||
"ActionManagement.Tooltip.Add": "Add",
|
||||
"ActionManagement.Tooltip.Refresh": "Refresh",
|
||||
"ActionManagement.Tooltip.Export": "Export",
|
||||
"ActionManagement.Tooltip.Save": "Save",
|
||||
"ActionManagement.Tooltip.Delete": "Delete",
|
||||
|
||||
"ActionManagement.Save.SelectedDeviceNull": "SelectedDevice is Null",
|
||||
"ActionManagement.Save.VarActionsNull": "VarActions is Null",
|
||||
"ActionManagement.Save.Success": "Action Save Success",
|
||||
"ActionManagement.Save.Error": "Action Save Error",
|
||||
"ActionManagement.Delete.Success": "Delete Action Success",
|
||||
"ActionManagement.Delete.Error": "Delete Action Error",
|
||||
"ActionManagement.Export.Success": "Export Action Success",
|
||||
"ActionManagement.Export.Error": "Export Action Error",
|
||||
|
||||
"AlarmRealTimeView.DataGrid.Desc": "Desc",
|
||||
"AlarmRealTimeView.DataGrid.StartTime": "StartTime",
|
||||
"AlarmRealTimeView.DataGrid.GroupName": "GroupName",
|
||||
"AlarmRealTimeView.DataGrid.LevelName": "LevelName",
|
||||
"AlarmRealTimeView.DataGrid.StopTime": "StopTime",
|
||||
"AlarmRealTimeView.DataGrid.Status": "Status",
|
||||
|
||||
"AlarmHistory.Warning.StartDateIsNull":"StartDateIsNull",
|
||||
"AlarmHistory.Warning.EndDateIsNull":"EndDateIsNull",
|
||||
"AlarmHistory.SelectGroup":"SelectGroup",
|
||||
"AlarmHistory.SelectLevel":"SelectLevel",
|
||||
|
||||
"AlarmHistory.Export.Success":"Export Success",
|
||||
"AlarmHistory.Export.Error":"Export Error",
|
||||
|
||||
"VariableMonitorView.Connected": "Connected State",
|
||||
"VariableMonitorView.ReadUseTime": "ReadUseTime",
|
||||
|
||||
"VariableMonitorView.DataGrid.Id": "Id",
|
||||
"VariableMonitorView.DataGrid.DeviceName": "DeviceName",
|
||||
"VariableMonitorView.DataGrid.Name": "Name",
|
||||
"VariableMonitorView.DataGrid.Address": "Address",
|
||||
"VariableMonitorView.DataGrid.Desc": "Desc",
|
||||
"VariableMonitorView.DataGrid.ArrayCount": "ArrayCount",
|
||||
"VariableMonitorView.DataGrid.DataType": "DataType",
|
||||
"VariableMonitorView.DataGrid.IsSuccess": "IsSuccess",
|
||||
"VariableMonitorView.DataGrid.Value": "Value",
|
||||
"VariableMonitorView.DataGrid.OldValue": "OldValue"
|
||||
|
||||
}
|
||||
141
Plugins/Driver/Cowain.Driver/i18n/zh-CN.json
Normal file
141
Plugins/Driver/Cowain.Driver/i18n/zh-CN.json
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
|
||||
"Menu.Toolbar.DeviceManagement": "设备",
|
||||
|
||||
"Menu.Sidebar.DeviceManagement": "设备管理",
|
||||
"Menu.Sidebar.TagManagement": "变量管理",
|
||||
"Menu.Sidebar.ActionManagement": "事件管理",
|
||||
"Menu.Sidebar.VariableMonitor": "变量监视",
|
||||
"Menu.Sidebar.AlarmRealTime": "实时报警",
|
||||
"Menu.Sidebar.AlarmHistory": "历史报警",
|
||||
|
||||
|
||||
"Button.Tooptip.Refresh": "刷新",
|
||||
"Button.Tooptip.Export": "导出",
|
||||
|
||||
"DeviceManagement.Dialog.Title": "设备编辑",
|
||||
|
||||
|
||||
"DeviceManagement.DataGrid.Id": "序号",
|
||||
"DeviceManagement.DataGrid.DeviceName": "设备名称",
|
||||
"DeviceManagement.DataGrid.DriverName": "驱动名称",
|
||||
"DeviceManagement.DataGrid.DeviceType": "设备类型",
|
||||
"DeviceManagement.DataGrid.Param": "参数",
|
||||
"DeviceManagement.DataGrid.Enable": "启用",
|
||||
"DeviceManagement.DataGrid.Desc": "备注",
|
||||
"DeviceManagement.DataGrid.Edit": "编辑",
|
||||
"DeviceManagement.DataGrid.Param.Placeholder": "鼠标悬停查看",
|
||||
|
||||
"DeviceManagement.Tooltip.Add": "添加设备",
|
||||
"DeviceManagement.Tooltip.Refresh": "刷新",
|
||||
"DeviceManagement.Tooltip.Export": "导出",
|
||||
"DeviceManagement.Tooltip.Edit": "编辑",
|
||||
"DeviceManagement.Tooltip.Delete": "删除",
|
||||
|
||||
"DeviceEditDilog.Error.DeviceNameNull": "设备名称不能为空",
|
||||
"DeviceEditDilog.Error.DriverNameNull": "驱动名称不能为空",
|
||||
"DeviceEditDilog.Error.DeviceTypeNull": "设备类型不能为空",
|
||||
"DeviceEditDilog.Error.ParamNull": "参数不能为空",
|
||||
"DeviceParam.Dialog.Title": "设备参数编辑",
|
||||
"DriverSelect.Dialog.Title": "驱动选择",
|
||||
"DeviceAddDilog.Error.DriverNull": "驱动选择错误",
|
||||
"DeviceEditDilog.Add.Success": "添加设备成功",
|
||||
"DeviceEditDilog.Add.Error": "添加设备失败",
|
||||
"DeviceEditDilog.Delete.Success": "删除设备成功",
|
||||
"DeviceEditDilog.Delete.Error": "删除设备失败",
|
||||
"DeviceEditDilog.Edit.Success": "设备编辑成功",
|
||||
"DeviceEditDilog.Edit.Error": "设备编辑错误",
|
||||
|
||||
"Cowain.Plugin.Driver.DeviceManagement": "设备管理页面",
|
||||
|
||||
"TagManagement.DataGrid.Id": "序号",
|
||||
"TagManagement.DataGrid.DeviceName": "设备名称",
|
||||
"TagManagement.DataGrid.Name": "变量名",
|
||||
"TagManagement.DataGrid.Address": "地址",
|
||||
"TagManagement.DataGrid.DataType": "数据类型",
|
||||
"TagManagement.DataGrid.Desc": "备注",
|
||||
"TagManagement.DataGrid.ArrayCount": "长度",
|
||||
"TagManagement.DataGrid.Edit": "编辑",
|
||||
"TagManagement.DataGrid.OperMode": "操作模式",
|
||||
"TagManagement.DataGrid.AlarmEnable": "报警启用",
|
||||
"TagManagement.DataGrid.AlarmValue": "报警条件",
|
||||
"TagManagement.DataGrid.AlarmMsg": "报警信息",
|
||||
"TagManagement.DataGrid.AlarmLevel": "报警级别",
|
||||
"TagManagement.DataGrid.AlarmGroup": "报警组",
|
||||
"TagManagement.DataGrid.Json": "参数",
|
||||
|
||||
"TagManagement.Tooltip.Add": "添加变量",
|
||||
"TagManagement.Tooltip.Refresh": "刷新",
|
||||
"TagManagement.Tooltip.Export": "导出",
|
||||
"TagManagement.Tooltip.Import": "导入",
|
||||
"TagManagement.Tooltip.Save": "保存",
|
||||
"TagManagement.Tooltip.Edit": "编辑",
|
||||
"TagManagement.Tooltip.Delete": "删除",
|
||||
|
||||
|
||||
"TagManagement.Save.Success": "保存变量成功",
|
||||
"TagManagement.Save.Error": "保存变量失败",
|
||||
"TagManagement.Delete.Success": "删除变量成功",
|
||||
"TagManagement.Delete.Error": "删除变量失败",
|
||||
"TagManagement.Export.Success": "导出变量成功",
|
||||
"TagManagement.Export.Error": "导出变量失败",
|
||||
"TagManagement.Import.Success": "导入变量成功",
|
||||
"TagManagement.Import.Error": "导入变量失败",
|
||||
|
||||
"ActionManagement.DataGrid.Id": "序号",
|
||||
"ActionManagement.DataGrid.DeviceName": "设备名称",
|
||||
"ActionManagement.DataGrid.ActionName": "动作名称",
|
||||
"ActionManagement.DataGrid.Desc": "备注",
|
||||
"ActionManagement.DataGrid.TagName": "变量名称",
|
||||
"ActionManagement.DataGrid.Condition": "条件",
|
||||
"ActionManagement.DataGrid.ActionValue": "值",
|
||||
"ActionManagement.DataGrid.Edit": "编辑",
|
||||
"ActionManagement.DataGrid.Param": "参数",
|
||||
|
||||
"ActionManagement.Tooltip.Add": "添加动作",
|
||||
"ActionManagement.Tooltip.Refresh": "刷新",
|
||||
"ActionManagement.Tooltip.Export": "导出",
|
||||
"ActionManagement.Tooltip.Save": "保存",
|
||||
"ActionManagement.Tooltip.Delete": "删除",
|
||||
|
||||
"ActionManagement.Save.SelectedDeviceNull": "请选择设备",
|
||||
"ActionManagement.Save.VarActionsNull": "没有事件",
|
||||
"ActionManagement.Save.Success": "保存事件成功",
|
||||
"ActionManagement.Save.Error": "保存事件失败",
|
||||
"ActionManagement.Delete.Success": "删除事件成功",
|
||||
"ActionManagement.Delete.Error": "删除事件失败",
|
||||
"ActionManagement.Export.Success": "导出事件成功",
|
||||
"ActionManagement.Export.Error": "导出事件失败",
|
||||
|
||||
"AlarmRealTimeView.DataGrid.Desc": "报警详情",
|
||||
"AlarmRealTimeView.DataGrid.StartTime": "报警时间",
|
||||
"AlarmRealTimeView.DataGrid.GroupName": "报警组",
|
||||
"AlarmRealTimeView.DataGrid.LevelName": "报警级别",
|
||||
"AlarmRealTimeView.DataGrid.StopTime": "消除时间",
|
||||
"AlarmRealTimeView.DataGrid.Status": "状态",
|
||||
|
||||
"AlarmHistory.Warning.StartDateIsNull":"开始时间不能为空",
|
||||
"AlarmHistory.Warning.EndDateIsNull":"结束时间不能为空",
|
||||
"AlarmHistory.SelectGroup":"报警组",
|
||||
"AlarmHistory.SelectLevel":"级别",
|
||||
|
||||
"AlarmHistory.Export.Success":"导出成功",
|
||||
"AlarmHistory.Export.Error":"导出失败",
|
||||
|
||||
"VariableMonitorView.Connected": "连接状态",
|
||||
"VariableMonitorView.ReadUseTime": "读取时间",
|
||||
|
||||
"VariableMonitorView.DataGrid.Id": "序号",
|
||||
"VariableMonitorView.DataGrid.DeviceName": "设备名称",
|
||||
"VariableMonitorView.DataGrid.Name": "变量名称",
|
||||
"VariableMonitorView.DataGrid.Address": "地址",
|
||||
"VariableMonitorView.DataGrid.Desc": "备注",
|
||||
"VariableMonitorView.DataGrid.ArrayCount": "数据长度",
|
||||
"VariableMonitorView.DataGrid.DataType": "数据类型",
|
||||
"VariableMonitorView.DataGrid.IsSuccess": "状态",
|
||||
"VariableMonitorView.DataGrid.Value": "值",
|
||||
"VariableMonitorView.DataGrid.OldValue": "旧值"
|
||||
|
||||
|
||||
|
||||
}
|
||||
239
Plugins/Driver/Driver.DeviceOpcUa/ExtensionObjectHelper.cs
Normal file
239
Plugins/Driver/Driver.DeviceOpcUa/ExtensionObjectHelper.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Plugin.Driver.DeviceOpcUa;
|
||||
|
||||
public static class ExtensionObjectHelper
|
||||
{
|
||||
public static string ReadStruct(string address, DataValue value, ISession? session)
|
||||
{
|
||||
if (session == null) return string.Empty;
|
||||
if (value == null || value.Value == null) return string.Empty;
|
||||
NodeId node = new NodeId(address);
|
||||
//获取Node 信息 验证Node的数据类型
|
||||
VariableNode? nodeInfo = session.ReadNode(node) as VariableNode;
|
||||
if (nodeInfo == null) return string.Empty;
|
||||
var datatypeNode = (session.ReadNode(nodeInfo.DataType)) as DataTypeNode;
|
||||
if (datatypeNode == null) return string.Empty;
|
||||
var typeDefine = datatypeNode.DataTypeDefinition.Body as StructureDefinition;
|
||||
if (typeDefine == null) return string.Empty;
|
||||
int index = 0;
|
||||
if (value.Value is ExtensionObject[])//数组
|
||||
{
|
||||
var res = new Dictionary<int, object?>();
|
||||
var values = value.Value as ExtensionObject[];
|
||||
if (values == null) return string.Empty;
|
||||
int i = 0;
|
||||
foreach (var item in values)
|
||||
{
|
||||
var obj = GetJsonFromExtensionObject(address, item, typeDefine, session, ref index);
|
||||
if (obj != null)
|
||||
{
|
||||
res[i] = obj;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
return JsonSerializer.Serialize(res, jsonOptions);
|
||||
}
|
||||
else //非数组
|
||||
{
|
||||
var values = value.Value as ExtensionObject;
|
||||
if (values == null) return string.Empty;
|
||||
var obj = GetJsonFromExtensionObject(address, values, typeDefine, session, ref index);
|
||||
if (obj == null) return string.Empty;
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
return JsonSerializer.Serialize(obj, jsonOptions);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static object? GetJsonFromExtensionObject(string address, ExtensionObject value, StructureDefinition structure, ISession session, ref int index)
|
||||
{
|
||||
var res = new Dictionary<string, object?>();
|
||||
var data = value.Body as byte[];
|
||||
if (data == null) return null;
|
||||
foreach (var field in structure.Fields)
|
||||
{
|
||||
if (field.ValueRank == 1)
|
||||
{
|
||||
//是数组,需要读取数组长度
|
||||
string fieldNodeId = $"{address}.{field.Name}"; // 例如:"StationStatus1.AlarmA1"
|
||||
VariableNode? fieldNode = session.ReadNode(new NodeId(fieldNodeId)) as VariableNode;
|
||||
if (fieldNode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (fieldNode.ArrayDimensions != null && fieldNode.ArrayDimensions.Count > 0)
|
||||
{
|
||||
int count = (int)BitConverter.ToUInt32(data.Skip(index).Take(4).ToArray(), 0);
|
||||
index += 4;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
res[$"{field.Name}[{i}]"] = BitConverter.ToBoolean(data.Skip(index + i).Take(1).ToArray(), 0);
|
||||
}
|
||||
index += count;
|
||||
}
|
||||
}
|
||||
else if (field.DataType.NamespaceIndex == 2)
|
||||
{
|
||||
//结构体嵌套
|
||||
var count = BitConverter.ToUInt32(data.Skip(index).Take(4).ToArray(), 0);
|
||||
index += 4;
|
||||
var datatypeNode1 = (session.ReadNode(field.DataType)) as DataTypeNode;
|
||||
if (datatypeNode1 == null) return null;
|
||||
var typeDefine = datatypeNode1.DataTypeDefinition.Body as StructureDefinition;
|
||||
if (typeDefine == null) return null;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
res[field.Name + i] = GetJsonFromExtensionObject(address, value, typeDefine, session, ref index);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string name = field.Name;
|
||||
if (field.DataType == DataTypeIds.String)
|
||||
{
|
||||
int length = (int)BitConverter.ToUInt32(data.Skip(index).Take(4).ToArray(), 0);
|
||||
index += 4;
|
||||
string re = Encoding.Default.GetString(data.Skip(index).Take(length).ToArray());
|
||||
res[name] = re;
|
||||
index += length;
|
||||
}
|
||||
else if (field.DataType == DataTypeIds.UInt32)
|
||||
{
|
||||
UInt32 re = BitConverter.ToUInt32(data.Skip(index).Take(4).ToArray(), 0);
|
||||
index += 4;
|
||||
res[name] = re;
|
||||
}
|
||||
else if (field.DataType == DataTypeIds.Float)
|
||||
{
|
||||
float re = BitConverter.ToSingle(data.Skip(index).Take(4).ToArray(), 0);
|
||||
index += 4;
|
||||
res[name] = re;
|
||||
}
|
||||
else if (field.DataType == DataTypeIds.Boolean)
|
||||
{
|
||||
bool re = BitConverter.ToBoolean(data.Skip(index).Take(1).ToArray());
|
||||
res[name] = re;
|
||||
index += 1;
|
||||
}
|
||||
else if (field.DataType == DataTypeIds.Double)
|
||||
{
|
||||
double re = BitConverter.ToDouble(data.Skip(index).Take(8).ToArray());
|
||||
res[name] = re;
|
||||
index += 8;
|
||||
}
|
||||
else if (field.DataType == DataTypeIds.Int16)
|
||||
{
|
||||
Int16 re = BitConverter.ToInt16(data.Skip(index).Take(2).ToArray());
|
||||
res[name] = re;
|
||||
index += 2;
|
||||
}
|
||||
else if (field.DataType == DataTypeIds.UInt16)
|
||||
{
|
||||
UInt16 re = BitConverter.ToUInt16(data.Skip(index).Take(2).ToArray());
|
||||
res[name] = re;
|
||||
index += 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析ExtensionObject为可序列化的字典对象
|
||||
/// </summary>
|
||||
/// <param name="extensionObject">OPC UA扩展对象</param>
|
||||
/// <returns>解析后的键值对字典</returns>
|
||||
public static object? ResolveExtensionObject(object extensionObject)
|
||||
{
|
||||
if (extensionObject == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// 适配不同OPC UA库的ExtensionObject类型(根据实际情况调整)
|
||||
Type extObjType = extensionObject.GetType();
|
||||
|
||||
// 方式1:如果是已知的结构体类型,直接转换
|
||||
if (extObjType.IsValueType && !extObjType.IsPrimitive && !extObjType.IsEnum)
|
||||
{
|
||||
return ConvertStructToDictionary(extensionObject);
|
||||
}
|
||||
|
||||
// 方式2:适配OPC UA标准ExtensionObject(根据实际库调整属性名)
|
||||
PropertyInfo? bodyProp = extObjType?.GetProperty("Body") ??
|
||||
extObjType?.GetProperty("WrappedValue") ??
|
||||
extObjType?.GetProperty("Value");
|
||||
|
||||
if (bodyProp != null)
|
||||
{
|
||||
object? bodyValue = bodyProp?.GetValue(extensionObject);
|
||||
if (bodyValue != null && bodyValue.GetType().IsValueType)
|
||||
{
|
||||
return ConvertStructToDictionary(bodyValue);
|
||||
}
|
||||
return bodyValue;
|
||||
}
|
||||
|
||||
// 方式3:无法解析时返回原始对象(最终会序列化为JSON)
|
||||
return extensionObject;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 解析失败时记录异常并返回原始对象
|
||||
System.Diagnostics.Debug.WriteLine($"解析结构体失败: {ex.Message}");
|
||||
return extensionObject;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将结构体转换为字典(便于JSON序列化)
|
||||
/// </summary>
|
||||
/// <param name="structObj">结构体对象</param>
|
||||
/// <returns>键值对字典</returns>
|
||||
private static object? ConvertStructToDictionary(object structObj)
|
||||
{
|
||||
if (structObj == null)
|
||||
return null;
|
||||
|
||||
Type structType = structObj.GetType();
|
||||
var dict = new System.Collections.Generic.Dictionary<string, object?>();
|
||||
|
||||
// 获取结构体的所有公共字段和属性
|
||||
foreach (FieldInfo field in structType.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
dict[field.Name] = field?.GetValue(structObj);
|
||||
}
|
||||
|
||||
foreach (PropertyInfo prop in structType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.CanRead)
|
||||
{
|
||||
dict[prop.Name] = prop.GetValue(structObj);
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
6
Plugins/Driver/Driver.DeviceOpcUa/OpcUaConsts.cs
Normal file
6
Plugins/Driver/Driver.DeviceOpcUa/OpcUaConsts.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Plugin.Driver.DeviceOpcUa;
|
||||
|
||||
public class OpcUaConsts
|
||||
{
|
||||
public const string PluginName = "DeviceOpcUa";
|
||||
}
|
||||
931
Plugins/Driver/Driver.DeviceOpcUa/OpcUaDriver.cs
Normal file
931
Plugins/Driver/Driver.DeviceOpcUa/OpcUaDriver.cs
Normal file
@@ -0,0 +1,931 @@
|
||||
using AspectInjector.Broker;
|
||||
using Cowain.Base.Helpers;
|
||||
using Cowain.Base.Models;
|
||||
using Driver.DeviceOpcUa.ViewModels;
|
||||
using Driver.DeviceOpcUa.Views;
|
||||
using HslCommunication.Core;
|
||||
using Ke.Bee.Localization.Localizer.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using OpcUaHelper;
|
||||
using Plugin.Cowain.Driver.Abstractions;
|
||||
using Plugin.Cowain.Driver.Attributes;
|
||||
using Plugin.Cowain.Driver.Models;
|
||||
using Plugin.Cowain.Driver.Models.Enum;
|
||||
using Plugin.Cowain.Driver.ViewModels;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
|
||||
|
||||
namespace Plugin.Driver.DeviceOpcUa
|
||||
{
|
||||
public enum LoginMode
|
||||
{
|
||||
//匿名模式
|
||||
Anonymous,
|
||||
//用户名密码模式
|
||||
Account,
|
||||
//证书模式
|
||||
Certificate
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Driver("OpcUaClient", "PLC", "OPC", "OpcUa客户端")]
|
||||
[DeviceParam(typeof(OpcUaParamViewModel), typeof(OpcUaParamDialog), typeof(OpcUaParamDialogViewModel))]
|
||||
public class OpcUaDriver : DriverBase
|
||||
{
|
||||
private OpcUaClient? opcUaClient;
|
||||
private readonly ILogger<OpcUaDriver> _logger;
|
||||
|
||||
public override string DeviceName { get; set; } = "未命名:" + typeof(OpcUaDriver);
|
||||
|
||||
//public override bool IsConnected { get { return opcUaClient?.Connected ?? false; } }
|
||||
|
||||
|
||||
|
||||
public string ServerUrl { get; set; } = "opc.tcp://192.168.114.16:4840";
|
||||
|
||||
public LoginMode LoginMode { get; set; } = LoginMode.Anonymous;
|
||||
|
||||
public string UserName { get; set; } = "admin";
|
||||
public string Password { get; set; } = "12345";
|
||||
public string CertificatePath { get; set; } = string.Empty;
|
||||
public string SecreKey { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public OpcUaDriver(ILocalizer localizer)
|
||||
{
|
||||
_logger = ServiceLocator.GetRequiredService<ILogger<OpcUaDriver>>();
|
||||
opcUaClient = new();
|
||||
opcUaClient.ConnectComplete += OnConnectComplete;
|
||||
opcUaClient.KeepAliveComplete += OnKeepAliveComplete;
|
||||
opcUaClient.ReconnectStarting += OnReconnectStarting;
|
||||
opcUaClient.ReconnectComplete += OnReconnectComplete;
|
||||
opcUaClient.OpcStatusChange += OnOpcStatusChange;
|
||||
}
|
||||
|
||||
private void OnOpcStatusChange(object? sender, OpcUaStatusEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Error)
|
||||
{
|
||||
_logger.LogWarning($"OPC UA重连失败:{DeviceModel?.DeviceName}");
|
||||
IsConnected = false;
|
||||
return;
|
||||
}
|
||||
if (!Regex.IsMatch(e.Text, "Connected"))
|
||||
{
|
||||
_logger.LogWarning($"OPC UA重连成功,{ServerUrl},{e.Text}");
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "OnOpcStatusChange异常");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReconnectComplete(object? sender, EventArgs e)
|
||||
{
|
||||
_logger.LogWarning($"OPC UA重连结束,{DeviceModel?.DeviceName}");
|
||||
}
|
||||
|
||||
private void OnReconnectStarting(object? sender, EventArgs e)
|
||||
{
|
||||
_logger.LogWarning($"OPC UA重连开始,{DeviceModel?.DeviceName}");
|
||||
}
|
||||
|
||||
private void OnKeepAliveComplete(object? sender, EventArgs e)
|
||||
{
|
||||
//_logger.LogWarning($"OPC UA每隔5秒会与服务器进行通讯验证,{DeviceModel.DeviceName}");
|
||||
}
|
||||
|
||||
private bool isSubscribed = false;
|
||||
private void OnConnectComplete(object? sender, EventArgs e)
|
||||
{
|
||||
var client = sender as OpcUaClient; //2.重连不成功
|
||||
if (client == null) return;
|
||||
IsConnected = client.Connected;
|
||||
if (!IsConnected) return;
|
||||
if (isSubscribed) return;
|
||||
try
|
||||
{
|
||||
client.RemoveAllSubscription();
|
||||
SubscribeNodes();
|
||||
isSubscribed = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsConnected = false;
|
||||
_logger.LogError(ex, $"订阅变量错误:{DeviceModel?.DeviceName}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public void SubscribeNodes()
|
||||
{
|
||||
if (DeviceModel == null) return;
|
||||
List<VariableViewModel> subInfoNodes = (from v in DeviceModel.Variables
|
||||
orderby v.Id
|
||||
where v.OperMode == OperModeEnum.Subscribe
|
||||
select v).ToList();
|
||||
_logger.LogInformation($"订阅变量开始:{DeviceModel.DeviceName}");
|
||||
foreach (var variable in subInfoNodes)
|
||||
{
|
||||
variable.IsSuccess = false;
|
||||
variable.Message = "开始订阅";
|
||||
string vkey = variable.Address + variable.DataType.ToString();
|
||||
SingleNodeIdDatasSubscription(vkey, variable.Address, (key, monitoredItem, args) =>
|
||||
{
|
||||
if (vkey == key)
|
||||
{
|
||||
MonitoredItemNotification? notification = args.NotificationValue as MonitoredItemNotification;
|
||||
if (notification != null)
|
||||
{
|
||||
variable.IsSuccess = StatusCode.IsGood(notification.Value.StatusCode);
|
||||
if (variable.IsSuccess)
|
||||
{
|
||||
GetValue(notification.Value, variable);
|
||||
}
|
||||
else
|
||||
{
|
||||
variable.Message = "更新失败";
|
||||
}
|
||||
variable.UpdateTime = notification.Message.PublishTime;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_logger.LogInformation($"订阅变量结束:{DeviceModel.DeviceName}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
public override void SetParam(string param)
|
||||
{
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
_logger.LogError($"{DeviceName},参数未空");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var _param = JsonSerializer.Deserialize<OpcUaParamViewModel>(param);
|
||||
if (_param == null)
|
||||
return;
|
||||
ServerUrl = _param.ServerUrl;
|
||||
UserName = _param.UserName;
|
||||
Password = _param.Password;
|
||||
CertificatePath = _param.CertificatePath;
|
||||
SecreKey = _param.SecreKey;
|
||||
if (Enum.TryParse(_param.LoginMode, out LoginMode resultf))
|
||||
{
|
||||
LoginMode = resultf;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{DeviceName},参数转换错误");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override async Task<bool> OpenAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (null != opcUaClient?.Session)
|
||||
{
|
||||
//如果session没问题,能关掉,为什么要重连呢,230606
|
||||
Close();
|
||||
}
|
||||
switch (LoginMode)
|
||||
{
|
||||
case LoginMode.Anonymous:
|
||||
await ConnectOfAnonymousAsync(ServerUrl);
|
||||
break;
|
||||
case LoginMode.Account:
|
||||
await OpenConnectOfAccountAsync(ServerUrl, UserName, Password);
|
||||
break;
|
||||
case LoginMode.Certificate:
|
||||
await OpenConnectOfCertificateAsync(ServerUrl, CertificatePath, SecreKey);
|
||||
break;
|
||||
default:
|
||||
_logger.LogError($"{DeviceName},未知的登录模式: {LoginMode}");
|
||||
return false;
|
||||
}
|
||||
return opcUaClient?.Connected ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{DeviceName},打开连接失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开连接【匿名方式】
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">服务器URL【格式:opc.tcp://服务器IP地址/服务名称】</param>
|
||||
private async Task ConnectOfAnonymousAsync(string serverUrl)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(serverUrl))
|
||||
{
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
opcUaClient.UserIdentity = new UserIdentity(new AnonymousIdentityToken());
|
||||
opcUaClient.ReconnectPeriod = 20000;
|
||||
await opcUaClient.ConnectServer(serverUrl);
|
||||
if (null != opcUaClient.Session)
|
||||
{
|
||||
opcUaClient.Session.OperationTimeout = 5000; // 执行操作时的超时时间。它指定了客户端等待服务器响应的最长时间,以毫秒为单位
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"连接失败,ServerUrl={serverUrl}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开连接【账号方式】
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">服务器URL【格式:opc.tcp://服务器IP地址/服务名称】</param>
|
||||
/// <param name="userName">用户名称</param>
|
||||
/// <param name="userPwd">用户密码</param>
|
||||
public async Task OpenConnectOfAccountAsync(string serverUrl, string userName, string userPwd)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(serverUrl) &&
|
||||
!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(userPwd))
|
||||
{
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
opcUaClient.UserIdentity = new UserIdentity(userName, userPwd);
|
||||
opcUaClient.ReconnectPeriod = 20000;
|
||||
await opcUaClient.ConnectServer(serverUrl);
|
||||
if (null != opcUaClient.Session)
|
||||
{
|
||||
opcUaClient.Session.OperationTimeout = 5000; // 执行操作时的超时时间。它指定了客户端等待服务器响应的最长时间,以毫秒为单位
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"连接失败,ServerUrl={serverUrl}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开连接【证书方式】
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">服务器URL【格式:opc.tcp://服务器IP地址/服务名称】</param>
|
||||
/// <param name="certificatePath">证书路径</param>
|
||||
/// <param name="secreKey">密钥</param>
|
||||
public async Task OpenConnectOfCertificateAsync(string serverUrl, string certificatePath, string secreKey)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(serverUrl) &&
|
||||
!string.IsNullOrEmpty(certificatePath) && !string.IsNullOrEmpty(secreKey))
|
||||
{
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
X509Certificate2 certificate = new X509Certificate2(certificatePath, secreKey, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
|
||||
opcUaClient.UserIdentity = new UserIdentity(certificate);
|
||||
opcUaClient.ReconnectPeriod = 20000;
|
||||
await opcUaClient.ConnectServer(serverUrl);
|
||||
if (null != opcUaClient.Session)
|
||||
{
|
||||
opcUaClient.Session.OperationTimeout = 5000; // 执行操作时的超时时间。它指定了客户端等待服务器响应的最长时间,以毫秒为单位
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"连接失败,ServerUrl={serverUrl}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override bool Close()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsConnected = false;
|
||||
isSubscribed = false;
|
||||
_logger.LogInformation($"{DeviceName}-{ServerUrl}->关闭连接");
|
||||
|
||||
if (opcUaClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
opcUaClient.RemoveAllSubscription();
|
||||
if (opcUaClient.Session != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 优雅关闭会话(通知服务器释放资源)
|
||||
if (opcUaClient.Session.Connected)
|
||||
{
|
||||
opcUaClient.Session.Close();
|
||||
}
|
||||
}
|
||||
catch (Exception closeEx)
|
||||
{
|
||||
_logger.LogWarning(closeEx, $"{DeviceName}->优雅关闭会话失败,强制终止");
|
||||
}
|
||||
}
|
||||
opcUaClient.Disconnect();
|
||||
opcUaClient.ReconnectPeriod = 0;
|
||||
}
|
||||
catch (Exception clientEx)
|
||||
{
|
||||
_logger.LogError(clientEx, $"{DeviceName}->清理OPC UA客户端资源失败");
|
||||
// 即使清理失败,也不中断Close流程
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{DeviceName}-{ServerUrl}->关闭连接完成");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{DeviceName}-{ServerUrl}->关闭连接失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"PLC开始清理:{DeviceName}-{ServerUrl}");
|
||||
// 释放托管和非托管资源
|
||||
Close();
|
||||
opcUaClient = null;
|
||||
// 标记对象已被释放
|
||||
_logger.LogInformation($"PLC清理完成:{DeviceName}-{ServerUrl}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"PLC清理错误:{DeviceName}-{ServerUrl}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取到当前节点的值【同步读取】
|
||||
/// </summary>
|
||||
/// <typeparam name="T">节点对应的数据类型</typeparam>
|
||||
/// <param name="nodeId">节点</param>
|
||||
/// <returns>返回当前节点的值</returns>
|
||||
public ResultModel<T> GetCurrentNodeValue<T>(string nodeId) where T : notnull
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
return ResultModel<T>.Error("nodeId is null");
|
||||
}
|
||||
if (!IsConnected)
|
||||
{
|
||||
return ResultModel<T>.Error("isConnected Error");
|
||||
}
|
||||
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return ResultModel<T>.Error("opcUaClient is null");
|
||||
}
|
||||
try
|
||||
{
|
||||
T value = opcUaClient.ReadNode<T>(nodeId);
|
||||
return ResultModel<T>.Success(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"读取失败:{DeviceName}-{ServerUrl},nodeid={nodeId}");
|
||||
return ResultModel<T>.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取到当前节点的值【异步读取】
|
||||
/// </summary>
|
||||
/// <typeparam name="T">节点对应的数据类型</typeparam>
|
||||
/// <param name="nodeId">节点</param>
|
||||
/// <returns>返回当前节点的值</returns>
|
||||
public async Task<ResultModel<T>> GetCurrentNodeValueAsync<T>(string nodeId) where T : notnull
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
return ResultModel<T>.Error("nodeId is null");
|
||||
}
|
||||
if (!IsConnected)
|
||||
{
|
||||
return ResultModel<T>.Error("isConnected Error");
|
||||
}
|
||||
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return ResultModel<T>.Error("opcUaClient is null");
|
||||
}
|
||||
try
|
||||
{
|
||||
T value = await opcUaClient.ReadNodeAsync<T>(nodeId);
|
||||
return ResultModel<T>.Success(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"读取失败:{DeviceName}-{ServerUrl},nodeid={nodeId}");
|
||||
return ResultModel<T>.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取到批量节点数据【异步读取】
|
||||
/// </summary>
|
||||
/// <param name="nodeIds">节点列表</param>
|
||||
/// <returns>返回节点数据字典</returns>
|
||||
[LogAndSwallow]
|
||||
public async Task<ResultModel<Dictionary<string, DataValue>>> GetNodeValuesAsync(List<NodeId> nodeIdList)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
return ResultModel<Dictionary<string, DataValue>>.Error("isConnected Error");
|
||||
}
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return ResultModel<Dictionary<string, DataValue>>.Error("opcUaClient is null");
|
||||
}
|
||||
Dictionary<string, DataValue> dicNodeInfo = new Dictionary<string, DataValue>();
|
||||
if (nodeIdList != null && nodeIdList.Count > 0)
|
||||
{
|
||||
List<DataValue> dataValues = await opcUaClient.ReadNodesAsync(nodeIdList.ToArray());
|
||||
int count = nodeIdList.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
AddInfoToDic(dicNodeInfo, nodeIdList[i].ToString(), dataValues[i]);
|
||||
}
|
||||
|
||||
}
|
||||
return ResultModel<Dictionary<string, DataValue>>.Success(dicNodeInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取节点数组信息
|
||||
/// </summary>
|
||||
[LogAndSwallow]
|
||||
public async Task<ResultModel<T>> GetArrayValuesAsync<T>(string nodeId, ushort count) where T : notnull
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
return ResultModel<T>.Error("isConnected Error");
|
||||
}
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return ResultModel<T>.Error("opcUaClient is null");
|
||||
}
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
return ResultModel<T>.Error("nodeId is null or empty");
|
||||
}
|
||||
if (count == 0)
|
||||
{
|
||||
return ResultModel<T>.Error("count must be greater than 0");
|
||||
}
|
||||
// 根据 nodeId 和 count 生成递增的数组索引
|
||||
string[] tags = new string[count];
|
||||
for (ushort i = 0; i < count; i++)
|
||||
{
|
||||
// 假设 nodeId 格式为 "ns=3;s=\"test\".\"Array\"[0]"
|
||||
// 需要将索引递增:Array[0], Array[1], Array[2]...
|
||||
if (nodeId.Contains("["))
|
||||
{
|
||||
// 如果 nodeId 已经包含索引,提取基础地址并重新生成
|
||||
int bracketIndex = nodeId.LastIndexOf('[');
|
||||
string baseAddress = nodeId.Substring(0, bracketIndex);
|
||||
tags[i] = $"{baseAddress}[{i}]";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果 nodeId 不包含索引,直接添加索引
|
||||
tags[i] = $"{nodeId}[{i}]";
|
||||
}
|
||||
}
|
||||
// 读取多个节点
|
||||
var values = await opcUaClient.ReadNodesAsync<T>(tags);
|
||||
if (values != null && values.Count > 0)
|
||||
{
|
||||
// 如果 T 是数组类型,需要将 List<T> 转换为 T[]
|
||||
if (typeof(T).IsArray)
|
||||
{
|
||||
// 获取数组的元素类型
|
||||
Type? elementType = typeof(T).GetElementType();
|
||||
if (elementType != null)
|
||||
{
|
||||
// 将 List<T> 转换为数组
|
||||
var array = Array.CreateInstance(elementType, values.Count);
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
array.SetValue(values[i], i);
|
||||
}
|
||||
return ResultModel<T>.Success((T)(object)array);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel<T>.Error("读取节点数组失败:类型未知");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果 T 不是数组类型,返回第一个值
|
||||
return ResultModel<T>.Success(values[0]);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResultModel<T>.Error("读取节点数组失败:未获取到数据");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加数据到字典中(相同键的则采用最后一个键对应的值)
|
||||
/// </summary>
|
||||
/// <param name="dic">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="dataValue">值</param>
|
||||
private void AddInfoToDic(Dictionary<string, DataValue> dic, string key, DataValue dataValue)
|
||||
{
|
||||
if (dic != null)
|
||||
{
|
||||
if (!dic.ContainsKey(key))
|
||||
{
|
||||
|
||||
dic.Add(key, dataValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
dic[key] = dataValue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static ResultModel<T1> ToResultModel<T1, T2>(ResultModel<T2> read) where T1 : notnull where T2 : notnull
|
||||
{
|
||||
if (!read.IsSuccess) return ResultModel<T1>.Error(read.ErrorMessage);
|
||||
return ResultModel<T1>.Success((T1)(object)read.Data);
|
||||
}
|
||||
|
||||
[LogAndSwallow]
|
||||
public override async Task<ResultModel<T>> ReadAsync<T>(string address, DataTypeEnum dataType, ushort arrayCount)
|
||||
{
|
||||
if (opcUaClient == null || IsConnected == false)
|
||||
{
|
||||
return ResultModel<T>.Error("PLC未连接");
|
||||
}
|
||||
if (arrayCount > 1)
|
||||
{
|
||||
return await ReadArrayAsync<T>(address, dataType, arrayCount);
|
||||
}
|
||||
return dataType switch
|
||||
{
|
||||
DataTypeEnum.Bit => ToResultModel<T, bool>(await GetCurrentNodeValueAsync<bool>(address)),
|
||||
DataTypeEnum.Byte => ToResultModel<T, byte>(await GetCurrentNodeValueAsync<byte>(address)),
|
||||
DataTypeEnum.Int16 => ToResultModel<T, short>(await GetCurrentNodeValueAsync<short>(address)),
|
||||
DataTypeEnum.Uint16 => ToResultModel<T, ushort>(await GetCurrentNodeValueAsync<ushort>(address)),
|
||||
DataTypeEnum.Int32 => ToResultModel<T, int>(await GetCurrentNodeValueAsync<int>(address)),
|
||||
DataTypeEnum.Uint32 => ToResultModel<T, uint>(await GetCurrentNodeValueAsync<uint>(address)),
|
||||
DataTypeEnum.Real => ToResultModel<T, float>(await GetCurrentNodeValueAsync<float>(address)),
|
||||
DataTypeEnum.String => ToResultModel<T, string>(await GetCurrentNodeValueAsync<string>(address)),
|
||||
DataTypeEnum.Wstring => ToResultModel<T, string>(await GetCurrentNodeValueAsync<string>(address)),
|
||||
DataTypeEnum.DateTime => ToResultModel<T, DateTime>(await GetCurrentNodeValueAsync<DateTime>(address)),
|
||||
_ => ResultModel<T>.Error($"不支持的数据类型 {dataType}")
|
||||
};
|
||||
}
|
||||
[LogAndSwallow]
|
||||
public async Task<ResultModel<T>> ReadArrayAsync<T>(string address, DataTypeEnum dataType, ushort arrayCount) where T : notnull
|
||||
{
|
||||
return dataType switch
|
||||
{
|
||||
DataTypeEnum.Bit => ToResultModel<T, bool[]>(await GetArrayValuesAsync<bool[]>(address, arrayCount)),
|
||||
DataTypeEnum.Byte => ToResultModel<T, byte[]>(await GetArrayValuesAsync<byte[]>(address, arrayCount)),
|
||||
DataTypeEnum.Int16 => ToResultModel<T, short>(await GetArrayValuesAsync<short>(address, arrayCount)),
|
||||
DataTypeEnum.Uint16 => ToResultModel<T, ushort>(await GetArrayValuesAsync<ushort>(address, arrayCount)),
|
||||
DataTypeEnum.Int32 => ToResultModel<T, int>(await GetArrayValuesAsync<int>(address, arrayCount)),
|
||||
DataTypeEnum.Uint32 => ToResultModel<T, uint>(await GetArrayValuesAsync<uint>(address, arrayCount)),
|
||||
DataTypeEnum.Real => ToResultModel<T, float>(await GetArrayValuesAsync<float>(address, arrayCount)),
|
||||
DataTypeEnum.String => ToResultModel<T, string>(await GetCurrentNodeValueAsync<string>(address)),
|
||||
DataTypeEnum.Wstring => ToResultModel<T, string>(await GetCurrentNodeValueAsync<string>(address)),
|
||||
DataTypeEnum.DateTime => ToResultModel<T, DateTime>(await GetCurrentNodeValueAsync<DateTime>(address)),
|
||||
_ => ResultModel<T>.Error($"不支持的数据类型 {dataType}")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public override async Task<bool> ReadVariablesAsync(DeviceViewModel device, CancellationToken token)
|
||||
{
|
||||
if (!IsConnected || opcUaClient == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
List<VariableViewModel> readNodes = (from v in device.Variables
|
||||
orderby v.Id
|
||||
where v.OperMode == OperModeEnum.Read
|
||||
select v).ToList();
|
||||
//筛选出需要读取的变量
|
||||
if (readNodes.Count == 0)
|
||||
{
|
||||
_logger.LogInformation($"ReadThreadAsync无待读取变量,延迟{device.MinPeriod}ms后重试:{device.DeviceName}");
|
||||
await Task.Delay(1000, token);
|
||||
return false;
|
||||
}
|
||||
List<NodeId> nodeIds = new List<NodeId>();
|
||||
foreach (var variable in readNodes)
|
||||
{
|
||||
nodeIds.Add(new NodeId(variable.Address));
|
||||
}
|
||||
bool readSuccess = true;
|
||||
var values = opcUaClient.ReadNodes(nodeIds.ToArray());
|
||||
|
||||
for (int i = 0; i < nodeIds.Count; i++)
|
||||
{
|
||||
if (values[i] == null || null == values[i].Value
|
||||
|| null == values[i].WrappedValue.Value
|
||||
|| !StatusCode.IsGood(values[i].StatusCode))
|
||||
{
|
||||
readSuccess = false;
|
||||
readNodes[i].IsSuccess = false;
|
||||
readNodes[i].Message = "更新失败";
|
||||
}
|
||||
else
|
||||
{
|
||||
GetValue(values[i], readNodes[i]);
|
||||
}
|
||||
}
|
||||
return readSuccess;
|
||||
}
|
||||
|
||||
|
||||
// 修改后的GetValue方法
|
||||
private void GetValue(DataValue value, VariableViewModel variable)
|
||||
{
|
||||
// 空值校验
|
||||
if (value == null)
|
||||
{
|
||||
variable.Value = string.Empty;
|
||||
variable.Message = "数据值为空";
|
||||
variable.IsSuccess = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 处理结构体/扩展对象
|
||||
if (value.WrappedValue.TypeInfo.BuiltInType == BuiltInType.ExtensionObject)
|
||||
{
|
||||
|
||||
string resolvedObj = ExtensionObjectHelper.ReadStruct(variable.Address, value, opcUaClient?.Session);
|
||||
if (string.IsNullOrEmpty(resolvedObj))
|
||||
{
|
||||
variable.Value = resolvedObj;
|
||||
variable.Message = "结构体解析失败";
|
||||
variable.IsSuccess = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
variable.Value = resolvedObj;
|
||||
variable.Message = "结构体解析成功";
|
||||
variable.IsSuccess = true;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2. 处理数组
|
||||
if (value.Value is Array arrayValue)
|
||||
{
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
variable.Value = JsonSerializer.Serialize(arrayValue, jsonOptions);
|
||||
variable.Message = "数组解析成功";
|
||||
variable.IsSuccess = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 处理普通数值类型
|
||||
string json = variable.Json;
|
||||
object originValue = value.Value;
|
||||
|
||||
// 3.1 有缩放配置的情况
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
double scaleRatio = 1.0;
|
||||
try
|
||||
{
|
||||
var scaleObj = JsonSerializer.Deserialize<TagJson>(json);
|
||||
if (scaleObj != null)
|
||||
{
|
||||
scaleRatio = scaleObj.Scale;
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
// 反序列化失败时明确提示
|
||||
System.Diagnostics.Debug.WriteLine($"缩放配置解析失败: {ex.Message}");
|
||||
scaleRatio = 1.0;
|
||||
}
|
||||
|
||||
// 数值转换和缩放
|
||||
double finalValue = 0;
|
||||
if (originValue != null)
|
||||
{
|
||||
// 支持更多数值类型的转换
|
||||
if (originValue is IConvertible convertibleValue)
|
||||
{
|
||||
finalValue = convertibleValue.ToDouble(null) * scaleRatio;
|
||||
}
|
||||
else if (double.TryParse(originValue.ToString(), out double numValue))
|
||||
{
|
||||
finalValue = numValue * scaleRatio;
|
||||
}
|
||||
}
|
||||
|
||||
variable.Value = finalValue.ToString();
|
||||
variable.Message = "数值缩放转换成功";
|
||||
}
|
||||
// 3.2 无缩放配置的情况
|
||||
else
|
||||
{
|
||||
variable.Value = originValue?.ToString() ?? string.Empty;
|
||||
variable.Message = "数值直接转换成功";
|
||||
}
|
||||
|
||||
variable.IsSuccess = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 全局异常捕获
|
||||
variable.Value = string.Empty;
|
||||
variable.Message = $"转换失败: {ex.Message}";
|
||||
variable.IsSuccess = false;
|
||||
// 记录详细异常信息
|
||||
System.Diagnostics.Debug.WriteLine($"GetValue方法异常: {ex.ToString()}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单节点数据订阅
|
||||
/// </summary>
|
||||
/// <param name="key">订阅的关键字(必须唯一)</param>
|
||||
/// <param name="nodeId">节点:"ns=3;s=\"test\".\"Static_1\""</param>
|
||||
/// <param name="callback">数据订阅的回调方法</param>
|
||||
public void SingleNodeIdDatasSubscription(string key, string nodeId, Action<string, MonitoredItem, MonitoredItemNotificationEventArgs> callback)
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
if (opcUaClient == null) return;
|
||||
try
|
||||
{
|
||||
opcUaClient.AddSubscription(key, nodeId, callback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"订阅节点异常:{nodeId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取到当前节点的值【同步读取】
|
||||
/// </summary>
|
||||
/// <typeparam name="T">节点对应的数据类型</typeparam>
|
||||
/// <param name="nodeId">节点</param>
|
||||
/// <returns>返回当前节点的值</returns>
|
||||
public ResultModel WriteSingleNodeId<T>(string nodeId, T value) where T : notnull
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
return ResultModel.Error("nodeId is null");
|
||||
}
|
||||
if (!IsConnected)
|
||||
{
|
||||
return ResultModel.Error("isConnected Error");
|
||||
}
|
||||
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return ResultModel.Error("opcUaClient is null");
|
||||
}
|
||||
try
|
||||
{
|
||||
bool ret = opcUaClient.WriteNode(nodeId, value);
|
||||
if (ret) return ResultModel.Success();
|
||||
return ResultModel.Error("WriteNode Error");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"读取失败:{DeviceName}-{ServerUrl},nodeid={nodeId}");
|
||||
return ResultModel.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取到当前节点的值【异步读取】
|
||||
/// </summary>
|
||||
/// <typeparam name="T">节点对应的数据类型</typeparam>
|
||||
/// <param name="nodeId">节点</param>
|
||||
/// <returns>返回当前节点的值</returns>
|
||||
public async Task<ResultModel> WriteSingleNodeIdAsync<T>(string nodeId, T value) where T : notnull
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
return ResultModel.Error("nodeId is null");
|
||||
}
|
||||
if (!IsConnected)
|
||||
{
|
||||
return ResultModel.Error("isConnected Error");
|
||||
}
|
||||
|
||||
if (opcUaClient == null)
|
||||
{
|
||||
return ResultModel.Error("opcUaClient is null");
|
||||
}
|
||||
try
|
||||
{
|
||||
bool ret = await opcUaClient.WriteNodeAsync(nodeId, value);
|
||||
if (ret) return ResultModel.Success();
|
||||
return ResultModel.Error("WriteNode Error");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"读取失败:{DeviceName}-{ServerUrl},nodeid={nodeId}");
|
||||
return ResultModel.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override IReadWriteDevice? GetReadWrite()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
[LogAndSwallow]
|
||||
public override async Task<ResultModel> WriteAsync<T>(string address, DataTypeEnum dataType, T value)
|
||||
{
|
||||
if (opcUaClient == null || IsConnected == false)
|
||||
{
|
||||
return ResultModel.Error("PLC未连接");
|
||||
}
|
||||
return dataType switch
|
||||
{
|
||||
DataTypeEnum.Bit => await WriteSingleNodeIdAsync(address, Unsafe.As<T, bool>(ref value)),
|
||||
DataTypeEnum.Byte => await WriteSingleNodeIdAsync(address, Unsafe.As<T, byte>(ref value)),
|
||||
DataTypeEnum.Int16 => await WriteSingleNodeIdAsync(address, Unsafe.As<T, short>(ref value)),
|
||||
DataTypeEnum.Uint16 => await WriteSingleNodeIdAsync(address, Unsafe.As<T, ushort>(ref value)),
|
||||
DataTypeEnum.Int32 => await WriteSingleNodeIdAsync(address, Unsafe.As<T, int>(ref value)),
|
||||
DataTypeEnum.Uint32 => await WriteSingleNodeIdAsync(address, Unsafe.As<T, uint>(ref value)),
|
||||
DataTypeEnum.Real => await WriteSingleNodeIdAsync(address, Unsafe.As<T, float>(ref value)),
|
||||
DataTypeEnum.String => await WriteSingleNodeIdAsync(address, value.ToString()),
|
||||
DataTypeEnum.Wstring => await WriteSingleNodeIdAsync(address, value.ToString()),
|
||||
DataTypeEnum.DateTime => await WriteSingleNodeIdAsync(address, Unsafe.As<T, DateTime>(ref value)),
|
||||
_ => ResultModel.Error($"不支持的数据类型: {dataType}")
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Cowain.Base.Abstractions.Localization;
|
||||
using Cowain.Base.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Plugin.Driver.DeviceOpcUa;
|
||||
|
||||
internal class OpcUaLocalizationResourceContributor(IOptions<AppSettings> appSettings) :
|
||||
LocalizationResourceContributorBase(appSettings, OpcUaConsts.PluginName)
|
||||
{
|
||||
}
|
||||
27
Plugins/Driver/Driver.DeviceOpcUa/OpcUaPlugin.cs
Normal file
27
Plugins/Driver/Driver.DeviceOpcUa/OpcUaPlugin.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Cowain.Base.Abstractions.Plugin;
|
||||
using Ke.Bee.Localization.Providers.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Plugin.Driver.DeviceOpcUa
|
||||
{
|
||||
public class OpcUaPlugin : PluginBase
|
||||
{
|
||||
public override string PluginName => "OpcUa";
|
||||
|
||||
public override R? Execute<T, R>(string methodName, T? parameters)
|
||||
where T : default
|
||||
where R : class
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
}
|
||||
|
||||
public override void RegisterServices(IServiceCollection services, List<Assembly>? _assemblies)
|
||||
{
|
||||
services.AddSingleton<ILocalizationResourceContributor, OpcUaLocalizationResourceContributor>();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!--<Import Project="../../../Directory.Version.props" />-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- 确保 NuGet 包的 DLL 被复制 -->
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="i18n\en-US.json" />
|
||||
<None Remove="i18n\zh-CN.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="i18n\en-US.json" />
|
||||
<AvaloniaResource Include="i18n\zh-CN.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpcUaHelper" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Cowain.Base\Cowain.Base.csproj" />
|
||||
<ProjectReference Include="..\Cowain.Driver\Plugin.Cowain.Driver.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Views\OpcUaParamDialog.axaml.cs">
|
||||
<DependentUpon>OpcUaParamDialog.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<Target Name="CopyFilesToDestination" AfterTargets="AfterBuild" Condition="true">
|
||||
<!-- 确定相对路径到目标目录 -->
|
||||
<PropertyGroup>
|
||||
<!-- 这里使用 MSBuild 的内置属性来构造正确的路径 -->
|
||||
<TargetDirectory>../../../Cowain.TestProject/Plugins/DeviceOpcUa</TargetDirectory>
|
||||
</PropertyGroup>
|
||||
<!-- 确保目标目录存在 -->
|
||||
<MakeDir Directories="$(TargetDirectory)" Condition="!Exists('$(TargetDirectory)')" />
|
||||
|
||||
<ItemGroup>
|
||||
<Source1 Include="i18n\*.*" />
|
||||
<FilesToCopy Include="$(OutputPath)**\Plugin.Driver.DeviceOpcUa.pdb" />
|
||||
<FilesToCopy Include="$(OutputPath)**\Plugin.Driver.DeviceOpcUa.deps.json" />
|
||||
<FilesToCopy Include="$(OutputPath)**/HslCommunication.dll" />
|
||||
<FilesToCopy Include="$(OutputPath)**/Newtonsoft.Json.dll" />
|
||||
<FilesToCopy Include="$(OutputPath)**/System.IO.Ports.dll" />
|
||||
<FilesToCopy Include="$(OutputPath)**\*Opc*.dll" />
|
||||
</ItemGroup>
|
||||
<!-- 复制文件到目标目录 -->
|
||||
<Copy SourceFiles="@(FilesToCopy)" DestinationFolder="$(TargetDirectory)\%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(Source1)" DestinationFolder="$(TargetDirectory)\i18n\" />
|
||||
<!-- 输出TargetDirectory -->
|
||||
<Message Text="TargetDirectory: $(TargetDirectory)" Importance="high" />
|
||||
|
||||
</Target>
|
||||
|
||||
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user