Skip to main content

Step 6: Handle the state RPC

The dashboard needs a way to turn the relay on and off. Expose a state RPC on the device.

Create device/main/actuator.cpp with a small wrapper around the relay GPIO:

#include "actuator.h"
#include "app_shared.h"
#include "driver/gpio.h"

static inline int level_for(bool on) {
const bool high = RELAY_ACTIVE_HIGH ? on : !on;
return high ? 1 : 0;
}

void actuator_init(void) {
gpio_config_t cfg = {};
cfg.pin_bit_mask = (1ULL << RELAY_GPIO);
cfg.mode = GPIO_MODE_OUTPUT;
gpio_config(&cfg);

gpio_set_level(RELAY_GPIO, level_for(false));
}

void actuator_set(bool on) {
gpio_set_level(RELAY_GPIO, level_for(on));
}

actuator_init() configures GPIO 26 and drives it to the OFF level, so the relay never energizes during boot. actuator_set(bool on) translates the boolean into the correct level for the module's polarity (active-LOW in this build) and writes it to the pin.

Call actuator_init() from main.cpp before WiFi starts:

extern "C" void app_main(void) {
nvs_flash_init();
g_state_events = xEventGroupCreate();

actuator_init();
wifi_init_and_wait();
device_task_start();
sensor_task_start();
}

Add the RPC handler to device/main/device_task.cpp:

#include "actuator.h"
#include "cJSON.h"

static void rpc_send_error(relay_rpc_request_t *req, const char *message) {
cJSON *err = cJSON_CreateObject();
cJSON_AddStringToObject(err, "message", message);
req->error(err);
cJSON_Delete(err);
}

static void rpc_set_state(relay_rpc_request_t *req) {
cJSON *payload = cJSON_Parse(req->payload);
cJSON *on = cJSON_GetObjectItem(payload, "on");

if (!on || (!cJSON_IsBool(on) && !cJSON_IsNumber(on))) {
rpc_send_error(req, "expected { \"on\": true | false }");
cJSON_Delete(payload);
return;
}

bool target = cJSON_IsTrue(on) || (cJSON_IsNumber(on) && on->valueint != 0);
actuator_set(target);

cJSON *resp = cJSON_CreateObject();
cJSON_AddBoolToObject(resp, "on", target);
cJSON_AddStringToObject(resp, "status", "ok");
req->respond(resp);

cJSON_Delete(resp);
cJSON_Delete(payload);
}

Register the handler inside device_task, right after connect():

g_device->connect();
g_device->rpc.listen(RPC_SET_STATE, rpc_set_state);

Invalid payloads return a structured RPC error rather than a timeout. The caller sees the error message, not a generic failure.

Test it

Two ways to verify, pick whichever fits where you are in the tutorial.

A. Local check. Reflash the device and watch the serial log. You should see:

I (4412) device-task: Registered RPC handler: state

The firmware boots, connects, and registers the handler. No end-to-end test yet; the RPC will be exercised from the dashboard.

B. End-to-end check. Complete Dashboard Step 5 to build the button that calls this RPC, then come back and press Start session in the browser. The relay should click shut and the load should turn on. Press Stop session and it should open.