Add configurable DS18B20 temperature probe support

This commit is contained in:
2026-06-04 14:25:34 -06:00
parent f46e72fc7f
commit 0afd78284e
6 changed files with 490 additions and 48 deletions
+55
View File
@@ -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
}
+38
View File
@@ -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 <slot> <number>
Assigns a detected physical sensor to a configured temp slot.
Example:
assign temp 1 1
assign temp 2 2
save
## clear temp <slot>
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.
+34
View File
@@ -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}
@@ -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 <slot> <number>");
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();
+135 -36
View File
@@ -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 <slot> <number>");
}
}
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 (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)) {
tempsF[i] = cToF(tempC);
online[i] = true;
sensors.tempsF[configIndex] = cToF(tempC);
sensors.online[configIndex] = true;
}
}
sensors.temp1 = tempsF[0];
sensors.temp2 = tempsF[1];
sensors.temp3 = tempsF[2];
sensors.temp4 = tempsF[3];
sensors.temp1Online = online[0];
sensors.temp2Online = online[1];
sensors.temp3Online = online[2];
sensors.temp4Online = online[3];
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;
}
}
}
bool assignTempSensorByIndex(int configIndex, int scanIndex) {
if (configIndex < 0 || configIndex >= MAX_TEMP_SENSORS) {
return false;
}
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;
}
+12 -9
View File
@@ -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 <Arduino.h>
#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);