Skip to main content

Command & Control

Overview​

Command and Control lets a customer application send a command to a controllable point in the Mapped Graph. A common use case is writing a new value to a BACnet point, such as changing a setpoint or commanding an output.

Command and Control uses NATS as the customer-facing command channel. NATS is a messaging system. Customers connect to Mapped over NATS, send a command request to a known subject, and wait for a command response.

Draft note: Replace all {PLACEHOLDER} values before publishing.

Current Support​

AreaCurrent behavior
Customer connectionNATS
Command payload formatProtocol Buffers
Current field protocol supportBACnet
Current edge pathGateway outbound WebSocket
Command persistenceNot persistent today
Customer response handlingThe customer application must remain connected to receive the response.
Request authorizationSame authorization model as the Mapped GraphQL API
Point referencePoint ID or mapping key
Important: Commands are not persistent today. The customer application must keep its NATS connection active until Mapped sends the response. A response may return quickly, or it may take longer depending on the command timeout and downstream device behavior.

Command and Control Architecture​

At a high level, the customer application sends a command to Mapped over NATS. Mapped validates the request, authorizes the point reference, routes the command to the correct gateway, and returns the command response.

  1. The customer application opens a NATS connection to Mapped.
  2. The customer sends a command request to the Command and Control NATS subject.
  3. The command request includes an authorization header, a point reference, and a typed value.
  4. Mapped validates that the point exists and that the provided request credentials are authorized to access it.
  5. Mapped resolves the target mapping key, determines the gateway, and finds an active gateway WebSocket connection.
  6. Mapped sends the command to the gateway.
  7. The gateway writes the value to the downstream BACnet device.
  8. Mapped returns the response to the customer application.

Requesting Access​

Customers request Command and Control access through Mapped Support. Mapped completes the required backend and gateway configuration.

StepOwnerAction
1CustomerRequest Command and Control access from Mapped Support
2MappedOpen an internal ticket to generate NATS credentials
3MappedGenerate NATS credentials for the customer account
4MappedProvide the NATS credentials to the customer
5CustomerChoose a NATS client library and integrate it into the customer application
6MappedEnable Command and Control on the UG bootstrap, if the target points are UG-backed
Note: Command and Control is not enabled on the gateway by default. It must be enabled through a request to Mapped Support.

Connecting with NATS​

Customers typically use an existing NATS client library, rather than building their own client. NATS provides official and community-supported clients for common programming languages.

Use the following connection information:

SettingValue
NATS URLwss://d2c.edge-dev.mapped.com
NATS subjectcloud.control.v1.write.prop
CredentialsProvided by Mapped Support
Payload encodingProtocol Buffers
Request messagemapped.cloud.api.control.WritePropRequest
Response messagemapped.cloud.api.control.WritePropResponse
Protobuf sourcecontrol.proto in mapped/pb
Important: NATS credentials are connection credentials only. They allow the customer application to connect to Mapped. They do not authorize an individual command.

Authorization​

Every command request must include an authorization header. Command and Control uses the same authorization rules as the Mapped GraphQL API.

The authorization header is evaluated per request. This is separate from the NATS credentials used to establish the connection.

CredentialPurposeScope
NATS credentialsEstablish a NATS connection to MappedCustomer account
Authorization headerAuthorize the specific command requestOrganization and allowed resources
The authorization header can use:
  • Personal Access Token (PAT)
  • JSON Web Token (JWT), where supported

Sending a Command​

Each command request includes:

FieldRequiredDescription
Authorization headerYesUses the same rules as the Mapped GraphQL API
Point referenceYesEither a point ID or a mapping key
ValueYesThe typed value to write
PriorityNoOptional BACnet priority index
The point reference can be either a Mapped point ID or a mapping key.

If the request includes a point ID, Mapped Cloud looks up the corresponding mapping key using the request credentials. If the request includes a mapping key, Mapped Cloud still validates the mapping key using the request credentials. This validation also acts as an authorization check. If the point does not exist, or if it belongs to a different organization, the request fails.

Request Example​

Use the protobuf request message defined in control.proto.

// npm install @nats-io/nats-core (Node 22+, for its built-in WebSocket)
// WritePropRequest and WritePropResponse: generated with ts-proto (oneof=unions) from https://github.com/mapped/pb
import {headers, wsconnect} from '@nats-io/nats-core';
import {WritePropRequest, WritePropResponse} from './mapped/cloud/api/control/control';

