自定义接口

本指南展示了如何使用 FastInterface 框架在 realvirtual 中构建您自己的工业通信接口。FastInterface 提供线程安全的高性能通信,具有自动信号管理和连接处理功能。

概述

realvirtual 的 FastInterface 框架使您可以轻松地将仿真连接到外部系统,如 PLC、机器人、IoT 设备或任何具有网络 API 的系统。您只需实现三个核心方法,FastInterface 会自动处理线程、重连、信号管理和 UI 集成。

您将学到:

  • 设置 FastInterface 类结构
  • 实现连接和通信逻辑
  • 处理具有线程安全性的信号交换
  • 使用元数据进行协议映射
  • 调试和排除接口故障

FastInterface 架构

FastInterface 架构概览

此图展示了简化的 FastInterface 开发方法:

FastInterface 基础

FastInterfaceBase 类自动提供六项核心服务:

  • 线程管理:处理后台线程生命周期
  • 自动重连:故障时自动恢复连接
  • 信号发现:查找和管理所有子信号组件
  • 错误处理:全面的错误恢复和日志记录
  • 状态管理:连接状态跟踪和监控
  • Unity 集成:无缝的 Inspector UI 和 Unity 兼容性

简单的 3 方法实现

您只需实现 3 个方法即可创建完整的接口:

  1. EstablishConnection() - 设置您的协议连接(TCP、串口等)
  2. CommunicationLoop() - 与外部系统持续交换数据
  3. CloseConnection() - 清理资源并关闭连接

数据流架构

底部展示了线程安全的数据流

  • Unity 线程:管理 GameObject 和信号组件
  • 后台线程:安全运行您的协议实现
  • 外部系统:您的目标设备(PLC、机器人、IoT、服务器)

信号交换使用内置方法:

  • 读取信号GetInputsForPLC() - Unity → 外部系统
  • 写入信号SetOutputsFromPLC(data) - 外部系统 → Unity

核心组件:

  1. FastInterfaceBase 类 - 提供线程管理、重连逻辑和信号管理基础设施的基础
  2. 后台线程 - 所有协议通信在专用后台线程上运行,保持 Unity 主线程响应
  3. 连接状态管理 - 自动连接监控,具有视觉状态指示器和错误报告
  4. Signal Manager - 处理子信号对象的自动发现,管理线程安全的信号数据交换
  5. 线程安全通信 - 内置机制,在 Unity 主线程和后台通信线程之间安全传递数据
  6. 精确时序控制 - 自动集成 realvirtual 的 PrePost FixedUpdate 系统,实现确定性信号处理(在 realvirtual 6.0.5+ 中可用)

标准工作流程:

该架构确保所有 FastInterface 实现的一致模式:

  • 初始化 → 将 Inspector 属性复制到线程安全变量
  • 连接 → 在后台线程上建立连接,具有自动重试逻辑
  • 通信 → 持续循环读取 Unity 信号、发送到外部系统、接收响应并更新 Unity 信号
  • 监控 → 具有性能指标的实时连接状态跟踪
  • 清理 → 正确的资源清理和优雅关闭

主要优势:

  • 一致性 - 所有自定义接口遵循相同的成熟架构模式
  • 可靠性 - 内置连接恢复和错误处理
  • 性能 - 非阻塞通信,具有优化的信号变更检测
  • 可见性 - Inspector 集成显示连接状态、周期计数和时序指标
  • 安全性 - 线程安全设计防止 Unity 线程问题

这种标准化方法意味着您可以专注于实现特定的协议逻辑,而 FastInterface 会自动处理所有复杂的线程、连接管理和 Unity 集成。

精确时序控制 (realvirtual 6.0.5+)

FastInterface 自动与 realvirtual 的 PrePost FixedUpdate 系统集成,为工业自动化应用提供确定性时序。此系统确保所有 FastInterface 组件在相对于 Unity 物理计算的精确相同时刻处理信号。

时序架构


PreFixedUpdateUnity FixedUpdatePostFixedUpdate
      ↓               ↓                  ↓
   PLCUnity      Physics/Logic      UnityPLC

