/ firmware / src / services / cloudevents.cpp
cloudevents.cpp
  1  #include "cloudevents.h"
  2  #include <config.h>
  3  #include <services/system.h>
  4  #include "../sensors/registry.h"
  5  #include <manager.h>
  6  
  7  #include <Arduino.h>
  8  #include <WiFi.h>
  9  #include <ESPAsyncWebServer.h>
 10  #include <ArduinoJson.h>
 11  
 12  #include <time.h>
 13  
 14  static const char *MIME_CLOUDEVENTS_BATCH = "application/cloudevents-batch+json";
 15  static const char *SPECVERSION = "1.0";
 16  
 17  // ─────────────────────────────────────────────────────────────────────────────
 18  //  Helpers
 19  // ─────────────────────────────────────────────────────────────────────────────
 20  
 21  static String cloudevents_source(void) {
 22    return String("urn:apidae-systems:tenant:") + config::cloudevents::TENANT +
 23           ":site:" + config::cloudevents::SITE;
 24  }
 25  
 26  static String cloudevents_event_id(const char *type_name, uint16_t sequence) {
 27    return String(type_name) + "-" +
 28           String(static_cast<unsigned long>(millis())) + "-" +
 29           String(sequence);
 30  }
 31  
 32  static String cloudevents_now_iso8601(void) {
 33    const time_t now = time(nullptr);
 34    if (now <= 0) return "";
 35  
 36    struct tm utc_time;
 37    gmtime_r(&now, &utc_time);
 38    char buffer[32] = {0};
 39    strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &utc_time);
 40    return String(buffer);
 41  }
 42  
 43  static JsonObject cloudevents_add_event(JsonArray events, uint16_t sequence,
 44                                          const String &source,
 45                                          const char *type_name,
 46                                          const String &time_iso) {
 47    JsonObject event = events.add<JsonObject>();
 48    event["specversion"] = SPECVERSION;
 49    event["id"] = cloudevents_event_id(type_name, sequence);
 50    event["source"] = source;
 51    event["type"] = type_name;
 52    event["datacontenttype"] = asyncsrv::T_application_json;
 53    if (time_iso.length() > 0) {
 54      event["time"] = time_iso;
 55    }
 56    return event;
 57  }
 58  
 59  // ─────────────────────────────────────────────────────────────────────────────
 60  //  Status event (system info, not a sensor)
 61  // ─────────────────────────────────────────────────────────────────────────────
 62  
 63  static void append_status_event(JsonArray events, uint16_t sequence,
 64                                  const String &source, const String &time_iso) {
 65    JsonObject event = cloudevents_add_event(events, sequence, source,
 66                                             "status.v1", time_iso);
 67    JsonObject data = event["data"].to<JsonObject>();
 68    SystemQuery query = {
 69      .preferred_storage = StorageKind::LittleFS,
 70      .snapshot = {},
 71    };
 72    services::system::accessSnapshot(&query);
 73    data["memory_heap"] = query.snapshot.heap_free;
 74    data["chip_model"] = query.snapshot.chip_model;
 75    data["chip_cores"] = query.snapshot.chip_cores;
 76    data["chip_revision"] = query.snapshot.chip_revision;
 77    data["ipv4_address"] = query.snapshot.network.ip;
 78    data["wifi_rssi"] = query.snapshot.network.rssi;
 79    data["uptime_seconds"] = millis() / 1000UL;
 80  }
 81  
 82  // ─────────────────────────────────────────────────────────────────────────────
 83  //  Sensor serializers (SensorKind → JSON fields)
 84  // ─────────────────────────────────────────────────────────────────────────────
 85  
 86  static void serialize_temperature_humidity(const void *raw, JsonObject &out) {
 87    auto *d = static_cast<const TemperatureHumiditySensorData *>(raw);
 88    out["model"] = d->model ? d->model : "unknown";
 89    out["temperature_celsius"] = d->temperature_celsius;
 90    out["relative_humidity_percent"] = d->relative_humidity_percent;
 91  }
 92  
 93  static void serialize_voltage(const void *raw, JsonObject &out) {
 94    auto *d = static_cast<const VoltageSensorData *>(raw);
 95    out["gain"] = sensors::voltage::accessGainLabel();
 96    JsonArray voltage = out["voltage"].to<JsonArray>();
 97    for (size_t ch = 0; ch < config::voltage::CHANNEL_COUNT; ch++)
 98      voltage.add(d->channel_volts[ch]);
 99  }
