OPC UA → Modbus TCP 桥接
老设备只支持 Modbus, 新设备只暴露 OPC UA. 写一个轻量桥: 订阅 OPC UA → 写 Modbus 寄存器. 用 C# + NModbus 示例.
配套示例
- 推到云 → OPC UA → MQTT 桥接
- 多 Tag 监控 → 大规模监控
依赖
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.Serial或EasyModbusTCP.
断线哨兵值
OPC UA 断线时 Modbus 寄存器不会自动清零 — 上层应用可能误以为数据正常. 建议挂 ua.Events.Disconnected 把对应寄存器写一个"无效值哨兵" (如 0xFFFF).