信号处理流程:

  1. PreFixedUpdate 阶段:所有 FastInterface 实例同步处理 PLC 输出(从 PLC 到 Unity 对象的数据)
  2. FixedUpdate 阶段:Unity 运行物理计算、组件逻辑和运动更新
  3. PostFixedUpdate 阶段:所有 FastInterface 实例同步处理 PLC 输入(从 Unity 对象到 PLC 的数据)

无需配置:FastInterface 自动参与时序系统——无需额外的设置或配置。

FastInterface Inspector

FastInterface Inspector 面板

FastInterface Inspector 为您的自定义接口提供全面的监控和配置。理解这些字段对于有效的接口开发和故障排除至关重要:

State 部分

  • State:显示当前连接状态及视觉指示器:
  • 🟢 Connected(绿色)- 接口已连接并成功通信
  • 🔴 Disconnected(红色)- 接口未连接
  • 🟡 Connecting(黄色)- 接口正在尝试建立连接
  • 🟠 Error(橙色)- 接口遇到错误,将尝试重连
  • Input Signals:自动发现的输入信号数量(Unity → 外部系统)
  • Output Signals:自动发现的输出信号数量(外部系统 → Unity)
  • Comm Cycle Ms:实际通信周期时间(毫秒)(性能指标)
  • Cycle Count:自连接以来完成的通信周期总数

Configuration 部分

  • Update Cycle Ms:通信循环的目标周期时间(毫秒)
  • Only Transmit Changed Inputs:✅ 性能优化 - 仅发送更改的输入值
  • Auto Reconnect:✅ 连接丢失时自动尝试重连
  • Reconnect Interval Seconds:重连尝试之间的等待时间
  • Max Reconnect Attempts:最大连续重连尝试次数(-1 = 无限)
  • Debug Mode:✅ 启用详细日志记录用于故障排除

Inspector 的主要优势:

  1. 实时监控 - 在运行时实时查看连接状态和性能指标
  2. 性能优化 - "Only Transmit Changed Inputs"显著减少网络流量
  3. 自动恢复 - 内置重连逻辑处理临时连接丢失
  4. 开发支持 - 调试模式提供详细的故障排除日志
  5. 信号发现 - 自动计数和管理所有子信号对象

为什么使用 FastInterface:

  • 线程安全的后台通信,不阻塞 Unity
  • 自动重连,具有可自定义的重试逻辑
  • 零配置信号发现 - 自动查找所有子信号
  • 高性能,具有优化的信号交换和变更检测
  • 内置调试,具有全面的日志记录和状态可视化
ℹ️

新标准:FastInterface 是所有 realvirtual 接口开发的新标准。所有新接口都应使用 FastInterface 以获得最佳性能、可靠性和可维护性。

参考实现:有关生产质量的示例,请查看 realvirtual 代码库中的这些 FastInterface 实现:

* KEBA Interface - 工业机器人控制器通信

* KUKA Interface - KUKA 机器人集成

* MQTT Interface - IoT 消息协议

遗留说明:realvirtual 中的一些较旧接口仍使用已弃用的 InterfaceBaseClass/InterfaceThreadedBaseClass 方法,并将在未来版本中迁移到 FastInterface。

ℹ️

从遗留接口迁移? 如果您有使用已弃用的 InterfaceBaseClass 或 InterfaceThreadedBaseClass 构建的现有接口,请参见遗留接口(已弃用)获取迁移指南和参考文档。

使用模板快速入门

创建自定义接口最快的方法是使用蓝图模板。从 realvirtual 代码库中复制这些工作模板之一:

  • BlueprintFastInterfaceSimple.cs - 基本协议的最小模板
  • BlueprintFastInterface.cs - 具有高级功能的完整模板

复制 /Assets/realvirtual/Interfaces/Common/Fast/BlueprintFastInterfaceSimple.cs 并将 TODO 部分替换为您的协议逻辑:


using System.Threading;
using System.Threading.Tasks;

namespace realvirtual
{
    public class MyCustomInterface : FastInterfaceBase
    {
        [Header("Connection Settings")]
        public string IPAddress = "192.168.1.100";
        public int Port = 502;
        
        private TcpClient tcpClient;
        private NetworkStream stream;
        
