# RelayX Device SDK for C++ (ESP32)
Official C++ SDK for connecting ESP32 IoT devices to the RelayX platform over NATS.
## Installation
Add the following to your project's `main/idf_component.yml`:
```yaml
dependencies:
relayx/device-cpp: ">=0.1.0"
```
Then in your `main/CMakeLists.txt`:
```cmake
idf_component_register(
SRCS "main.cpp"
INCLUDE_DIRS "."
REQUIRES device-cpp nvs_flash esp_wifi
)
```
The component manager will automatically pull `device-cpp` and all its dependencies when you run `idf.py build`.
Requires ESP-IDF v5.0+ (tested on v6.0).
## Quick Start
```cpp
#include "relay/device.h"
#include "cJSON.h"
relay_device_config_t config = {
.api_key = "ey...",
.secret = "SUAXXXXX...", // NKey seed
.mode = RELAY_MODE_PRODUCTION
};
RelayDevice* device = new RelayDevice(config);
device->connect();
// Publish telemetry
device->telemetry.publish_number("temperature", 22.5);
// Listen for RPC calls
device->rpc.listen("ping", [](relay_rpc_request_t* req) {
cJSON* resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "pong", true);
req->respond(resp);
cJSON_Delete(resp);
});
// Listen for commands
device->command.listen("reboot", [](const cJSON* data) {
esp_restart();
});
// Main loop
while (true) {
device->process();
vTaskDelay(pdMS_TO_TICKS(10));
}
device->disconnect();
```
## Configuration
```cpp
relay_device_config_t config = {
.api_key = "...", // API Key
.secret = "...", // Secret
.mode = RELAY_MODE_PRODUCTION // or RELAY_MODE_TEST
};
```
- **api_key**: Your API Key to connect to the RelayX network
- **secret**: Your Secret to connect to the RelayX network
- **mode**: `RELAY_MODE_PRODUCTION` for when device is in production mode. `RELAY_MODE_TEST` for when device is in test mode.
## Functionality
### Connection
```cpp
RelayDevice* device = new RelayDevice(config);
// Listen for connection status changes
device->connection.on_status([](relay_connection_status_t status) {
switch (status) {
case RELAY_STATUS_CONNECTED: // Initial connection established
case RELAY_STATUS_DISCONNECTED: // Connection lost
case RELAY_STATUS_RECONNECTING: // Attempting to reconnect
case RELAY_STATUS_RECONNECTED: // Reconnection successful
}
});
relay_err_t err = device->connect();
if (err != RELAY_OK) {
// Handle error
}
// Must be called in a loop - pumps NATS, fires callbacks, handles reconnect
while (true) {
device->process();
vTaskDelay(pdMS_TO_TICKS(10));
}
device->disconnect();
```
The SDK automatically reconnects when the connection drops. On reconnect, buffered messages are flushed.
### Telemetry
Fire-and-forget sensor data publishing. Readings are validated against the device schema fetched on connect.
```cpp
// Number
device->telemetry.publish_number("temperature", 22.5);
// String
device->telemetry.publish_string("status", "running");
// Boolean
device->telemetry.publish_bool("motor_on", true);
// JSON (pass a cJSON object - encoded as native msgpack, not stringified)
cJSON* obj = cJSON_CreateObject();
cJSON_AddNumberToObject(obj, "x", 1.0);
cJSON_AddNumberToObject(obj, "y", 2.0);
device->telemetry.publish_json("position", obj);
cJSON_Delete(obj);
```
Each reading is automatically timestamped using the server-synced SNTP clock. Payload format: `{value: <typed>, timestamp: <ms>}`.
If the device schema is loaded, the SDK validates that the metric name exists and the type matches. Unknown metrics or type mismatches return `RELAY_ERR_VALIDATION`. Schema fetch failure is non-fatal - validation is skipped.
### Remote Procedure Calls (RPC)
Register handlers for incoming RPC calls.
```cpp
device->rpc.listen("get_status", [](relay_rpc_request_t* req) {
// req->payload contains the JSON request data
cJSON* resp = cJSON_CreateObject();
cJSON_AddNumberToObject(resp, "uptime", 12345);
cJSON_AddNumberToObject(resp, "heap_free", esp_get_free_heap_size());
req->respond(resp);
cJSON_Delete(resp);
});
// Error response
device->rpc.listen("dangerous_op", [](relay_rpc_request_t* req) {
cJSON* err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "message", "not allowed");
req->error(err);
cJSON_Delete(err);
});
// Unregister
device->rpc.off("get_status");
```
Registering a duplicate listener returns `RELAY_ERR_DUPLICATE_LISTENER`.
### Commands
One-way commands from server to device. Incoming msgpack payload is automatically decoded to `cJSON*`.
```cpp
device->command.listen("reboot", [](const cJSON* data) {
// data is a decoded cJSON object (do NOT delete it)
char* str = cJSON_PrintUnformatted(data);
ESP_LOGI("app", "Reboot command: %s", str);
free(str);
esp_restart();
});
// Unregister
device->command.off("reboot");
```
### Config
Get and set device configuration on the server. Uses core NATS request/reply with one-shot callbacks.
```cpp
// Get config
device->config_mgr.get([](relay_err_t err, const char* data, size_t len) {
if (err == RELAY_OK) {
ESP_LOGI("app", "Config: %.*s", (int)len, data);
}
});
// Set config
cJSON* new_config = cJSON_CreateObject();
cJSON_AddNumberToObject(new_config, "sample_rate", 10);
device->config_mgr.set(new_config, [](relay_err_t err) {
if (err == RELAY_OK) {
ESP_LOGI("app", "Config updated");
}
});
cJSON_Delete(new_config);
```
### Events
Fire-and-forget event publishing of events (fault codes, etc)
```cpp
device->event.send_number("battery_low", 15.0);
device->event.send_string("alert", "door_opened");
device->event.send_bool("motion_detected", true);
cJSON* obj = cJSON_CreateObject();
cJSON_AddNumberToObject(obj, "lat", 37.7749);
cJSON_AddNumberToObject(obj, "lng", -122.4194);
device->event.send_json("location", obj);
cJSON_Delete(obj);
```
### Time
Uses ESP-IDF's built-in SNTP with `time.google.com`. System clock is set automatically on connect.
```cpp
// Automatically initialized on device->connect()
// Milliseconds since Unix epoch
int64_t ms = device->time.now();
// Seconds since Unix epoch
int64_t sec = device->time.now_seconds();
```
`time.now()` does NOT require an active connection - it uses the system clock.
## Error Handling
All SDK operations return `relay_err_t` error codes. No C++ exceptions.
| Error Code | Description |
|---|---|
| `RELAY_OK` | Success |
| `RELAY_ERR_BUFFERED` | Message queued offline (informational, not a failure) |
| `RELAY_ERR_NOT_CONNECTED` | Operation requires active connection |
| `RELAY_ERR_TIMEOUT` | Request/reply timed out |
| `RELAY_ERR_VALIDATION` | Invalid metric name, schema type mismatch, or bad subject token |
| `RELAY_ERR_DUPLICATE_LISTENER` | Listener with this name already registered |
| `RELAY_ERR_CONNECT_FAILED` | NATS connection failed |
| `RELAY_ERR_PUBLISH_FAILED` | JetStream publish failed |
| `RELAY_ERR_SUBSCRIBE_FAILED` | Subscription creation failed |
| `RELAY_ERR_BUFFER_FULL` | Offline buffer full (oldest message was dropped) |
| `RELAY_ERR_INVALID_CONFIG` | Missing required config fields |
| `RELAY_ERR_PAYLOAD_TOO_LARGE` | Message exceeds `RELAY_MAX_PAYLOAD_SIZE` |
## Offline Behavior
- **Telemetry & Events**: Buffered in a static ring buffer. Flushed automatically on reconnect.
- **RPC & Commands**: Subscriptions are re-established on reconnect.
- **Config**: Returns `RELAY_ERR_NOT_CONNECTED` when offline.
- **Publish returns**: `RELAY_OK` if sent, `RELAY_ERR_BUFFERED` if queued offline.
## Compile-Time Configuration
Override any default before including `relay/device.h`:
```cpp
#define RELAY_LOG_LEVEL 0 // 0=NONE, 1=ERROR, 2=WARN, 3=INFO (default), 4=DEBUG
#define RELAY_OFFLINE_BUFFER_SIZE 20 // Max buffered messages (default 10)
#define RELAY_MAX_PAYLOAD_SIZE 1024 // Max message payload bytes (default 512)
#define RELAY_RECONNECT_INTERVAL_MS 5000 // Reconnect attempt interval (default 3000)
```
Disable subsystems you don't need to save RAM:
```cpp
#define RELAY_ENABLE_EVENT 0
#define RELAY_ENABLE_COMMAND 0
#define RELAY_ENABLE_RPC 0
#define RELAY_ENABLE_CONFIG 0
```
Minimum required: `RELAY_ENABLE_TELEMETRY` + `RELAY_ENABLE_CONNECTION` (always on).
## Testing
```bash
cd test
idf.py set-target esp32
idf.py build && idf.py -p /dev/tty.usbserial-110 flash monitor
```
The test suite uses ESP-IDF's Unity framework and runs on-device. It covers all internal utilities (subject builder, offline buffer, listener registry, msgpack encoder/decoder, JWT parser, callback dispatch) and integration tests against the live server.
## License
Apache-2.0
idf.py add-dependency "relayx/device-cpp^0.1.0"