跳到主要内容

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://localhost:4840";
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);
}
}

分段说明

第 1 步: Modbus Slave 启动

var slaveNet = factory.CreateSlaveNetwork(listener);
var slave = factory.CreateSlave(unitId: 1);
slaveNet.AddSlave(slave);
_ = slaveNet.ListenAsync();

NModbus 支持单监听口多个 unitId, 多设备桥接时 unitId 区分.

第 2 步: 类型转换 (重点)

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

关键陷阱: Modbus 寄存器是 16 位, OPC UA Float (32 位) 必须拆 2 个寄存器, Double (64 位) 拆 4 个. 字节序 (Big / Little Endian, Word Swap) 各厂家不一致, 调试时拿 Modbus Master 工具 (Modbus Poll) 对一下.

第 3 步: 反向桥接 (Modbus → OPC UA Write)

slave.ModbusSlaveRequestReceived += (s, ev) =>
{
if (ev.Message.FunctionCode == 6 /* Write Single Reg */)
{
// ev 内取 reg 值, 找映射表, 调 ua.Write(...)
}
};

Modbus Master 写过来时, 通过 ModbusSlaveRequestReceived 事件转发到 OPC UA Write.

注意事项

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

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


相关链接