        protected override async Task EstablishConnection(CancellationToken cancellationToken)
        {
            // 连接到您的系统
            tcpClient = new TcpClient();
            await tcpClient.ConnectAsync(IPAddress, Port);
            stream = tcpClient.GetStream();
        }
        
        protected override async Task CommunicationLoop(CancellationToken cancellationToken)
        {
            // 读取 Unity 信号以发送到外部系统
            var inputs = GetInputsForPLC();
            
            // 使用您的协议发送数据
            await SendData(inputs);
            
            // 从外部系统接收数据
            var receivedData = await ReceiveData();
            
            // 用接收的数据更新 Unity 信号
            SetOutputsFromPLC(receivedData);
        }
        
        protected override void CloseConnection()
        {
            stream?.Close();
            tcpClient?.Close();
        }
    }
}

分步实现

步骤 1:创建您的接口类

首先创建一个继承自 FastInterfaceBase 的新 C# 脚本:


using UnityEngine;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net.Sockets;

namespace realvirtual
{
    public class CustomNetworkInterface : FastInterfaceBase
    {
        [Header("Network Connection")]
        public string ServerIP = "192.168.1.100";
        public int Port = 8080;
        
        [Header("Protocol Settings")]
        public string RequestCommand = "GET_DATA";
        public int TimeoutMs = 1000;
        
        // 连接对象
        private TcpClient tcpClient;
        private NetworkStream stream;
        
        // 用于后台线程访问的线程安全副本
        private string threadSafeServerIP;
        private int threadSafePort;
    }
}

要点:

  • 始终继承自 FastInterfaceBase
  • 为连接设置添加 Inspector 属性
  • 将连接对象声明为私有字段
  • 使用 [Header] 属性组织 Inspector 设置

步骤 2:实现连接逻辑

EstablishConnection 方法在启动通信时在后台线程上运行一次:


protected override async Task EstablishConnection(CancellationToken cancellationToken)
{
    ThreadSafeLogger.LogInfo($"Connecting to server {threadSafeServerIP}:{threadSafePort}", GetType().Name);
    
    try
    {
        tcpClient = new TcpClient();
        await tcpClient.ConnectAsync(threadSafeServerIP, threadSafePort);
        stream = tcpClient.GetStream();
        
        // 设置套接字选项以实现可靠通信
        tcpClient.ReceiveTimeout = 2000;
        tcpClient.SendTimeout = 2000;
        
        ThreadSafeLogger.LogInfo("Network connection established", GetType().Name);
    }
    catch (Exception ex)
    {
        ThreadSafeLogger.LogError($"Failed to connect: {ex.Message}", GetType().Name);
        throw; // 重新抛出以触发重连逻辑
    }
}

protected override void CopyPropertiesToThreadSafe()
{
    threadSafeServerIP = ServerIP;
    threadSafePort = Port;
}

重要:

  • 在后台线程中使用 ThreadSafeLogger 而非 Debug.Log
  • 连接失败时抛出异常 - FastInterface 会自动处理重连
  • 使用 CopyPropertiesToThreadSafe() 将 Inspector 属性复制到线程安全字段
  • 在连接尝试期间使用 CancellationToken 进行响应式关闭

步骤 3:构建通信循环

CommunicationLoop 方法在连接建立后在后台线程上重复运行:


protected override async Task CommunicationLoop(CancellationToken cancellationToken)
{
    try
    {
        // 步骤 1:从 Unity 读取输入信号(要发送到外部系统的数据)
        var inputs = GetInputsForPLC();
        
        // 步骤 2:如果我们有输入,则将数据发送到外部系统
        if (inputs.Count > 0)
        {
            await SendDataToServer(inputs);
        }
        
        // 步骤 3:从外部系统请求数据
        var receivedData = await RequestDataFromServer();
        
        // 步骤 4:用接收的数据更新 Unity 输出信号
        if (receivedData.Count > 0)
        {
            SetOutputsFromPLC(receivedData);
        }
    }
    catch (Exception ex)
    {
        ThreadSafeLogger.LogError($"Communication error: {ex.Message}", GetType().Name);
        throw; // 重新抛出以触发重连
    }
}

