feat(wifi): v1.9.2 - WiFi resilience and credential persistence (#1)
## Problem
Previous versions would clear WiFi credentials on connection failure,
requiring manual reconfiguration via captive portal after each WiFi outage.
## Solution
- Never clear credentials on connection failure
- Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max)
- Fallback AP ("TJ56654-Setup") enabled after ~5 min of failed attempts
- Dual STA+AP mode allows configuration while still retrying connection
- AP automatically disabled when WiFi reconnects
## Changes
### WiFi Credential Handling
- Try SDK-stored credentials first (from WiFiManager)
- Fall back to EEPROM config if SDK credentials fail
- Fixes OTA update credential loss issue
### User Experience
- Display shows "No WiFi" with retry countdown instead of cryptic numbers
- "!" indicator in date line when WiFi is disconnected
- Clock continues running with last synced time during outage
### Network Activity (~50 requests/day)
| Service | Interval | Endpoint |
|---------|----------|----------|
| NTP | 1 hour | pool.ntp.org:123 (UDP) |
| Weather | 30 min | api.open-meteo.com (HTTP) |
| mDNS | continuous | 224.0.0.251 (multicast) |
Co-authored-by: Alex Petrochenko <petrochenko@life-pay.ru>
This commit is contained in:
co-authored by
Alex Petrochenko
parent
0f2860c25e
commit
c3bfaa26bc
@@ -1,73 +0,0 @@
|
|||||||
name: Build Firmware
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
release:
|
|
||||||
types: [ created ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Arduino CLI
|
|
||||||
uses: arduino/setup-arduino-cli@v1
|
|
||||||
|
|
||||||
- name: Install ESP8266 platform
|
|
||||||
run: |
|
|
||||||
arduino-cli core update-index
|
|
||||||
arduino-cli core install esp8266:esp8266
|
|
||||||
|
|
||||||
- name: Install libraries
|
|
||||||
run: |
|
|
||||||
arduino-cli lib install "Adafruit GFX Library"
|
|
||||||
arduino-cli lib install "Adafruit SSD1306"
|
|
||||||
arduino-cli lib install "NTPClient"
|
|
||||||
arduino-cli lib install "WiFiManager"
|
|
||||||
arduino-cli lib install "AsyncHTTPRequest_Generic"
|
|
||||||
arduino-cli lib install "ESPAsyncTCP"
|
|
||||||
|
|
||||||
- name: Compile firmware
|
|
||||||
run: |
|
|
||||||
arduino-cli compile --fqbn esp8266:esp8266:generic \
|
|
||||||
--build-property "build.flash_size=1M64" \
|
|
||||||
--build-property "build.flash_mode=dio" \
|
|
||||||
--output-dir build \
|
|
||||||
src/clock_ntp_ota_v1.9.ino
|
|
||||||
|
|
||||||
- name: Check firmware size
|
|
||||||
run: |
|
|
||||||
SIZE=$(stat -c%s "build/clock_ntp_ota_v1.9.ino.bin")
|
|
||||||
echo "Firmware size: $SIZE bytes"
|
|
||||||
MAX_SIZE=470000 # 470KB max for OTA
|
|
||||||
if [ $SIZE -gt $MAX_SIZE ]; then
|
|
||||||
echo "ERROR: Firmware too large ($SIZE > $MAX_SIZE)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: firmware
|
|
||||||
path: |
|
|
||||||
build/clock_ntp_ota_v1.9.ino.bin
|
|
||||||
build/clock_ntp_ota_v1.9.ino.elf
|
|
||||||
build/clock_ntp_ota_v1.9.ino.map
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Upload release assets
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
|
||||||
asset_path: build/clock_ntp_ota_v1.9.ino.bin
|
|
||||||
asset_name: esp8266-weather-clock-${{ github.event.release.tag_name }}.bin
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
build/
|
||||||
|
*.bak
|
||||||
|
*.bak2
|
||||||
File diff suppressed because it is too large
Load Diff
+190
-74
@@ -1,5 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* TJ-56-654 Weather Clock - Custom NTP Firmware with OTA v1.9.1
|
* TJ-56-654 Weather Clock - Custom NTP Firmware with OTA v1.9.2
|
||||||
|
*
|
||||||
|
* Version 1.9.2 Changes (WIFI RESILIENCE):
|
||||||
|
* - FIXED: No longer clears WiFi credentials on connection failure
|
||||||
|
* - IMPROVED: Infinite retry with exponential backoff (up to 5 min between attempts)
|
||||||
|
* - IMPROVED: Shows "No WiFi" status on display instead of cryptic numbers
|
||||||
|
* - IMPROVED: AP mode only on first boot (empty credentials) or manual trigger
|
||||||
|
* - IMPROVED: Clock continues running with last synced time during WiFi outage
|
||||||
*
|
*
|
||||||
* Version 1.9.1 Changes (HYBRID ASYNC FIX):
|
* Version 1.9.1 Changes (HYBRID ASYNC FIX):
|
||||||
* - FIXED STARTUP: WiFi now synchronous in setup() for proper initialization
|
* - FIXED STARTUP: WiFi now synchronous in setup() for proper initialization
|
||||||
@@ -69,7 +76,7 @@
|
|||||||
|
|
||||||
// Configuration structure with validation
|
// Configuration structure with validation
|
||||||
#define CONFIG_MAGIC 0xC10CC10C // Magic number to validate EEPROM data
|
#define CONFIG_MAGIC 0xC10CC10C // Magic number to validate EEPROM data
|
||||||
#define FIRMWARE_VERSION "1.9.1"
|
#define FIRMWARE_VERSION "1.9.2"
|
||||||
|
|
||||||
struct Config {
|
struct Config {
|
||||||
uint32_t magic = CONFIG_MAGIC; // Magic number for validation
|
uint32_t magic = CONFIG_MAGIC; // Magic number for validation
|
||||||
@@ -122,6 +129,7 @@ void ICACHE_FLASH_ATTR setupOTA();
|
|||||||
void ICACHE_FLASH_ATTR setupWebServer();
|
void ICACHE_FLASH_ATTR setupWebServer();
|
||||||
void ICACHE_FLASH_ATTR clearDisplay();
|
void ICACHE_FLASH_ATTR clearDisplay();
|
||||||
void ICACHE_FLASH_ATTR showNumber(int num, bool leadingZeros);
|
void ICACHE_FLASH_ATTR showNumber(int num, bool leadingZeros);
|
||||||
|
void ICACHE_FLASH_ATTR showNoWiFi(unsigned long nextRetrySeconds);
|
||||||
void ICACHE_FLASH_ATTR showStartupAnimation();
|
void ICACHE_FLASH_ATTR showStartupAnimation();
|
||||||
void ICACHE_FLASH_ATTR showIP();
|
void ICACHE_FLASH_ATTR showIP();
|
||||||
void ICACHE_FLASH_ATTR handleRoot();
|
void ICACHE_FLASH_ATTR handleRoot();
|
||||||
@@ -200,12 +208,14 @@ NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000);
|
|||||||
|
|
||||||
// Exponential backoff retry configuration
|
// Exponential backoff retry configuration
|
||||||
struct RetryConfig {
|
struct RetryConfig {
|
||||||
uint8_t maxRetries = 3;
|
uint8_t maxRetries = 3; // For NTP/Weather: give up after 3 tries
|
||||||
uint8_t currentRetry = 0;
|
uint8_t currentRetry = 0;
|
||||||
unsigned long nextRetryTime = 0;
|
unsigned long nextRetryTime = 0;
|
||||||
|
unsigned long maxBackoffMs = 8000; // Max backoff 8 seconds for NTP/Weather
|
||||||
|
|
||||||
unsigned long getBackoffDelay() {
|
unsigned long getBackoffDelay() {
|
||||||
return 1000UL * (1UL << currentRetry); // 1s, 2s, 4s, 8s...
|
unsigned long delay = 1000UL * (1UL << currentRetry); // 1s, 2s, 4s, 8s...
|
||||||
|
return (delay > maxBackoffMs) ? maxBackoffMs : delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
void scheduleRetry() {
|
void scheduleRetry() {
|
||||||
@@ -231,6 +241,33 @@ struct RetryConfig {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// WiFi retry configuration - infinite retries with longer backoff
|
||||||
|
struct WiFiRetryConfig {
|
||||||
|
uint8_t currentRetry = 0;
|
||||||
|
unsigned long nextRetryTime = 0;
|
||||||
|
static const unsigned long MAX_BACKOFF_MS = 300000; // Max 5 minutes between retries
|
||||||
|
|
||||||
|
unsigned long getBackoffDelay() {
|
||||||
|
// 5s, 10s, 20s, 40s, 80s, 160s, 300s (max)
|
||||||
|
unsigned long delay = 5000UL * (1UL << currentRetry);
|
||||||
|
return (delay > MAX_BACKOFF_MS) ? MAX_BACKOFF_MS : delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleRetry() {
|
||||||
|
nextRetryTime = millis() + getBackoffDelay();
|
||||||
|
if (currentRetry < 10) currentRetry++; // Cap at 10 to prevent overflow
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isRetryTime() {
|
||||||
|
return nextRetryTime > 0 && millis() >= nextRetryTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
currentRetry = 0;
|
||||||
|
nextRetryTime = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Async HTTP client for non-blocking weather fetch
|
// Async HTTP client for non-blocking weather fetch
|
||||||
AsyncHTTPRequest weatherRequest;
|
AsyncHTTPRequest weatherRequest;
|
||||||
|
|
||||||
@@ -278,6 +315,7 @@ const unsigned long WIFI_TIMEOUT_MS = 10000; // 10 second timeout for v1.7 migr
|
|||||||
// Retry configurations with exponential backoff
|
// Retry configurations with exponential backoff
|
||||||
RetryConfig ntpRetry;
|
RetryConfig ntpRetry;
|
||||||
RetryConfig weatherRetry;
|
RetryConfig weatherRetry;
|
||||||
|
WiFiRetryConfig wifiRetry;
|
||||||
|
|
||||||
// Web server
|
// Web server
|
||||||
ESP8266WebServer server(80);
|
ESP8266WebServer server(80);
|
||||||
@@ -398,20 +436,45 @@ void loop() {
|
|||||||
server.handleClient();
|
server.handleClient();
|
||||||
MDNS.update();
|
MDNS.update();
|
||||||
|
|
||||||
// WiFi reconnection logic (async, non-blocking)
|
// WiFi reconnection logic (async, non-blocking with exponential backoff)
|
||||||
// If WiFi disconnects during operation, try to reconnect asynchronously
|
|
||||||
static unsigned long lastWiFiCheck = 0;
|
static unsigned long lastWiFiCheck = 0;
|
||||||
if (millis() - lastWiFiCheck > 5000) { // Check every 5 seconds
|
if (millis() - lastWiFiCheck > 1000) { // Check every second
|
||||||
|
lastWiFiCheck = millis();
|
||||||
|
|
||||||
|
// If WiFi was connected but now disconnected
|
||||||
if (WiFi.status() != WL_CONNECTED && wifiConnState == WIFI_CONN_CONNECTED) {
|
if (WiFi.status() != WL_CONNECTED && wifiConnState == WIFI_CONN_CONNECTED) {
|
||||||
Serial.println("⚠️ WiFi disconnected, attempting async reconnect...");
|
Serial.println("⚠️ WiFi disconnected!");
|
||||||
WiFi.begin(); // Try to reconnect with saved credentials
|
wifiConnState = WIFI_CONN_FAILED;
|
||||||
|
internetConnected = false;
|
||||||
|
wifiRetry.reset(); // Start fresh retry sequence
|
||||||
|
wifiRetry.scheduleRetry(); // Schedule first retry
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's time to retry
|
||||||
|
if (wifiConnState == WIFI_CONN_FAILED && wifiRetry.isRetryTime()) {
|
||||||
|
Serial.printf("🔄 WiFi retry attempt (backoff level %d)...\n", wifiRetry.currentRetry);
|
||||||
|
|
||||||
|
// After 5+ failed attempts (~5 min), enable fallback AP in dual mode
|
||||||
|
if (wifiRetry.currentRetry >= 5 && WiFi.getMode() != WIFI_AP_STA) {
|
||||||
|
Serial.println("📡 Enabling fallback AP (dual mode) for manual configuration");
|
||||||
|
WiFi.mode(WIFI_AP_STA);
|
||||||
|
WiFi.softAP("TJ56654-Setup", "12345678");
|
||||||
|
Serial.print("Fallback AP IP: ");
|
||||||
|
Serial.println(WiFi.softAPIP());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try SDK-stored credentials first, then EEPROM
|
||||||
|
if (strlen(config.password) > 0) {
|
||||||
|
WiFi.begin(config.ssid, config.password);
|
||||||
|
} else {
|
||||||
|
WiFi.begin(); // Use SDK-stored credentials from WiFiManager
|
||||||
|
}
|
||||||
wifiConnState = WIFI_CONN_CONNECTING;
|
wifiConnState = WIFI_CONN_CONNECTING;
|
||||||
wifiConnectStart = millis();
|
wifiConnectStart = millis();
|
||||||
}
|
}
|
||||||
lastWiFiCheck = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process async WiFi reconnection (only if reconnecting, not during initial setup)
|
// Process async WiFi reconnection
|
||||||
processWiFiConnection();
|
processWiFiConnection();
|
||||||
|
|
||||||
// Process async NTP response (non-blocking check)
|
// Process async NTP response (non-blocking check)
|
||||||
@@ -635,63 +698,54 @@ void processWiFiConnection() {
|
|||||||
// Check connection status
|
// Check connection status
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
wifiConnState = WIFI_CONN_CONNECTED;
|
wifiConnState = WIFI_CONN_CONNECTED;
|
||||||
Serial.println("\n✅ WiFi connected (async)!");
|
wifiRetry.reset(); // Success! Reset retry counter
|
||||||
|
internetConnected = true;
|
||||||
|
Serial.println("\n✅ WiFi connected!");
|
||||||
Serial.print("SSID: ");
|
Serial.print("SSID: ");
|
||||||
Serial.println(WiFi.SSID());
|
Serial.println(WiFi.SSID());
|
||||||
Serial.print("IP: ");
|
Serial.print("IP: ");
|
||||||
Serial.println(WiFi.localIP());
|
Serial.println(WiFi.localIP());
|
||||||
|
|
||||||
// Sync connected SSID to config for display purposes
|
// Disable fallback AP if it was enabled (switch back to STA only)
|
||||||
|
if (WiFi.getMode() == WIFI_AP_STA) {
|
||||||
|
Serial.println("📡 Disabling fallback AP (back to STA mode)");
|
||||||
|
WiFi.softAPdisconnect(true);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync connected SSID to config for display purposes (don't clear on failure!)
|
||||||
|
if (strlen(config.ssid) == 0) {
|
||||||
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
|
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
// Show IP on display
|
// Show IP on display
|
||||||
showIP();
|
showIP();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check timeout
|
// Check timeout for this attempt
|
||||||
if (millis() - wifiConnectStart > WIFI_TIMEOUT_MS) {
|
if (millis() - wifiConnectStart > WIFI_TIMEOUT_MS) {
|
||||||
|
// Connection attempt failed - schedule retry with backoff
|
||||||
|
// DO NOT clear credentials!
|
||||||
|
wifiRetry.scheduleRetry();
|
||||||
|
unsigned long nextRetryMs = wifiRetry.getBackoffDelay();
|
||||||
|
|
||||||
|
Serial.printf("\n⚠️ WiFi connection failed. Retry in %lu seconds\n", nextRetryMs / 1000);
|
||||||
|
|
||||||
wifiConnState = WIFI_CONN_FAILED;
|
wifiConnState = WIFI_CONN_FAILED;
|
||||||
Serial.println("\n⚠️ WiFi async connection timeout");
|
internetConnected = false;
|
||||||
Serial.println("Falling back to WiFiManager...");
|
|
||||||
|
|
||||||
// Clear old credentials from config
|
// Show "No WiFi" status on display
|
||||||
config.ssid[0] = '\0';
|
showNoWiFi(nextRetryMs / 1000);
|
||||||
config.password[0] = '\0';
|
|
||||||
saveConfig();
|
|
||||||
|
|
||||||
// Fall back to WiFiManager
|
|
||||||
WiFiManager wifiManager;
|
|
||||||
wifiManager.setConfigPortalTimeout(180);
|
|
||||||
showNumber(0xAF, false);
|
|
||||||
|
|
||||||
Serial.println("Starting WiFiManager captive portal...");
|
|
||||||
if (!wifiManager.autoConnect("TJ56654-Setup", "12345678")) {
|
|
||||||
Serial.println("WiFiManager failed. Starting fallback AP...");
|
|
||||||
WiFi.mode(WIFI_AP);
|
|
||||||
WiFi.softAP("TJ56654-Clock", "12345678");
|
|
||||||
Serial.print("Fallback AP IP: ");
|
|
||||||
Serial.println(WiFi.softAPIP());
|
|
||||||
} else {
|
|
||||||
Serial.println("WiFiManager connected!");
|
|
||||||
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
|
|
||||||
saveConfig();
|
|
||||||
showIP();
|
|
||||||
}
|
|
||||||
|
|
||||||
wifiConnState = WIFI_CONN_CONNECTED; // Mark as handled
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still connecting, update display
|
// Still connecting, show progress dots in serial only (no display spam)
|
||||||
static uint8_t connectAttempts = 0;
|
static unsigned long lastDot = 0;
|
||||||
static unsigned long lastAttemptDisplay = 0;
|
if (millis() - lastDot > 500) {
|
||||||
if (millis() - lastAttemptDisplay > 500) {
|
|
||||||
Serial.print(".");
|
Serial.print(".");
|
||||||
showNumber(connectAttempts, false);
|
lastDot = millis();
|
||||||
connectAttempts++;
|
|
||||||
lastAttemptDisplay = millis();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,13 +756,14 @@ void ICACHE_FLASH_ATTR setupWiFi() {
|
|||||||
|
|
||||||
// Set hostname before connecting
|
// Set hostname before connecting
|
||||||
WiFi.hostname(config.hostname);
|
WiFi.hostname(config.hostname);
|
||||||
|
|
||||||
// MIGRATION FROM v1.7: If config has old credentials, try them SYNCHRONOUSLY
|
|
||||||
// This ensures OTA/web/NTP can initialize properly after WiFi is connected
|
|
||||||
if (strlen(config.ssid) > 0) {
|
|
||||||
Serial.println("Found saved credentials, connecting synchronously...");
|
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.begin(config.ssid, config.password);
|
|
||||||
|
// STRATEGY: First try SDK-stored credentials (from WiFiManager), then EEPROM config
|
||||||
|
// WiFiManager stores credentials in ESP flash separately from our EEPROM
|
||||||
|
|
||||||
|
// Try 1: Use WiFi.begin() without params - uses SDK stored credentials
|
||||||
|
Serial.println("Trying SDK-stored credentials...");
|
||||||
|
WiFi.begin(); // Uses credentials stored by WiFiManager/SDK
|
||||||
|
|
||||||
// SYNCHRONOUS wait for connection (max 10 seconds)
|
// SYNCHRONOUS wait for connection (max 10 seconds)
|
||||||
Serial.print("Connecting to WiFi");
|
Serial.print("Connecting to WiFi");
|
||||||
@@ -731,8 +786,9 @@ void ICACHE_FLASH_ATTR setupWiFi() {
|
|||||||
Serial.print("DNS: ");
|
Serial.print("DNS: ");
|
||||||
Serial.println(WiFi.dnsIP());
|
Serial.println(WiFi.dnsIP());
|
||||||
|
|
||||||
// Sync connected SSID to config
|
// Sync connected SSID to our config
|
||||||
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
|
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
|
||||||
|
// Note: password stays in SDK storage, we don't have access to it
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
|
||||||
// Show IP on display
|
// Show IP on display
|
||||||
@@ -740,17 +796,33 @@ void ICACHE_FLASH_ATTR setupWiFi() {
|
|||||||
|
|
||||||
wifiConnState = WIFI_CONN_CONNECTED;
|
wifiConnState = WIFI_CONN_CONNECTED;
|
||||||
return; // Success!
|
return; // Success!
|
||||||
} else {
|
}
|
||||||
Serial.println("\n⚠️ WiFi connection failed after 10 seconds");
|
|
||||||
// Clear old credentials and fall through to WiFiManager
|
// Try 2: If we have EEPROM credentials, try those
|
||||||
config.ssid[0] = '\0';
|
if (strlen(config.ssid) > 0 && strlen(config.password) > 0) {
|
||||||
config.password[0] = '\0';
|
Serial.println("\nTrying EEPROM credentials...");
|
||||||
saveConfig();
|
WiFi.begin(config.ssid, config.password);
|
||||||
|
|
||||||
|
attempts = 0;
|
||||||
|
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
showNumber(attempts, false);
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
Serial.println("\n✅ WiFi connected via EEPROM credentials!");
|
||||||
|
showIP();
|
||||||
|
wifiConnState = WIFI_CONN_CONNECTED;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No v1.7 credentials - use WiFiManager (blocking, but only on first boot)
|
// Both attempts failed - check if we have ANY stored credentials
|
||||||
Serial.println("No saved credentials, using WiFiManager...");
|
// If not, use WiFiManager for initial setup
|
||||||
|
if (strlen(config.ssid) == 0) {
|
||||||
|
Serial.println("\nNo saved credentials, using WiFiManager...");
|
||||||
WiFiManager wifiManager;
|
WiFiManager wifiManager;
|
||||||
wifiManager.setConfigPortalTimeout(180); // 3 minutes timeout
|
wifiManager.setConfigPortalTimeout(180); // 3 minutes timeout
|
||||||
|
|
||||||
@@ -776,10 +848,6 @@ void ICACHE_FLASH_ATTR setupWiFi() {
|
|||||||
Serial.println(WiFi.SSID());
|
Serial.println(WiFi.SSID());
|
||||||
Serial.print("IP: ");
|
Serial.print("IP: ");
|
||||||
Serial.println(WiFi.localIP());
|
Serial.println(WiFi.localIP());
|
||||||
Serial.print("Gateway: ");
|
|
||||||
Serial.println(WiFi.gatewayIP());
|
|
||||||
Serial.print("DNS: ");
|
|
||||||
Serial.println(WiFi.dnsIP());
|
|
||||||
|
|
||||||
// Sync connected SSID to config for display purposes
|
// Sync connected SSID to config for display purposes
|
||||||
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
|
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
|
||||||
@@ -787,7 +855,15 @@ void ICACHE_FLASH_ATTR setupWiFi() {
|
|||||||
|
|
||||||
// Show IP on display
|
// Show IP on display
|
||||||
showIP();
|
showIP();
|
||||||
wifiConnState = WIFI_CONN_CONNECTED; // Mark as handled
|
wifiConnState = WIFI_CONN_CONNECTED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have credentials but WiFi is not available - will retry in loop()
|
||||||
|
Serial.println("\n⚠️ WiFi not available. Will retry in background.");
|
||||||
|
wifiConnState = WIFI_CONN_FAILED;
|
||||||
|
wifiRetry.scheduleRetry();
|
||||||
|
showNoWiFi(wifiRetry.getBackoffDelay() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// OTA setup
|
// OTA setup
|
||||||
@@ -866,7 +942,7 @@ void ICACHE_FLASH_ATTR setupWebServer() {
|
|||||||
|
|
||||||
// Update OLED display with current time
|
// Update OLED display with current time
|
||||||
// BLUE zone (top 48px): Large time
|
// BLUE zone (top 48px): Large time
|
||||||
// YELLOW zone (bottom 16px): Date
|
// YELLOW zone (bottom 16px): Date + WiFi status
|
||||||
void updateDisplay() {
|
void updateDisplay() {
|
||||||
static unsigned long lastUpdate = 0;
|
static unsigned long lastUpdate = 0;
|
||||||
|
|
||||||
@@ -876,17 +952,29 @@ void updateDisplay() {
|
|||||||
|
|
||||||
display.clearDisplay();
|
display.clearDisplay();
|
||||||
|
|
||||||
if (!timeClient.isTimeSet()) {
|
// Check if we have ANY time source (NTPClient or async sync)
|
||||||
|
bool hasTime = timeClient.isTimeSet() || timeIsSynced;
|
||||||
|
|
||||||
|
if (!hasTime) {
|
||||||
display.setTextSize(3);
|
display.setTextSize(3);
|
||||||
display.setTextColor(SSD1306_WHITE);
|
display.setTextColor(SSD1306_WHITE);
|
||||||
display.setCursor(20, 24);
|
display.setCursor(20, 24);
|
||||||
display.println("--:--");
|
display.println("--:--");
|
||||||
|
|
||||||
|
// Show WiFi status in yellow zone
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.setCursor(25, 52);
|
||||||
|
if (wifiConnState == WIFI_CONN_CONNECTED) {
|
||||||
|
display.print("Syncing NTP...");
|
||||||
|
} else {
|
||||||
|
display.print("No WiFi");
|
||||||
|
}
|
||||||
display.display();
|
display.display();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate local time with DST
|
// Get epoch from best available source
|
||||||
unsigned long epochTime = timeClient.getEpochTime();
|
unsigned long epochTime = timeIsSynced ? getAsyncEpoch() : timeClient.getEpochTime();
|
||||||
unsigned long localTime = epochTime + getTotalOffset(epochTime);
|
unsigned long localTime = epochTime + getTotalOffset(epochTime);
|
||||||
|
|
||||||
int hours = (localTime / 3600) % 24;
|
int hours = (localTime / 3600) % 24;
|
||||||
@@ -903,10 +991,16 @@ void updateDisplay() {
|
|||||||
time_t t = epochTime;
|
time_t t = epochTime;
|
||||||
struct tm *ptm = gmtime(&t);
|
struct tm *ptm = gmtime(&t);
|
||||||
|
|
||||||
// Format: "Thu 02.01"
|
// Format: "Thu 02.01" or "! Thu 02.01" if no WiFi
|
||||||
const char* days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
const char* days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
||||||
char dateStr[16];
|
char dateStr[20];
|
||||||
|
|
||||||
|
if (wifiConnState != WIFI_CONN_CONNECTED) {
|
||||||
|
// Show "!" indicator when WiFi is down
|
||||||
|
sprintf(dateStr, "!%s %02d.%02d", days[ptm->tm_wday], ptm->tm_mday, ptm->tm_mon + 1);
|
||||||
|
} else {
|
||||||
sprintf(dateStr, "%s %02d.%02d", days[ptm->tm_wday], ptm->tm_mday, ptm->tm_mon + 1);
|
sprintf(dateStr, "%s %02d.%02d", days[ptm->tm_wday], ptm->tm_mday, ptm->tm_mon + 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Center in yellow zone (Y: 48-63)
|
// Center in yellow zone (Y: 48-63)
|
||||||
int dateWidth = strlen(dateStr) * 12; // Size 2 = ~12px per char
|
int dateWidth = strlen(dateStr) * 12; // Size 2 = ~12px per char
|
||||||
@@ -1109,6 +1203,28 @@ void ICACHE_FLASH_ATTR showNumber(int num, bool leadingZeros) {
|
|||||||
display.display();
|
display.display();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show "No WiFi" status on OLED with retry countdown
|
||||||
|
void ICACHE_FLASH_ATTR showNoWiFi(unsigned long nextRetrySeconds) {
|
||||||
|
display.clearDisplay();
|
||||||
|
|
||||||
|
// === BLUE ZONE: "No WiFi" message ===
|
||||||
|
display.setTextSize(2);
|
||||||
|
display.setTextColor(SSD1306_WHITE);
|
||||||
|
display.setCursor(20, 8);
|
||||||
|
display.println("No WiFi");
|
||||||
|
|
||||||
|
// === YELLOW ZONE: Retry info ===
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.setCursor(10, 52);
|
||||||
|
if (nextRetrySeconds < 60) {
|
||||||
|
display.printf("Retry in %lu sec", nextRetrySeconds);
|
||||||
|
} else {
|
||||||
|
display.printf("Retry in %lu min", nextRetrySeconds / 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
// Dummy function for compatibility (not needed for OLED)
|
// Dummy function for compatibility (not needed for OLED)
|
||||||
void displaySegments(const uint8_t segments[]) {
|
void displaySegments(const uint8_t segments[]) {
|
||||||
// Not used with OLED - kept for compatibility
|
// Not used with OLED - kept for compatibility
|
||||||
|
|||||||
Reference in New Issue
Block a user