Complete production‑grade firmware example with auto‑reconnect, heartbeat state reporting, and delivery‑confirmed command processing.
/*
* ================================================================
* NodeMCU Smart Bulb Controller - 4 Devices
* ================================================================
*
* ESP8266 / NodeMCU
*
* Features:
* - 4 independently controlled relay outputs
* - HTTPS communication with ODIVORA Smart Dashboard API
* - Persistent relay state using LittleFS
* - Device heartbeat
* - Remote ON / OFF / TOGGLE commands
* - State confirmation
* - Device reconciliation after boot
* - Automatic WiFi reconnection
* - HTTP/JSON error handling
* - Request timeouts
*
* ODIVORA
* Smart Dashboard Device Controller
* ================================================================
*/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
// ================================================================
// WIFI CONFIGURATION
// ================================================================
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
// ================================================================
// SERVER CONFIGURATION
// ================================================================
const char* SERVER_URL = "https://odivora.com";
const char* WIFI_BOARD_ID = "ESP8266";
const char* API_KEY ="YOUR_API_KEY";
const char* API_SECRET ="YOUR_API_SECRET";
// ================================================================
// DEVICE CONFIGURATION
// ================================================================
#define DEVICE_COUNT 4
const char* DEVICE_IDS[DEVICE_COUNT] = {
"SD-A42EACCBB767",
"SD-64316F62789F",
"SD-5D22DC343C9B",
"SD-E2C60A2E5EDD"
};
// Relay pins
const int RELAY_PINS[DEVICE_COUNT] = {
D1,
D2,
D5,
D6
};
// Set to true if your relay module activates when pin is LOW.
const bool RELAY_ACTIVE_LOW = false;
// ================================================================
// ONBOARD LED CONFIGURATION
// ================================================================
// NodeMCU onboard LED (D4/GPIO2) - active LOW (ON when pin is LOW)
const int LED_PIN = LED_BUILTIN;
const bool LED_ACTIVE_LOW = true;
// Blink timing
const unsigned long BLINK_ON_MS = 100; // LED on duration
const unsigned long BLINK_OFF_MS = 100; // LED off duration
const int BLINK_COUNT = 3; // Number of blinks per command
// ================================================================
// TIMING
// ================================================================
const unsigned long HEARTBEAT_INTERVAL = 4000; // 4 seconds - well within 30s server timeout
const unsigned long COMMAND_INTERVAL = 750;
const unsigned long WIFI_RETRY_INTERVAL = 5000;
const unsigned long HEARTBEAT_RETRY_INTERVAL = 2000; // Retry failed heartbeats after 2s
const unsigned long HTTP_TIMEOUT = 6000;
// ================================================================
// PERSISTENT STATE
// ================================================================
const char* STATE_FILE = "/relay_state.json";
// ================================================================
// RUNTIME STATE
// ================================================================
bool bulbState[DEVICE_COUNT] = {
false,
false,
false,
false
};
String lastCommand[DEVICE_COUNT];
/*
* Per-device heartbeat timers plus a single round-robin cursor.
*
* Command checks are batched: ONE request returns pending commands for every
* relay, so a command is picked up on the next 750ms poll no matter how many
* devices the board drives. loop() runs, in priority order, the batch command
* poll then the round-robin device's heartbeat, spreading blocking HTTPS/TLS
* requests so one sluggish request cannot stall every device (the ESP8266
* performs one blocking request at a time).
*/
unsigned long lastHeartbeat[DEVICE_COUNT] = { 0, 0, 0, 0 };
unsigned long lastCmdCheck = 0;
unsigned long lastHeartbeatRetry = 0;
unsigned long lastWiFiRetry = 0;
int pollIndex = 0;
bool heartbeatFailed[DEVICE_COUNT] = { false, false, false, false };
// ================================================================
// HTTP CLIENTS
// ================================================================
WiFiClientSecure secureClient;
WiFiClient plainClient;
bool useTLS = false;
// ================================================================
// FORWARD DECLARATIONS
// ================================================================
void connectWiFi();
void reconnectWiFi();
void setRelay(int idx, bool on);
void blinkLED(int count);
void executeCommand(int idx, const String& cmd);
void heartbeatDevice(int i);
void retryHeartbeatDevice(int i);
void checkDeviceCommandsBatch();
void confirmState(int idx);
void reconcileAllDevices();
bool loadPersistentState();
bool savePersistentState();
bool beginHttp(HTTPClient& http, const String& path);
String urlEncode(const String& value);
void printWiFiStatus();
void printDeviceStates();
// ================================================================
// SETUP
// ================================================================
void setup() {
Serial.begin(115200);
delay(200);
Serial.println();
Serial.println();
Serial.println("================================================");
Serial.println(" ODIVORA NODEMCU SMART BULB CONTROLLER");
Serial.println("================================================");
// --------------------------------------------------------------
// Determine HTTP / HTTPS mode
// --------------------------------------------------------------
useTLS = String(SERVER_URL).startsWith("https://");
Serial.print("Server: ");
Serial.println(SERVER_URL);
if (useTLS) {
Serial.println("HTTPS mode enabled.");
/*
* This disables certificate verification.
*
* It is convenient for ESP8266 deployments because the device
* does not need a hard-coded certificate.
*
* For maximum security, replace this with setCACert().
*/
secureClient.setInsecure();
secureClient.setTimeout(HTTP_TIMEOUT / 1000);
} else {
Serial.println("HTTP mode enabled.");
plainClient.setTimeout(HTTP_TIMEOUT / 1000);
}
// --------------------------------------------------------------
// Initialize LittleFS
// --------------------------------------------------------------
Serial.println();
Serial.println("Initializing LittleFS...");
if (!LittleFS.begin()) {
Serial.println("ERROR: LittleFS initialization failed.");
/*
* Try formatting the filesystem.
*
* WARNING:
* This can erase existing LittleFS data.
*/
Serial.println("Attempting LittleFS format...");
if (!LittleFS.format()) {
Serial.println("ERROR: LittleFS format failed.");
} else {
if (LittleFS.begin()) {
Serial.println("LittleFS initialized after format.");
} else {
Serial.println("ERROR: LittleFS still unavailable.");
}
}
} else {
Serial.println("LittleFS OK.");
}
// --------------------------------------------------------------
// Load saved relay states
// --------------------------------------------------------------
Serial.println();
Serial.println("Loading persistent relay states...");
if (!loadPersistentState()) {
Serial.println(
"No valid saved state found. "
"Using all relays OFF."
);
for (int i = 0; i < DEVICE_COUNT; i++) {
bulbState[i] = false;
}
} else {
Serial.println("Saved relay states restored.");
}
// --------------------------------------------------------------
// Configure relay pins
// --------------------------------------------------------------
Serial.println();
Serial.println("Initializing relay outputs...");
for (int i = 0; i < DEVICE_COUNT; i++) {
pinMode(RELAY_PINS[i], OUTPUT);
// Restore saved state.
setRelay(i, bulbState[i]);
Serial.print("Device ");
Serial.print(i + 1);
Serial.print(" [");
Serial.print(DEVICE_IDS[i]);
Serial.print("] -> ");
Serial.println(
bulbState[i] ? "ON" : "OFF"
);
}
// --------------------------------------------------------------
// Configure onboard LED
// --------------------------------------------------------------
Serial.println();
Serial.println("Initializing onboard LED...");
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LED_ACTIVE_LOW ? HIGH : LOW); // LED off initially
Serial.println("Onboard LED ready.");
// --------------------------------------------------------------
// Connect WiFi
// --------------------------------------------------------------
Serial.println();
connectWiFi();
// --------------------------------------------------------------
// Synchronize local state with server
// --------------------------------------------------------------
if (WiFi.status() == WL_CONNECTED) {
Serial.println();
Serial.println("Reconciling devices with server...");
reconcileAllDevices();
} else {
Serial.println(
"WiFi unavailable. "
"Server reconciliation skipped."
);
}
Serial.println();
Serial.println("================================================");
Serial.println(" CONTROLLER READY");
Serial.println("================================================");
printDeviceStates();
}
// ================================================================
// MAIN LOOP
// ================================================================
void loop() {
// --------------------------------------------------------------
// WiFi monitoring
// --------------------------------------------------------------
if (WiFi.status() != WL_CONNECTED) {
reconnectWiFi();
/*
* Do not attempt API requests while offline.
*/
delay(10);
return;
}
unsigned long now = millis();
// --------------------------------------------------------------
// Round-robin cursor selects which device gets its heartbeat pass
// --------------------------------------------------------------
int i = pollIndex;
pollIndex = (pollIndex + 1) % DEVICE_COUNT;
// 1) Batch command poll has top priority: commands for EVERY relay are
// fetched in one request, so a heartbeat can never delay command pickup.
if (now - lastCmdCheck >= COMMAND_INTERVAL) {
lastCmdCheck = now;
checkDeviceCommandsBatch();
}
// 2) Heartbeat for the same device when its interval has elapsed. The
// timestamp captured before the command poll keeps a stable cadence even
// when that device just processed a command.
if (now - lastHeartbeat[i] >= HEARTBEAT_INTERVAL) {
lastHeartbeat[i] = now;
heartbeatDevice(i);
}
// 3) Retry a failed heartbeat, at most one device per pass.
if (heartbeatFailed[i] &&
now - lastHeartbeatRetry >= HEARTBEAT_RETRY_INTERVAL) {
lastHeartbeatRetry = now;
retryHeartbeatDevice(i);
}
yield();
}
// ================================================================
// HTTP BEGIN
// ================================================================
bool beginHttp(HTTPClient& http, const String& path) {
String url = String(SERVER_URL) + path;
Serial.print("HTTP URL: ");
Serial.println(url);
http.setTimeout(HTTP_TIMEOUT);
bool result = false;
if (useTLS) {
result = http.begin(
secureClient,
url
);
} else {
result = http.begin(
plainClient,
url
);
}
if (!result) {
Serial.println(
"ERROR: HTTPClient.begin() failed."
);
return false;
}
return true;
}
// ================================================================
// HEARTBEAT
// ================================================================
void heartbeatDevice(int i) {
if (WiFi.status() != WL_CONNECTED) {
return;
}
HTTPClient http;
/*
* Construct state JSON.
*
* Example:
* {"power":true,"timestamp":123456}
*/
DynamicJsonDocument stateDoc(256);
stateDoc["power"] = bulbState[i];
stateDoc["timestamp"] = millis();
String stateJson;
serializeJson(
stateDoc,
stateJson
);
String path =
"/api/v1/smart_dashboard/device/" +
String(DEVICE_IDS[i]) +
"/heartbeat?status=online" +
"&wifi_board_id=" +
urlEncode(WIFI_BOARD_ID) +
"&initial_state=" +
urlEncode(stateJson);
if (!beginHttp(http, path)) {
http.end();
heartbeatFailed[i] = true;
return;
}
/*
* Include authentication.
*/
http.addHeader(
"X-API-Key",
API_KEY
);
http.addHeader(
"X-API-Secret",
API_SECRET
);
http.addHeader(
"Accept",
"application/json"
);
int code = http.GET();
Serial.print("Heartbeat ");
Serial.print(i + 1);
Serial.print(" -> HTTP ");
Serial.println(code);
if (code < 0) {
Serial.print(
"Heartbeat error: "
);
Serial.println(
http.errorToString(code)
);
heartbeatFailed[i] = true;
} else if (code >= 200 && code < 300) {
Serial.println(
"Heartbeat accepted."
);
heartbeatFailed[i] = false;
} else {
Serial.print(
"Heartbeat rejected. Response: "
);
Serial.println(
http.getString()
);
heartbeatFailed[i] = true;
}
http.end();
}
// ================================================================
// HEARTBEAT RETRY
// ================================================================
void retryHeartbeatDevice(int i) {
if (!heartbeatFailed[i]) {
return;
}
if (WiFi.status() != WL_CONNECTED) {
return;
}
HTTPClient http;
DynamicJsonDocument stateDoc(256);
stateDoc["power"] = bulbState[i];
stateDoc["timestamp"] = millis();
String stateJson;
serializeJson(
stateDoc,
stateJson
);
String path =
"/api/v1/smart_dashboard/device/" +
String(DEVICE_IDS[i]) +
"/heartbeat?status=online" +
"&wifi_board_id=" +
urlEncode(WIFI_BOARD_ID) +
"&initial_state=" +
urlEncode(stateJson);
if (!beginHttp(http, path)) {
http.end();
return;
}
http.addHeader(
"X-API-Key",
API_KEY
);
http.addHeader(
"X-API-Secret",
API_SECRET
);
http.addHeader(
"Accept",
"application/json"
);
int code = http.GET();
Serial.print("Retry heartbeat ");
Serial.print(i + 1);
Serial.print(" -> HTTP ");
Serial.println(code);
if (code >= 200 && code < 300) {
Serial.println("Retry heartbeat accepted.");
heartbeatFailed[i] = false;
}
http.end();
}
// ================================================================
// COMMAND CHECK
// ================================================================
void checkDeviceCommandsBatch() {
if (WiFi.status() != WL_CONNECTED) {
return;
}
HTTPClient http;
// One request covers every relay, so command pickup is bounded by a single
// recurring 750ms poll instead of the old round-robin (up to 4 x 1.5s).
String deviceIds = String(DEVICE_IDS[0]);
for (int k = 1; k < DEVICE_COUNT; k++) {
deviceIds += ",";
deviceIds += DEVICE_IDS[k];
}
String path =
"/api/v1/smart_dashboard/devices/status?wifi_board_id=" +
urlEncode(WIFI_BOARD_ID) +
"&device_ids=" +
urlEncode(deviceIds);
if (!beginHttp(http, path)) {
http.end();
return;
}
http.addHeader(
"X-API-Key",
API_KEY
);
http.addHeader(
"X-API-Secret",
API_SECRET
);
http.addHeader(
"Accept",
"application/json"
);
int code = http.GET();
Serial.print("Batch status -> HTTP ");
Serial.println(code);
if (code == HTTP_CODE_OK) {
String response = http.getString();
Serial.print("Response: ");
Serial.println(response);
DynamicJsonDocument doc(2048);
DeserializationError error =
deserializeJson(
doc,
response
);
if (error) {
Serial.print(
"JSON parse error: "
);
Serial.println(
error.c_str()
);
} else {
JsonArray devices =
doc["devices"].as<JsonArray>();
if (!devices.isNull()) {
for (JsonObject entry : devices) {
// Map each returned entry to its relay index by device id; the
// server may order the entries arbitrarily.
String deviceId =
entry["device_id"].as<String>();
int idx = -1;
for (int k = 0; k < DEVICE_COUNT; k++) {
if (deviceId == DEVICE_IDS[k]) {
idx = k;
break;
}
}
if (idx < 0) {
continue;
}
if (entry.containsKey("command") &&
!entry["command"].isNull()) {
String cmd =
entry["command"].as<String>();
cmd.trim();
cmd.toLowerCase();
Serial.print("Device ");
Serial.print(idx + 1);
Serial.print(" Command: ");
Serial.println(cmd);
/*
* Only execute a command if it is different
* from the previously processed command.
*/
if (cmd.length() > 0 &&
cmd != lastCommand[idx]) {
Serial.print(
"New command detected for device "
);
Serial.println(idx + 1);
lastCommand[idx] = cmd;
executeCommand(
idx,
cmd
);
} else if (cmd.length() > 0 &&
cmd == lastCommand[idx]) {
/*
* Duplicate command - already executed once. Re-confirm the
* current state so the server can mark the pending command
* complete instead of leaving it queued for the full command
* timeout (which made commands appear as never executed).
*/
Serial.println(
"Duplicate command - re-confirming state"
);
confirmState(idx);
}
}
}
}
}
} else if (code < 0) {
Serial.print(
"Status request error: "
);
Serial.println(
http.errorToString(code)
);
} else {
Serial.print(
"Status request rejected: "
);
Serial.println(
http.getString()
);
}
http.end();
}
// ================================================================
// EXECUTE COMMAND
// ================================================================
void executeCommand(
int idx,
const String& cmd
) {
if (idx < 0 || idx >= DEVICE_COUNT) {
return;
}
Serial.println();
Serial.print("Executing command for device ");
Serial.print(idx + 1);
Serial.print(": ");
Serial.println(cmd);
bool newState;
// --------------------------------------------------------------
// ON
// --------------------------------------------------------------
if (cmd == "on") {
newState = true;
}
// --------------------------------------------------------------
// OFF
// --------------------------------------------------------------
else if (cmd == "off") {
newState = false;
}
// --------------------------------------------------------------
// TOGGLE
// --------------------------------------------------------------
else if (cmd == "toggle") {
newState = !bulbState[idx];
}
// --------------------------------------------------------------
// Unknown command
// --------------------------------------------------------------
else {
Serial.print(
"Unknown command: "
);
Serial.println(cmd);
return;
}
// --------------------------------------------------------------
// Apply state
// --------------------------------------------------------------
bulbState[idx] = newState;
setRelay(
idx,
newState
);
// --------------------------------------------------------------
// Blink LED to indicate command execution
// --------------------------------------------------------------
blinkLED(BLINK_COUNT);
// --------------------------------------------------------------
// Persist state
// --------------------------------------------------------------
if (!savePersistentState()) {
Serial.println(
"WARNING: Failed to save relay state."
);
}
// --------------------------------------------------------------
// Confirm with server
// --------------------------------------------------------------
confirmState(idx);
Serial.print("Device ");
Serial.print(idx + 1);
Serial.print(" is now ");
Serial.println(
newState ? "ON" : "OFF"
);
}
// ================================================================
// SET RELAY
// ================================================================
void setRelay(
int idx,
bool on
) {
if (idx < 0 || idx >= DEVICE_COUNT) {
return;
}
int outputState;
if (RELAY_ACTIVE_LOW) {
outputState =
on ? LOW : HIGH;
} else {
outputState =
on ? HIGH : LOW;
}
digitalWrite(
RELAY_PINS[idx],
outputState
);
}
// ================================================================
// BLINK LED
// ================================================================
void blinkLED(int count) {
for (int i = 0; i < count; i++) {
// Turn LED ON
digitalWrite(LED_PIN, LED_ACTIVE_LOW ? LOW : HIGH);
delay(BLINK_ON_MS);
// Turn LED OFF
digitalWrite(LED_PIN, LED_ACTIVE_LOW ? HIGH : LOW);
// Don't delay after the last blink
if (i < count - 1) {
delay(BLINK_OFF_MS);
}
}
}
// ================================================================
// CONFIRM STATE
// ================================================================
void confirmState(int idx) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println(
"Cannot confirm state: WiFi offline."
);
return;
}
HTTPClient http;
String path =
"/api/v1/smart_dashboard/device/" +
String(DEVICE_IDS[idx]) +
"/confirm_state";
if (!beginHttp(http, path)) {
http.end();
return;
}
DynamicJsonDocument doc(256);
doc["power"] = bulbState[idx];
doc["wifi_board_id"] = WIFI_BOARD_ID;
String payload;
serializeJson(
doc,
payload
);
http.addHeader(
"Content-Type",
"application/json"
);
http.addHeader(
"Accept",
"application/json"
);
http.addHeader(
"X-API-Key",
API_KEY
);
http.addHeader(
"X-API-Secret",
API_SECRET
);
Serial.print(
"Confirming device "
);
Serial.print(idx + 1);
Serial.print(": ");
Serial.println(payload);
int code =
http.POST(payload);
Serial.print(
"Confirm response: HTTP "
);
Serial.println(code);
if (code < 0) {
Serial.print(
"Confirm error: "
);
Serial.println(
http.errorToString(code)
);
} else if (code >= 200 && code < 300) {
Serial.println(
"State confirmed successfully."
);
} else {
Serial.print(
"State confirmation failed: "
);
Serial.println(
http.getString()
);
}
http.end();
}
// ================================================================
// RECONCILE DEVICES
// ================================================================
void reconcileAllDevices() {
for (int i = 0; i < DEVICE_COUNT; i++) {
if (WiFi.status() != WL_CONNECTED) {
return;
}
HTTPClient http;
String path =
"/api/v1/smart_dashboard/device/" +
String(DEVICE_IDS[i]) +
"/reconcile";
if (!beginHttp(http, path)) {
http.end();
continue;
}
DynamicJsonDocument doc(256);
doc["power"] =
bulbState[i];
doc["wifi_board_id"] =
WIFI_BOARD_ID;
String payload;
serializeJson(
doc,
payload
);
http.addHeader(
"Content-Type",
"application/json"
);
http.addHeader(
"Accept",
"application/json"
);
http.addHeader(
"X-API-Key",
API_KEY
);
http.addHeader(
"X-API-Secret",
API_SECRET
);
Serial.print(
"Reconciling device "
);
Serial.print(i + 1);
Serial.print(": ");
Serial.println(payload);
int code =
http.POST(payload);
Serial.print(
"Reconcile response: HTTP "
);
Serial.println(code);
if (code < 0) {
Serial.print(
"Reconcile error: "
);
Serial.println(
http.errorToString(code)
);
} else if (code >= 200 && code < 300) {
Serial.println(
"Device reconciled successfully."
);
} else {
Serial.print(
"Reconcile rejected: "
);
Serial.println(
http.getString()
);
}
http.end();
delay(100);
yield();
}
}
// ================================================================
// SAVE PERSISTENT STATE
// ================================================================
bool savePersistentState() {
if (!LittleFS.begin()) {
Serial.println(
"ERROR: LittleFS unavailable."
);
return false;
}
DynamicJsonDocument doc(512);
JsonArray states =
doc.createNestedArray("states");
for (int i = 0; i < DEVICE_COUNT; i++) {
states.add(
bulbState[i]
);
}
File file =
LittleFS.open(
STATE_FILE,
"w"
);
if (!file) {
Serial.println(
"ERROR: Could not open state file for writing."
);
return false;
}
size_t bytes =
serializeJson(
doc,
file
);
file.close();
if (bytes == 0) {
Serial.println(
"ERROR: Failed to write state file."
);
return false;
}
Serial.println(
"Relay state saved to LittleFS."
);
return true;
}
// ================================================================
// LOAD PERSISTENT STATE
// ================================================================
bool loadPersistentState() {
if (!LittleFS.begin()) {
Serial.println(
"ERROR: LittleFS unavailable."
);
return false;
}
if (!LittleFS.exists(STATE_FILE)) {
Serial.println(
"State file does not exist yet."
);
return false;
}
File file =
LittleFS.open(
STATE_FILE,
"r"
);
if (!file) {
Serial.println(
"ERROR: Could not open state file."
);
return false;
}
DynamicJsonDocument doc(512);
DeserializationError error =
deserializeJson(
doc,
file
);
file.close();
if (error) {
Serial.print(
"ERROR: State JSON parse failed: "
);
Serial.println(
error.c_str()
);
return false;
}
if (!doc.containsKey("states")) {
Serial.println(
"ERROR: State file has no states array."
);
return false;
}
JsonArray states =
doc["states"].as<JsonArray>();
if (states.size() != DEVICE_COUNT) {
Serial.println(
"ERROR: Invalid number of saved states."
);
return false;
}
for (int i = 0; i < DEVICE_COUNT; i++) {
bulbState[i] =
states[i].as<bool>();
}
return true;
}
// ================================================================
// WIFI CONNECTION
// ================================================================
void connectWiFi() {
Serial.println(
"Connecting to WiFi..."
);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
WiFi.begin(
WIFI_SSID,
WIFI_PASSWORD
);
unsigned long start =
millis();
while (
WiFi.status() != WL_CONNECTED &&
millis() - start < 30000
) {
delay(500);
Serial.print(".");
yield();
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println(
"WiFi connected."
);
printWiFiStatus();
} else {
Serial.println(
"ERROR: WiFi connection timed out."
);
}
}
// ================================================================
// WIFI RECONNECTION
// ================================================================
void reconnectWiFi() {
if (
millis() - lastWiFiRetry <
WIFI_RETRY_INTERVAL
) {
return;
}
lastWiFiRetry =
millis();
Serial.println();
Serial.println(
"WiFi disconnected. Reconnecting..."
);
WiFi.disconnect();
delay(100);
WiFi.begin(
WIFI_SSID,
WIFI_PASSWORD
);
}
// ================================================================
// WIFI STATUS
// ================================================================
void printWiFiStatus() {
Serial.print(
"SSID: "
);
Serial.println(
WiFi.SSID()
);
Serial.print(
"IP: "
);
Serial.println(
WiFi.localIP()
);
Serial.print(
"Signal: "
);
Serial.print(
WiFi.RSSI()
);
Serial.println(
" dBm"
);
Serial.print(
"MAC: "
);
Serial.println(
WiFi.macAddress()
);
}
// ================================================================
// DEVICE STATUS
// ================================================================
void printDeviceStates() {
Serial.println();
Serial.println(
"Current relay states:"
);
for (int i = 0; i < DEVICE_COUNT; i++) {
Serial.print(
"Device "
);
Serial.print(
i + 1
);
Serial.print(
" ["
);
Serial.print(
DEVICE_IDS[i]
);
Serial.print(
"] = "
);
Serial.println(
bulbState[i]
? "ON"
: "OFF"
);
}
}
// ================================================================
// URL ENCODING
// ================================================================
String urlEncode(
const String& value
) {
String encoded;
const char* hex =
"0123456789ABCDEF";
for (size_t i = 0; i < value.length(); i++) {
unsigned char c =
static_cast<unsigned char>(
value[i]
);
/*
* RFC3986 unreserved characters:
*
* A-Z
* a-z
* 0-9
* -
* .
* _
* ~
*/
if (
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '.' ||
c == '_' ||
c == '~'
) {
encoded +=
static_cast<char>(c);
} else {
encoded += '%';
encoded +=
hex[(c >> 4) & 0x0F];
encoded +=
hex[c & 0x0F];
}
}
return encoded;
}