private async Task SendDataToServer(Dictionary<string, object> inputs)
{
    // 将 Unity 信号转换为您的协议格式
    var pairs = new List<string>();
    foreach (var kv in inputs)
    {
        pairs.Add($"{kv.Key}={kv.Value}");
    }
    var message = $"SET:{string.Join(",", pairs)}\r\n";
    var messageBytes = Encoding.UTF8.GetBytes(message);
    
    await stream.WriteAsync(messageBytes, 0, messageBytes.Length);
}

private async Task<Dictionary<string, object>> RequestDataFromServer()
{
    // 发送请求
    var request = Encoding.UTF8.GetBytes("GET:ALL\r\n");
    await stream.WriteAsync(request, 0, request.Length);
    
    // 读取响应
    var buffer = new byte[1024];
    int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
    var response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
    
    // 将响应解析为 Unity 信号格式
    return ParseServerResponse(response);
}

关键概念:

  • GetInputsForPLC():以 Dictionary 返回所有输入信号值
  • SetOutputsFromPLC():用接收的数据更新输出信号
  • 信号元数据:使用 TryGetSignalMetadataSafe() 从后台线程安全访问信号配置
  • 错误处理:使用 try/catch 和 ThreadSafeLogger 进行稳健的错误报告

步骤 4:处理信号交换

FastInterface 提供两个用于信号数据交换的主要方法:


// 在 CommunicationLoop 中 - 读取要发送到外部系统的信号
var allInputs = GetInputsForPLC(); // 获取所有输入信号
var changedOnly = GetChangedInputsForPLC(); // 性能优化 - 仅更改的信号

// 用从外部系统接收的数据更新输出信号
var outputData = new Dictionary<string, object>
{
    ["Motor1_Running"] = true,
    ["Motor1_Speed"] = 1500,
    ["Temperature"] = 23.5f,
    ["Status"] = "Running"
};
SetOutputsFromPLC(outputData);

性能优化:

  • 在 Inspector 中使用 OnlyTransmitChangedInputs = true 以仅发送更改的输入信号
  • 使用 threadSafeOnlyTransmitChangedInputs 线程安全地访问此设置
  • 使用适当的数据类型 - FastInterface 自动处理转换

步骤 5:实现连接清理

CloseConnection 方法在停止通信时在后台线程上运行:


protected override void CloseConnection()
{
    try
    {
        ThreadSafeLogger.LogInfo("Closing network connection", GetType().Name);
        
        stream?.Close();
        stream?.Dispose();
        tcpClient?.Close();
        tcpClient?.Dispose();
        
        stream = null;
        tcpClient = null;
    }
    catch (Exception ex)
    {
        ThreadSafeLogger.LogError($"Error during cleanup: {ex.Message}", GetType().Name);
    }
}

高级功能

使用信号元数据进行协议映射

信号元数据允许您在每个信号中存储特定于协议的配置:


// 在导入信号时设置元数据(仅主线程)
protected virtual void OnInterfaceStarting()
{
    // 查找所有信号并添加 Modbus 元数据
    foreach (var signal in this.GetAllSignals())
    {
        if (signal.name.Contains("Motor"))
        {
            signal.SetMetadata("RegisterAddress", 100);
            signal.SetMetadata("DataType", "INT16");
        }
        else if (signal.name.Contains("Temperature"))
        {
            signal.SetMetadata("RegisterAddress", 200);
            signal.SetMetadata("DataType", "FLOAT32");
        }
    }
}

// 从后台线程安全访问元数据
protected override async Task CommunicationLoop(CancellationToken cancellationToken)
{
    foreach (var input in GetInputsForPLC())
    {
        // 线程安全的元数据访问
        if (TryGetSignalMetadataSafe<int>(input.Key, "RegisterAddress", out int address))
        {
            var dataType = GetSignalMetadataSafe<string>(input.Key, "DataType", "INT16");
            await WriteRegister(address, ConvertValue(input.Value, dataType));
        }
    }
}

动态信号导入

您可以使用信号管理 API 动态创建和更新信号:


