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

CommandUNO R4 WiFiESP32ESP8266Arduino 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", "..."]
}
FieldTypeDescription
upintegerUptime in milliseconds
memintegerFree heap/RAM in bytes
verstringFirmware version string
macstringMAC address (colon-separated, uppercase hex)
ipstringCurrent IP address (WiFi boards only)
capabilitiesarray of stringsAll commands this firmware supports

[!IMPORTANT] The capabilities array 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", "..."]
}
FieldDescription
typeAlways "ANNOUNCE"
macMAC address — used as unique hardware identifier
ipCurrent IP — backend uses this to register the controller
modelBoard model string — must be consistent between reboots
firmwareFirmware version string
capabilitiesSame list as in INFO

[!IMPORTANT] The model field must be a stable, consistent string. Do not generate it dynamically. Define it as a #define constant. The backend uses it as the templateId to 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 model string must exactly match the id of 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 same IPAddress.toString() method.