remote-relay

The actuator shape. A GPIO, one shell command that drives it, and a state publish after every change so nothing has to poll. lib/thesada-mod-example-remote-relay/ on the firmware dev branch; ships with the next release.

What it demonstrates

Thing Where in the code
An event-driven module loop() is empty; all the work hangs off the command
One command, every transport Shell::registerCommand("relay.set", ...) in begin(); the same handler answers on the serial console, POST /api/cmd, and the MQTT topic <prefix>/cli/relay.set (anchored below)
The argv convention argv[0] is the command name, so the argument is argv[1]
Active-low hardware active_low in config flips the pin polarity without touching the logic
State out both paths <prefix>/sensor/relay carries {"on":true}, and the bus event relay carries the same
Registration MODULE_REGISTER(ExampleRemoteRelay, PRIORITY_OUTPUT), guarded by ENABLE_EXAMPLE_REMOTE_RELAY

A command registered once is dispatched from every transport: the MQTT client strips <prefix>/cli/ and hands the rest to the shell, and /api/cmd does the same with its JSON body.

Config

"example_remote_relay": {
  "enabled":    true,
  "pin":        4,
  "active_low": false
}

The relay is driven off at boot regardless of what it was before, and that first set(false) publishes the state like any other change.

Turn it on

Uncomment ENABLE_EXAMPLE_REMOTE_RELAY in src/thesada_config.h, build and flash as for any module. Boot log:

[INF][Registry] registry.module_init priority=60 name=ExampleRemoteRelay
[INF][ExRelay] example_relay.set on=0
[INF][ExRelay] example_relay.ready pin=4 active_low=0

Expected output

Any of the three transports, same result:

relay.set on
on
mosquitto_pub -t 'thesada/sht31/cli/relay.set' -m 'toggle'
mosquitto_sub -t 'thesada/sht31/sensor/relay' -v -C 1
thesada/sht31/sensor/relay {"on":false}

A bad argument answers Usage: relay.set on|off|toggle and changes nothing. Each change logs example_relay.set on=<0|1>.

The code

src/ExampleRemoteRelay.h:

// thesada-fw - ExampleRemoteRelay.h
// Starter module: a GPIO that an inbound command flips. The actuator shape -
// no timer, one shell command that is reachable over serial, HTTP and MQTT
// alike, and a state publish after every change.
// SPDX-License-Identifier: GPL-3.0-only
#pragma once

#include <Arduino.h>
#include <Module.h>

class ExampleRemoteRelay : public Module {
public:
  void begin() override;
  void loop() override {}
  const char* name() override { return "ExampleRemoteRelay"; }
  const char* configKey() override { return "example_remote_relay"; }
  void status(ShellOutput out) override;

private:
  void set(bool on);
  void publishState();

  int  _pin       = 4;
  bool _activeLow = false;
  bool _on        = false;
};

src/ExampleRemoteRelay.cpp:

// thesada-fw - ExampleRemoteRelay.cpp
// SPDX-License-Identifier: GPL-3.0-only
#include <thesada_config.h>
#include "ExampleRemoteRelay.h"
#include <Config.h>
#include <EventBus.h>
#include <MQTTClient.h>
#include <Log.h>
#include <Shell.h>
#include <ModuleRegistry.h>
#include <ArduinoJson.h>
#include <string.h>

#ifdef ENABLE_EXAMPLE_REMOTE_RELAY

static const char* TAG = "ExRelay";

// Configure the pin, drive it off, and register the one command.
// in: config block example_remote_relay (pin, active_low).
// out: `relay.set` registered on the shell.
void ExampleRemoteRelay::begin() {
  JsonObject cfg = Config::get();
  _pin       = cfg["example_remote_relay"]["pin"]        | 4;
  _activeLow = cfg["example_remote_relay"]["active_low"] | false;
  pinMode(_pin, OUTPUT);
  set(false);

  // One registration covers every transport. The shell dispatches
  // `relay.set` whether it arrived on the serial console, POST /api/cmd, or
  // the MQTT topic <prefix>/cli/relay.set - the module never sees which.
  // argv[0] is the command name, so the first argument is argv[1].
  Shell::registerCommand("relay.set", "on|off|toggle - drive the example relay",
    [this](int argc, char** argv, ShellOutput out) {
      const char* arg = (argc > 1) ? argv[1] : "";
      if      (strcmp(arg, "on") == 0)     set(true);
      else if (strcmp(arg, "off") == 0)    set(false);
      else if (strcmp(arg, "toggle") == 0) set(!_on);
      else { out("Usage: relay.set on|off|toggle"); return; }
      out(_on ? "on" : "off");
    });

  Log::kvf(TAG, "example_relay.ready pin=%d active_low=%d", _pin, (int)_activeLow);
}

// Drive the pin and announce the new state.
// in: on. out: GPIO level (honouring active_low), then publishState().
void ExampleRemoteRelay::set(bool on) {
  _on = on;
  digitalWrite(_pin, (on != _activeLow) ? HIGH : LOW);
  Log::kvf(TAG, "example_relay.set on=%d", (int)on);
  publishState();
}

// Publish after every change so a dashboard never has to poll. The event
// carries the same payload for anything on the bus (Lua rules, the display).
// in: none. out: <prefix>/sensor/relay {"on":bool} + EventBus "relay".
void ExampleRemoteRelay::publishState() {
  JsonObject  cfg    = Config::get();
  const char* prefix = cfg["mqtt"]["topic_prefix"] | "thesada/node";
  char topic[96];
  snprintf(topic, sizeof(topic), "%s/sensor/relay", prefix);
  MQTTClient::publish(topic, _on ? "{\"on\":true}" : "{\"on\":false}");

  JsonDocument doc;
  doc["on"] = _on;
  EventBus::publish("relay", doc.as<JsonObject>());
}

// One line for `module.status`.
// in: out sink. out: "pin=<n> state=on|off".
void ExampleRemoteRelay::status(ShellOutput out) {
  char line[48];
  snprintf(line, sizeof(line), "pin=%d state=%s", _pin, _on ? "on" : "off");
  out(line);
}

MODULE_REGISTER(ExampleRemoteRelay, PRIORITY_OUTPUT)

#endif  // ENABLE_EXAMPLE_REMOTE_RELAY

Thesada - AGPL-3.0-only (app) / GPL-3.0-only (firmware) / CC BY-NC-SA 4.0 (docs) - License

This site uses Just the Docs, a documentation theme for Jekyll.