[Button("Import Modbus Signals")]
private void ImportSignalsFromModbus()
{
    // 示例:从 Modbus 配置导入
    var signalConfigs = new[]
    {
        new { Name = "Motor1_Running", Type = SignalType.Bool, Direction = SignalDirection.Output, Address = 100 },
        new { Name = "Motor1_Speed", Type = SignalType.Int, Direction = SignalDirection.Input, Address = 101 },
        new { Name = "Temperature", Type = SignalType.Float, Direction = SignalDirection.Output, Address = 200 }
    };
    
    foreach (var config in signalConfigs)
    {
        var metadata = new Dictionary<string, object>
        {
            ["RegisterAddress"] = config.Address,
            ["DataType"] = config.Type.ToString()
        };
        
        // 创建或更新带元数据的信号
        var signal = this.CreateOrUpdateSignal(config.Name, config.Type, config.Direction, metadata);
        Debug.Log($"Created/Updated signal: {signal.name}");
    }
    
    // 刷新信号管理器以更新内部缓存
    this.RefreshSignalManager();
}

线程安全的属性访问

对于需要后台线程访问的 Inspector 属性,将其复制到线程安全字段:


public class MyInterface : FastInterfaceBase
{
    [Header("Settings")]
    public string ServerAddress = "192.168.1.100";
    public int Timeout = 1000;
    public bool EnableExtendedLogging = false;
    
    // 线程安全副本
    private string threadSafeServerAddress;
    private int threadSafeTimeout;
    private bool threadSafeLogging;
    
    // 在后台线程启动前自动调用
    protected override void CopyPropertiesToThreadSafe()
    {
        threadSafeServerAddress = ServerAddress;
        threadSafeTimeout = Timeout;
        threadSafeLogging = EnableExtendedLogging;
    }
    
    protected override async Task EstablishConnection(CancellationToken cancellationToken)
    {
        // 在后台线程中使用线程安全副本
        client.ConnectTimeout = threadSafeTimeout;
        await client.ConnectAsync(threadSafeServerAddress, 502);
        
        if (threadSafeLogging)
        {
            ThreadSafeLogger.LogInfo("Extended logging enabled", GetType().Name);
        }
    }
}
ℹ️

真实接口示例:有关完整的生产实现,请查看 Assets/realvirtual/Interfaces/ 中基于 FastInterface 的接口:

FastInterface 标准(推荐)

* KEBA Interface - 具有全面错误处理的现代工业机器人控制器

* KUKA Interface - 具有实时通信的高级机器人集成

* MQTT Interface - 具有自动主题管理的 IoT 协议实现

遗留实现(正在迁移):

* S7、OPC-UA 和 Modbus 接口仍使用较旧的方法,但展示了批量操作和协议特定优化等高级功能。

最佳实践

性能优化

  1. 对输入信号使用变更检测

   // 在 Inspector 或代码中启用
   OnlyTransmitChangedInputs = true;

   // 或手动检查变更
   var changedInputs = GetChangedInputsForPLC();
  1. 缓存信号元数据以获得更好的性能:

   private Dictionary<string, int> signalToRegisterMap = new Dictionary<string, int>();

   protected virtual void OnInterfaceStarting()
   {
       foreach (var signal in this.GetAllSignals())
       {
           if (signal.GetMetadata<int>("RegisterAddress", out int address))
           {
               signalToRegisterMap[signal.GetSignalName()] = address;
           }
       }
   }
  1. 尽可能批量操作

   // 而不是逐个写入寄存器
   foreach (var input in inputs)
       await WriteSingleRegister(address, value);

   // 一次写入多个寄存器
   await WriteMultipleRegisters(startAddress, allValues);

线程安全指南

  1. 切勿从后台线程方法EstablishConnectionCommunicationLoopCloseConnection访问 Unity GameObject
  2. 在后台线程中使用 ThreadSafeLogger 而非 Debug.Log:

   // 后台线程 - 正确
   ThreadSafeLogger.LogInfo("Connection established", GetType().Name);
   ThreadSafeLogger.LogError("Failed to connect", GetType().Name);

   // 后台线程 - 错误
   Debug.Log("This will cause errors!");
  1. 将 Inspector 属性复制到线程安全字段:

   // Inspector 属性(仅主线程)
   public string ServerIP = "192.168.1.100";

   // 线程安全副本(后台线程安全)
   private string threadSafeServerIP;

   protected override void CopyPropertiesToThreadSafe()
   {
       threadSafeServerIP = ServerIP; // 自动调用
   }
  1. 使用线程安全的信号元数据访问

   // 后台线程安全
   if (TryGetSignalMetadataSafe<int>(signalName, "Address", out int address))
   {
       // 安全使用 address
   }

   // 仅主线程
   signal.SetMetadata("Address", 100);

