Skip to content

Writing back to underlying systems

Automation systems like BACnet provide low-level write access to automation system objects. However, building secure and reliable applications on top of this functionality can be difficult, since making use of of these protocols may require detailed knowledge of the protocols and devices.

The command subsystem of NF provides uniform access to the write facility of underlying systems, and encapsulates writes within commands. Commands provide a number of benefits:

  • They allow uniform access to underlying write APIs, without the need for a detailed understanding of the network addresses and data types in use.
  • They make it easy to make a set of writes at the same time, which succeed or fail together.
  • They support lifetimes along with a write. After a command lifetime ends, the writes making up the command will be released back to the underlying system.
  • They support an internal priority system, allowing multiple writers from an application to use a single priority level in the underlying system.

Command Lifecycle

A command starts when a client creates a command context by calling StartCommand. A command context is a container for other actions that may be taken like reads and writes; all that is needed to create a context is:

  • A lifetime timestamp or duration after which the command should be reverted.
  • A name, so clients can ensure only a single context is started for a particular activity.

The sequence diagram below shows an example using a command context. The client first creates a command context, and then makes a read and two writes associated with that context. After that point, the context will stay active until cancelled or it expires. When the context ends, the system will automatically generate clear requests to "un-write" the points that had been written inside of the context, and return the system to its previous state.

sequenceDiagram
    autonumber
    Client->>NF: StartCommand
    NF-->>Client: Context Id
    activate NF
    Client->>NF: Read
    Client->>NF: Write(1)
    NF-->>Client: WriteResult(1)
    Client->>NF: Write(2)
    NF-->>Client: WriteResult(2)
    note over NF,Client: Client continues making <br>reads and writes 
    opt Context cancelled
    note right of NF: The context ends when<br>cancelled or expired.
    Client->>NF: CancelCommand
    end
    deactivate NF
    break Context ends
    Client --> NF: Clear(1)
    note right of NF: These clears are generated <br>by the system- the client isn't<br>involved
    Client --> NF: Clear(2)
    end

Command Execution

Command execution occurs when the command is submitted; before any actual writes are made, error checking occurs to check that all points exist and that the values to be writen are valid, and that no other error conditions are present.

If no errors occur, the system makes writes to underlying system using the BACnet HPL. In some situations, not all points may be written to; for instance, if another command has already written to the same point at a higher priority.

These examples use default values for most options to write one point to the value "2".

import grpc, time
from normalgw.hpl import command_pb2
from normalgw.hpl import command_pb2_grpc

nfurl = os.getenv("NFURL", "http://localhost:8080")
# point uuids to write to
uuids = ['210f241e-14b3-11ec-b198-0fcad975e239']

# get a command context id for this command. if the name is in use,
# this will return an error instead of allowing you to create two
# conflicting command contexts.
cmd = requests.post(nfurl + "/api/v2/command", json={
    "name": "test context",
    "duration": "30s",
})
command_context_id = (cmd.json())["id"]

res = requests.post(nfurl + "/api/v2/command/write", json={
    "command_id": command_context_id,
    "writes": [ {
        "point": {
            "uuid": u,
            "layer": "hpl:bacnet:1",
        },
        "value": {
           # the value needs to be an ApplicationDataValue.  However,
            # unlike the direct BACnet API, the command service
            # performs type conversion.  Therefore (for instance) even
            # though we are writing to the present-value of an Analog
            # Value in this example (which has type real), you may
            # also send a double or unsigned here,
            "real": "50",
        },
    } for u in uuids ],
})

Expiration Mechanism

When the lifetype expires, the command will be ended and reverted in one of several ways.

  1. If another command has submitted a lower-priority write to that point which hasn't expired, that value will be written.
  2. If the underlying system is BACnet and the point being written to has a priority array, the priority array will be cleared at the NF priority.
  3. If the underlying point does not have a priority array, and the value which was written at the beginning of the command is still the present value, the previous value will be written back.
  4. If the point's value has changed during the command, no action will be taken.
  5. If an error occurs when attempting to write back or clear a point, the command will still be ended, and the point will be added to a "dirty" list. Retries to clear points on this list occur periodically until successful.

Application Considerations

Reliability

The function of the lifetime value is to allow application writes to provide a mechanism to release control back to the underlying system in the case that their application suffers an outage; that doesn't rely on the application to track which overrides are in place.

Generally, the lifetimes should be set to relatively short values and then extended using the ExtendCommand API; effectively implementing a "keepalive" heartbeat from the application.

If the device NF is running on fails or becomes segmented from the underlying BACnet system, expiration cannot occur until communication or the device is restored.

Type Conversion

Underlying systems like BACnet require type information to access their APIs. For instance, and Analog Value requires "real" floating point data.

To avoid the need for application writers to understand the underlying type systems in detail, the NF Command subsystem attempts to make sensible type conversions on behalf of clients. For instance, writing an integer value to a real-valued object will work as expected as long as the integer can be represented exactly in the floating point type.

Some type errors will still occur; for instance, attempting to write a string to a numerical or boolean type. The truncate_floats option controls the behavor when a non-integer value is written to an integer valued float. By default this is an error; if truncate_floats=true, the integer part of the float will be written in this situation.

Prioritization

Two priority levels are relevent in most application.

The BACnet Priority level is a number between 1 and 16, and specifies which element of the priority array is written two when available. The BACNET_PRIORITY environment variable for the command process sets this value; the default is 12.

The System Priority level specifies which commands in the system are higher priority and thus take effect when multiple commands write to the same point. The cancel_on_conflict command option will cause starting a command to fail because the a point is already written to at the same priority level.