const nc = await wsconnect({servers: 'wss://d2c.edge-dev.mapped.com', token: process.env.NATS_TOKEN});

const hdrs = headers();
hdrs.set('Authorization', `token ${process.env.MAPPED_PAT}`); // or "Bearer <JWT>"
hdrs.set('x-mapped-org-id', process.env.MAPPED_ORG_ID!);

const request = WritePropRequest.create({
payload: {
pointReferenceId: 'PNTxxxxxxxxxxxxxxxxxxxxxxxx',
value: {value: {$case: 'float64Value', float64Value: 72}},
},
});

// Keep the connection open until the response arrives: responses are not persisted.
const msg = await nc.request('cloud.control.v1.write.prop', WritePropRequest.encode(request).finish(), {
headers: hdrs,
timeout: 30_000,
});
console.log(WritePropResponse.toJSON(WritePropResponse.decode(msg.data)));

await nc.close();

In the JSON below, headers are the NATS message headers and request is the WritePropRequest, sent as binary protobuf.

{
"headers": {
"Authorization": "token PERSONAL_ACCESS_TOKEN",
"x-mapped-org-id": "ORG_ID"
},
"request": {
"payload": {
"pointReferenceId": "PNTxxxxxxxxxxxxxxxxxxxxxxxx",
"value": {
"float64Value": 72
}
}
}
}

The same command can reference a mapping key instead of a point ID.

// Send it as in the first example; only the request changes.
const request = WritePropRequest.create({
payload: {
pointReferenceId: 'msrc://CONNECTOR_ID@SOURCE/GATEWAY_ID/DEVICE_ADDRESS/OBJECT_ID',
value: {value: {$case: 'float64Value', float64Value: 72}},
},
});
{
"headers": {
"Authorization": "token PERSONAL_ACCESS_TOKEN",
"x-mapped-org-id": "ORG_ID"
},
"request": {
"payload": {
"pointReferenceId": "msrc://CONNECTOR_ID@SOURCE/GATEWAY_ID/DEVICE_ADDRESS/OBJECT_ID",
"value": {
"float64Value": 72
}
}
}
}

Values and Data Types​

Command values use a typed value mode; the value should match the point data type shown in the Mapped Graph.

Common value types include:

Value typeExample
Booleantrue
Integer72
Float72.0
String"occupied"
Some downstream systems accept values that do not exactly match the type advertised in the graph. Mapped does not strictly prevent customers from attempting a different type. If the device or protocol rejects the value, Mapped returns an error.

The gateway may perform safe type conversions when the conversion does not result in data loss. For example, if a controller expects a float and the customer sends integer 72, the gateway may convert it to 72.0.

Draft note: Confirm the exact safe conversion rules with Engineering before publishing. The recommended customer guidance is to match the point data type shown in the Mapped Graph whenever possible.

BACnet Priority​

For BACnet commands, the request can optionally include a priority.

If priority is omitted or set to null, Mapped attempts to command the point normally.

If priority is set, Mapped attempts to write the value at that BACnet priority. The request carries it as index, which is zero-based: index 7 writes BACnet priority 8.

// Send it as in the first example; only the request changes.
const request = WritePropRequest.create({
payload: {
pointReferenceId: 'PNTxxxxxxxxxxxxxxxxxxxxxxxx',
value: {value: {$case: 'boolValue', boolValue: true}},
index: 7, // zero-based: index 7 writes BACnet priority 8
},
});
{
"headers": {
"Authorization": "token PERSONAL_ACCESS_TOKEN",
"x-mapped-org-id": "ORG_ID"
},
"request": {
"payload": {
"pointReferenceId": "PNTxxxxxxxxxxxxxxxxxxxxxxxx",
"value": {
"boolValue": true
},
"index": 7
}
}
}

Responses and Errors​

An application can either send a command and not wait for a response, or keep the connection open and wait for a response.

For applications that need confirmation, keep the NATS connection active until the response is received.

Mapped returns standardized control result codes. When available, Mapped also includes the raw downstream device or protocol error. For example, if a BACnet controller returns a useful error, Mapped can include that error in the response.