错误处理和调试

  1. 全面的错误处理

   protected override async Task CommunicationLoop(CancellationToken cancellationToken)
   {
       try
       {
           await DoProtocolCommunication();
       }
       catch (SocketException ex)
       {
           ThreadSafeLogger.LogError($"Network error: {ex.Message}", GetType().Name);
           throw; // 重新抛出以触发重连
       }
       catch (TimeoutException ex)
       {
           ThreadSafeLogger.LogWarning($"Communication timeout: {ex.Message}", GetType().Name);
           throw;
       }
       catch (Exception ex)
       {
           ThreadSafeLogger.LogError($"Unexpected error: {ex.Message}", GetType().Name);
           throw;
       }
   }
  1. 启用调试模式进行详细日志记录:

   // 在 Inspector 或代码中设置
   DebugMode = true;

   // 添加条件日志
   if (threadSafeDebugMode)
   {
       ThreadSafeLogger.LogInfo($"Sending {inputs.Count} signals", GetType().Name);
   }
  1. 在 Inspector 中监控连接状态
    • State:显示当前连接状态及视觉指示器
    • CycleCount:完成的通信周期数
    • CommCycleMs:实际通信时序
    • ErrorMessage:连接失败时的最后一条错误消息

测试您的接口

  1. 从蓝图开始 - 复制 BlueprintFastInterface.cs 作为工作基础
  2. 先测试连接逻辑

   protected override async Task EstablishConnection(CancellationToken cancellationToken)
   {
       ThreadSafeLogger.LogInfo("Testing connection...", GetType().Name);
       await Task.Delay(100); // 模拟成功连接
       ThreadSafeLogger.LogInfo("Connection test successful", GetType().Name);
   }
  1. 添加仿真模式用于无外部硬件的测试:

   [Header("Testing")]
   public bool SimulationMode = true;

   protected override async Task CommunicationLoop(CancellationToken cancellationToken)
   {
       if (threadSafeSimulationMode)
       {
           // 模拟接收数据用于测试
           var testOutputs = new Dictionary<string, object>
           {
               ["TestBool"] = UnityEngine.Random.value > 0.5f,
               ["TestInt"] = UnityEngine.Random.Range(0, 100)
           };
           SetOutputsFromPLC(testOutputs);
       }
       else
       {
           // 真实协议实现
           await DoRealCommunication();
       }
   }

故障排除

常见问题

接口无法连接:

  • 检查网络连接和防火墙设置
  • 验证 IP 地址和端口配置
  • 查看 Unity 控制台和接口 Inspector 中的错误消息
  • 使用外部工具测试(telnet、协议特定客户端)

信号不更新:

  • 验证信号是接口 GameObject 的子对象
  • 检查信号方向(Input 与 Output)
  • 启用 Debug Mode 进行详细的信号日志记录
  • 确保元数据映射正确

性能问题:

  • 启用 OnlyTransmitChangedInputs 以减少网络流量
  • 如果通信过于频繁,增加 UpdateCycleMs
  • 使用批量操作而非单独写入信号
  • 检查 CommunicationLoop 中是否有阻塞操作

线程错误:

  • 切勿从后台线程方法访问 Unity GameObject
  • 在后台线程中使用 ThreadSafeLogger 而非 Debug.Log
  • 将 Inspector 属性复制到线程安全字段
  • 使用线程安全的元数据访问方法

