Gateway RPC: checkHealth, readGPIO, readAllGPIO — Design
- Date: 2026-05-13
- Scope:
gateway_firmware/ - Status: Approved (pending user spec review)
1. Goal
Add three synchronous RPC commands to the ESP32 gateway so ThingsBoard (and operators) can:
- Probe firmware/runtime health on demand (
checkHealth). - Read the logical state of a single PCF8574 relay pin (
readGPIO). - Read the logical state of all eight PCF8574 relay pins (
readAllGPIO).
These reads are read-only and must not flow through the existing async command pipeline used by setGPIO.
2. Non-goals
- Reading ESP32-native GPIOs (only PCF8574 pins 0–7 are exposed).
- Long-running diagnostics or self-tests beyond an in-process snapshot.
- Mutating any device state. Read failures must not flip
IoState::ioBoardOnline. - Reviving the legacy
src/rpc_handler.cpp; new commands live insrc/mqtt/rpc_server.cpponly.
3. Architecture
All three RPCs are handled inline in RpcServer::onMessage. They are synchronous and never touch command_queue, command_worker, or command_history.
onMessage(topic, payload):
parse JSON, extract method
switch method:
"setGPIO" -> existing async queue path (unchanged)
"checkHealth" -> handleCheckHealth(topic)
"readGPIO" -> handleReadGpio(topic, params)
"readAllGPIO" -> handleReadAllGpio(topic)
else -> existing "unknown_method" response
A new helper sendJsonResponse(requestTopic, bodyJson) rewrites /request/ to /response/ and publishes. The existing sendResponse(topic, accepted, pin, value, error) envelope ({pin,value,changed,queued,error}) remains exclusive to setGPIO; reads/health use their own JSON shapes because changed/queued are meaningless for reads.
Constructor change
RpcServer gains a pointer dependency on the PCF8574 driver (used only by the two read handlers). In main.cpp the real Pcf8574Driver& is passed; tests pass a fake. This is the only constructor-signature change.
RpcServer(QueueHandle_t commandQueue,
MqttClient& mqttClient,
IoState* ioState,
CommandHistory* commandHistory,
Pcf8574Driver* pcf); // NEW
Health field sources
Read directly inside handleCheckHealth, mirroring HealthMonitor::_publish():
| Field | Source |
|---|---|
uptimeSec |
millis() / 1000 |
freeHeap |
ESP.getFreeHeap() |
wifiConnected |
WiFi.status() == WL_CONNECTED |
wifiRssi |
WiFi.RSSI() if connected, else 0 |
mqttConnected |
always true at response time |
ioBoardOnline |
IoState::isIoBoardOnline() |
fwVersion |
new constexpr const char* FW_VERSION in config.h |
4. RPC Specifications
4.1 checkHealth
Request
{ "method": "checkHealth" }
Response
{
"ok": true,
"uptimeSec": 12345,
"freeHeap": 187432,
"wifiConnected": true,
"wifiRssi": -58,
"mqttConnected": true,
"ioBoardOnline": true,
"fwVersion": "1.4.0"
}
checkHealth never errors.
4.2 readGPIO
Request
{ "method": "readGPIO", "params": { "pin": 3 } }
Validation: pin must be an integer in [0, 7].
Preconditions: IoState::isIoBoardOnline() must be true.
Reading: call Pcf8574Driver::readPort(value). Logical state for the requested pin is computed from value using PCF8574_RELAY_ACTIVE_LOW:
bit = (value >> pin) & 0x1
logical = PCF8574_RELAY_ACTIVE_LOW ? (bit == 0 ? 1 : 0) : bit
Success response
{ "pin": 3, "value": 1 }
4.3 readAllGPIO
Request
{ "method": "readAllGPIO" }
Preconditions: same isIoBoardOnline() check.
Success response
{
"values": [0, 1, 0, 0, 1, 1, 0, 0],
"raw": 50
}
values[i]— logical ON/OFF for piniafter active-low translation.raw— the raw PCF8574 port byte (debug aid).
5. Error Model
| Condition | Response |
|---|---|
| Invalid JSON | {"error":"invalid_payload"} |
| Unknown method | {"error":"unknown_method"} |
readGPIO missing/out-of-range pin |
{"error":"invalid_pin"} |
readGPIO/readAllGPIO and !isIoBoardOnline() |
{"error":"io_board_offline"} |
Pcf8574Driver::readPort() returns false |
{"error":"io_read_failed"} |
| MQTT not connected | No response published (existing behavior). |
Read failures must not call IoState::setIoBoardOnline(false). That side effect is reserved for command_worker.
For readGPIO, the invalid_pin error response includes the offending pin value when available (mirroring how setGPIO echoes its parsed pin).
6. Logging
- Each handler logs at
verbose:rpc.<method> latency_ms=<n>to match the existing pattern inonMessage. - I²C
readPort()failure logs aterror.
7. Testing
New tests live under test/native_rpc/ (extends existing pio test -e native_rpc environment). A fake Pcf8574Driver (or a thin IPcf8574Reader interface — pick whichever requires the smallest change to the production class) supplies controlled bytes / failure states.
checkHealthhappy path — response contains expected keys, types correct,uptimeSecis monotonic across two calls.readGPIOhappy path — fake driver returns a known byte; assertvaluereflects active-low translation for multiple pin indices.readGPIOvalidation —pinmissing / non-integer /-1/8→invalid_pin.readGPIOio_board_offline—IoStateoffline →io_board_offline, fake driverreadPortnever invoked.readGPIOio_read_failed— fake driver returns false →io_read_failed,IoStateunchanged (asserted).readAllGPIOhappy path — known byte →valuesarray correctly translated,rawequals the input byte.readAllGPIOfailure / offline — same as 4 & 5.- Regression —
setGPIOstill queues and responds with the existing{pin,value,changed,queued}envelope; unknown method still returnsunknown_method.
8. Documentation
Update gateway_firmware/rpc_commands.md:
- Add sections for
checkHealth,readGPIO,readAllGPIOwith request/response examples. - Add an error-code table covering:
invalid_payload,unknown_method,invalid_pin,io_board_offline,io_read_failed. - Note that the legacy
checkConnectionis superseded bycheckHealth.
9. Out-of-Scope / Follow-ups
- Removing the dead
src/rpc_handler.cppis intentionally not part of this change; track separately. - Modbus subsystem health (e.g.
modbus_last_ok_ms) is not exposed yet because it is not tracked anywhere today; revisit when that timestamp exists.