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
| Area | Current behavior |
|---|---|
| Customer connection | NATS |
| Command payload format | Protocol Buffers |
| Current field protocol support | BACnet |
| Current edge path | Gateway outbound WebSocket |
| Command persistence | Not persistent today |
| Customer response handling | The customer application must remain connected to receive the response. |
| Request authorization | Same authorization model as the Mapped GraphQL API |
| Point reference | Point 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.
- The customer application opens a NATS connection to Mapped.
- The customer sends a command request to the Command and Control NATS subject.
- The command request includes an authorization header, a point reference, and a typed value.
- Mapped validates that the point exists and that the provided request credentials are authorized to access it.
- Mapped resolves the target mapping key, determines the gateway, and finds an active gateway WebSocket connection.
- Mapped sends the command to the gateway.
- The gateway writes the value to the downstream BACnet device.
- 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.
| Step | Owner | Action |
|---|---|---|
| 1 | Customer | Request Command and Control access from Mapped Support |
| 2 | Mapped | Open an internal ticket to generate NATS credentials |
| 3 | Mapped | Generate NATS credentials for the customer account |
| 4 | Mapped | Provide the NATS credentials to the customer |
| 5 | Customer | Choose a NATS client library and integrate it into the customer application |
| 6 | Mapped | Enable 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:
| Setting | Value |
|---|---|
| NATS URL | wss://d2c.edge-dev.mapped.com |
| NATS subject | cloud.control.v1.write.prop |
| Credentials | Provided by Mapped Support |
| Payload encoding | Protocol Buffers |
| Request message | mapped.cloud.api.control.WritePropRequest |
| Response message | mapped.cloud.api.control.WritePropResponse |
| Protobuf source | control.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.
| Credential | Purpose | Scope |
|---|---|---|
| NATS credentials | Establish a NATS connection to Mapped | Customer account |
| Authorization header | Authorize the specific command request | Organization 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:
| Field | Required | Description |
|---|---|---|
| Authorization header | Yes | Uses the same rules as the Mapped GraphQL API |
| Point reference | Yes | Either a point ID or a mapping key |
| Value | Yes | The typed value to write |
| Priority | No | Optional 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 type | Example |
|---|---|
| Boolean | true |
| Integer | 72 |
| Float | 72.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:
{
"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):
{
"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 case | Expected behavior | Result |
|---|---|---|
| Point ID does not exist | Request fails | CONTROLRESULT_ERROR_BAD_REQUEST (Point not found or access denied) |
| Mapping key does not exist | Request fails | CONTROLRESULT_ERROR_BAD_REQUEST, or CONTROLRESULT_ERROR_UNKNOWN_MAPPING_KEY when the gateway does not know the device or point |
| Point is outside the authorized organization | Request fails | CONTROLRESULT_ERROR_BAD_REQUEST (Point not found or access denied) |
| No active WebSocket exists for the gateway | Request fails fast | CONTROLRESULT_ERROR_BAD_REQUEST (No active session found for Gateway ID) |
| Downstream device rejects the value | Error returned from the device or protocol | The 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 converted | Error returned | CONTROLRESULT_ERROR_VALUE_TYPE_INVALID |
| Command times out | Timeout error returned | CONTROLRESULT_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.