System Commands
System commands are required in every CeresFlows firmware, regardless of what sensors or actuators the board supports. They allow the backend to discover, identify, and manage controllers.
Compatibility
| Command | UNO R4 WiFi | ESP32 | ESP8266 | Arduino Nano |
|---|---|---|---|---|
PING | ✅ | ✅ | ✅ | ✅ |
STATUS | ✅ | ✅ | ✅ | ✅ |
INFO | ✅ | ✅ | ✅ | ✅ |
RESET | ✅ | ✅ | ✅ | ✅ (watchdog) |
HYDROPONICS_DISCOVERY | ✅ | ✅ | ✅ | ❌ No WiFi |
PING
The simplest command. Used by the backend to verify the controller is alive and responsive.
Request:
{"cmd": "PING"}
Response:
{"ok": 1, "pong": 1}
Implementation:
if (strcmp(cmd, "PING") == 0) return "{\"ok\":1,\"pong\":1}";
STATUS
Returns a minimal alive confirmation with uptime.
Request:
{"cmd": "STATUS"}
Response:
{"ok": 1, "status": "running", "up": 14523}
up— milliseconds since last boot (millis())
Implementation:
if (strcmp(cmd, "STATUS") == 0) {
String r = "{\"ok\":1,\"status\":\"running\",\"up\":";
r += millis();
r += "}";
return r;
}
INFO
Returns full controller identification. The backend calls this once after a controller is registered to determine its capabilities.
Request:
{"cmd": "INFO"}
Response:
{
"ok": 1,
"up": 14523,
"mem": 18400,
"ver": "1.1-v55-r4",
"mac": "A4:CF:12:AB:CD:EF",
"ip": "192.168.1.105",
"capabilities": ["ANALOG", "DIGITAL_READ", "DHT_READ", "I2C_TRANSFER", "..."]
}
| Field | Type | Description |
|---|---|---|
up | integer | Uptime in milliseconds |
mem | integer | Free heap/RAM in bytes |
ver | string | Firmware version string |
mac | string | MAC address (colon-separated, uppercase hex) |
ip | string | Current IP address (WiFi boards only) |
capabilities | array of strings | All commands this firmware supports |
[!IMPORTANT] The
capabilitiesarray is how the CeresFlows backend knows which sensor and actuator types are compatible with this controller. Always declare exactly what your firmware implements — do not include commands you have not implemented.
Capabilities List
Only list commands your firmware actually handles. A command listed in capabilities but not implemented in the router will return ERR_UNKNOWN_CMD — the backend will show this as a malfunction.
const char* CAPABILITIES[] = {
"ANALOG", "DIGITAL_READ", "DIGITAL_WRITE", "RELAY_SET",
"PWM_WRITE", "SERVO_WRITE", "DHT_READ", "ONEWIRE_READ_TEMP",
"ULTRASONIC_TRIG_ECHO", "I2C_TRANSFER", "MODBUS_RTU",
"UART_TRANSFER", "PULSE_COUNT", "HX711_READ", "ONEWIRE_SCAN", "SPI_TRANSFER"
};
const int CAPABILITIES_COUNT = 16;
// ⚠️ ESP8266 example: remove commands you have not implemented.
// If you don't implement SPI_TRANSFER, do NOT list it here.
const char* CAPABILITIES[] = {
"ANALOG", "DIGITAL_READ", "DIGITAL_WRITE", "RELAY_SET",
"DHT_READ", "ONEWIRE_READ_TEMP", "I2C_TRANSFER", "UART_TRANSFER",
"PULSE_COUNT", "ULTRASONIC_TRIG_ECHO"
};
const int CAPABILITIES_COUNT = 10;
// ⚠️ Nano: only list what fits in 32 KB Flash and 2 KB RAM.
// Omit bus commands if RAM is insufficient after testing.
const char* CAPABILITIES[] = {
"ANALOG", "DIGITAL_READ", "DIGITAL_WRITE", "RELAY_SET",
"DHT_READ", "ONEWIRE_READ_TEMP", "ULTRASONIC_TRIG_ECHO"
};
const int CAPABILITIES_COUNT = 7;
Free Memory by Board
// ARM-specific: calculate free memory using stack/heap pointer distance
extern "C" char* sbrk(int incr);
int freeMemory() {
char top;
return &top - reinterpret_cast<char*>(sbrk(0));
}
int freeMemory() {
return (int)ESP.getFreeHeap();
}
int freeMemory() {
return (int)ESP.getFreeHeap();
}
// AVR-specific: calculate free memory between heap and stack
extern int __heap_start, *__brkval;
int freeMemory() {
int v;
return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);
}
MAC Address
#include <WiFiS3.h>
String getMacString() {
byte mac[6];
WiFi.macAddress(mac);
char buf[18];
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
#include <WiFi.h>
String getMacString() {
byte mac[6];
WiFi.macAddress(mac);
char buf[18];
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
#include <ESP8266WiFi.h>
String getMacString() {
byte mac[6];
WiFi.macAddress(mac);
char buf[18];
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
// ❌ Arduino Nano has no WiFi and no MAC address.
// In the INFO response, omit the "mac" and "ip" fields entirely:
// {"ok":1, "up":14523, "mem":512, "ver":"1.0-nano", "capabilities":[...]}
RESET
Forces an immediate software reset of the controller. The backend uses this after changing configuration or to recover from a suspected lock-up.
Request:
{"cmd": "RESET"}
Response: The firmware attempts to send a confirmation before resetting. Due to the reset, delivery is not guaranteed.
{"ok": 1, "msg": "Resetting..."}
[!WARNING] After RESET, all runtime state is lost. Any UART or Modbus configuration that was not persisted to EEPROM will be gone. EEPROM-persisted pin configuration will be restored on boot. See EEPROM Persistence.
Reset Implementation — Board-Specific
// ARM Cortex-M4: NVIC system reset register
if (strcmp(cmd, "RESET") == 0) {
Serial.println(F("{\"ok\":1,\"msg\":\"Resetting...\"}"));
delay(100); // Allow Serial to flush before reset
NVIC_SystemReset(); // ARM-specific — defined in core_cm4.h
return "";
}
// ESP32: use esp_restart() from esp_system.h
#include "esp_system.h"
if (strcmp(cmd, "RESET") == 0) {
Serial.println(F("{\"ok\":1,\"msg\":\"Resetting...\"}"));
delay(100);
esp_restart(); // ESP32-specific
return "";
}
// ESP8266: use ESP.restart()
if (strcmp(cmd, "RESET") == 0) {
Serial.println(F("{\"ok\":1,\"msg\":\"Resetting...\"}"));
delay(100);
ESP.restart(); // ESP8266-specific
return "";
}
// AVR: use Watchdog timer to force a reset
#include <avr/wdt.h>
if (strcmp(cmd, "RESET") == 0) {
Serial.println(F("{\"ok\":1,\"msg\":\"Resetting...\"}"));
delay(100);
wdt_enable(WDTO_15MS); // Enable watchdog with 15ms timeout
while (1) {} // Lock — watchdog fires and resets
return "";
}
[!NOTE] There is no single cross-platform software reset function in Arduino. Always use the correct function for your chip. Using the wrong reset function will either fail to compile or fail silently.
HYDROPONICS_DISCOVERY
Used for automatic network discovery. The backend sends a UDP broadcast to the network and expects all controllers to respond with their identification.
This command is only applicable to WiFi-capable boards. Arduino Nano cannot respond to UDP broadcasts.
Request (UDP Broadcast):
{"cmd": "HYDROPONICS_DISCOVERY"}
Response:
{
"type": "ANNOUNCE",
"mac": "A4:CF:12:AB:CD:EF",
"ip": "192.168.1.105",
"model": "Arduino_Uno_R4_WiFi",
"firmware": "1.1-v55-r4",
"capabilities": ["ANALOG", "DIGITAL_READ", "DHT_READ", "..."]
}
| Field | Description |
|---|---|
type | Always "ANNOUNCE" |
mac | MAC address — used as unique hardware identifier |
ip | Current IP — backend uses this to register the controller |
model | Board model string — must be consistent between reboots |
firmware | Firmware version string |
capabilities | Same list as in INFO |
[!IMPORTANT] The
modelfield must be a stable, consistent string. Do not generate it dynamically. Define it as a#defineconstant. The backend uses it as thetemplateIdto load the correct controller template (pin map, capabilities).
Model Name Constants
#define BOARD_MODEL "Arduino_Uno_R4_WiFi"
#define BOARD_MODEL "ESP32_WROOM_32"
#define BOARD_MODEL "ESP8266_NodeMCU"
[!WARNING] The
modelstring must exactly match theidof the controller template registered in the CeresFlows backend. A mismatch means the backend cannot load the correct port map for the controller, and sensor assignment will fail.
Discovery Response Builder
String buildDiscoveryResponse() {
String r = "{\"type\":\"ANNOUNCE\",\"mac\":\"";
r += getMacString();
r += "\",\"ip\":\"";
r += WiFi.localIP().toString(); // Replace with ESP equivalent if needed
r += "\",\"model\":\"" BOARD_MODEL "\"";
r += ",\"firmware\":\"" FIRMWARE_VERSION "\"";
r += ",\"capabilities\":";
r += buildCapabilitiesJson(); // Builds the ["CMD1","CMD2",...] array
r += "}";
return r;
}
[!NOTE]
WiFi.localIP().toString()works identically on UNO R4 WiFi, ESP32, and ESP8266 — all implement the sameIPAddress.toString()method.