Skip to content

Commit

Permalink
Initial BACNet implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
barnstee committed Nov 26, 2024
1 parent 2397f90 commit 33b6b3d
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 26 deletions.
96 changes: 96 additions & 0 deletions BACNetClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@

namespace Opc.Ua.Edge.Translator
{
using Opc.Ua.Edge.Translator.Interfaces;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO.BACnet;
using System.Threading.Tasks;

public class BACNetClient : IAsset
{
private string _endpoint = string.Empty;
private BacnetClient _client;

public void Connect(string ipAddress, int port)
{
try
{
_endpoint = ipAddress;

BacnetIpUdpProtocolTransport transport = new(0xBAC0, false);
_client = new BacnetClient(transport);
_client.OnIam += OnIAm;
_client.Start();
_client.WhoIs();

Log.Logger.Information("Connected to BACNet device at " + ipAddress);
}
catch (Exception ex)
{
Log.Logger.Error(ex.Message, ex);
}
}

private void OnIAm(BacnetClient sender, BacnetAddress adr, uint deviceid, uint maxapdu, BacnetSegmentations segmentation, ushort vendorid)
{
Log.Logger.Information($"Detected device {deviceid} at {adr}");

IList<BacnetValue> value_list;
sender.ReadPropertyRequest(adr, new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, deviceid), BacnetPropertyIds.PROP_OBJECT_LIST, out value_list);

LinkedList<BacnetObjectId> object_list = new LinkedList<BacnetObjectId>();
foreach (BacnetValue value in value_list)
{
if (Enum.IsDefined(typeof(BacnetObjectTypes), ((BacnetObjectId)value.Value).Type))
{
object_list.AddLast((BacnetObjectId)value.Value);
}
}

foreach (BacnetObjectId object_id in object_list)
{
//read all properties
IList<BacnetValue> values = null;
try
{
if (!sender.ReadPropertyRequest(adr, object_id, BacnetPropertyIds.PROP_PRESENT_VALUE, out values))
{
Log.Logger.Error("Couldn't fetch 'present value' for object: " + object_id.ToString());
continue;
}
}
catch (Exception ex)
{
Log.Logger.Error("Couldn't fetch 'present value' for object: " + object_id.ToString() + ": " + ex.Message);
continue;
}

Log.Logger.Information("Object Name: " + object_id.ToString() + ", Property Id: " + values[0].Tag.ToString() + ", Value: " + values[0].Value.ToString());
}
}

public void Disconnect()
{
// nothing to do
}

public string GetRemoteEndpoint()
{
return _endpoint;
}

public Task<byte[]> Read(string addressWithinAsset, byte unitID, string function, ushort count)
{
// TODO
return Task.FromResult((byte[]) null);
}

public Task Write(string addressWithinAsset, byte unitID, string function, byte[] values, bool singleBitOnly)
{
// TODO
return Task.CompletedTask;
}
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ UA Edge Translator can be controlled through the use of just 2 OPC UA methods re

## Supported "Southbound" Asset Interfaces

In this reference implementation, Modbus TCP, OPC UA, Siemens S7Comm (experimental), Mitsubishi MC Protocol (experimental), Rockwell CIP-Ethernet/IP (experimental), and Beckhoff ADS (experimental) are supported. Other interfaces can easily be added by implementing the IAsset interface. There is also a tool provided that can convert from an OPC UA nodeset file (with instance variable nodes defined in it), an AutomationML file, a TwinCAT file, or an Asset Admin Shell file, to a WoT Thing Model file.
In this reference implementation, Modbus TCP, OPC UA, Siemens S7Comm (experimental), Mitsubishi MC Protocol (experimental), Rockwell CIP-Ethernet/IP (experimental), Beckhoff ADS (experimental) and BACNet (experimental) are supported. Other interfaces can easily be added by implementing the IAsset interface. There is also a tool provided that can convert from an OPC UA nodeset file (with instance variable nodes defined in it), an AutomationML file, a TwinCAT file, or an Asset Admin Shell file, to a WoT Thing Model file.

## Running UA Edge Translator from a Docker environment

Expand Down
174 changes: 149 additions & 25 deletions UANodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,14 @@ private void AddNodesForAssetManagement(IList<IReference> objectsFolderReference
_assetManagement.DeleteAsset.InputArguments.Create(SystemContext, deleteAssetInputArgumentsPassiveNode);

// create a variable listing our supported WoT protocol bindings
_uaVariables.Add("SupportedWoTBindings", CreateVariable(_assetManagement, "SupportedWoTBindings", new ExpandedNodeId(DataTypes.UriString), WoTConNamespaceIndex, false, new string[6] {
_uaVariables.Add("SupportedWoTBindings", CreateVariable(_assetManagement, "SupportedWoTBindings", new ExpandedNodeId(DataTypes.UriString), WoTConNamespaceIndex, false, new string[7] {
"https://www.w3.org/2019/wot/modbus",
"https://www.w3.org/2019/wot/opcua",
"https://www.w3.org/2019/wot/s7",
"https://www.w3.org/2019/wot/mcp",
"https://www.w3.org/2019/wot/eip",
"https://www.w3.org/2019/wot/ads"
"https://www.w3.org/2019/wot/ads",
"http://www.w3.org/2022/bacnet"
}));

// add everything to our server namespace
Expand Down Expand Up @@ -748,6 +749,25 @@ private void AddTag(ThingDescription td, object form, string assetId, byte unitI

_tags[assetId].Add(tag);
}

if (td.Base.ToLower().StartsWith("bacnet://"))
{
// create an asset tag and add to our list
GenericForm adsForm = JsonConvert.DeserializeObject<GenericForm>(form.ToString());
AssetTag tag = new()
{
Name = variableId,
Address = adsForm.Href,
UnitID = unitId,
Type = adsForm.Type.ToString(),
PollingInterval = (int)adsForm.PollingTime,
Entity = null,
MappedUAExpandedNodeID = NodeId.ToExpandedNodeId(_uaVariables[variableId].NodeId, Server.NamespaceUris).ToString(),
MappedUAFieldPath = fieldPath
};

_tags[assetId].Add(tag);
}
}

private void AssetConnectionTest(ThingDescription td, out byte unitId)
Expand All @@ -757,91 +777,106 @@ private void AssetConnectionTest(ThingDescription td, out byte unitId)

if (td.Base.ToLower().StartsWith("modbus+tcp://"))
{
string[] modbusAddress = td.Base.Split(new char[] { ':', '/' });
if ((modbusAddress.Length != 6) || (modbusAddress[0] != "modbus+tcp"))
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 6) || (address[0] != "modbus+tcp"))
{
throw new Exception("Expected Modbus server address in the format modbus+tcp://ipaddress:port/unitID!");
}

// check if we can reach the Modbus asset
unitId = byte.Parse(modbusAddress[5]);
unitId = byte.Parse(address[5]);
ModbusTCPClient client = new();
client.Connect(modbusAddress[3], int.Parse(modbusAddress[4]));
client.Connect(address[3], int.Parse(address[4]));

assetInterface = client;
}

if (td.Base.ToLower().StartsWith("opc.tcp://"))
{
string[] opcuaAddress = td.Base.Split(new char[] { ':', '/' });
if ((opcuaAddress.Length != 5) || (opcuaAddress[0] != "opc.tcp"))
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 5) || (address[0] != "opc.tcp"))
{
throw new Exception("Expected OPC UA server address in the format opc.tcp://ipaddress:port!");
}

// check if we can reach the OPC UA asset
UAClient client = new();
client.Connect(opcuaAddress[3], int.Parse(opcuaAddress[4]));
client.Connect(address[3], int.Parse(address[4]));

assetInterface = client;
}

