GPIO Commands

GPIO commands are the foundation of hardware control. They read analog voltages, read digital states, and write digital output states. These commands work on every supported board, but with important differences in pin counts, ADC resolution, and voltage limits.

[!IMPORTANT] Read the Board Reference Matrix — Analog Input section before using ANALOG on ESP8266 or ESP32. The ADC range and maximum input voltage differ significantly from Arduino boards.


ANALOG

Reads the analog voltage on a pin and returns a raw ADC value.

Compatibility

BoardSupportedPinsADC RangeMax voltage
Arduino UNO R4 WiFi✅A0–A50–1023 (10-bit)5V
ESP32 WROOM-32✅GPIO34, 35, 36, 390–4095 (12-bit)3.3V
ESP8266 NodeMCU⚠️A0 only0–10231.0V max 🔴
Arduino Nano✅A0–A50–1023 (10-bit)5V

[!CAUTION] ESP8266: Maximum input voltage is 1.0V, not 3.3V or 5V. Exceeding 1.0V on the A0 pin will damage the ESP8266. If your sensor outputs 0–5V, you need a voltage divider (e.g., 3.2 kΩ + 1 kΩ) before the A0 pin.

[!WARNING] ESP32 ADC returns 0–4095, not 0–1023. If your calibration in CeresFlows is configured for a 10-bit range (0–1023), readings from an ESP32 will appear approximately 4× higher than expected. Configure your device template with the correct ADC range for your board.

Request:

{"cmd": "ANALOG", "id": 1, "args": ["A0"]}

Or pipe: ANALOG|A0

Response:

{"ok": 1, "pin": "A0", "value": 487, "id": 1}

Implementation:

String handleAnalog(const char* params) {
  if (!params || strlen(params) < 1)
    return F("{\"ok\":0,\"error\":\"ERR_MISSING_PARAMETER\"}");

  int pin = parsePin(String(params));
  if (pin == -1) return F("{\"ok\":0,\"error\":\"ERR_INVALID_PIN\"}");

  int value = analogRead(pin);   // Returns 0–1023 on 10-bit boards
  String r = "{\"ok\":1,\"pin\":\"";
  r += params; r += "\",\"value\":"; r += value; r += "}";
  return r;
}
// ⚠️ ESP32 analogRead() returns 0–4095 (12-bit ADC).
// Use only ADC1 pins (GPIO32–39) for reliable readings.
// ADC2 pins (GPIO0, 2, 4, 12–15, 25–27) may interfere with WiFi.
String handleAnalog(const char* params) {
  if (!params || strlen(params) < 1)
    return F("{\"ok\":0,\"error\":\"ERR_MISSING_PARAMETER\"}");

  int pin = parsePin(String(params));
  if (pin == -1) return F("{\"ok\":0,\"error\":\"ERR_INVALID_PIN\"}");

  int value = analogRead(pin);   // Returns 0–4095 on ESP32
  String r = "{\"ok\":1,\"pin\":\"";
  r += params; r += "\",\"value\":"; r += value; r += "}";
  return r;
}
// ⚠️ ESP8266 has ONLY ONE analog pin (A0, GPIO17 internally).
// ⚠️ Maximum safe input voltage is 1.0V — use a voltage divider for 3.3V/5V sensors.
String handleAnalog(const char* params) {
  // Ignore pin parameter — ESP8266 has only one analog pin
  int value = analogRead(A0);    // Always reads A0
  String r = "{\"ok\":1,\"pin\":\"A0\",\"value\":";
  r += value; r += "}";
  return r;
}

DIGITAL_READ

Reads the digital state of a pin (HIGH or LOW, returned as 1 or 0).

Compatibility

BoardSupportedNotes
Arduino UNO R4 WiFi✅D0–D13
ESP32 WROOM-32✅Most GPIO. Avoid GPIO34–39 (input-only, no pull-up)
ESP8266 NodeMCU✅GPIO0–GPIO15. GPIO16 has no internal pull-up
Arduino Nano✅D0–D13 (D0/D1 shared with USB Serial)

Request:

{"cmd": "DIGITAL_READ", "id": 2, "args": ["D4"]}

Or pipe: DIGITAL_READ|D4

Response:

{"ok": 1, "pin": "D4", "value": 1, "id": 2}
  • value: 1 = HIGH, 0 = LOW

Implementation:

