Skip to content

Commit

Permalink
BACNet implementation of discovery, read and write.
Browse files Browse the repository at this point in the history
  • Loading branch information
barnstee committed Nov 26, 2024
1 parent f9df73b commit 45efe28
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 40 deletions.
94 changes: 60 additions & 34 deletions BACNetClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,45 @@ 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(-1, -1, new BacnetAddress(BacnetAddressTypes.IP, _endpoint));

Log.Logger.Information("Connected to BACNet device at " + ipAddress);
string[] addresses = ipAddress.Split('/');
if (addresses.Length == 2)
{
_endpoint = addresses[0];
uint deviceId = uint.Parse(addresses[1]);

BacnetIpUdpProtocolTransport transport = new(0xBAC0, false);
_client = new BacnetClient(transport);
_client.Start();

Connect(new BacnetAddress(BacnetAddressTypes.IP, _endpoint), deviceId, 0, BacnetSegmentations.SEGMENTATION_NONE, 0);
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)
private void Connect(BacnetAddress adr, uint deviceid, uint maxapdu, BacnetSegmentations segmentation, ushort vendorid)
{
Log.Logger.Information($"Detected device {deviceid} at {adr}");

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

var objectCount = value_list.First().As<uint>();
for (uint i = 1; i <= objectCount; i++)
_client.ReadPropertyRequest(adr, deviceObjId, BacnetPropertyIds.PROP_OBJECT_LIST, out IList<BacnetValue> value_list, arrayIndex: 0);
if (value_list != null)
{
sender.ReadPropertyRequest(adr, deviceObjId, BacnetPropertyIds.PROP_OBJECT_LIST, out value_list, arrayIndex: i);
Log.Logger.Information("Object " + value_list[0].Tag + ": " + value_list[0].Value);

BacnetValue Value;
ReadScalarValue(sender, adr, deviceObjId.instance, value_list[0].As<BacnetObjectId>(), BacnetPropertyIds.PROP_OBJECT_NAME, out Value);
Log.Logger.Information("Name: " + Value.Value.ToString());

ReadScalarValue(sender, adr, deviceObjId.instance, value_list[0].As<BacnetObjectId>(), BacnetPropertyIds.PROP_PRESENT_VALUE, out Value);
Log.Logger.Information("Value: " + Value.Value.ToString());
var objectCount = value_list.First().As<uint>();
for (uint i = 1; i <= objectCount; i++)
{
_client.ReadPropertyRequest(adr, deviceObjId, BacnetPropertyIds.PROP_OBJECT_LIST, out value_list, arrayIndex: i);
Log.Logger.Information("Object " + value_list[0].Tag + ": " + value_list[0].Value);

BacnetValue Value;
ReadScalarValue(adr, deviceObjId.instance, value_list[0].As<BacnetObjectId>(), BacnetPropertyIds.PROP_OBJECT_NAME, out Value);
Log.Logger.Information("Name: " + Value.Value.ToString());

ReadScalarValue(adr, deviceObjId.instance, value_list[0].As<BacnetObjectId>(), BacnetPropertyIds.PROP_PRESENT_VALUE, out Value);
Log.Logger.Information("Value: " + Value.Value.ToString());
}
}
}

Expand All @@ -68,33 +72,55 @@ public string GetRemoteEndpoint()

public Task<byte[]> Read(string addressWithinAsset, byte unitID, string function, ushort count)
{
// TODO
return Task.FromResult((byte[]) null);
try
{
BacnetObjectId deviceObjId = new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, unitID);
BacnetValue Value;

ReadScalarValue(new BacnetAddress(BacnetAddressTypes.IP, _endpoint), deviceObjId.instance, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_VALUE, byte.Parse(function)), BacnetPropertyIds.PROP_PRESENT_VALUE, out Value);
return Task.FromResult(BitConverter.GetBytes(float.Parse(Value.Value.ToString())));
}
catch (Exception ex)
{
Log.Logger.Error(ex.Message);
return Task.FromResult((byte[])null);
}
}

public Task Write(string addressWithinAsset, byte unitID, string function, byte[] values, bool singleBitOnly)
{
// TODO
try
{
BacnetObjectId deviceObjId = new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, unitID);
BacnetValue value = new(values[0]);

WriteScalarValue(new BacnetAddress(BacnetAddressTypes.IP, _endpoint), deviceObjId.instance, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_VALUE, byte.Parse(function)), BacnetPropertyIds.PROP_PRESENT_VALUE, value);
}
catch (Exception ex)
{
Log.Logger.Error(ex.Message);
}

return Task.CompletedTask;
}

bool ReadScalarValue(BacnetClient sender, BacnetAddress adr, uint device_id, BacnetObjectId BacnetObject, BacnetPropertyIds Proprerty, out BacnetValue Value)
bool ReadScalarValue(BacnetAddress adr, uint device_id, BacnetObjectId BacnetObject, BacnetPropertyIds Proprerty, out BacnetValue value)
{
Value = new BacnetValue(null);
value = new BacnetValue(null);

if (!sender.ReadPropertyRequest(adr, BacnetObject, Proprerty, out IList<BacnetValue> NoScalarValue))
if (!_client.ReadPropertyRequest(adr, BacnetObject, Proprerty, out IList<BacnetValue> NoScalarValue))
{
return false;
}

Value = NoScalarValue[0];
value = NoScalarValue[0];
return true;
}

bool WriteScalarValue(BacnetClient sender, BacnetAddress adr, uint device_id, BacnetObjectId BacnetObject, BacnetPropertyIds Proprerty, BacnetValue Value)
bool WriteScalarValue(BacnetAddress adr, uint device_id, BacnetObjectId BacnetObject, BacnetPropertyIds Proprerty, BacnetValue Value)
{
BacnetValue[] NoScalarValue = { Value };
return sender.WritePropertyRequest(adr, BacnetObject, Proprerty, NoScalarValue);
return _client.WritePropertyRequest(adr, BacnetObject, Proprerty, NoScalarValue);
}
}
}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ 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), 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.
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.

Note: Since BACNet uses connectionless UDP messages, BACNet support is limited to running UA Edge Translator natively, i.e. NOT within a Docker container!

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
4 changes: 2 additions & 2 deletions Samples/BACNet.td.jsonld
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"scheme": "nosec"
}
},
"base": "bacnet://192.168.120.236",
"base": "bacnet://192.168.120.236/1005",
"properties": {
"temperature_setpoint": {
"@type": "brick:Cooling_Zone_Air_Temperature_Setpoint",
Expand Down Expand Up @@ -63,7 +63,7 @@
"op": [
"readproperty"
],
"href": "bacnet://5/0,85"
"href": "bacnet://1005/0,85"
}
]
}
Expand Down
6 changes: 3 additions & 3 deletions UANodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -875,14 +875,14 @@ private void AssetConnectionTest(ThingDescription td, out byte unitId)
if (td.Base.ToLower().StartsWith("bacnet://"))
{
string[] address = td.Base.Split(new char[] { ':', '/' });
if ((address.Length != 4) || (address[0] != "bacnet"))
if ((address.Length != 5) || (address[0] != "bacnet"))
{
throw new Exception("Expected BACNet device address in the format bacnet://ipaddress!");
throw new Exception("Expected BACNet device address in the format bacnet://ipaddress/deviceId!");
}

// check if we can reach the BACNet asset
BACNetClient client = new();
client.Connect(address[3], 0);
client.Connect(address[3] + "/" + address[4], 0);

assetInterface = client;
}
Expand Down

0 comments on commit 45efe28

Please sign in to comment.