if (td.Base.ToLower().StartsWith("s7://"))
{
string[] opcuaAddress = td.Base.Split(new char[] { ':', '/' });
if ((opcuaAddress.Length != 5) || (opcuaAddress[0] != "s7"))
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 5) || (address[0] != "s7"))
{
throw new Exception("Expected S7 PLC address in the format s7://ipaddress:port!");
}

// check if we can reach the OPC UA asset
// check if we can reach the Siemens asset
SiemensClient client = new();
client.Connect(opcuaAddress[3], int.Parse(opcuaAddress[4]));
client.Connect(address[3], int.Parse(address[4]));

assetInterface = client;
}

if (td.Base.ToLower().StartsWith("mcp://"))
{
string[] opcuaAddress = td.Base.Split(new char[] { ':', '/' });
if ((opcuaAddress.Length != 5) || (opcuaAddress[0] != "mcp"))
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 5) || (address[0] != "mcp"))
{
throw new Exception("Expected Mitsubishi PLC address in the format mcp://ipaddress:port!");
}

// check if we can reach the OPC UA asset
// check if we can reach the Mitsubishi asset
MitsubishiClient client = new();
client.Connect(opcuaAddress[3], int.Parse(opcuaAddress[4]));
client.Connect(address[3], int.Parse(address[4]));

assetInterface = client;
}

if (td.Base.ToLower().StartsWith("eip://"))
{
string[] opcuaAddress = td.Base.Split(new char[] { ':', '/' });
if ((opcuaAddress.Length != 4) || (opcuaAddress[0] != "eip"))
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 4) || (address[0] != "eip"))
{
throw new Exception("Expected Rockwell PLC address in the format eip://ipaddress:port!");
}

