Начальный каркас: прошивка ESP32 для зоны "Гроубокс"
Реализованы sensor-1 (DHT22, телеметрия temperature/humidity) и
реле-актуаторы light-1/pump-1/fan-1 (turn_on/turn_off/set_level) по
протоколу MQTT платформы home_automatization (devices/{id}/telemetry|commands|ack).
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
config.h
|
||||||
|
build/
|
||||||
|
*.bin
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# growbox (ESP32-контроллер)
|
||||||
|
|
||||||
|
Прошивка для реального ESP32, обслуживающая зону "Гроубокс" платформы
|
||||||
|
[home_automatization](../../home_automatization). Одна плата владеет четырьмя
|
||||||
|
устройствами: `sensor-1` (DHT22) и три реле-актуатора — `light-1`, `pump-1`,
|
||||||
|
`fan-1`.
|
||||||
|
|
||||||
|
## Протокол (MQTT, mosquitto из docker-compose платформы, порт 1883)
|
||||||
|
|
||||||
|
- `devices/{device_id}/telemetry` — публикует контроллер, JSON
|
||||||
|
`{"device_id","zone_id","sensor_type","value","timestamp"}`, timestamp —
|
||||||
|
RFC3339 UTC. `sensor-1` шлёт по одному сообщению на `temperature` и
|
||||||
|
`humidity` раз в `TELEMETRY_INTERVAL_MS`.
|
||||||
|
- `devices/{device_id}/commands` — подписывается контроллер (wildcard
|
||||||
|
`devices/+/commands`, фильтрует по своим 3 актуаторам), JSON
|
||||||
|
`{"action":"turn_on"|"turn_off"|"set_level","level"?}`.
|
||||||
|
- `devices/{device_id}/ack` — публикует контроллер после применения команды,
|
||||||
|
JSON `{"state":{"power":"on"|"off","level"?}}`.
|
||||||
|
|
||||||
|
Реального диммирования нет (обычные реле), поэтому `set_level` трактуется как
|
||||||
|
порог: `level > 0` включает реле, `level <= 0` выключает.
|
||||||
|
|
||||||
|
device_id в конфиге должны совпадать с `devices.external_id`, засеянными в
|
||||||
|
Laravel (`DemoGrowboxSeeder` использует `sensor-1`/`fan-1`), а `zone_id` — с
|
||||||
|
`zones.id` зоны "Гроубокс" в Postgres.
|
||||||
|
|
||||||
|
## Железо
|
||||||
|
|
||||||
|
| Назначение | Пин (по умолчанию) |
|
||||||
|
|-----------------|---------------------|
|
||||||
|
| DHT22 data | GPIO4 |
|
||||||
|
| Реле — light-1 | GPIO16 |
|
||||||
|
| Реле — pump-1 | GPIO17 |
|
||||||
|
| Реле — fan-1 | GPIO18 |
|
||||||
|
|
||||||
|
Реле-модули по умолчанию считаются активными по LOW (`RELAY_ACTIVE_LOW` в
|
||||||
|
`config.h`) — типично для дешёвых китайских модулей на оптопаре. Если ваши
|
||||||
|
реле активны по HIGH — поменяйте флаг.
|
||||||
|
|
||||||
|
DHT22 требует подтягивающий резистор ~10кОм между data и VCC, если он не
|
||||||
|
встроен в конкретный модуль.
|
||||||
|
|
||||||
|
## Библиотеки (Arduino Library Manager)
|
||||||
|
|
||||||
|
- `PubSubClient` (Nick O'Leary) — MQTT-клиент
|
||||||
|
- `ArduinoJson` (Benoit Blanchon, v7) — сериализация/парсинг JSON
|
||||||
|
- `DHT sensor library` (Adafruit) + зависимость `Adafruit Unified Sensor`
|
||||||
|
- Board support: `esp32` (Espressif Systems) через Boards Manager
|
||||||
|
|
||||||
|
## Настройка и прошивка
|
||||||
|
|
||||||
|
1. `cp config.h.example config.h`, заполнить Wi-Fi SSID/пароль, IP хоста, на
|
||||||
|
котором поднят `docker compose` платформы (mosquitto слушает `0.0.0.0:1883`
|
||||||
|
— доступен по IP машины в локальной сети), `zone_id` и `device_id`.
|
||||||
|
2. Открыть `growbox.ino` в Arduino IDE, выбрать плату (ESP32 Dev Module или
|
||||||
|
конкретную модель), порт.
|
||||||
|
3. Прошить. Открыть Serial Monitor на 115200 бод — там лог подключения к
|
||||||
|
Wi-Fi/NTP/MQTT и каждой публикации телеметрии/команды.
|
||||||
|
|
||||||
|
## Проверка end-to-end
|
||||||
|
|
||||||
|
При работающем `docker compose` в `home_automatization` и засеянных
|
||||||
|
Postgres-данных (зона + `sensor-1`/`light-1`/`pump-1`/`fan-1`):
|
||||||
|
|
||||||
|
- телеметрия должна доходить до ClickHouse через ingest-service;
|
||||||
|
- команда через Laravel UI (или напрямую через `grpcurl` в
|
||||||
|
device-control-service) должна прийти на плату и вызвать щелчок реле, а
|
||||||
|
затем ack — обновить `reported_state`/`last_seen` в Redis device shadow.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Скопировать в config.h и заполнить своими значениями.
|
||||||
|
// config.h в .gitignore — секреты (Wi-Fi, адрес брокера) в репозиторий не попадают.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// --- Wi-Fi ---
|
||||||
|
#define WIFI_SSID "your-wifi-ssid"
|
||||||
|
#define WIFI_PASSWORD "your-wifi-password"
|
||||||
|
|
||||||
|
// --- MQTT (mosquitto из home_automatization/docker-compose.yml, порт 1883, без auth в dev) ---
|
||||||
|
#define MQTT_BROKER_HOST "192.168.1.100"
|
||||||
|
#define MQTT_BROKER_PORT 1883
|
||||||
|
#define MQTT_CLIENT_ID "growbox-esp32"
|
||||||
|
|
||||||
|
// --- Зона и устройства ---
|
||||||
|
// zone_id — id зоны "Гроубокс" в Postgres (laravel-app, таблица zones).
|
||||||
|
// device_id должны совпадать с devices.external_id, засеянными в Laravel.
|
||||||
|
#define ZONE_ID "1"
|
||||||
|
#define DEVICE_ID_SENSOR "sensor-1"
|
||||||
|
#define DEVICE_ID_LIGHT "light-1"
|
||||||
|
#define DEVICE_ID_PUMP "pump-1"
|
||||||
|
#define DEVICE_ID_FAN "fan-1"
|
||||||
|
|
||||||
|
// --- Пины (ESP32 DevKitC) ---
|
||||||
|
#define PIN_DHT22 4
|
||||||
|
#define PIN_RELAY_LIGHT 16
|
||||||
|
#define PIN_RELAY_PUMP 17
|
||||||
|
#define PIN_RELAY_FAN 18
|
||||||
|
|
||||||
|
// Большинство дешёвых реле-модулей активны по LOW (сигнал LOW = реле включено).
|
||||||
|
#define RELAY_ACTIVE_LOW true
|
||||||
|
|
||||||
|
// --- Телеметрия ---
|
||||||
|
#define TELEMETRY_INTERVAL_MS 30000UL
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include "devices.h"
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
struct Actuator {
|
||||||
|
const char* deviceId;
|
||||||
|
uint8_t pin;
|
||||||
|
bool on;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Actuator actuators[] = {
|
||||||
|
{DEVICE_ID_LIGHT, PIN_RELAY_LIGHT, false},
|
||||||
|
{DEVICE_ID_PUMP, PIN_RELAY_PUMP, false},
|
||||||
|
{DEVICE_ID_FAN, PIN_RELAY_FAN, false},
|
||||||
|
};
|
||||||
|
static const size_t actuatorCount = sizeof(actuators) / sizeof(actuators[0]);
|
||||||
|
|
||||||
|
static void relayWrite(uint8_t pin, bool on) {
|
||||||
|
bool level = RELAY_ACTIVE_LOW ? !on : on;
|
||||||
|
digitalWrite(pin, level ? HIGH : LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
void devicesBegin() {
|
||||||
|
for (size_t i = 0; i < actuatorCount; i++) {
|
||||||
|
pinMode(actuators[i].pin, OUTPUT);
|
||||||
|
relayWrite(actuators[i].pin, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Actuator* findActuator(const String& deviceId) {
|
||||||
|
for (size_t i = 0; i < actuatorCount; i++) {
|
||||||
|
if (deviceId == actuators[i].deviceId) {
|
||||||
|
return &actuators[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Реального диммирования у нас нет (обычные реле on/off), поэтому
|
||||||
|
// set_level трактуется как порог: level > 0 включает реле, level <= 0
|
||||||
|
// выключает. Ack всегда содержит и power, и level, чтобы отражать
|
||||||
|
// реальное состояние железа.
|
||||||
|
bool devicesApplyCommand(const String& deviceId, const String& action, double level, JsonDocument& outState) {
|
||||||
|
Actuator* actuator = findActuator(deviceId);
|
||||||
|
if (actuator == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == "turn_on") {
|
||||||
|
actuator->on = true;
|
||||||
|
} else if (action == "turn_off") {
|
||||||
|
actuator->on = false;
|
||||||
|
} else if (action == "set_level") {
|
||||||
|
actuator->on = level > 0;
|
||||||
|
outState["level"] = level;
|
||||||
|
} else {
|
||||||
|
Serial.print("[devices] unknown action for ");
|
||||||
|
Serial.print(deviceId);
|
||||||
|
Serial.print(": ");
|
||||||
|
Serial.println(action);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
relayWrite(actuator->pin, actuator->on);
|
||||||
|
outState["power"] = actuator->on ? "on" : "off";
|
||||||
|
|
||||||
|
Serial.print("[devices] ");
|
||||||
|
Serial.print(deviceId);
|
||||||
|
Serial.print(" -> ");
|
||||||
|
Serial.println(actuator->on ? "on" : "off");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
// Инициализирует пины реле (light/pump/fan), выключает всё при старте.
|
||||||
|
void devicesBegin();
|
||||||
|
|
||||||
|
// Применяет команду {"action": "turn_on"|"turn_off"|"set_level", "level"?}
|
||||||
|
// к устройству deviceId, если это одно из известных реле-актуаторов этой
|
||||||
|
// платы (light-1/pump-1/fan-1 из config.h). Заполняет outState фактическим
|
||||||
|
// применённым состоянием для последующей публикации в devices/{id}/ack.
|
||||||
|
// Возвращает false, если deviceId этой плате не принадлежит.
|
||||||
|
bool devicesApplyCommand(const String& deviceId, const String& action, double level, JsonDocument& outState);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Контроллер зоны "Гроубокс" для платформы home_automatization.
|
||||||
|
// Одна плата ESP32 владеет четырьмя устройствами зоны:
|
||||||
|
// sensor-1 (DHT22, телеметрия temperature/humidity)
|
||||||
|
// light-1, pump-1, fan-1 (реле, turn_on/turn_off/set_level)
|
||||||
|
// Протокол общения с device-control-service/ingest-service описан в README.md.
|
||||||
|
#include "config.h"
|
||||||
|
#include "devices.h"
|
||||||
|
#include "mqtt_link.h"
|
||||||
|
#include "net.h"
|
||||||
|
#include "telemetry.h"
|
||||||
|
#include "time_sync.h"
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
devicesBegin();
|
||||||
|
netBegin();
|
||||||
|
timeSyncBegin();
|
||||||
|
mqttBegin();
|
||||||
|
telemetryBegin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
netEnsureConnected();
|
||||||
|
mqttEnsureConnected();
|
||||||
|
mqttLoop();
|
||||||
|
telemetryTick();
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#include "mqtt_link.h"
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "devices.h"
|
||||||
|
#include "time_sync.h"
|
||||||
|
|
||||||
|
static WiFiClient wifiClient;
|
||||||
|
static PubSubClient mqttClient(wifiClient);
|
||||||
|
|
||||||
|
// devices/{device_id}/commands -> device_id, или "" если топик не подходит.
|
||||||
|
static String deviceIdFromCommandTopic(const String& topic) {
|
||||||
|
if (!topic.startsWith("devices/") || !topic.endsWith("/commands")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return topic.substring(8, topic.length() - 9); // strip "devices/" and "/commands"
|
||||||
|
}
|
||||||
|
|
||||||
|
static void publishAck(const String& deviceId, JsonDocument& state) {
|
||||||
|
JsonDocument ackDoc;
|
||||||
|
ackDoc["state"] = state.as<JsonObject>();
|
||||||
|
|
||||||
|
char buf[256];
|
||||||
|
size_t len = serializeJson(ackDoc, buf, sizeof(buf));
|
||||||
|
|
||||||
|
String topic = "devices/" + deviceId + "/ack";
|
||||||
|
mqttClient.publish(topic.c_str(), (const uint8_t*)buf, len, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onMessage(char* topic, byte* payload, unsigned int length) {
|
||||||
|
String deviceId = deviceIdFromCommandTopic(String(topic));
|
||||||
|
if (deviceId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument cmd;
|
||||||
|
DeserializationError err = deserializeJson(cmd, payload, length);
|
||||||
|
if (err) {
|
||||||
|
Serial.print("[mqtt] invalid command payload on ");
|
||||||
|
Serial.println(topic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String action = cmd["action"] | "";
|
||||||
|
double level = cmd["level"] | 0.0;
|
||||||
|
if (action.isEmpty()) {
|
||||||
|
Serial.print("[mqtt] command missing action on ");
|
||||||
|
Serial.println(topic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument state;
|
||||||
|
if (devicesApplyCommand(deviceId, action, level, state)) {
|
||||||
|
publishAck(deviceId, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqttBegin() {
|
||||||
|
mqttClient.setServer(MQTT_BROKER_HOST, MQTT_BROKER_PORT);
|
||||||
|
mqttClient.setCallback(onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqttEnsureConnected() {
|
||||||
|
if (mqttClient.connected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.print("[mqtt] connecting to ");
|
||||||
|
Serial.print(MQTT_BROKER_HOST);
|
||||||
|
if (mqttClient.connect(MQTT_CLIENT_ID)) {
|
||||||
|
Serial.println(" connected");
|
||||||
|
mqttClient.subscribe("devices/+/commands", 1);
|
||||||
|
} else {
|
||||||
|
Serial.print(" failed, rc=");
|
||||||
|
Serial.println(mqttClient.state());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqttLoop() {
|
||||||
|
mqttClient.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void mqttPublishTelemetry(const String& deviceId, const String& sensorType, double value) {
|
||||||
|
JsonDocument doc;
|
||||||
|
doc["device_id"] = deviceId;
|
||||||
|
doc["zone_id"] = ZONE_ID;
|
||||||
|
doc["sensor_type"] = sensorType;
|
||||||
|
doc["value"] = value;
|
||||||
|
doc["timestamp"] = nowRFC3339();
|
||||||
|
|
||||||
|
char buf[256];
|
||||||
|
size_t len = serializeJson(doc, buf, sizeof(buf));
|
||||||
|
|
||||||
|
String topic = "devices/" + deviceId + "/telemetry";
|
||||||
|
mqttClient.publish(topic.c_str(), (const uint8_t*)buf, len, false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Подключение к mosquitto, подписка на devices/+/commands, диспетчеризация
|
||||||
|
// команд в devices.cpp и публикация ack. См. протокол в README.md.
|
||||||
|
void mqttBegin();
|
||||||
|
void mqttEnsureConnected();
|
||||||
|
void mqttLoop();
|
||||||
|
|
||||||
|
// Публикует одно показание телеметрии в devices/{deviceId}/telemetry.
|
||||||
|
void mqttPublishTelemetry(const String& deviceId, const String& sensorType, double value);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#include "net.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
void netBegin() {
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||||
|
|
||||||
|
Serial.print("[net] connecting to ");
|
||||||
|
Serial.print(WIFI_SSID);
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
}
|
||||||
|
Serial.print("\n[net] connected, ip=");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызывается из loop() на каждой итерации — переподключает Wi-Fi,
|
||||||
|
// если связь пропала (роутер перезагрузился, слабый сигнал и т.д.).
|
||||||
|
void netEnsureConnected() {
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Serial.println("[net] connection lost, reconnecting");
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||||
|
|
||||||
|
unsigned long start = millis();
|
||||||
|
while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
|
||||||
|
delay(250);
|
||||||
|
}
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
Serial.print("[net] reconnected, ip=");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Подключение к Wi-Fi и переподключение при обрыве.
|
||||||
|
void netBegin();
|
||||||
|
void netEnsureConnected();
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include "telemetry.h"
|
||||||
|
|
||||||
|
#include <DHT.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "mqtt_link.h"
|
||||||
|
#include "time_sync.h"
|
||||||
|
|
||||||
|
static DHT dht(PIN_DHT22, DHT22);
|
||||||
|
static unsigned long lastPublish = 0;
|
||||||
|
|
||||||
|
void telemetryBegin() {
|
||||||
|
dht.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void telemetryTick() {
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (now - lastPublish < TELEMETRY_INTERVAL_MS && lastPublish != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Без синхронизированного времени ingest-service отклонит timestamp
|
||||||
|
// (или он будет ложным), поэтому просто пропускаем цикл и ждём.
|
||||||
|
if (!timeSyncIsReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastPublish = now;
|
||||||
|
|
||||||
|
float humidity = dht.readHumidity();
|
||||||
|
float temperature = dht.readTemperature();
|
||||||
|
if (isnan(humidity) || isnan(temperature)) {
|
||||||
|
Serial.println("[telemetry] DHT22 read failed, skipping this cycle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mqttPublishTelemetry(DEVICE_ID_SENSOR, "temperature", temperature);
|
||||||
|
mqttPublishTelemetry(DEVICE_ID_SENSOR, "humidity", humidity);
|
||||||
|
|
||||||
|
Serial.print("[telemetry] temperature=");
|
||||||
|
Serial.print(temperature);
|
||||||
|
Serial.print(" humidity=");
|
||||||
|
Serial.println(humidity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Инициализация DHT22.
|
||||||
|
void telemetryBegin();
|
||||||
|
|
||||||
|
// Вызывать на каждой итерации loop(): сама решает, пора ли публиковать
|
||||||
|
// очередное показание (см. TELEMETRY_INTERVAL_MS в config.h).
|
||||||
|
void telemetryTick();
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#include "time_sync.h"
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static bool timeReady = false;
|
||||||
|
|
||||||
|
void timeSyncBegin() {
|
||||||
|
// UTC, без летнего времени — платформа хранит и парсит время в UTC.
|
||||||
|
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
||||||
|
|
||||||
|
Serial.print("[time] syncing via NTP");
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
unsigned long start = millis();
|
||||||
|
// 8 * 3600 — грубая нижняя граница правдоподобного unix-времени,
|
||||||
|
// пока NTP не ответит time() возвращает эпоху около 0.
|
||||||
|
while (now < 8 * 3600 && millis() - start < 15000) {
|
||||||
|
delay(250);
|
||||||
|
Serial.print(".");
|
||||||
|
now = time(nullptr);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
timeReady = now >= 8 * 3600;
|
||||||
|
if (timeReady) {
|
||||||
|
Serial.println("[time] synced");
|
||||||
|
} else {
|
||||||
|
Serial.println("[time] sync timed out, will retry in background");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool timeSyncIsReady() {
|
||||||
|
if (!timeReady) {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
timeReady = now >= 8 * 3600;
|
||||||
|
}
|
||||||
|
return timeReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
String nowRFC3339() {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm tmStruct;
|
||||||
|
gmtime_r(&now, &tmStruct);
|
||||||
|
|
||||||
|
char buf[21];
|
||||||
|
strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tmStruct);
|
||||||
|
return String(buf);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// ingest-service требует валидный RFC3339 timestamp в телеметрии, поэтому
|
||||||
|
// время синхронизируется по NTP один раз при старте (см. README).
|
||||||
|
void timeSyncBegin();
|
||||||
|
bool timeSyncIsReady();
|
||||||
|
|
||||||
|
// Текущее UTC время в формате RFC3339, например "2026-08-11T12:34:56Z".
|
||||||
|
String nowRFC3339();
|
||||||
Reference in New Issue
Block a user