首次提交:本地项目同步到Gitea

This commit is contained in:
zhusenlin
2026-03-02 09:08:20 +08:00
commit 1fb681fb34
371 changed files with 31868 additions and 0 deletions

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

View File

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

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

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

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

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

View 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("未查询到名称为「系统」的报警组将使用默认ID1");
}
// 查询名称为"错误"的报警级别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("未查询到名称为「错误」的报警级别将使用默认ID1");
}
}
catch (OperationCanceledException)
{
// 正常取消,仅日志记录,不抛异常
_logger.LogInformation("报警配置缓存初始化被取消");
}
catch (Exception ex)
{
_logger.LogError(ex, "初始化报警配置缓存失败将使用默认ID1");
}
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));
// 修改4Task.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();
}
}
}
}

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

View 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 ConditionAttribute : Attribute
{
public string Name { get; }
public string Desc { get; }
public ConditionAttribute(string name, string desc)
{
Name = name;
Desc = desc;
}
}

View File

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

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

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

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

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

View 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"]
}
]
}
]

View File

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

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

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

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

View File

@@ -0,0 +1,6 @@
namespace Plugin.Cowain.Driver;
public class DriverConsts
{
public const string PluginName = "Driver";
}

View File

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

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

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

View File

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

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

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

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

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

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

View File

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

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

View File

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

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

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

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

View 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; } = "未分类";
}

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,3 @@
## 首次发布 1.0.0
* 第一版

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

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

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

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

View File

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

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

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Plugin.Cowain.Driver.Views;
public partial class DeviceEditDialog : UserControl
{
public DeviceEditDialog()
{
InitializeComponent();
}
}

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

View 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": "旧值"
}

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

View File

@@ -0,0 +1,6 @@
namespace Plugin.Driver.DeviceOpcUa;
public class OpcUaConsts
{
public const string PluginName = "DeviceOpcUa";
}

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

View File

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

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

View File

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