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
ANALOGon 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
| Board | Supported | Pins | ADC Range | Max voltage |
|---|---|---|---|---|
| Arduino UNO R4 WiFi | ✅ | A0–A5 | 0–1023 (10-bit) | 5V |
| ESP32 WROOM-32 | ✅ | GPIO34, 35, 36, 39 | 0–4095 (12-bit) | 3.3V |
| ESP8266 NodeMCU | ⚠️ | A0 only | 0–1023 | 1.0V max 🔴 |
| Arduino Nano | ✅ | A0–A5 | 0–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
| Board | Supported | Notes |
|---|---|---|
| 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
| Board | Supported | TTL Supported | Notes |
|---|---|---|---|
| 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: 0turns the relay ON, andstate: 1turns 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 Code | Cause | Fix |
|---|---|---|
ERR_MISSING_PARAMETER | No pin provided | Include pin in args[0] |
ERR_INVALID_PIN | Pin string could not be parsed | Use D5, A0, or 5 format |
ERR_INVALID_FORMAT | Missing | separator in params | Check Pipe format — use DIGITAL_WRITE|D5|1 |
ERR_INVALID_VALUE | State is not 0 or 1 | Only 0 and 1 are valid states |