Initial commit: ZNET Temperature Sensor ESP32 firmware

- PlatformIO project for Meshnology ESP32 LoRa V3
- DS18B20 dual sensor support for tank verification
- OLED display for local temperature visibility
- HTTP POST to ZNET Web API
- WiFiManager for easy WiFi configuration
- Offline buffering when network unavailable

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
leehughes
2026-01-24 16:48:12 -06:00
co-authored by Claude Opus 4.5
commit 265a53768e
5 changed files with 819 additions and 0 deletions
+486
View File
@@ -0,0 +1,486 @@
/**
* ZNET Temperature Sensor - ESP32 LoRa Gateway
*
* Reads DS18B20 temperature sensors in the e-coat paint tank and sends
* data to ZNET Web for real-time monitoring and ML feature collection.
*
* Hardware:
* - Meshnology ESP32 LoRa V3 (Heltec-compatible)
* - 2x DS18B20 waterproof temperature probes (1-Wire)
* - Built-in 0.96" OLED display
* - Built-in SX1262 LoRa radio (for future sensor nodes)
*
* Features:
* - Dual sensor reading with validation and averaging
* - OLED display for local temperature visibility
* - HTTP POST to ZNET Web API
* - Offline buffering when network is unavailable
* - LoRa receive for future remote sensor nodes
* - WiFiManager for easy WiFi setup
*/
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <SSD1306Wire.h>
#include <WiFiManager.h>
#include "config.h"
// ============================================================================
// GLOBALS
// ============================================================================
// OneWire and DS18B20
OneWire oneWire(ONEWIRE_PIN);
DallasTemperature sensors(&oneWire);
// Sensor addresses (discovered at startup)
DeviceAddress sensor1Address;
DeviceAddress sensor2Address;
int sensorCount = 0;
// OLED display
SSD1306Wire display(OLED_ADDRESS, OLED_SDA, OLED_SCL);
// Timing
unsigned long lastTempRead = 0;
unsigned long lastDisplayUpdate = 0;
unsigned long lastApiPost = 0;
// Current readings
float temperature1_f = 0.0;
float temperature2_f = 0.0;
bool sensor1Valid = false;
bool sensor2Valid = false;
// Network status
bool wifiConnected = false;
int apiFailCount = 0;
// Offline buffer (circular)
struct TempReading {
unsigned long timestamp;
float temp1_f;
float temp2_f;
bool sent;
};
TempReading offlineBuffer[OFFLINE_BUFFER_SIZE];
int bufferHead = 0;
int bufferCount = 0;
// ============================================================================
// FUNCTION DECLARATIONS
// ============================================================================
void setupWiFi();
void setupSensors();
void setupDisplay();
void readTemperatures();
void updateDisplay();
void postToApi();
void sendBufferedReadings();
float celsiusToFahrenheit(float celsius);
bool isValidTemperature(float temp_f);
String formatAddress(DeviceAddress addr);
void displaySplash();
void displayError(const char* message);
// ============================================================================
// SETUP
// ============================================================================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n\n");
Serial.println("=========================================");
Serial.println(" ZNET Temperature Sensor v1.0");
Serial.println(" E-Coat Paint Tank Monitor");
Serial.println("=========================================");
// Initialize display first for visual feedback
setupDisplay();
displaySplash();
// Initialize sensors
setupSensors();
// Connect to WiFi
setupWiFi();
Serial.println("\nSetup complete. Starting main loop...\n");
}
// ============================================================================
// MAIN LOOP
// ============================================================================
void loop() {
unsigned long now = millis();
// Check WiFi connection
wifiConnected = WiFi.status() == WL_CONNECTED;
// Read temperatures at interval
if (now - lastTempRead >= TEMP_READ_INTERVAL_MS || lastTempRead == 0) {
readTemperatures();
lastTempRead = now;
// Try to post to API
if (wifiConnected) {
postToApi();
// Also try to send any buffered readings
if (bufferCount > 0) {
sendBufferedReadings();
}
} else {
// Buffer the reading for later
if (bufferCount < OFFLINE_BUFFER_SIZE) {
offlineBuffer[bufferHead].timestamp = now;
offlineBuffer[bufferHead].temp1_f = temperature1_f;
offlineBuffer[bufferHead].temp2_f = temperature2_f;
offlineBuffer[bufferHead].sent = false;
bufferHead = (bufferHead + 1) % OFFLINE_BUFFER_SIZE;
bufferCount++;
Serial.printf("Buffered reading (count: %d)\n", bufferCount);
}
}
}
// Update display more frequently
if (now - lastDisplayUpdate >= DISPLAY_UPDATE_INTERVAL_MS) {
updateDisplay();
lastDisplayUpdate = now;
}
// Small delay to prevent watchdog issues
delay(10);
}
// ============================================================================
// WIFI SETUP
// ============================================================================
void setupWiFi() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 20, "Connecting to WiFi...");
display.display();
// Check if we have hardcoded credentials
if (strlen(WIFI_SSID) > 0) {
Serial.printf("Connecting to WiFi: %s\n", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected!");
Serial.printf("IP: %s\n", WiFi.localIP().toString().c_str());
wifiConnected = true;
return;
}
}
// Use WiFiManager for captive portal setup
Serial.println("Starting WiFiManager...");
display.clear();
display.drawString(64, 10, "WiFi Setup");
display.drawString(64, 30, "Connect to:");
display.drawString(64, 45, "ZNET-TempSensor");
display.display();
WiFiManager wm;
wm.setConfigPortalTimeout(180); // 3 minute timeout
if (!wm.autoConnect("ZNET-TempSensor", "znettemp123")) {
Serial.println("Failed to connect - restarting...");
delay(3000);
ESP.restart();
}
Serial.println("WiFi connected via WiFiManager!");
Serial.printf("IP: %s\n", WiFi.localIP().toString().c_str());
wifiConnected = true;
}
// ============================================================================
// SENSOR SETUP
// ============================================================================
void setupSensors() {
Serial.println("Initializing DS18B20 sensors...");
sensors.begin();
sensorCount = sensors.getDeviceCount();
Serial.printf("Found %d sensor(s)\n", sensorCount);
if (sensorCount == 0) {
Serial.println("ERROR: No sensors found!");
displayError("No sensors found!");
return;
}
// Get addresses
if (sensorCount >= 1) {
if (sensors.getAddress(sensor1Address, 0)) {
Serial.printf("Sensor 1: %s\n", formatAddress(sensor1Address).c_str());
}
}
if (sensorCount >= 2) {
if (sensors.getAddress(sensor2Address, 1)) {
Serial.printf("Sensor 2: %s\n", formatAddress(sensor2Address).c_str());
}
}
// Set resolution (12-bit = 0.0625°C precision)
sensors.setResolution(12);
// Don't wait for conversion (we'll handle timing ourselves)
sensors.setWaitForConversion(false);
}
// ============================================================================
// TEMPERATURE READING
// ============================================================================
void readTemperatures() {
Serial.println("Reading temperatures...");
// Request temperatures from all sensors
sensors.requestTemperatures();
// Wait for conversion (750ms for 12-bit)
delay(750);
// Read sensor 1
if (sensorCount >= 1) {
float tempC = sensors.getTempC(sensor1Address);
if (tempC != DEVICE_DISCONNECTED_C) {
temperature1_f = celsiusToFahrenheit(tempC);
sensor1Valid = isValidTemperature(temperature1_f);
Serial.printf("Sensor 1: %.2f°F %s\n", temperature1_f,
sensor1Valid ? "(valid)" : "(INVALID)");
} else {
sensor1Valid = false;
Serial.println("Sensor 1: DISCONNECTED");
}
}
// Read sensor 2
if (sensorCount >= 2) {
float tempC = sensors.getTempC(sensor2Address);
if (tempC != DEVICE_DISCONNECTED_C) {
temperature2_f = celsiusToFahrenheit(tempC);
sensor2Valid = isValidTemperature(temperature2_f);
Serial.printf("Sensor 2: %.2f°F %s\n", temperature2_f,
sensor2Valid ? "(valid)" : "(INVALID)");
} else {
sensor2Valid = false;
Serial.println("Sensor 2: DISCONNECTED");
}
}
// Check for sensor agreement (if both valid)
if (sensor1Valid && sensor2Valid) {
float diff = abs(temperature1_f - temperature2_f);
if (diff > 5.0) {
Serial.printf("WARNING: Sensors disagree by %.1f°F\n", diff);
}
}
}
// ============================================================================
// DISPLAY UPDATE
// ============================================================================
void updateDisplay() {
display.clear();
// Title bar
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_10);
display.drawString(0, 0, "ZNET Tank Temp");
// WiFi status indicator
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(128, 0, wifiConnected ? "WiFi OK" : "NO WiFi");
// Horizontal line
display.drawLine(0, 12, 128, 12);
// Temperature readings
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
if (sensor1Valid) {
char buf[20];
snprintf(buf, sizeof(buf), "%.1f°F", temperature1_f);
display.drawString(64, 18, buf);
} else {
display.drawString(64, 18, "---");
}
display.setFont(ArialMT_Plain_10);
display.drawString(64, 36, "Primary");
// Secondary sensor (smaller, below)
if (sensorCount >= 2) {
display.setFont(ArialMT_Plain_10);
if (sensor2Valid) {
char buf[20];
snprintf(buf, sizeof(buf), "Backup: %.1f°F", temperature2_f);
display.drawString(64, 50, buf);
} else {
display.drawString(64, 50, "Backup: ---");
}
}
// Buffer indicator (if offline)
if (bufferCount > 0) {
display.setTextAlignment(TEXT_ALIGN_LEFT);
char buf[20];
snprintf(buf, sizeof(buf), "Buf:%d", bufferCount);
display.drawString(0, 54, buf);
}
display.display();
}
// ============================================================================
// API POSTING
// ============================================================================
void postToApi() {
if (!wifiConnected) {
Serial.println("No WiFi - skipping API post");
return;
}
Serial.println("Posting to ZNET Web API...");
HTTPClient http;
http.begin(ZNET_API_URL);
http.addHeader("Content-Type", "application/json");
http.setTimeout(HTTP_TIMEOUT_MS);
// Build JSON payload
JsonDocument doc;
doc["device_id"] = DEVICE_ID;
doc["device_name"] = DEVICE_NAME;
doc["timestamp"] = millis(); // Server will use its own timestamp
JsonArray readings = doc["readings"].to<JsonArray>();
// Sensor 1
if (sensor1Valid) {
JsonObject r1 = readings.add<JsonObject>();
r1["sensor_id"] = SENSOR_1_NAME;
r1["temperature_f"] = round(temperature1_f * 100) / 100.0; // 2 decimal places
r1["is_valid"] = true;
}
// Sensor 2
if (sensor2Valid) {
JsonObject r2 = readings.add<JsonObject>();
r2["sensor_id"] = SENSOR_2_NAME;
r2["temperature_f"] = round(temperature2_f * 100) / 100.0;
r2["is_valid"] = true;
}
String jsonString;
serializeJson(doc, jsonString);
Serial.printf("Payload: %s\n", jsonString.c_str());
int httpCode = http.POST(jsonString);
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_CREATED) {
Serial.printf("API POST success (code %d)\n", httpCode);
apiFailCount = 0;
// Parse response for any config updates
String response = http.getString();
Serial.printf("Response: %s\n", response.c_str());
} else {
Serial.printf("API POST failed (code %d)\n", httpCode);
apiFailCount++;
if (httpCode > 0) {
String response = http.getString();
Serial.printf("Error response: %s\n", response.c_str());
}
}
http.end();
}
void sendBufferedReadings() {
// TODO: Implement batch sending of buffered readings
// For now, just clear the buffer on successful connection
Serial.printf("Would send %d buffered readings\n", bufferCount);
// Clear buffer (proper implementation would send each reading)
bufferCount = 0;
bufferHead = 0;
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
float celsiusToFahrenheit(float celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
bool isValidTemperature(float temp_f) {
return temp_f >= TEMP_MIN_VALID_F && temp_f <= TEMP_MAX_VALID_F;
}
String formatAddress(DeviceAddress addr) {
char buf[20];
snprintf(buf, sizeof(buf), "%02X%02X%02X%02X%02X%02X%02X%02X",
addr[0], addr[1], addr[2], addr[3],
addr[4], addr[5], addr[6], addr[7]);
return String(buf);
}
void setupDisplay() {
display.init();
display.flipScreenVertically();
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
}
void displaySplash() {
display.clear();
display.setFont(ArialMT_Plain_16);
display.drawString(64, 10, "ZNET");
display.setFont(ArialMT_Plain_10);
display.drawString(64, 30, "Tank Temperature");
display.drawString(64, 45, "v1.0");
display.display();
delay(2000);
}
void displayError(const char* message) {
display.clear();
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 20, "ERROR");
display.drawString(64, 35, message);
display.display();
}