跳到主要内容

OPC UA → Modbus TCP 桥接

老设备只支持 Modbus, 新设备只暴露 OPC UA. 写一个轻量桥: 订阅 OPC UA → 写 Modbus 寄存器. 用 C# + NModbus 示例.

配套示例

依赖

dotnet add package DarraOpcUa
dotnet add package NModbus

完整代码

using DarraOpcUa_Client;
using NModbus;
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

class Program
{
// 映射表: OPC UA NodeId → Modbus 寄存器地址
record TagMap(string NodeId, ushort Reg, string Type);

static readonly TagMap[] Mapping = new[]
{
new TagMap("ns=2;s=Boiler1.Temperature", 0, "Float"), // 占 reg 0/1
new TagMap("ns=2;s=Boiler1.Pressure", 2, "Float"), // 占 reg 2/3
new TagMap("ns=2;s=Boiler1.IsRunning", 100, "Bool"), // coil 100
};

static async Task Main()
{
const string opcuaEndpoint = "opc.tcp://192.168.1.100:4840"; // 替换成现场 PLC 工控机 IP
const int modbusPort = 502;

// 1. 启动 Modbus TCP Slave
var listener = new TcpListener(IPAddress.Any, modbusPort);
listener.Start();
var factory = new ModbusFactory();
var slaveNet = factory.CreateSlaveNetwork(listener);
var slave = factory.CreateSlave(unitId: 1);
slaveNet.AddSlave(slave);
_ = slaveNet.ListenAsync();
Console.WriteLine($"[OK] Modbus TCP Slave listen on :{modbusPort}");

// 2. 启动 OPC UA Client
using var ua = new DarraOpcUa(opcuaEndpoint);
ua.Connect();
Console.WriteLine($"[OK] OPC UA connected to {opcuaEndpoint}");

// 3. 订阅 + 类型转换写 Modbus
using var sub = ua.CreateSubscription(500);
sub.DataChanged += (s, e) =>
{
var map = Mapping.FirstOrDefault(m => m.NodeId == e.NodeId);
if (map == null || !e.Status.IsGood) return;

try
{
switch (map.Type)
{
case "Float":
var f = float.Parse(e.ValueString);
var bytes = BitConverter.GetBytes(f);
slave.DataStore.HoldingRegisters[map.Reg] = BitConverter.ToUInt16(bytes, 0);
slave.DataStore.HoldingRegisters[map.Reg + 1] = BitConverter.ToUInt16(bytes, 2);
break;
case "Bool":
slave.DataStore.CoilDiscretes[map.Reg] = bool.Parse(e.ValueString);
break;
}
Console.WriteLine($" {e.NodeId} = {e.ValueString} -> Modbus[{map.Reg}]");
}
catch (Exception ex) { Console.WriteLine($" map error: {ex.Message}"); }
};

sub.AddMany(Mapping.Select(m => m.NodeId).ToList());

Console.WriteLine("\nBridge 运行中, Ctrl+C 退出...");
await Task.Delay(Timeout.Infinite);
}
}

代码要点

  • Modbus 寄存器是 16 位, Float 拆 2 个 / Double 拆 4 个; 字节序 (Big/Little Endian, Word Swap) 各厂家不同, 用 Modbus Poll 对调
  • HoldingRegisters / InputRegisters / Coils / DiscreteInputs 四张表独立, 设计映射前先想清楚放哪张
  • 反向写 (Modbus Master → OPC UA) 挂 ModbusSlaveRequestReceived 事件, 在里面调 ua.Write
  • 单监听口可挂多个 unitId, 一个进程桥接多设备
  • NModbus 不支持 RTU over TCP, 要 RTU 改用 NModbus.SerialEasyModbusTCP

注意事项

  • HoldingRegisters / InputRegisters / Coils / DiscreteInputs 四张表独立, 设计映射前先想清楚.
  • NModbus 不支持 Modbus RTU over TCP, 要 RTU 用 NModbus.SerialEasyModbusTCP.
断线哨兵值

OPC UA 断线时 Modbus 寄存器不会自动清零 — 上层应用可能误以为数据正常. 建议挂 ua.Events.Disconnected 把对应寄存器写一个"无效值哨兵" (如 0xFFFF).

相关链接