relayx/device-cpp

0.1.0

Latest
uploaded 4 hours ago
RelayX C++ Device SDK for ESP32

readme

# 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

Links

Supports all targets

License: Apache-2.0

To add this component to your project, run:

idf.py add-dependency "relayx/device-cpp^0.1.0"

download archive

Stats

  • Archive size
    Archive size ~ 105.00 KB
  • Downloaded in total
    Downloaded in total 0 times
  • Downloaded this version
    This version: 0 times

Badge

relayx/device-cpp version: 0.1.0
|