100  
101  static void serialize_current(const void *raw, JsonObject &out) {
102    auto *d = static_cast<const CurrentSensorData *>(raw);
103    out["current_mA"] = d->current_mA;
104    out["bus_voltage_V"] = d->bus_voltage_V;
105    out["shunt_voltage_mV"] = d->shunt_voltage_mV;
106    out["power_mW"] = d->power_mW;
107    out["energy_J"] = d->energy_J;
108    out["charge_C"] = d->charge_C;
109    out["die_temperature_C"] = d->die_temperature_C;
110  }
111  
112  static void serialize_co2(const void *raw, JsonObject &out) {
113    auto *d = static_cast<const CO2SensorData *>(raw);
114    out["model"] = d->model;
115    out["co2_ppm"] = d->co2_ppm;
116    out["temperature"] = d->temperature_celsius;
117    out["humidity"] = d->relative_humidity_percent;
118  }
119  
120  static void serialize_barometric_pressure(const void *raw, JsonObject &out) {
121    auto *d = static_cast<const BarometricPressureSensorData *>(raw);
122    out["model"] = d->model;
123    out["pressure_hpa"] = d->pressure_hpa;
124    out["temperature_celsius"] = d->temperature_celsius;
125  }
126  
127  static void serialize_wind_speed(const void *raw, JsonObject &out) {
128    auto *d = static_cast<const WindSpeedSensorData *>(raw);
129    out["wind_speed_kilometers_per_hour"] = d->kilometers_per_hour;
130  }
131  
132  static void serialize_wind_direction(const void *raw, JsonObject &out) {
133    auto *d = static_cast<const WindDirectionSensorData *>(raw);
134    out["wind_direction_degrees"] = d->degrees;
135    out["wind_direction_angle_slice"] = d->slice;
136  }
137  
138  static void serialize_solar_radiation(const void *raw, JsonObject &out) {
139    auto *d = static_cast<const SolarRadiationSensorData *>(raw);
140    out["watts_per_square_meter"] = d->watts_per_square_meter;
141  }
142  
143  static void serialize_soil(const void *raw, JsonObject &out) {
144    auto *d = static_cast<const SoilSensorData *>(raw);
145    out["slave_id"] = d->slave_id;
146    out["temperature_celsius"] = d->temperature_celsius;
147    out["moisture_percent"] = d->moisture_percent;
148    out["conductivity"] = d->conductivity;
149    out["salinity"] = d->salinity;
150    out["tds"] = d->tds;
151  }
152  
153  struct SensorSerializer {
154    SensorKind kind;
155    const char *event_type;
156    void (*serialize)(const void *data, JsonObject &out);
157  };
158  
159  static const SensorSerializer SERIALIZERS[] = {
160    {SensorKind::TemperatureHumidity, "sensors.temperature_and_humidity.v1", serialize_temperature_humidity},
161    {SensorKind::Voltage,             "sensors.power.v1",                   serialize_voltage},
162    {SensorKind::Current,             "sensors.current.v1",                 serialize_current},
163    {SensorKind::CarbonDioxide,       "sensors.carbon_dioxide.v1",          serialize_co2},
164    {SensorKind::BarometricPressure,   "sensors.barometric_pressure.v1",    serialize_barometric_pressure},
165    {SensorKind::WindSpeed,           "sensors.wind_speed.v1",              serialize_wind_speed},
166    {SensorKind::WindDirection,       "sensors.wind_direction.v1",          serialize_wind_direction},
167    {SensorKind::SolarRadiation,      "sensors.solar_radiation.v1",         serialize_solar_radiation},
168    {SensorKind::Soil,                "sensors.soil.v1",                    serialize_soil},
169  };
170  
171  static const SensorSerializer *find_serializer(SensorKind kind) {
172    for (const auto &s : SERIALIZERS) {
173      if (s.kind == kind) return &s;
174    }
175    return nullptr;
176  }
177  
178  // ─────────────────────────────────────────────────────────────────────────────
179  //  Registry-driven sensor event appender
180  // ─────────────────────────────────────────────────────────────────────────────
181  
182  static void append_sensor_events(JsonArray events, uint16_t &sequence,
183                                   const String &source, const String &time_iso) {
184    for (uint8_t i = 0; i < sensors::registry::entryCount(); i++) {
185      const SensorEntry *entry = sensors::registry::entry(i);
186      if (!entry || !entry->isAvailable()) continue;
187  
188      const SensorSerializer *ser = find_serializer(entry->kind);
189      if (!ser) continue;
190  
191      uint8_t count = entry->instanceCount();
192      if (count == 0) continue;
193  
194      JsonObject event = cloudevents_add_event(events, sequence++, source,
195                                               ser->event_type, time_iso);
196      JsonObject data = event["data"].to<JsonObject>();
197  
198      if (count == 1) {
199        const void *snapshot = sensors::registry::latest(entry->kind, 0);
200        bool ok = sensors::registry::valid(entry->kind, 0);
201        data["read_ok"] = ok;
202        if (ok && snapshot) {
203          ser->serialize(snapshot, data);
204        }
205      } else {
206        data["sensor_count"] = count;
207        JsonArray instances = data["sensors"].to<JsonArray>();
208        uint16_t successful = 0;
209        for (uint8_t j = 0; j < count; j++) {
210          const void *snapshot = sensors::registry::latest(entry->kind, j);
211          bool ok = sensors::registry::valid(entry->kind, j);
212          JsonObject inst = instances.add<JsonObject>();
213          inst["index"] = j;
214          inst["read_ok"] = ok;
215          if (ok && snapshot) {
216            ser->serialize(snapshot, inst);
217            successful++;
218          }
219        }
220        data["successful_reads"] = successful;
221      }
222    }
223  }
224  
225  // ─────────────────────────────────────────────────────────────────────────────
226  //  Route handler
227  // ─────────────────────────────────────────────────────────────────────────────
228  
229  static void handle_cloudevents_get(AsyncWebServerRequest *request) {
230    JsonDocument payload;
231    JsonArray events = payload.to<JsonArray>();
232  
233    const String source = cloudevents_source();
234    const String time_iso = cloudevents_now_iso8601();
235  
236    uint16_t sequence = 0;
237    append_status_event(events, sequence++, source, time_iso);
238    append_sensor_events(events, sequence, source, time_iso);
239  
240    AsyncResponseStream *response =
241        request->beginResponseStream(MIME_CLOUDEVENTS_BATCH);
242    serializeJson(payload, *response);
243    request->send(response);
244  }
245  
246  void services::cloudevents::registerRoutes(AsyncWebServer *server) {
247    if (!server) return;
248    server->on("/api/cloudevents", HTTP_GET, handle_cloudevents_get);
249  }
250  
251  // ─────────────────────────────────────────────────────────────────────────────
252  //  Tests — describe("CloudEvents")
253  // ─────────────────────────────────────────────────────────────────────────────
254  #ifdef PIO_UNIT_TESTING
255  
256  #include <testing/utils.h>
257  
258  static void test_cloudevents_source_format(void) {
259    WHEN("the CloudEvents source string is generated");
260    THEN("it contains tenant and site config");
261  
262    String source = cloudevents_source();
263    String expected = String("urn:apidae-systems:tenant:") +
264                       config::cloudevents::TENANT + ":site:" +
265                       config::cloudevents::SITE;
266    TEST_ASSERT_EQUAL_STRING_MESSAGE(
267        expected.c_str(),
268        source.c_str(),
269        "device: source string should use tenant and site config");
270  
271    TEST_MESSAGE(source.c_str());
272  }
273  
274  static void test_cloudevents_event_id_includes_type(void) {
275    WHEN("an event ID is generated");
276    THEN("it starts with the type prefix");
277  
278    String event_id = cloudevents_event_id("status.v1", 0);
279    TEST_ASSERT_TRUE_MESSAGE(event_id.startsWith("status.v1-"),
280        "device: event ID should start with type name");
281  
282    TEST_MESSAGE(event_id.c_str());
283  }
284  
285  void services::cloudevents::test(void) {
286    MODULE("CloudEvents");
287    RUN_TEST(test_cloudevents_source_format);
288    RUN_TEST(test_cloudevents_event_id_includes_type);
289  }
290  
291  #endif