A request Mapped rejects before it reaches the gateway (a missing header, a point that does not exist or is outside the organization, a gateway that is not connected) has no requestId:

Rejected by Mapped
{
"payload": {
"result": "CONTROLRESULT_ERROR_BAD_REQUEST",
"errorDescription": "Point not found or access denied: PNTxxxxxxxxxxxxxxxxxxxxxxxx"
}
}

An error from the device carries the requestId and, for protocols that report them, the protocol error class and code. Here a BACnet controller denies the write (error class 2, Property; error code 40, Write Access Denied):

Rejected by the device
{
"requestId": "REQUEST_ID",
"payload": {
"pointReferenceId": "PNTxxxxxxxxxxxxxxxxxxxxxxxx",
"result": "CONTROLRESULT_ERROR_VALUE_WRITE_ACCESS_DENIED",
"errorDescription": "Property: Write Access Denied",
"connectorErrorClass": "2",
"connectorErrorCode": "40"
}
}

Common failure cases include:

Failure caseExpected behaviorResult
Point ID does not existRequest failsCONTROLRESULT_ERROR_BAD_REQUEST (Point not found or access denied)
Mapping key does not existRequest failsCONTROLRESULT_ERROR_BAD_REQUEST, or CONTROLRESULT_ERROR_UNKNOWN_MAPPING_KEY when the gateway does not know the device or point
Point is outside the authorized organizationRequest failsCONTROLRESULT_ERROR_BAD_REQUEST (Point not found or access denied)
No active WebSocket exists for the gatewayRequest fails fastCONTROLRESULT_ERROR_BAD_REQUEST (No active session found for Gateway ID)
Downstream device rejects the valueError returned from the device or protocolThe device result, such as CONTROLRESULT_ERROR_VALUE_WRITE_ACCESS_DENIED, CONTROLRESULT_ERROR_VALUE_OUT_OF_RANGE or CONTROLRESULT_ERROR_DEVICE_OTHER, with the protocol error class and code
Value type cannot be safely convertedError returnedCONTROLRESULT_ERROR_VALUE_TYPE_INVALID
Command times outTimeout error returnedCONTROLRESULT_ERROR_TIMEOUT when the device does not answer; CONTROLRESULT_ERROR_BAD_REQUEST when Mapped stops waiting for the gateway

Gateway Routing​

For routing, the gateway establishes an outbound WebSocket connection to Mapped Cloud and announces its gateway ID. When Mapped receives a command request, it resolves the point reference, determines the target mapping key, extracts the gateway ID, and looks for an active WebSocket connection for that gateway.

If an active connection exists, Mapped sends the command to the UG. If there is no active connection, Mapped fails fast and returns an error to the customer.

Note: The UG connection is outbound from the customer environment to Mapped Cloud.

Mapping Keys​

Mapped can accept either a point ID or a mapping key in the command request.

Mapped ultimately needs a mapping key to route the command. If the application provides a point ID, Mapped resolves the mapping key. If the application provides a mapping key, Mapped validates that it exists and is authorized.

In some cases, a point's source identity can change at the edge. For example, if a BACnet point changes IP address, the gateway may contribute a new point in the core graph. The resolved graph may merge the old and new contributions into the same point because they share a BACnet object ID. For routing, Mapped Cloud uses the latest usable mapping key known to the gateway.

Command Persistence​

Command responses are not persisted today. The application must remain connected to receive the response.

Mapped chose NATS to support future persistent command workflows. In the future, Mapped may support NATS JetStream, which could allow a customer to send commands, disconnect, reconnect later, and retrieve stored responses.

Troubleshooting​

Command request is rejected​

Check that the request includes a valid authorization header and that the token has access to the organization and point.

Point cannot be found​

Confirm that the point ID or mapping key exists in the Mapped Graph. If using a mapping key, confirm that it is the current mapping key for the target point.

Command fails immediately​

The target gateway may not have an active WebSocket connection. Confirm that the gateway is online and that Command and Control is enabled.

Device rejects the value​

Confirm that the value type matches the point type shown in the Mapped Graph. If the device requires a specific BACnet data type, priority, or proprietary value, use the value expected by the downstream system.

No response is received​

Confirm that the application kept the NATS connection active after sending the request. Command responses are not persisted today.