// check if we can reach the OPC UA asset
// check if we can reach the Ethernet/IP asset
RockwellClient client = new();
client.Connect(opcuaAddress[3], 0);
client.Connect(address[3], 0);

assetInterface = client;
}

if (td.Base.ToLower().StartsWith("ads://"))
{
string[] opcuaAddress = td.Base.Split(new char[] { ':', '/' });
if ((opcuaAddress.Length != 6) || (opcuaAddress[0] != "ads"))
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 6) || (address[0] != "ads"))
{
throw new Exception("Expected Beckhoff PLC address in the format ads://ipaddress:port!");
}

// check if we can reach the OPC UA asset
// check if we can reach the Beckhoff asset
BeckhoffClient client = new();
client.Connect(address[3] + ":" + address[4], int.Parse(address[5]));

assetInterface = client;
}

if (td.Base.ToLower().StartsWith("bacnet://"))
{
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 6) || (address[0] != "bacnet"))
{
throw new Exception("Expected BACNet device address in the format bacnet://ipaddress:port!");
}

// check if we can reach the BACNet asset
BeckhoffClient client = new();
client.Connect(opcuaAddress[3] + ":" + opcuaAddress[4], int.Parse(opcuaAddress[5]));
client.Connect(address[3] + ":" + address[4], int.Parse(address[5]));

assetInterface = client;
}
Expand Down Expand Up @@ -1012,6 +1047,11 @@ private ServiceResult OnWriteValue(ISystemContext context, NodeState node, Numer
HandleBeckhoffDataWrite(tag, assetId, value.ToString());
}

if (_assets[assetId] is BACNetClient)
{
HandleBACNetDataWrite(tag, assetId, value.ToString());
}

_uaVariables[tag.Name].Value = value;
_uaVariables[tag.Name].Timestamp = DateTime.UtcNow;
_uaVariables[tag.Name].ClearChangeMasks(SystemContext, false);
Expand Down Expand Up @@ -1082,6 +1122,11 @@ private void UpdateNodeValues(object assetNameObject)
{
HandleBeckhoffDataRead(tag, assetId);
}

if (_assets[assetId] is BACNetClient)
{
HandleBACNetDataRead(tag, assetId);
}
}
}
catch (Exception ex)
Expand Down Expand Up @@ -1713,5 +1758,84 @@ private void HandleBeckhoffDataWrite(AssetTag tag, string assetId, string value)

_assets[assetId].Write(addressParts[0], 0, string.Empty, tagBytes, false).GetAwaiter().GetResult();
}

private void HandleBACNetDataRead(AssetTag tag, string assetId)
{
string[] addressParts = tag.Address.Split(['?', '&', '=']);

if (addressParts.Length == 2)
{
byte[] tagBytes = null;
try
{
tagBytes = _assets[assetId].Read(addressParts[0], 0, null, ushort.Parse(addressParts[1])).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Log.Logger.Error(ex.Message, ex);

// try reconnecting
string[] remoteEndpoint = _assets[assetId].GetRemoteEndpoint().Split(':');
_assets[assetId].Disconnect();
_assets[assetId].Connect(remoteEndpoint[0] + ":" + remoteEndpoint[1], int.Parse(remoteEndpoint[2]));
}

if ((tagBytes != null) && (tagBytes.Length > 0))
{
object value = null;
if (tag.Type == "Float")
{
value = BitConverter.ToSingle(tagBytes);
}
else if (tag.Type == "Boolean")
{
value = BitConverter.ToBoolean(tagBytes);
}
else if (tag.Type == "Integer")
{
value = BitConverter.ToInt32(tagBytes);
}
else if (tag.Type == "String")
{
value = Encoding.UTF8.GetString(tagBytes);
}
else
{
throw new ArgumentException("Type not supported by BACNet.");
}

UpdateUAServerVariable(tag, value);
}
}
}

private void HandleBACNetDataWrite(AssetTag tag, string assetId, string value)
{
string[] addressParts = tag.Address.Split(['?', '&', '=']);
byte[] tagBytes = null;

if (tag.Type == "Float")
{
tagBytes = BitConverter.GetBytes(float.Parse(value));
}
else if (tag.Type == "Boolean")
{
tagBytes = BitConverter.GetBytes(bool.Parse(value));
}
else if (tag.Type == "Integer")
{
tagBytes = BitConverter.GetBytes(int.Parse(value));
}
else if (tag.Type == "String")
{
tagBytes = Encoding.UTF8.GetBytes(value);
}
else
{
throw new ArgumentException("Type not supported by BACNet.");
}

_assets[assetId].Write(addressParts[0], 0, string.Empty, tagBytes, false).GetAwaiter().GetResult();
}
}
}

0 comments on commit 33b6b3d

Please sign in to comment.