From 0afd78284e08c03938174db74e2889cdf9af770f Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 4 Jun 2026 14:25:34 -0600 Subject: [PATCH] Add configurable DS18B20 temperature probe support --- docs/API.md | 55 +++++ docs/SERIAL_COMMANDS.md | 38 +++ docs/UART_PROTOCOL.md | 34 +++ .../overland-controller.ino | 219 +++++++++++++++++- .../esp32/overland-controller/sensors.cpp | 171 +++++++++++--- firmware/esp32/overland-controller/sensors.h | 21 +- 6 files changed, 490 insertions(+), 48 deletions(-) diff --git a/docs/API.md b/docs/API.md index 9600357..e320fd8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -538,3 +538,58 @@ Runtime behavior: AP always remains enabled. STA tries saved networks by priority. If STA disconnects, saved networks are retried every 30 seconds. + +--- + +# Temperature Probe Setup API + +## Scan Temperature Sensors + + POST /temps/scan + +Response: + + { + "type": "temp_scan_response", + "ok": true, + "devices": [ + { + "index": 1, + "address": "28:AA:BB:CC:DD:EE:FF:00" + } + ] + } + +## Assign Temperature Sensor + + POST /temps/assign + +Request: + + { + "id": "temp_1", + "index": 1 + } + +Alternative request: + + { + "slot": 1, + "index": 1 + } + +## Clear Temperature Sensor + + POST /temps/clear + +Request: + + { + "id": "temp_1" + } + +Alternative request: + + { + "slot": 1 + } diff --git a/docs/SERIAL_COMMANDS.md b/docs/SERIAL_COMMANDS.md index f038891..173d248 100644 --- a/docs/SERIAL_COMMANDS.md +++ b/docs/SERIAL_COMMANDS.md @@ -414,3 +414,41 @@ Attempt STA WiFi connection using saved networks by priority. AP remains available at: 192.168.4.1 + +--- + +# Temperature Probe Setup + +## scan temps + +Scans the DS18B20 1-Wire bus and prints detected sensor addresses. + +Example: + + scan temps + +## assign temp + +Assigns a detected physical sensor to a configured temp slot. + +Example: + + assign temp 1 1 + assign temp 2 2 + save + +## clear temp + +Clears and disables a configured temp slot. + +Example: + + clear temp 1 + +Wiring reminder: + + DS18B20 red -> 3.3V + DS18B20 black -> GND + DS18B20 yellow -> GPIO4 + +A single 4.7k resistor is required between 3.3V and GPIO4/data. diff --git a/docs/UART_PROTOCOL.md b/docs/UART_PROTOCOL.md index 03b02c4..6198a8c 100644 --- a/docs/UART_PROTOCOL.md +++ b/docs/UART_PROTOCOL.md @@ -232,3 +232,37 @@ Preferred response: {"type":"config_wifi","networks":[{"ssid":"Starlink","password":"password","priority":1},{"ssid":"Home WiFi","password":"password","priority":2}]} Lower priority numbers are tried first. + +--- + +# Temperature Probe Setup + +## Scan Temperature Sensors + +Pico sends: + + {"type":"scan_temps"} + +ESP32 responds: + + {"type":"temp_scan_response","ok":true,"devices":[{"index":1,"address":"28:AA:BB:CC:DD:EE:FF:00"}]} + +## Assign Temperature Sensor + +Pico sends: + + {"type":"assign_temp","id":"temp_1","index":1} + +Alternative: + + {"type":"assign_temp","slot":1,"index":1} + +## Clear Temperature Sensor + +Pico sends: + + {"type":"clear_temp","id":"temp_1"} + +Alternative: + + {"type":"clear_temp","slot":1} diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index 682bbc4..df77e18 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -474,9 +474,9 @@ void buildStatusDocument(JsonDocument& doc) { temp["id"] = appConfig.tempSensors[i].id; temp["name"] = appConfig.tempSensors[i].name; temp["enabled"] = appConfig.tempSensors[i].enabled; - temp["online"] = tempOnline[i]; - if (tempOnline[i]) { - temp["temperature_f"] = tempValues[i]; + temp["online"] = sensors.online[i]; + if (sensors.online[i]) { + temp["temperature_f"] = sensors.tempsF[i]; } else { temp["temperature_f"] = nullptr; } @@ -912,6 +912,82 @@ void handleUartMessage(const String& line) { return; } + if (strcmp(type, "scan_temps") == 0) { + scanTempSensors(); + + DynamicJsonDocument response(2048); + response["type"] = "temp_scan_response"; + response["ok"] = true; + + JsonArray devices = response.createNestedArray("devices"); + + int count = getTempScanResultCount(); + for (int i = 0; i < count; i++) { + JsonObject device = devices.createNestedObject(); + device["index"] = i + 1; + device["address"] = getTempScanResultAddress(i); + } + + serializeJson(response, DashboardSerial); + DashboardSerial.println(); + return; + } + + if (strcmp(type, "assign_temp") == 0) { + String id = doc["id"] | ""; + int slot = doc["slot"] | 0; + int index = doc["index"] | 0; + + int configIndex = -1; + + if (id.length() > 0) { + configIndex = findTempConfigIndexForUart(id); + } else if (slot >= 1 && slot <= MAX_TEMP_SENSORS) { + configIndex = slot - 1; + } + + if (configIndex < 0) { + sendError(DashboardSerial, "unknown_temp_sensor"); + return; + } + + if (!assignTempSensorByIndex(configIndex, index - 1)) { + sendError(DashboardSerial, "invalid_temp_selection"); + return; + } + + saveConfig(); + sendConfigResponse(DashboardSerial); + return; + } + + if (strcmp(type, "clear_temp") == 0) { + String id = doc["id"] | ""; + int slot = doc["slot"] | 0; + + int configIndex = -1; + + if (id.length() > 0) { + configIndex = findTempConfigIndexForUart(id); + } else if (slot >= 1 && slot <= MAX_TEMP_SENSORS) { + configIndex = slot - 1; + } + + if (configIndex < 0) { + sendError(DashboardSerial, "unknown_temp_sensor"); + return; + } + + if (!clearTempSensor(configIndex)) { + sendError(DashboardSerial, "unknown_temp_sensor"); + return; + } + + saveConfig(); + sendConfigResponse(DashboardSerial); + return; + } + if (strcmp(type, "save_config") == 0) { saveConfig(); sendConfigResponse(DashboardSerial); @@ -1191,6 +1267,47 @@ void handleDebugSerial() { } } + if (command == "scan temps") { + scanTempSensors(); + printSensorAddresses(); + return; + } + + if (command.startsWith("assign temp ")) { + String rest = command.substring(12); + int space = rest.indexOf(' '); + + if (space < 0) { + Serial.println("Invalid command. Use: assign temp "); + return; + } + + int slot = rest.substring(0, space).toInt(); + int index = rest.substring(space + 1).toInt(); + + if (!assignTempSensorByIndex(slot - 1, index - 1)) { + Serial.println("Invalid temp slot or sensor number."); + return; + } + + saveConfig(); + Serial.println("OK temp sensor assigned"); + return; + } + + if (command.startsWith("clear temp ")) { + int slot = command.substring(11).toInt(); + + if (!clearTempSensor(slot - 1)) { + Serial.println("Invalid temp slot."); + return; + } + + saveConfig(); + Serial.println("OK temp sensor cleared"); + return; + } + if (command.startsWith("tempname ")) { int firstSpace = command.indexOf(' '); int secondSpace = command.indexOf(' ', firstSpace + 1); @@ -1524,6 +1641,102 @@ void handleWifiClear() { sendWifiConfigJson(); } + +void sendTempScanJson() { + DynamicJsonDocument doc(2048); + + doc["type"] = "temp_scan_response"; + doc["ok"] = true; + + JsonArray devices = doc.createNestedArray("devices"); + + int count = getTempScanResultCount(); + + for (int i = 0; i < count; i++) { + JsonObject device = devices.createNestedObject(); + device["index"] = i + 1; + device["address"] = getTempScanResultAddress(i); + } + + String output; + serializeJson(doc, output); + server.send(200, "application/json", output); +} + +void handleTempScan() { + scanTempSensors(); + sendTempScanJson(); +} + +void handleTempAssign() { + DynamicJsonDocument doc(512); + DeserializationError error = deserializeJson(doc, server.arg("plain")); + + if (error) { + server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}"); + return; + } + + String id = doc["id"] | ""; + int slot = doc["slot"] | 0; + int index = doc["index"] | 0; + + int configIndex = -1; + + if (id.length() > 0) { + configIndex = findTempConfigIndexForUart(id); + } else if (slot >= 1 && slot <= MAX_TEMP_SENSORS) { + configIndex = slot - 1; + } + + if (configIndex < 0) { + server.send(400, "application/json", "{\"ok\":false,\"error\":\"unknown_temp_sensor\"}"); + return; + } + + if (!assignTempSensorByIndex(configIndex, index - 1)) { + server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_temp_selection\"}"); + return; + } + + saveConfig(); + sendConfigJson(); +} + +void handleTempClear() { + DynamicJsonDocument doc(512); + DeserializationError error = deserializeJson(doc, server.arg("plain")); + + if (error) { + server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}"); + return; + } + + String id = doc["id"] | ""; + int slot = doc["slot"] | 0; + + int configIndex = -1; + + if (id.length() > 0) { + configIndex = findTempConfigIndexForUart(id); + } else if (slot >= 1 && slot <= MAX_TEMP_SENSORS) { + configIndex = slot - 1; + } + + if (configIndex < 0) { + server.send(400, "application/json", "{\"ok\":false,\"error\":\"unknown_temp_sensor\"}"); + return; + } + + if (!clearTempSensor(configIndex)) { + server.send(400, "application/json", "{\"ok\":false,\"error\":\"unknown_temp_sensor\"}"); + return; + } + + saveConfig(); + sendConfigJson(); +} + void handleSaveConfig() { saveConfig(); sendConfigJson(); diff --git a/firmware/esp32/overland-controller/sensors.cpp b/firmware/esp32/overland-controller/sensors.cpp index eff6d69..d38f9a9 100644 --- a/firmware/esp32/overland-controller/sensors.cpp +++ b/firmware/esp32/overland-controller/sensors.cpp @@ -4,13 +4,15 @@ #include "config.h" #include "sensors.h" +#include "app_config.h" SensorData sensors; OneWire oneWire(ONEWIRE_PIN); DallasTemperature ds18b20(&oneWire); -DeviceAddress sensorAddresses[4]; +DeviceAddress sensorAddresses[MAX_TEMP_SENSORS]; +String sensorAddressStrings[MAX_TEMP_SENSORS]; int sensorCount = 0; float cToF(float c) { @@ -21,67 +23,164 @@ bool validTempC(float tempC) { return tempC != DEVICE_DISCONNECTED_C && tempC > -55 && tempC < 125; } -void printAddress(DeviceAddress address) { +String addressToString(DeviceAddress address) { + String output = ""; + for (uint8_t i = 0; i < 8; i++) { - if (address[i] < 16) Serial.print("0"); - Serial.print(address[i], HEX); - if (i < 7) Serial.print(":"); + if (address[i] < 16) output += "0"; + output += String(address[i], HEX); + if (i < 7) output += ":"; } + + output.toUpperCase(); + return output; +} + +void printAddress(DeviceAddress address) { + Serial.print(addressToString(address)); +} + +bool addressMatches(DeviceAddress address, const String& expected) { + if (expected.length() == 0) { + return false; + } + + String actual = addressToString(address); + String normalizedExpected = expected; + normalizedExpected.toUpperCase(); + + return actual == normalizedExpected; +} + +void clearSensorRuntimeData() { + for (int i = 0; i < MAX_TEMP_SENSORS; i++) { + sensors.tempsF[i] = -127.0; + sensors.online[i] = false; + } +} + +int scanTempSensors() { + ds18b20.begin(); + + sensorCount = ds18b20.getDeviceCount(); + + if (sensorCount > MAX_TEMP_SENSORS) { + sensorCount = MAX_TEMP_SENSORS; + } + + for (int i = 0; i < MAX_TEMP_SENSORS; i++) { + sensorAddressStrings[i] = ""; + } + + for (int i = 0; i < sensorCount; i++) { + if (ds18b20.getAddress(sensorAddresses[i], i)) { + sensorAddressStrings[i] = addressToString(sensorAddresses[i]); + } + } + + return sensorCount; +} + +int getTempScanResultCount() { + return sensorCount; +} + +String getTempScanResultAddress(int index) { + if (index < 0 || index >= sensorCount) { + return ""; + } + + return sensorAddressStrings[index]; } void printSensorAddresses() { Serial.println("DS18B20 Sensors Found:"); for (int i = 0; i < sensorCount; i++) { - Serial.print("Sensor "); - Serial.print(i); - Serial.print(": "); - printAddress(sensorAddresses[i]); - Serial.println(); + Serial.print(i + 1); + Serial.print(") "); + Serial.println(sensorAddressStrings[i]); } if (sensorCount == 0) { Serial.println("No DS18B20 sensors found."); + } else { + Serial.println("Use: assign temp "); } } void initSensors() { - ds18b20.begin(); - sensorCount = ds18b20.getDeviceCount(); - - if (sensorCount > 4) { - sensorCount = 4; - } - - for (int i = 0; i < sensorCount; i++) { - ds18b20.getAddress(sensorAddresses[i], i); - } - + clearSensorRuntimeData(); + scanTempSensors(); printSensorAddresses(); } void updateSensors() { + clearSensorRuntimeData(); + ds18b20.requestTemperatures(); - float tempsF[4] = {-127, -127, -127, -127}; - bool online[4] = {false, false, false, false}; + for (int configIndex = 0; configIndex < MAX_TEMP_SENSORS; configIndex++) { + if (!appConfig.tempSensors[configIndex].enabled) { + continue; + } - for (int i = 0; i < sensorCount; i++) { - float tempC = ds18b20.getTempC(sensorAddresses[i]); + String configuredAddress = appConfig.tempSensors[configIndex].address; - if (validTempC(tempC)) { - tempsF[i] = cToF(tempC); - online[i] = true; + if (configuredAddress.length() == 0) { + // Backward-compatible behavior: + // if no address is assigned, use physical scan order. + if (configIndex < sensorCount) { + float tempC = ds18b20.getTempC(sensorAddresses[configIndex]); + + if (validTempC(tempC)) { + sensors.tempsF[configIndex] = cToF(tempC); + sensors.online[configIndex] = true; + } + } + + continue; + } + + for (int physicalIndex = 0; physicalIndex < sensorCount; physicalIndex++) { + if (!addressMatches(sensorAddresses[physicalIndex], configuredAddress)) { + continue; + } + + float tempC = ds18b20.getTempC(sensorAddresses[physicalIndex]); + + if (validTempC(tempC)) { + sensors.tempsF[configIndex] = cToF(tempC); + sensors.online[configIndex] = true; + } + + break; } } +} - sensors.temp1 = tempsF[0]; - sensors.temp2 = tempsF[1]; - sensors.temp3 = tempsF[2]; - sensors.temp4 = tempsF[3]; +bool assignTempSensorByIndex(int configIndex, int scanIndex) { + if (configIndex < 0 || configIndex >= MAX_TEMP_SENSORS) { + return false; + } - sensors.temp1Online = online[0]; - sensors.temp2Online = online[1]; - sensors.temp3Online = online[2]; - sensors.temp4Online = online[3]; + if (scanIndex < 0 || scanIndex >= sensorCount) { + return false; + } + + appConfig.tempSensors[configIndex].address = sensorAddressStrings[scanIndex]; + appConfig.tempSensors[configIndex].enabled = true; + + return true; +} + +bool clearTempSensor(int configIndex) { + if (configIndex < 0 || configIndex >= MAX_TEMP_SENSORS) { + return false; + } + + appConfig.tempSensors[configIndex].address = ""; + appConfig.tempSensors[configIndex].enabled = false; + + return true; } diff --git a/firmware/esp32/overland-controller/sensors.h b/firmware/esp32/overland-controller/sensors.h index 6f6d309..7fca724 100644 --- a/firmware/esp32/overland-controller/sensors.h +++ b/firmware/esp32/overland-controller/sensors.h @@ -1,19 +1,22 @@ #pragma once -struct SensorData { - float temp1 = -127.0; - float temp2 = -127.0; - float temp3 = -127.0; - float temp4 = -127.0; +#include +#include "app_config.h" - bool temp1Online = false; - bool temp2Online = false; - bool temp3Online = false; - bool temp4Online = false; +struct SensorData { + float tempsF[MAX_TEMP_SENSORS]; + bool online[MAX_TEMP_SENSORS]; }; extern SensorData sensors; void initSensors(); void updateSensors(); + +int scanTempSensors(); +int getTempScanResultCount(); +String getTempScanResultAddress(int index); void printSensorAddresses(); + +bool assignTempSensorByIndex(int configIndex, int scanIndex); +bool clearTempSensor(int configIndex);