using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading; using PropertyGridLib.Attributes; using PropertyGridLib.Controls; using TeamAAS.Communication; using TeamAAS.Core; using TeamAAS.Communication.Interfaces; using TeamAAS.FlowEditor.Execution; using TeamAAS.FlowEditor.Models; using TeamAAS.FlowEditor.Plugins; using TeamAAS.FlowEngine.FormulaData; using Plugins.Standard.Models; namespace Plugins.Standard.Models { // ───────────────────────────────────────────────────────────────────── // 通讯标准节点模型(批量读 / 批量写)。 // 设计要点:一个节点一次事务处理整组点位 —— 减少往返、结果按键值输出、 // 失败逐项报告(哪一项失败不影响其余项的执行与结果记录)。 // ───────────────────────────────────────────────────────────────────── /// 通讯批量读取节点模型。 [Serializable] public class CommBatchReadModel : BasePluginModel { [Category("I.设备")] [DisplayName("1.通讯设备名")] [Description("通讯管理器中配置的设备名称(须与名称完全一致)")] public string DeviceName { get; set; } = ""; [Category("II.读取项")] [DisplayName("1.读取列表")] [Description("地址→键名 映射列表;运行后每个键名成为本节点输出(下游用 &{本节点.键名} 绑定)")] [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)] public List Items { get; set; } = new List(); [Category("III.选项")] [DisplayName("1.失败时整体失败")] [Description("开=任一项读取失败则节点 Failed;关=失败项记入 错误项 输出但节点仍成功")] public bool FailOnAnyError { get; set; } = false; } /// 批量读取项:设备地址 → 输出键名。 [Serializable] public class CommReadItem { [Category("读取项")] [DisplayName("1.地址")] [Description("设备地址(DB块/寄存器/Tag,随设备类型解释)")] public string Address { get; set; } = ""; [Category("读取项")] [DisplayName("2.键名")] [Description("输出键名(下游绑定的名字)")] public string Key { get; set; } = ""; public override string ToString() => string.IsNullOrWhiteSpace(Key) ? Address : $"{Key} ← {Address}"; } /// 通讯批量写入节点模型。 [Serializable] public class CommBatchWriteModel : BasePluginModel { [Category("I.设备")] [DisplayName("1.通讯设备名")] [Description("通讯管理器中配置的设备名称(须与名称完全一致)")] public string DeviceName { get; set; } = ""; [Category("II.写入项")] [DisplayName("1.写入列表")] [Description("地址←来源值 映射列表;来源值可绑定上游输出公式(如 &{检测结果.面积})")] [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)] public List Items { get; set; } = new List(); [Category("III.选项")] [DisplayName("1.失败时整体失败")] [Description("开=任一项写入失败则节点 Failed;关=失败项记入 错误项 输出但节点仍成功")] public bool FailOnAnyError { get; set; } = true; } /// 批量写入项:目标地址 ← 来源值(公式/常量)。 [Serializable] public class CommWriteItem { public CommWriteItem() { Source = new FormulaBound(); } [Category("写入项")] [DisplayName("1.地址")] [Description("设备地址(DB块/寄存器/Tag,随设备类型解释)")] public string Address { get; set; } = ""; [Category("写入项")] [DisplayName("2.来源值")] [Description("要写入的值;可绑定上游输出(&{节点.键})或填常量")] [FormulaEditor(typeof(PluginDataProvider))] public FormulaBound Source { get; set; } public override string ToString() => $"{Address} ← {Source?.Formula ?? Source?.Value?.ToString() ?? ""}"; } } namespace Plugins.Standard { // ───────────────────────────────────────────────────────────────────── // 通讯标准节点(批量读 / 批量写)。 // 参考 VisionKit 的通讯节点形态并按 TeamAAS 节点契约实现;后续优化方向: // - 支持 PLC 适配层一次事务批量读写(当前逐项调用 ICommunication.Read/WriteValue); // - 地址解析缓存与自动重连重试。 // ───────────────────────────────────────────────────────────────────── /// 批量读取:一次执行读取整组地址,结果按键名输出,失败项逐条记录。 [Serializable] [Plugin("批量读取", PluginCategory.通讯模块, typeof(CommBatchReadModel), "Download", NodeShape = NodeCategory.Normal, Description = "通讯批量读取:一次事务读取整组地址,结果按键名输出;逐项错误报告,可用公式绑定设备名")] public class CommBatchReadPlugin : BasePlugin { public override List DeclareOutputs() { var list = new List { new OutputField("成功项数", typeof(int)), new OutputField("错误项数", typeof(int)), new OutputField("结果", typeof(bool)), }; var items = Model?.Items ?? new List(); foreach (var item in items) { if (item == null || string.IsNullOrWhiteSpace(item.Key)) continue; list.Add(new OutputField(item.Key, typeof(object))); } return list; } public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary results) { var res = new Dictionary(); results = res; NodeRunStatus Fail(string m) { res["Error"] = m; res["结果"] = false; Log(1, $"批量读取失败: {m}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; } ICommunication device = FindDevice(Model.DeviceName); if (device == null) return Fail($"找不到通讯设备: {Model.DeviceName}"); if (!device.IsConnected) { Log(3, "设备未连接,尝试连接…"); try { device.Connect(); } catch (Exception ex) { return Fail($"设备连接失败: {ex.Message}"); } if (!device.IsConnected) return Fail("设备连接失败(无法建立连接)"); } int ok = 0, err = 0; var errors = new List(); foreach (var item in Model.Items ?? new List()) { if (token.IsCancellationRequested) return Fail("已取消"); if (item == null || string.IsNullOrWhiteSpace(item.Address)) continue; var key = string.IsNullOrWhiteSpace(item.Key) ? item.Address : item.Key; try { res[key] = device.ReadValue(item.Address); ok++; Log(4, $"读取[{item.Address}] → {key} = {res[key]}"); } catch (Exception ex) { err++; errors.Add($"{key}({item.Address}): {ex.Message}"); res[key] = null; Log(2, $"读取失败[{item.Address}]: {ex.Message}", TeamAAS.LogLevel.Warning); } } res["成功项数"] = ok; res["错误项数"] = err; if (errors.Count > 0) res["错误项"] = string.Join("; ", errors); res["结果"] = err == 0 || !Model.FailOnAnyError; if (err > 0 && Model.FailOnAnyError) { Log(1, $"批量读取有 {err} 项失败: {string.Join("; ", errors)}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; } Log(2, $"批量读取完成:成功 {ok} 项,失败 {err} 项"); return NodeRunStatus.Success; } internal static ICommunication FindDevice(string name) { if (string.IsNullOrWhiteSpace(name)) return null; return CoreManager.Communication?.GetByName(name); } } /// 批量写入:一次执行写入整组地址(来源可绑定公式),失败项逐条记录。 [Serializable] [Plugin("批量写入", PluginCategory.通讯模块, typeof(CommBatchWriteModel), "Upload", NodeShape = NodeCategory.Normal, Description = "通讯批量写入:一次事务写入整组地址(来源绑定公式/常量);逐项错误报告")] public class CommBatchWritePlugin : BasePlugin { public override List DeclareOutputs() => new List { new OutputField("成功项数", typeof(int)), new OutputField("错误项数", typeof(int)), new OutputField("结果", typeof(bool)), }; public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary results) { var res = new Dictionary(); results = res; NodeRunStatus Fail(string m) { res["Error"] = m; res["结果"] = false; Log(1, $"批量写入失败: {m}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; } ICommunication device = CommBatchReadPlugin.FindDevice(Model.DeviceName); if (device == null) return Fail($"找不到通讯设备: {Model.DeviceName}"); if (!device.IsConnected) { Log(3, "设备未连接,尝试连接…"); try { device.Connect(); } catch (Exception ex) { return Fail($"设备连接失败: {ex.Message}"); } if (!device.IsConnected) return Fail("设备连接失败(无法建立连接)"); } int ok = 0, err = 0; var errors = new List(); foreach (var item in Model.Items ?? new List()) { if (token.IsCancellationRequested) return Fail("已取消"); if (item == null || string.IsNullOrWhiteSpace(item.Address)) continue; object value; try { value = !string.IsNullOrWhiteSpace(item.Source?.Formula) ? RegistryOrDebug.ResolveFormula(GetModel.FlowId, item.Source.Formula) : item.Source?.Value; } catch (Exception ex) { err++; errors.Add($"{item.Address}: 公式解析失败 {ex.Message}"); continue; } try { device.WriteValue(item.Address, value); ok++; Log(4, $"写入[{item.Address}] ← {value}"); } catch (Exception ex) { err++; errors.Add($"{item.Address}: {ex.Message}"); Log(2, $"写入失败[{item.Address}]: {ex.Message}", TeamAAS.LogLevel.Warning); } } res["成功项数"] = ok; res["错误项数"] = err; if (errors.Count > 0) res["错误项"] = string.Join("; ", errors); res["结果"] = err == 0 || !Model.FailOnAnyError; if (err > 0 && Model.FailOnAnyError) { Log(1, $"批量写入有 {err} 项失败: {string.Join("; ", errors)}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; } Log(2, $"批量写入完成:成功 {ok} 项,失败 {err} 项"); return NodeRunStatus.Success; } } }