调试工具

  1. Inspector 调试
    • 启用 Debug Mode 进行详细日志记录
    • 监控 State 字段查看连接状态
    • 检查 ErrorMessage 获取失败详情
    • 观察 CycleCountCommCycleMs 了解性能
    • 控制台日志

   ThreadSafeLogger.LogInfo($"Processing {inputs.Count} inputs", GetType().Name);
   ThreadSafeLogger.LogInfoIf(threadSafeDebugMode, "Debug details here", GetType().Name);
  1. 信号状态监控

   // 定期记录所有信号值
   if (privateCycleCount % 100 == 0)
   {
       foreach (var input in GetInputsForPLC())
       {
           ThreadSafeLogger.LogInfo($"Input {input.Key} = {input.Value}", GetType().Name);
       }
   }

FastInterface API 参考

FastInterface 为自定义实现中的信号操作提供了几种高性能辅助方法。这些方法通过扩展方法提供,并提供线程安全的优化信号访问。

信号值操作


// 类型安全地获取信号值
T GetSignalValue<T>(string signalName)
bool TryGetSignalValue<T>(string signalName, out T value)

// 类型安全地设置信号值
void SetSignalValue<T>(string signalName, T value)
bool TrySetSignalValue<T>(string signalName, T value)

// 在 CommunicationLoop 中的示例:
var motorSpeed = this.GetSignalValue<int>("Motor1_Speed");
var isRunning = this.GetSignalValue<bool>("Motor1_Running");

this.SetSignalValue("Temperature", 23.5f);
this.SetSignalValue("Status", "Connected");

批量信号操作


// 用于通信循环的高性能批量操作
Dictionary<string, object> GetInputsForPLC()    // Unity → 外部系统
void SetOutputsFromPLC(Dictionary<string, object> values)  // 外部系统 → Unity

// 一次获取所有信号值
Dictionary<string, object> GetAllSignalValues()

// 高效设置多个信号
void SetMultipleSignalValues(Dictionary<string, object> values)

// 使用示例:
protected override async Task CommunicationLoop(CancellationToken cancellationToken)
{
    // 读取 Unity 信号以发送到外部系统
    var inputs = GetInputsForPLC();
    await SendToExternalSystem(inputs);
    
    // 接收数据并更新 Unity 信号
    var receivedData = await ReceiveFromExternalSystem();
    SetOutputsFromPLC(receivedData);
}

信号发现和管理


// 信号发现和检查
bool HasSignal(string signalName)
int GetSignalCount()
Signal GetSignalComponent(string signalName)

// 按方向获取信号
IEnumerable<Signal> GetInputSignals()
IEnumerable<Signal> GetOutputSignals() 
IEnumerable<Signal> GetAllSignals()

// 获取信号名称(线程安全)
List<string> GetInputSignalNames()
List<string> GetOutputSignalNames()
List<string> GetAllSignalNames()

// 使用示例:
protected virtual void OnInterfaceStarting()
{
    var inputCount = this.GetInputSignals().Count();
    var outputCount = this.GetOutputSignals().Count();
    ThreadSafeLogger.LogInfo($"Found {inputCount} inputs, {outputCount} outputs", GetType().Name);
}

线程安全的元数据访问


// 从后台线程安全获取元数据
T GetSignalMetadataSafe<T>(string signalName, string key, T defaultValue = default(T))
bool HasSignalMetadataSafe(string signalName, string key)
Dictionary<string, object> GetAllSignalMetadataSafe(string signalName)

// 设置元数据(仅主线程)
void SetSignalMetadata(string signalName, string key, object value)

// 示例:使用元数据进行寄存器映射
protected override async Task CommunicationLoop(CancellationToken cancellationToken)
{
    var inputs = GetInputsForPLC();
    foreach (var input in inputs)
    {
        // 从元数据获取寄存器地址
        if (TryGetSignalMetadataSafe<int>(input.Key, "RegisterAddress", out int address))
        {
            await WriteModbusRegister(address, input.Value);
        }
    }
}

动态信号创建


// 动态创建信号
Signal CreateSignalSafe(string name, SignalType signalType, SignalDirection direction)
Signal CreateSignalWithMetadata(string name, SignalType signalType, SignalDirection direction, 
    Dictionary<string, object> metadata = null)

// 创建或更新现有信号
Signal CreateOrUpdateSignal(string signalName, SignalType signalType, SignalDirection direction, 
    Dictionary<string, object> metadata = null)

