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:
+486
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user