String handleDigitalRead(const char* params) {
  if (!params || strlen(params) < 1)
    return F("{\"ok\":0,\"error\":\"ERR_MISSING_PARAMETER\"}");

  int pin = parsePin(String(params));
  if (pin == -1) return F("{\"ok\":0,\"error\":\"ERR_INVALID_PIN\"}");

  int state = digitalRead(pin);
  String r = "{\"ok\":1,\"pin\":\"";
  r += params; r += "\",\"value\":"; r += state; r += "}";
  return r;
}

[!WARNING] On Arduino Nano, pins D0 (RX) and D1 (TX) are shared with the USB serial interface. Reading or writing these pins while using Serial for CeresFlows communication will cause data corruption. Avoid using D0/D1 for sensors or actuators on boards where Serial is the primary transport.


DIGITAL_WRITE

Sets a digital pin HIGH or LOW. Optionally accepts a TTL duration (seconds) after which the pin reverts to the opposite state.

Compatibility

BoardSupportedTTL SupportedNotes
Arduino UNO R4 WiFi✅✅
ESP32 WROOM-32✅✅
ESP8266 NodeMCU✅✅
Arduino Nano✅✅

Request formats:

{"cmd": "DIGITAL_WRITE", "id": 3, "args": ["D5", "1"]}
{"cmd": "DIGITAL_WRITE", "id": 4, "args": ["D5", "1", "10"]}

Or pipe: DIGITAL_WRITE|D5|1 / DIGITAL_WRITE|D5|1|10

  • Arg 0: Pin
  • Arg 1: State ("1" = HIGH, "0" = LOW)
  • Arg 2 (optional): TTL duration in seconds — pin reverts after this time

Response (without TTL):

{"ok": 1, "pin": "D5", "state": 1, "id": 3}

Response (with TTL):

{"ok": 1, "pin": "D5", "state": 1, "ttl": 10, "id": 4}

[!NOTE] The TTL Watchdog mechanism is explained in detail in TTL Watchdog. It is a safety feature that ensures relays and pumps do not stay ON indefinitely if the backend loses communication.

Implementation:

String handleDigitalWrite(const char* params) {
  if (!params || strlen(params) < 3)
    return F("{\"ok\":0,\"error\":\"ERR_MISSING_PARAMETER\"}");

  char buf[32];
  strncpy(buf, params, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0';
  char* delim = strchr(buf, '|');
  if (!delim) return F("{\"ok\":0,\"error\":\"ERR_INVALID_FORMAT\"}");
  *delim = '\0';

  int pin = parsePin(String(buf));
  if (pin == -1) return F("{\"ok\":0,\"error\":\"ERR_INVALID_PIN\"}");

  const char* nextPart = delim + 1;
  int state = 0;
  long ttl = 0;
  char* ttlDelim = strchr(nextPart, '|');
  if (ttlDelim) {
    *ttlDelim = '\0';
    state = atoi(nextPart);
    ttl = atol(ttlDelim + 1);
  } else {
    state = atoi(nextPart);
  }

  if (state != 0 && state != 1)
    return F("{\"ok\":0,\"error\":\"ERR_INVALID_VALUE\"}");

  pinMode(pin, OUTPUT);
  digitalWrite(pin, state);

  // Register TTL if provided (see TTL Watchdog chapter)
  bool ttlSet = registerTTL(pin, state, ttl);

  String r = "{\"ok\":1,\"pin\":\"";
  r += buf; r += "\",\"state\":"; r += state;
  if (ttlSet) { r += ",\"ttl\":"; r += ttl; }
  r += "}";
  return r;
}

RELAY_SET

Alias for DIGITAL_WRITE. Functionally identical — the same handler is called. The separate command name makes intent explicit in templates and logs.

Request:

{"cmd": "RELAY_SET", "id": 5, "args": ["D7", "1", "30"]}

Response:

{"ok": 1, "pin": "D7", "state": 1, "ttl": 30, "id": 5}

Implementation:

// RELAY_SET simply delegates to DIGITAL_WRITE
if (strcmp(cmd, "RELAY_SET") == 0) return handleDigitalWrite(params);

[!NOTE] Relay modules typically use active-LOW logic — sending state: 0 turns the relay ON, and state: 1 turns it OFF. This depends on your specific relay module. Check your relay module’s datasheet. CeresFlows sends the state value literally — the firmware does not invert it.


Common Errors (GPIO)

Error CodeCauseFix
ERR_MISSING_PARAMETERNo pin providedInclude pin in args[0]
ERR_INVALID_PINPin string could not be parsedUse D5, A0, or 5 format
ERR_INVALID_FORMATMissing | separator in paramsCheck Pipe format — use DIGITAL_WRITE|D5|1
ERR_INVALID_VALUEState is not 0 or 1Only 0 and 1 are valid states