// 示例:从外部系统配置导入信号
[Button("Import PLC Signals")]
private void ImportPLCSignals()
{
    var signalConfigs = GetPLCConfiguration(); // 您获取 PLC 信号列表的方法
    
    foreach (var config in signalConfigs)
    {
        var metadata = new Dictionary<string, object>
        {
            ["RegisterAddress"] = config.Address,
            ["DataType"] = config.Type
        };
        
        var signal = this.CreateSignalWithMetadata(
            config.Name, 
            config.SignalType, 
            config.Direction, 
            metadata);
            
        ThreadSafeLogger.LogInfo($"Created signal: {signal.name}", GetType().Name);
    }
    
    // 创建信号后刷新管理器
    this.RefreshSignalManager();
}

Signal Manager 工具


// 创建/修改信号后刷新信号管理器
void RefreshSignalManager()

// 检查高性能模式是否激活
bool IsHighPerformanceModeActive()

// 信号创建后的使用示例:
private void SetupCustomSignals()
{
    // 创建您的信号...
    CreateSignalSafe("CustomSignal1", SignalType.Bool, SignalDirection.Input);
    CreateSignalSafe("CustomSignal2", SignalType.Float, SignalDirection.Output);
    
    // 刷新管理器以启用高性能访问
    this.RefreshSignalManager();
}

常见使用模式

模式 1:基于元数据的协议映射


// 设置阶段(主线程)
protected virtual void OnInterfaceStarting()
{
    foreach (var signal in this.GetAllSignals())
    {
        if (signal.name.Contains("Motor"))
        {
            signal.SetMetadata("RegisterAddress", GetMotorRegisterAddress(signal.name));
            signal.SetMetadata("DataType", "INT16");
        }
    }
    this.RefreshSignalManager();
}

// 通信阶段(后台线程)
protected override async Task CommunicationLoop(CancellationToken cancellationToken)
{
    var inputs = GetInputsForPLC();
    foreach (var input in inputs)
    {
        if (TryGetSignalMetadataSafe<int>(input.Key, "RegisterAddress", out int address))
        {
            var dataType = GetSignalMetadataSafe<string>(input.Key, "DataType", "INT16");
            await WriteRegisterWithType(address, input.Value, dataType);
        }
    }
}

模式 2:动态信号导入


[Button("Import from Configuration File")]
private void ImportSignalsFromConfig()
{
    var config = LoadConfigurationFile(); // 您的配置加载方法
    
    foreach (var signalDef in config.Signals)
    {
        var metadata = new Dictionary<string, object>
        {
            ["Address"] = signalDef.Address,
            ["ScaleFactor"] = signalDef.Scale,
            ["Unit"] = signalDef.Unit
        };
        
        this.CreateOrUpdateSignal(
            signalDef.Name,
            signalDef.Type,
            signalDef.Direction,
            metadata);
    }
    
    this.RefreshSignalManager();
    ThreadSafeLogger.LogInfo($"Imported {config.Signals.Count} signals", GetType().Name);
}

模式 3:高性能通信


protected override async Task CommunicationLoop(CancellationToken cancellationToken)
{
    try
    {
        // 高效批量读取所有 Unity 输入信号
        var inputs = GetInputsForPLC();
        
        if (inputs.Count > 0)
        {
            // 在一次操作中发送所有输入
            await SendBatchData(inputs);
        }
        
        // 在一次操作中接收所有数据
        var outputs = await ReceiveBatchData();
        
        if (outputs.Count > 0)
        {
            // 在一次批量中更新所有 Unity 输出信号
            SetOutputsFromPLC(outputs);
        }
    }
    catch (Exception ex)
    {
        ThreadSafeLogger.LogError($"Communication error: {ex.Message}", GetType().Name);
        throw; // 重新抛出以触发重连
    }
}
ℹ️

性能提示:

* 使用批量操作(GetInputsForPLC()SetOutputsFromPLC())而非单独访问信号以获得更好的性能

* 创建或修改信号后调用 RefreshSignalManager() 以启用高性能查找

* 使用元数据进行协议特定配置,避免硬编码地址或参数

* 始终在后台线程方法中使用 ThreadSafeLogger 而非 Debug.Log