BACnet server as a docker application

163 views Asked by At

On the Python BACnet stack bacpypes there is simple examples on how to make a BACnet server, like this mini_device.py on the git repo.

The BACpypes applications require a .ini file like a config file which looks like that states the address of the NIC card you want to use:

[BACpypes]
objectName: OpenDsm
address: 192.168.0.109/24 
objectIdentifier: 500001
maxApduLengthAccepted: 1024
segmentationSupported: segmentedBoth
vendorIdentifier: 15

Trying to turn this into a docker container if I put this mini_device.py in a dir with the BACpypes.ini, requirements.txt for bacypes, and a Dockerfile that looks like this:

# Use an official Python runtime as a parent image with Python 3.10
FROM python:3.10-alpine

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
RUN pip install -r requirements.txt

# Make port 47808 available to the world outside this container
EXPOSE 47808/udp

# Define environment variable
ENV PYTHONUNBUFFERED=1

# Run your Python script when the container launches
CMD ["python", "app.py"]

A run in terminal $ docker build -t bacnet-server-test .

It builds just fine but when running it with $ docker run -p 47808:47808/udp bacnet-server-test I get an OSError: [Errno 99] Address not available error I think because the BACpypes.ini file is stating an incorrect address to use.

Would anyone have any advice to research on this? Am sort of a newbie in Docker thanks for any tips. Ideally if its possible it would be nice for the Python script to just bind the address to like an eth0 adapter or something in Linux...?

Full traceback:

Traceback (most recent call last):
  File "/app/app.py", line 136, in <module>
    main()
  File "/app/app.py", line 96, in main
    test_application = SampleApplication(this_device, args.ini.address)
  File "/usr/local/lib/python3.10/site-packages/bacpypes/app.py", line 535, in __init__
    self.mux = UDPMultiplexer(self.localAddress)
  File "/usr/local/lib/python3.10/site-packages/bacpypes/bvllservice.py", line 96, in __init__
    self.directPort = UDPDirector(self.addrTuple)
  File "/usr/local/lib/python3.10/site-packages/bacpypes/udp.py", line 155, in __init__
    self.bind(address)
  File "/usr/local/lib/python3.10/asyncore.py", line 333, in bind
    return self.socket.bind(addr)
OSError: [Errno 99] Address not available
1

There are 1 answers

0
bbartling On

I think this is working...add in these packages:

import subprocess
import configparser

And add in these functions update_ini_address and get_ip_address and modifications to the main as shown below with keeping the rest of mini_device.py the same:

def get_ip_address():
    try:
        # This will return the IP address of the default route interface (which should be the host's primary IP)
        output = subprocess.check_output(["ip", "route", "get", "1"]).decode("utf-8")
        for line in output.split("\n"):
            if "src" in line:
                return line.strip().split("src")[1].split()[0]
    except Exception as e:
        print(f"An error occurred: {e}")
        return None


def update_ini_address(ini_file_path, new_address):
    config = configparser.ConfigParser()
    config.read(ini_file_path)
    if "BACpypes" in config:
        config["BACpypes"]["address"] = new_address
        with open(ini_file_path, "w") as configfile:
            config.write(configfile)


def main():
    global test_av, test_bv, test_application

    detected_ip_address = get_ip_address()
    if detected_ip_address:
        print(f"Detected IP Address: {detected_ip_address}")
        # Update the ini file
        update_ini_address("BACpypes.ini", f"{detected_ip_address}/24")
    else:
        print("Unable to find IP address. Using the one from ini file.")

    # make a parser
    parser = ConfigArgumentParser(description=__doc__)

    # parse the command line arguments
    args = parser.parse_args()

    if _debug:
        _log.debug("initialization")
    if _debug:
        _log.debug("    - args: %r", args)

    # make a device object
    this_device = LocalDeviceObject(ini=args.ini)
    if _debug:
        _log.debug("    - this_device: %r", this_device)

    # make a sample application
    test_application = SampleApplication(this_device, args.ini.address)

    # make an analog value object
    test_av = AnalogValueObject(
        objectIdentifier=("analogValue", 1),
        objectName="av",
        presentValue=0.0,
        statusFlags=[0, 0, 0, 0],
        covIncrement=1.0,
    )
    _log.debug("    - test_av: %r", test_av)

    # add it to the device
    test_application.add_object(test_av)
    _log.debug("    - object list: %r", this_device.objectList)

    # make a binary value object
    test_bv = BinaryValueObject(
        objectIdentifier=("binaryValue", 1),
        objectName="bv",
        presentValue="inactive",
        statusFlags=[0, 0, 0, 0],
    )
    _log.debug("    - test_bv: %r", test_bv)

    # add it to the device
    test_application.add_object(test_bv)

    # binary value task
    do_something_task = DoSomething(INTERVAL)
    do_something_task.install_task()

    _log.debug("running")

    run()

    _log.debug("fini")


if __name__ == "__main__":
    main()

The Dockerfile no changes are needed. This is working for me on running this on a Rasp Pi

$ sudo docker build -t bacnet-server-test .
$ sudo docker run --network="host" bacnet-server-test

And the Docker container running mini_device.py finds the host OS IP address where it appears to work fine. From a separate Windows computer on my LAN test bench the mini_device.py BACnet is responding to the BACnet who-is from a BACnet scan tool...and the points of mini_device.py works as expected...