From 265a53768ea792457ff9002177287b6cd4e2bc58 Mon Sep 17 00:00:00 2001 From: leehughes Date: Sat, 24 Jan 2026 16:48:12 -0600 Subject: [PATCH] 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 --- .gitignore | 29 +++ CLAUDE.md | 153 +++++++++++++++ include/config.h | 100 ++++++++++ platformio.ini | 51 +++++ src/main.cpp | 486 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 819 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 include/config.h create mode 100644 platformio.ini create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b43d7a --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5530d80 --- /dev/null +++ b/CLAUDE.md @@ -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 | diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..ab2bed3 --- /dev/null +++ b/include/config.h @@ -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 diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..29331d9 --- /dev/null +++ b/platformio.ini @@ -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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9caf141 --- /dev/null +++ b/src/main.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#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(); + + // Sensor 1 + if (sensor1Valid) { + JsonObject r1 = readings.add(); + 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(); + 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(); +}