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.
- If another command has submitted a lower-priority write to that point which hasn't expired, that value will be written.
- 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.
- 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.
- If the point's value has changed during the command, no action will be taken.
- 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.