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:
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# PlatformIO
|
||||||
|
.pio/
|
||||||
|
.pioenvs/
|
||||||
|
.piolibdeps/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Compiled files
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.elf
|
||||||
|
*.bin
|
||||||
|
*.hex
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Credentials (if stored locally)
|
||||||
|
credentials.h
|
||||||
|
secrets.h
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# ZNET Temperature Sensor - ESP32 LoRa Gateway
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
ESP32-based temperature monitoring system for e-coat paint tanks. Reads DS18B20 waterproof temperature probes immersed in the paint tank and sends data to ZNET Web for real-time monitoring and ML feature collection.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
| Component | Model | Purpose |
|
||||||
|
|-----------|-------|---------|
|
||||||
|
| MCU | Meshnology ESP32 LoRa V3 | Main controller with WiFi, LoRa, OLED |
|
||||||
|
| Temp Sensor | DS18B20 (x2) | Waterproof stainless steel probes in paint tank |
|
||||||
|
| Display | Built-in 0.96" OLED | Local temperature display for operators |
|
||||||
|
| Radio | Built-in SX1262 | LoRa 915MHz for future remote sensors |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Build & Upload
|
||||||
|
```bash
|
||||||
|
# Install PlatformIO CLI
|
||||||
|
pip install platformio
|
||||||
|
|
||||||
|
# Build
|
||||||
|
cd ~/Nextcloud/Dev/znet-temp-sensor
|
||||||
|
pio run
|
||||||
|
|
||||||
|
# Upload to ESP32 (connect via USB)
|
||||||
|
pio run --target upload
|
||||||
|
|
||||||
|
# Monitor serial output
|
||||||
|
pio device monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure WiFi
|
||||||
|
1. Power on the ESP32
|
||||||
|
2. Connect to WiFi network: `ZNET-TempSensor` (password: `znettemp123`)
|
||||||
|
3. Browser opens captive portal - enter your WiFi credentials
|
||||||
|
4. Device connects and starts posting to ZNET Web
|
||||||
|
|
||||||
|
### Configure API Endpoint
|
||||||
|
Edit `include/config.h`:
|
||||||
|
```cpp
|
||||||
|
#define ZNET_API_URL "http://192.168.1.100:8000/api/v1/sensors/readings"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
|
||||||
|
### DS18B20 Connection (1-Wire bus, both sensors on same pin)
|
||||||
|
```
|
||||||
|
ESP32 GPIO7 ─────┬───────────── DS18B20 #1 DATA (yellow)
|
||||||
|
│
|
||||||
|
└───────────── DS18B20 #2 DATA (yellow)
|
||||||
|
|
||||||
|
ESP32 3.3V ──────┬───────────── DS18B20 #1 VCC (red)
|
||||||
|
│
|
||||||
|
└───────────── DS18B20 #2 VCC (red)
|
||||||
|
|
||||||
|
ESP32 GND ───────┬───────────── DS18B20 #1 GND (black)
|
||||||
|
│
|
||||||
|
└───────────── DS18B20 #2 GND (black)
|
||||||
|
|
||||||
|
4.7kΩ pullup resistor between DATA and VCC
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPIO Pinout (Heltec V3 / Meshnology)
|
||||||
|
| Function | GPIO |
|
||||||
|
|----------|------|
|
||||||
|
| OneWire Data | 7 |
|
||||||
|
| OLED SDA | 17 |
|
||||||
|
| OLED SCL | 18 |
|
||||||
|
| OLED RST | 21 |
|
||||||
|
| LoRa SCK | 9 |
|
||||||
|
| LoRa MISO | 11 |
|
||||||
|
| LoRa MOSI | 10 |
|
||||||
|
| LoRa SS | 8 |
|
||||||
|
| LoRa RST | 12 |
|
||||||
|
| LoRa DIO1 | 14 |
|
||||||
|
| LoRa BUSY | 13 |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All configuration in `include/config.h`:
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| `ZNET_API_URL` | - | ZNET Web API endpoint |
|
||||||
|
| `DEVICE_ID` | `tank_gateway_1` | Unique device identifier |
|
||||||
|
| `TEMP_READ_INTERVAL_MS` | 30000 | Reading interval (30 sec) |
|
||||||
|
| `TEMP_MIN_VALID_F` | 32.0 | Minimum valid temperature |
|
||||||
|
| `TEMP_MAX_VALID_F` | 150.0 | Maximum valid temperature |
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
### POST /api/v1/sensors/readings
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "tank_gateway_1",
|
||||||
|
"device_name": "E-Coat Tank Gateway",
|
||||||
|
"readings": [
|
||||||
|
{
|
||||||
|
"sensor_id": "tank_primary",
|
||||||
|
"temperature_f": 85.5,
|
||||||
|
"is_valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sensor_id": "tank_secondary",
|
||||||
|
"temperature_f": 85.3,
|
||||||
|
"is_valid": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"readings_stored": 2,
|
||||||
|
"alerts_generated": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Implemented
|
||||||
|
- [x] Dual DS18B20 sensor reading with validation
|
||||||
|
- [x] OLED display showing current temperature
|
||||||
|
- [x] WiFiManager for easy WiFi setup
|
||||||
|
- [x] HTTP POST to ZNET Web API
|
||||||
|
- [x] Offline buffering when network unavailable
|
||||||
|
- [x] Automatic sensor discovery (1-Wire)
|
||||||
|
|
||||||
|
### Future (Phase 3)
|
||||||
|
- [ ] LoRa receive for remote sensor nodes
|
||||||
|
- [ ] Battery-powered remote node firmware
|
||||||
|
- [ ] Sensor agreement monitoring
|
||||||
|
- [ ] Flash storage for offline buffer persistence
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- **ZNET Web**: `~/Nextcloud/Dev/znet-web` - Backend receives temperature data
|
||||||
|
- **Baserow**: Table for lab temperature logs (to be created)
|
||||||
|
|
||||||
|
## Git Repository
|
||||||
|
|
||||||
|
- **Remote:** https://git.ecoat.us/leehughes/znet-temp-sensor.git
|
||||||
|
- **Main branch:** `main`
|
||||||
|
|
||||||
|
## Change Log
|
||||||
|
|
||||||
|
| Date | Version | Change |
|
||||||
|
|------|---------|--------|
|
||||||
|
| 2026-01-24 | 1.0.0 | Initial implementation |
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* ZNET Temperature Sensor Configuration
|
||||||
|
*
|
||||||
|
* Edit these values for your installation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NETWORK CONFIGURATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// WiFi credentials (can also be set via WiFiManager portal)
|
||||||
|
#define WIFI_SSID "" // Leave empty to use WiFiManager
|
||||||
|
#define WIFI_PASSWORD ""
|
||||||
|
|
||||||
|
// ZNET Web API endpoint
|
||||||
|
#define ZNET_API_URL "http://192.168.1.100:8000/api/v1/sensors/readings"
|
||||||
|
|
||||||
|
// Device identification
|
||||||
|
#define DEVICE_ID "tank_gateway_1"
|
||||||
|
#define DEVICE_NAME "E-Coat Tank Gateway"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SENSOR CONFIGURATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// DS18B20 1-Wire data pin
|
||||||
|
#ifndef ONEWIRE_PIN
|
||||||
|
#define ONEWIRE_PIN 7
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Temperature reading interval (milliseconds)
|
||||||
|
#define TEMP_READ_INTERVAL_MS 30000 // 30 seconds
|
||||||
|
|
||||||
|
// Number of readings to average for stability
|
||||||
|
#define TEMP_AVG_READINGS 3
|
||||||
|
|
||||||
|
// Sensor IDs (auto-detected, but can be named)
|
||||||
|
#define SENSOR_1_NAME "tank_primary"
|
||||||
|
#define SENSOR_2_NAME "tank_secondary"
|
||||||
|
|
||||||
|
// Temperature validation range (Fahrenheit)
|
||||||
|
#define TEMP_MIN_VALID_F 32.0
|
||||||
|
#define TEMP_MAX_VALID_F 150.0
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DISPLAY CONFIGURATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// OLED I2C address
|
||||||
|
#define OLED_ADDRESS 0x3C
|
||||||
|
|
||||||
|
// Display update interval (milliseconds)
|
||||||
|
#define DISPLAY_UPDATE_INTERVAL_MS 1000
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// LORA CONFIGURATION (for future remote sensor nodes)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// LoRa frequency (915 MHz for US)
|
||||||
|
#define LORA_FREQUENCY 915.0
|
||||||
|
|
||||||
|
// LoRa bandwidth (125 kHz)
|
||||||
|
#define LORA_BANDWIDTH 125.0
|
||||||
|
|
||||||
|
// LoRa spreading factor (7-12, higher = longer range but slower)
|
||||||
|
#define LORA_SPREADING_FACTOR 9
|
||||||
|
|
||||||
|
// LoRa coding rate (5-8)
|
||||||
|
#define LORA_CODING_RATE 7
|
||||||
|
|
||||||
|
// LoRa sync word (private network)
|
||||||
|
#define LORA_SYNC_WORD 0x12
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API RETRY CONFIGURATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Maximum retries for API calls
|
||||||
|
#define API_MAX_RETRIES 3
|
||||||
|
|
||||||
|
// Retry delay (milliseconds)
|
||||||
|
#define API_RETRY_DELAY_MS 5000
|
||||||
|
|
||||||
|
// HTTP timeout (milliseconds)
|
||||||
|
#define HTTP_TIMEOUT_MS 10000
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// BUFFER CONFIGURATION (for offline operation)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Maximum readings to buffer when offline
|
||||||
|
#define OFFLINE_BUFFER_SIZE 100
|
||||||
|
|
||||||
|
// Save buffer to flash on reboot
|
||||||
|
#define BUFFER_PERSIST_TO_FLASH true
|
||||||
|
|
||||||
|
#endif // CONFIG_H
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
; ZNET Temperature Sensor - ESP32 LoRa Gateway
|
||||||
|
;
|
||||||
|
; Board: Meshnology ESP32 LoRa V3 (Heltec-compatible)
|
||||||
|
; Features: DS18B20 temp sensors, OLED display, LoRa RX, WiFi TX
|
||||||
|
|
||||||
|
[env:heltec_wifi_lora_32_V3]
|
||||||
|
platform = espressif32
|
||||||
|
board = heltec_wifi_lora_32_V3
|
||||||
|
framework = arduino
|
||||||
|
|
||||||
|
; Serial monitor
|
||||||
|
monitor_speed = 115200
|
||||||
|
|
||||||
|
; Build flags
|
||||||
|
build_flags =
|
||||||
|
-DCORE_DEBUG_LEVEL=3
|
||||||
|
-DARDUINO_USB_MODE=1
|
||||||
|
; OLED display pins (Heltec V3)
|
||||||
|
-DOLED_SDA=17
|
||||||
|
-DOLED_SCL=18
|
||||||
|
-DOLED_RST=21
|
||||||
|
; LoRa pins (SX1262)
|
||||||
|
-DLORA_SCK=9
|
||||||
|
-DLORA_MISO=11
|
||||||
|
-DLORA_MOSI=10
|
||||||
|
-DLORA_SS=8
|
||||||
|
-DLORA_RST=12
|
||||||
|
-DLORA_DIO1=14
|
||||||
|
-DLORA_BUSY=13
|
||||||
|
; DS18B20 data pin
|
||||||
|
-DONEWIRE_PIN=7
|
||||||
|
|
||||||
|
; Libraries
|
||||||
|
lib_deps =
|
||||||
|
; OneWire for DS18B20
|
||||||
|
paulstoffregen/OneWire@^2.3.8
|
||||||
|
; Dallas Temperature library
|
||||||
|
milesburton/DallasTemperature@^3.11.0
|
||||||
|
; OLED display (SSD1306)
|
||||||
|
thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.6.1
|
||||||
|
; HTTP client (built-in, but explicit)
|
||||||
|
; JSON serialization
|
||||||
|
bblanchon/ArduinoJson@^7.2.1
|
||||||
|
; LoRa radio (RadioLib for SX1262)
|
||||||
|
jgromes/RadioLib@^7.1.2
|
||||||
|
; WiFi Manager for easy setup
|
||||||
|
tzapu/WiFiManager@^2.0.17
|
||||||
|
|
||||||
|
; Upload settings
|
||||||
|
upload_speed = 921600
|
||||||
+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