STA모드 추가

This commit is contained in:
2026-02-18 15:49:30 +00:00
parent 9bb14219af
commit 0baac2bf90
9 changed files with 794 additions and 150 deletions

View File

@@ -1,6 +1,6 @@
// ============================================================
// ESP32 Serial Logger
// Hardware: ESP-WROOM-32D DevKitC V4
// Hardware: ESP-WROOM-32D DevKitC V4 + DS3231 RTC
//
// Arduino IDE Settings:
// Board: "ESP32 Dev Module"
@@ -13,16 +13,19 @@
// PSRAM: Disabled
//
// Required Libraries:
// 1. WebSocketsServer by Links2004
// (Install: Library Manager -> "WebSockets" by Markus Sattler)
// 2. ArduinoJson by Benoit Blanchon v6.x
// (Install: Library Manager -> "ArduinoJson")
// 3. SD (built-in)
// 4. SPI (built-in)
// 1. WebSockets by Links2004
// 2. ArduinoJson v6.x
// 3. SoftWire by Steve Marple (+ AsyncDelay)
// 4. SD, SPI (built-in)
//
// Pin Assignments:
// UART2 TX: GPIO17 | UART2 RX: GPIO16
// SD CLK: GPIO14 | SD MISO: GPIO26 | SD MOSI: GPIO13 | SD CS: GPIO15
// SD MISO: GPIO19 | SD MOSI: GPIO23 | SD SCLK: GPIO18 | SD CS: GPIO5 (VSPI)
// RTC SDA: GPIO25 | RTC SCL: GPIO26 (DS3231 @ 0x68, SoftWire)
//
// WiFi:
// Boot → AP mode (always available)
// Settings → STA mode enable (AP+STA dual mode)
// ============================================================
#include <Arduino.h>
@@ -33,6 +36,7 @@
#include <ArduinoJson.h>
#include <SPI.h>
#include <SD.h>
#include <SoftWire.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
@@ -44,78 +48,119 @@
#include "serial_task.h"
#include "sdcard_task.h"
#include "web_task.h"
#include "rtc_task.h"
// --- WiFi STA state (controlled from web UI) ---
volatile bool staEnabled = false;
volatile bool staConnected = false;
char staSSID[64] = "";
char staPW[64] = "";
// ============================================================
// NTP Time Sync Task
// Only works in STA mode (internet required)
// In AP mode, time is synced from browser via WebSocket
// Enable STA mode (AP+STA dual) - called from web_task
// ============================================================
void ntpSyncTask(void *param) {
Serial.println("[Task] NTP Sync started");
bool wifiEnableSTA(const char *ssid, const char *password) {
if (!ssid || strlen(ssid) == 0) return false;
// Only attempt NTP if connected to external WiFi (STA mode)
if (WiFi.getMode() == WIFI_AP) {
Serial.println("[NTP] AP mode - skipping NTP, waiting for browser time sync");
// In AP mode, just keep task alive for potential future use
while (true) {
vTaskDelay(pdMS_TO_TICKS(60000));
}
strncpy(staSSID, ssid, sizeof(staSSID) - 1);
strncpy(staPW, password, sizeof(staPW) - 1);
Serial.printf("[WiFi] Enabling STA: SSID='%s'\n", staSSID);
// Switch to AP+STA dual mode
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
WiFi.begin(staSSID, staPW);
WiFi.setSleep(false);
staEnabled = true;
// Wait for connection (non-blocking, with timeout)
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
delay(500);
Serial.print(".");
}
Serial.println();
// STA mode - try NTP
configTime(NTP_GMT_OFFSET, NTP_DAYLIGHT_OFFSET, NTP_SERVER);
if (WiFi.status() == WL_CONNECTED) {
staConnected = true;
Serial.printf("[WiFi] STA connected! IP: %s (RSSI: %d)\n",
WiFi.localIP().toString().c_str(), WiFi.RSSI());
Serial.printf("[WiFi] AP still active: %s (%s)\n",
WIFI_AP_SSID, WiFi.softAPIP().toString().c_str());
struct tm timeinfo;
int retries = 0;
while (!getLocalTime(&timeinfo) && retries < 5) {
Serial.println("[NTP] Waiting for time sync...");
vTaskDelay(pdMS_TO_TICKS(1000));
retries++;
}
if (getLocalTime(&timeinfo)) {
Serial.printf("[NTP] Synced: %04d-%02d-%02d %02d:%02d:%02d\n",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
} else {
Serial.println("[NTP] Failed - will use browser time when client connects");
}
// Periodic re-sync every 1 hour
while (true) {
vTaskDelay(pdMS_TO_TICKS(3600000));
// STA 연결 성공 → NTP 시도
configTime(NTP_GMT_OFFSET, NTP_DAYLIGHT_OFFSET, NTP_SERVER);
Serial.println("[NTP] Time re-synced");
return true;
} else {
staConnected = false;
Serial.println("[WiFi] STA connection failed (will keep retrying)");
return false;
}
}
// ============================================================
// WiFi Connection (STA + fallback AP mode)
// Disable STA mode - return to AP only
// ============================================================
void wifiConnect() {
Serial.println("[WiFi] Connecting to: " + String(WIFI_SSID));
void wifiDisableSTA() {
Serial.println("[WiFi] Disabling STA, AP-only mode");
WiFi.disconnect(true);
delay(100);
WiFi.mode(WIFI_AP);
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
WiFi.setSleep(false);
staEnabled = false;
staConnected = false;
staSSID[0] = '\0';
staPW[0] = '\0';
unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (millis() - startTime > WIFI_CONNECT_TIMEOUT) {
Serial.println("\n[WiFi] STA failed! Starting AP mode...");
WiFi.mode(WIFI_AP);
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
Serial.println("[WiFi] AP SSID: " + String(WIFI_AP_SSID));
Serial.println("[WiFi] AP IP: " + WiFi.softAPIP().toString());
return;
Serial.printf("[WiFi] AP mode: %s (%s)\n",
WIFI_AP_SSID, WiFi.softAPIP().toString().c_str());
}
// ============================================================
// NTP + STA Monitor Task
// Periodically checks STA status, retries connection, syncs NTP
// ============================================================
void ntpSyncTask(void *param) {
Serial.println("[Task] NTP/WiFi monitor started");
vTaskDelay(pdMS_TO_TICKS(3000));
while (true) {
// If STA is enabled, monitor connection
if (staEnabled) {
if (WiFi.status() == WL_CONNECTED) {
if (!staConnected) {
staConnected = true;
Serial.printf("[WiFi] STA reconnected: %s\n",
WiFi.localIP().toString().c_str());
}
// Try NTP sync
struct tm timeinfo;
configTime(NTP_GMT_OFFSET, NTP_DAYLIGHT_OFFSET, NTP_SERVER);
if (getLocalTime(&timeinfo, 3000)) {
rtcSyncFromSystem();
static uint32_t ntpCount = 0;
if (++ntpCount % 60 == 1) { // Log every ~60 cycles
Serial.printf("[NTP] Synced: %04d-%02d-%02d %02d:%02d:%02d\n",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
}
}
} else {
staConnected = false;
// Auto-retry connection
if (strlen(staSSID) > 0) {
WiFi.begin(staSSID, staPW);
}
}
}
}
Serial.println();
Serial.println("[WiFi] Connected! IP: " + WiFi.localIP().toString());
Serial.println("[WiFi] RSSI: " + String(WiFi.RSSI()) + " dBm");
vTaskDelay(pdMS_TO_TICKS(60000)); // Check every 60s
}
}
// ============================================================
@@ -126,65 +171,85 @@ void setup() {
delay(1000);
Serial.println("============================================");
Serial.println(" ESP32 Serial Logger v1.0");
Serial.println(" ESP-WROOM-32D DevKitC V4");
Serial.println(" ESP32 Serial Logger v2.1");
Serial.println(" ESP-WROOM-32D DevKitC V4 + DS3231");
Serial.println("============================================");
Serial.printf(" Free Heap: %d bytes\n", ESP.getFreeHeap());
Serial.printf(" CPU Freq: %d MHz\n", getCpuFrequencyMhz());
Serial.println("============================================");
Serial.println("============================================\n");
// 1. WiFi
wifiConnect();
// 1. DS3231 RTC
Serial.println("--- Step 1: RTC ---");
bool hasRTC = rtcInit();
// 2. NTP (STA mode only) + Browser time sync (AP mode)
xTaskCreatePinnedToCore(ntpSyncTask, "NTP", TASK_STACK_NTP,
NULL, TASK_PRIORITY_NTP, NULL, 0);
if (WiFi.getMode() != WIFI_AP) {
delay(2000); // Brief wait for NTP only in STA mode
// 2. WiFi - Always start AP mode (immediate access)
Serial.println("\n--- Step 2: WiFi (AP mode) ---");
WiFi.mode(WIFI_AP);
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
Serial.printf("[WiFi] AP SSID: %s\n", WIFI_AP_SSID);
Serial.printf("[WiFi] AP IP: %s\n", WiFi.softAPIP().toString().c_str());
Serial.println("[WiFi] STA: disabled (enable via Settings page)");
// 3. Time status
Serial.println("\n--- Step 3: Time ---");
if (hasRTC && rtcStatus.timeSynced) {
Serial.println("[Time] Using RTC time");
} else {
Serial.println("[Time] AP mode: time will sync from browser on first connect");
Serial.println("[Time] Waiting for browser sync");
}
// 3. SD Card
// 4. SD Card
Serial.println("\n--- Step 4: SD Card ---");
sdTaskInit();
// 4. Serial2 UART
// 5. Serial2 UART
Serial.println("\n--- Step 5: Serial2 ---");
serialTaskInit();
// 5. Web Server + WebSocket
// 6. Web Server + WebSocket
Serial.println("\n--- Step 6: Web Server ---");
webTaskInit();
Serial.println("============================================");
Serial.println(" All tasks started!");
Serial.printf(" Free Heap: %d bytes\n", ESP.getFreeHeap());
// 7. RTC periodic sync task
rtcTaskInit();
String ip = (WiFi.getMode() == WIFI_AP) ?
WiFi.softAPIP().toString() : WiFi.localIP().toString();
Serial.println(" URL: http://" + ip);
// 8. NTP + WiFi monitor task (always running)
xTaskCreatePinnedToCore(ntpSyncTask, "NTP", TASK_STACK_NTP,
NULL, TASK_PRIORITY_NTP, NULL, 0);
// Status summary
Serial.println("\n============================================");
Serial.println(" All systems initialized!");
Serial.printf(" Free Heap: %d bytes\n", ESP.getFreeHeap());
Serial.printf(" AP URL: http://%s\n", WiFi.softAPIP().toString().c_str());
Serial.println("============================================");
Serial.println("\n Task Layout:");
Serial.println(" Core 1: SerialRX(5), SerialTX(4)");
Serial.println(" Core 0: SDLog(3), WebBC(2), NTP(1)");
Serial.println(" Core 0: SDLog(3), WebBC(2), RTC(1), NTP(1)");
Serial.println(" Loop : server.handleClient()");
Serial.println(" Time : NTP(STA) or Browser sync(AP)");
Serial.printf(" RTC: %s | Time: %s\n",
rtcStatus.available ? "OK" : "NO",
rtcStatus.timeSynced ? "Valid" : "Pending");
Serial.println("============================================\n");
}
// ============================================================
// Loop - handles HTTP requests (must be on main loop)
// Loop
// ============================================================
void loop() {
server.handleClient();
vTaskDelay(pdMS_TO_TICKS(2));
// Periodic status report (30s)
static unsigned long lastReport = 0;
if (millis() - lastReport > 30000) {
lastReport = millis();
Serial.printf("[SYS] Heap: %d | MinHeap: %d | SD: %s | WS: %d clients\n",
char timeBuf[24];
getTimestamp(timeBuf, sizeof(timeBuf));
Serial.printf("[SYS] %s | Heap:%d | SD:%s | WS:%d | RTC:%s | STA:%s\n",
timeBuf,
ESP.getFreeHeap(),
ESP.getMinFreeHeap(),
sdCardPresent() ? "OK" : "FAIL",
webSocket.connectedClients());
webSocket.connectedClients(),
rtcStatus.available ? "OK" : "NO",
staConnected ? "ON" : "OFF");
}
}

81
README.md Normal file
View File

@@ -0,0 +1,81 @@
# ESP32 Serial Logger v2.0
UART2 시리얼 모니터링 + SD카드 로깅 + WiFi 웹 인터페이스 + DS3231 RTC
## 하드웨어
- **보드:** ESP-WROOM-32D DevKitC V4
- **RTC:** DS3231 모듈 (I2C, ±2ppm 정밀도)
- **SD카드:** SPI 방식 MicroSD 모듈
## 핀 연결
```
ESP32 대상장치 (UART)
GPIO17 (TX) ──────────── RX
GPIO16 (RX) ──────────── TX
GND ──────────────────── GND
ESP32 SD Card Module (VSPI)
GPIO18 ──────────────── SCLK
GPIO19 ──────────────── MISO
GPIO23 ──────────────── MOSI
GPIO5 ──────────────── CS
3.3V ──────────────── VCC
GND ──────────────── GND
ESP32 DS3231 RTC Module (SoftWire I2C)
GPIO25 ──────────────── SDA
GPIO26 ──────────────── SCL
3.3V ──────────────── VCC
GND ──────────────── GND
```
> SoftWire 사용으로 임의 GPIO에 DS3231 연결 가능. GPIO 6~11은 SPI Flash 전용.
## Arduino IDE 설정
| 설정 | 값 |
|---|---|
| Board | ESP32 Dev Module |
| Upload Speed | 921600 |
| CPU Frequency | 240MHz (WiFi/BT) |
| Flash Frequency | 80MHz |
| Flash Mode | QIO |
| Flash Size | 4MB (32Mb) |
| Partition Scheme | Huge APP (3MB No OTA/1MB SPIFFS) |
| PSRAM | Disabled |
## 필수 라이브러리
| 라이브러리 | 설치 |
|---|---|
| WebSockets by Markus Sattler (Links2004) | Library Manager |
| ArduinoJson v6.x | Library Manager |
| SoftWire by Steve Marple | Library Manager → "SoftWire" |
| AsyncDelay by Steve Marple | Library Manager (SoftWire 의존) |
| SD, SPI | 설치 불필요 (내장) |
## 시간 동기화 전략 (장기 로깅용)
```
시간 우선순위:
1. NTP (STA 모드, 1시간마다 자동 보정)
2. 브라우저 시간 (WebSocket 접속 시 자동 전송)
3. DS3231 RTC (부팅 즉시 + 60초마다 드리프트 보정)
```
### 동작 흐름
1. 부팅 → DS3231에서 시간 읽어 시스템 시계 즉시 설정
2. 핸드폰 접속 → 브라우저 시간 → 시스템 시계 + DS3231 동시 설정
3. WiFi 끊김/장기 방치 → 60초마다 DS3231 → 시스템 시계 보정
4. STA 모드 → NTP 성공 시 RTC도 자동 업데이트
## FreeRTOS 태스크 구조
```
Core 1: SerialRX(Pri:5), SerialTX(Pri:4)
Core 0: SDLog(Pri:3), WebBroadcast(Pri:2), RTC(Pri:1), NTP(Pri:1)
Loop: server.handleClient()
```
## 로그 파일
- 경로: /logs/LOG_YYYYMMDD_HHMMSS.csv
- 형식: "Timestamp","Direction","Data"

View File

@@ -21,22 +21,31 @@
#define DEFAULT_BAUD_RATE 115200
#define DEFAULT_RX_BUFFER 4096
// --- HSPI SD Card Pin Configuration ---
#define SD_HSPI_CLK 14
#define SD_HSPI_MISO 26 // Remapped from GPIO12 (strapping pin)
#define SD_HSPI_MOSI 13
#define SD_HSPI_CS 15
// --- VSPI SD Card Pin Configuration ---
#define SD_VSPI_MISO 19
#define SD_VSPI_MOSI 23
#define SD_VSPI_SCLK 18
#define SD_VSPI_CS 5
// --- DS3231 RTC I2C Configuration (SoftWire) ---
// 소프트웨어 I2C → 임의 GPIO 사용 가능
#define RTC_SDA_PIN 25 // I2C SDA
#define RTC_SCL_PIN 26 // I2C SCL
#define DS3231_ADDRESS 0x68 // DS3231 I2C Address
#define RTC_SYNC_INTERVAL_MS 60000 // RTC → System clock 보정 주기 (60초)
// --- FreeRTOS Task Priorities ---
#define TASK_PRIORITY_SERIAL 5 // Highest - must not miss data
#define TASK_PRIORITY_SD_LOG 3 // Medium-high - buffered writes
#define TASK_PRIORITY_WEB 2 // Medium - user interface
#define TASK_PRIORITY_NTP 1 // Lowest - periodic sync
#define TASK_PRIORITY_RTC 1 // Low - periodic RTC sync
#define TASK_PRIORITY_NTP 1 // Lowest - periodic NTP sync
// --- FreeRTOS Task Stack Sizes ---
#define TASK_STACK_SERIAL 4096
#define TASK_STACK_SD_LOG 8192
#define TASK_STACK_WEB 8192
#define TASK_STACK_RTC 3072
#define TASK_STACK_NTP 4096
// --- Queue Configuration ---

272
rtc_task.cpp Normal file
View File

@@ -0,0 +1,272 @@
#include "rtc_task.h"
#include <sys/time.h>
#include <time.h>
RTCStatus rtcStatus = { false, false, 0, 0.0 };
SemaphoreHandle_t rtcMutex = NULL;
// SoftWire instance + required buffers
static SoftWire rtcWire(RTC_SDA_PIN, RTC_SCL_PIN);
static char swTxBuffer[16];
static char swRxBuffer[16];
// BCD conversion
static uint8_t bcd2dec(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); }
static uint8_t dec2bcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }
// ============================================================
// Initialize SoftWire I2C and probe DS3231
// ============================================================
bool rtcInit() {
rtcMutex = xSemaphoreCreateMutex();
rtcWire.setTxBuffer(swTxBuffer, sizeof(swTxBuffer));
rtcWire.setRxBuffer(swRxBuffer, sizeof(swRxBuffer));
rtcWire.setDelay_us(5);
rtcWire.begin();
// Probe DS3231 at address 0x68
rtcWire.beginTransmission(DS3231_ADDRESS);
uint8_t err = rtcWire.endTransmission();
if (err == 0) {
rtcStatus.available = true;
Serial.printf("[RTC] DS3231 found at 0x%02X (SDA=%d, SCL=%d) [SoftWire]\n",
DS3231_ADDRESS, RTC_SDA_PIN, RTC_SCL_PIN);
// Read temperature to verify communication
rtcStatus.temperature = rtcReadTemperature();
Serial.printf("[RTC] Temperature: %.2f C\n", rtcStatus.temperature);
// Check if RTC has valid time (year >= 2024)
time_t t = rtcReadTime();
struct tm tm;
localtime_r(&t, &tm);
if (tm.tm_year + 1900 >= 2024) {
rtcStatus.timeSynced = true;
Serial.printf("[RTC] Valid time: %04d-%02d-%02d %02d:%02d:%02d\n",
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
// Boot with RTC time immediately
rtcSyncToSystem();
Serial.println("[RTC] System clock set from RTC on boot");
} else {
Serial.println("[RTC] No valid time stored - waiting for browser sync");
}
// Disable DS3231 square wave, enable battery backup
xSemaphoreTake(rtcMutex, portMAX_DELAY);
rtcWire.beginTransmission(DS3231_ADDRESS);
rtcWire.write(0x0E); // Control register
rtcWire.write(0x04); // INTCN=1, no SQW
rtcWire.endTransmission();
xSemaphoreGive(rtcMutex);
return true;
} else {
rtcStatus.available = false;
Serial.printf("[RTC] DS3231 NOT found (err=%d). Running without RTC.\n", err);
return false;
}
}
// ============================================================
// Read time from DS3231 → epoch
// ============================================================
time_t rtcReadTime() {
if (!rtcStatus.available) return 0;
xSemaphoreTake(rtcMutex, portMAX_DELAY);
rtcWire.beginTransmission(DS3231_ADDRESS);
rtcWire.write(0x00);
rtcWire.endTransmission();
rtcWire.requestFrom((uint8_t)DS3231_ADDRESS, (uint8_t)7);
if (rtcWire.available() < 7) {
xSemaphoreGive(rtcMutex);
return 0;
}
uint8_t sec = bcd2dec(rtcWire.read() & 0x7F);
uint8_t min = bcd2dec(rtcWire.read());
uint8_t hour = bcd2dec(rtcWire.read() & 0x3F);
rtcWire.read(); // day of week (skip)
uint8_t date = bcd2dec(rtcWire.read());
uint8_t raw5 = rtcWire.read();
uint8_t month = bcd2dec(raw5 & 0x1F);
uint8_t year = bcd2dec(rtcWire.read());
xSemaphoreGive(rtcMutex);
uint16_t fullYear = 2000 + year;
if (raw5 & 0x80) fullYear += 100;
struct tm tm;
tm.tm_sec = sec;
tm.tm_min = min;
tm.tm_hour = hour;
tm.tm_mday = date;
tm.tm_mon = month - 1;
tm.tm_year = fullYear - 1900;
tm.tm_isdst = 0;
time_t epoch = mktime(&tm);
return epoch;
}
// ============================================================
// Write time to DS3231 from epoch
// ============================================================
bool rtcWriteTime(time_t epoch) {
if (!rtcStatus.available) return false;
struct tm tm;
localtime_r(&epoch, &tm);
xSemaphoreTake(rtcMutex, portMAX_DELAY);
rtcWire.beginTransmission(DS3231_ADDRESS);
rtcWire.write(0x00);
rtcWire.write(dec2bcd(tm.tm_sec));
rtcWire.write(dec2bcd(tm.tm_min));
rtcWire.write(dec2bcd(tm.tm_hour));
rtcWire.write(dec2bcd(tm.tm_wday + 1));
rtcWire.write(dec2bcd(tm.tm_mday));
rtcWire.write(dec2bcd(tm.tm_mon + 1));
rtcWire.write(dec2bcd((tm.tm_year + 1900) % 100));
uint8_t err = rtcWire.endTransmission();
xSemaphoreGive(rtcMutex);
if (err == 0) {
rtcStatus.timeSynced = true;
Serial.printf("[RTC] Written: %04d-%02d-%02d %02d:%02d:%02d\n",
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
return true;
}
Serial.printf("[RTC] Write FAILED (err=%d)\n", err);
return false;
}
// ============================================================
// Sync: RTC → ESP32 System Clock
// ============================================================
bool rtcSyncToSystem() {
if (!rtcStatus.available || !rtcStatus.timeSynced) return false;
time_t rtcEpoch = rtcReadTime();
if (rtcEpoch == 0) return false;
struct timeval tv;
tv.tv_sec = rtcEpoch;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
rtcStatus.syncCount++;
struct tm tm;
localtime_r(&rtcEpoch, &tm);
Serial.printf("[RTC->SYS] Sync #%u: %04d-%02d-%02d %02d:%02d:%02d\n",
rtcStatus.syncCount,
tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
return true;
}
// ============================================================
// Sync: ESP32 System Clock → RTC
// ============================================================
bool rtcSyncFromSystem() {
struct timeval tv;
gettimeofday(&tv, NULL);
if (tv.tv_sec < 1700000000) {
Serial.println("[RTC] System time not valid, skipping write to RTC");
return false;
}
bool ok = rtcWriteTime(tv.tv_sec);
if (ok) {
Serial.println("[RTC] System time -> RTC written OK");
}
return ok;
}
// ============================================================
// Read DS3231 internal temperature sensor
// ============================================================
float rtcReadTemperature() {
if (!rtcStatus.available) return -999.0;
xSemaphoreTake(rtcMutex, portMAX_DELAY);
rtcWire.beginTransmission(DS3231_ADDRESS);
rtcWire.write(0x11);
rtcWire.endTransmission();
rtcWire.requestFrom((uint8_t)DS3231_ADDRESS, (uint8_t)2);
if (rtcWire.available() < 2) {
xSemaphoreGive(rtcMutex);
return -999.0;
}
int8_t msb = (int8_t)rtcWire.read();
uint8_t lsb = rtcWire.read();
xSemaphoreGive(rtcMutex);
float temp = (float)msb + ((lsb >> 6) * 0.25f);
return temp;
}
// ============================================================
// RTC Periodic Sync Task
// ============================================================
void rtcSyncTask(void *param) {
Serial.println("[Task] RTC Sync started on core " + String(xPortGetCoreID()));
vTaskDelay(pdMS_TO_TICKS(5000));
while (true) {
vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS));
if (!rtcStatus.available || !rtcStatus.timeSynced) continue;
struct timeval sysTv;
gettimeofday(&sysTv, NULL);
time_t rtcEpoch = rtcReadTime();
if (rtcEpoch == 0) continue;
int32_t drift = (int32_t)(sysTv.tv_sec - rtcEpoch);
if (abs(drift) > 2) {
Serial.printf("[RTC] Drift detected: %+d sec -> correcting\n", drift);
rtcSyncToSystem();
} else {
static uint32_t logCounter = 0;
if (++logCounter % 10 == 0) {
rtcStatus.temperature = rtcReadTemperature();
Serial.printf("[RTC] OK (drift=%+ds, temp=%.1fC, syncs=%u)\n",
drift, rtcStatus.temperature, rtcStatus.syncCount);
}
}
}
}
// ============================================================
// Start RTC Task
// ============================================================
void rtcTaskInit() {
if (rtcStatus.available) {
xTaskCreatePinnedToCore(rtcSyncTask, "RTC", TASK_STACK_RTC,
NULL, TASK_PRIORITY_RTC, NULL, 0);
Serial.println("[RTC] Sync task started (interval: " +
String(RTC_SYNC_INTERVAL_MS / 1000) + "s)");
}
}

46
rtc_task.h Normal file
View File

@@ -0,0 +1,46 @@
#ifndef RTC_TASK_H
#define RTC_TASK_H
#include <Arduino.h>
#include <SoftWire.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include "config.h"
// RTC status
struct RTCStatus {
bool available; // DS3231 detected on I2C
bool timeSynced; // RTC has been set with valid time
uint32_t syncCount; // Number of RTC → System syncs
float temperature; // DS3231 internal temperature
};
extern RTCStatus rtcStatus;
extern SemaphoreHandle_t rtcMutex;
// Initialize DS3231 and read time
bool rtcInit();
// Read time from DS3231 → returns epoch (0 = fail)
time_t rtcReadTime();
// Write time to DS3231 from epoch
bool rtcWriteTime(time_t epoch);
// Sync: RTC → ESP32 system clock
bool rtcSyncToSystem();
// Sync: ESP32 system clock → RTC
bool rtcSyncFromSystem();
// Read DS3231 temperature (°C, ±0.25 resolution)
float rtcReadTemperature();
// RTC periodic sync task
void rtcSyncTask(void *param);
// Start RTC task
void rtcTaskInit();
#endif // RTC_TASK_H

View File

@@ -2,7 +2,7 @@
#include "serial_task.h"
#include <time.h>
static SPIClass hspi(HSPI);
static SPIClass vspi(VSPI);
volatile bool sdLoggingActive = false;
char currentLogFileName[64] = "";
static File logFile;
@@ -12,9 +12,9 @@ static SemaphoreHandle_t sdMutex = NULL;
void sdTaskInit() {
sdMutex = xSemaphoreCreateMutex();
hspi.begin(SD_HSPI_CLK, SD_HSPI_MISO, SD_HSPI_MOSI, SD_HSPI_CS);
vspi.begin(SD_VSPI_SCLK, SD_VSPI_MISO, SD_VSPI_MOSI, SD_VSPI_CS);
if (!SD.begin(SD_HSPI_CS, hspi, 4000000)) {
if (!SD.begin(SD_VSPI_CS, vspi, 4000000)) {
Serial.println("[SD] Card mount FAILED!");
sdReady = false;
} else {
@@ -26,14 +26,15 @@ void sdTaskInit() {
sdReady = true;
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("[SD] Card mounted. Type: %d, Size: %lluMB\n", cardType, cardSize);
hspi.setFrequency(20000000);
vspi.setFrequency(20000000);
if (!SD.exists(LOG_DIR)) {
SD.mkdir(LOG_DIR);
Serial.println("[SD] Created /logs directory");
}
sdCreateNewLogFile();
sdLoggingActive = true;
// Don't auto-start logging - user starts manually via web UI
sdLoggingActive = false;
Serial.println("[SD] Ready (logging OFF - start via web UI)");
}
}
@@ -88,11 +89,12 @@ void sdLoggingTask(void *param) {
"\"%s\",\"%c\",\"", entry->timestamp,
entry->direction == 'T' ? 'T' : 'R');
// Write data as plain string (no hex encoding)
for (int i = 0; i < entry->dataLen && len < (int)sizeof(csvLine) - 4; i++) {
char c = entry->data[i];
if (c == '"') { csvLine[len++] = '"'; csvLine[len++] = '"'; }
else if (c >= 0x20 && c < 0x7F) { csvLine[len++] = c; }
else { len += snprintf(csvLine + len, sizeof(csvLine) - len, "\\x%02X", (uint8_t)c); }
else if (c >= 0x20) { csvLine[len++] = c; }
// Skip control chars (0x00~0x1F) silently
}
len += snprintf(csvLine + len, sizeof(csvLine) - len, "\"\r\n");
logFile.write((uint8_t*)csvLine, len);

View File

@@ -135,7 +135,11 @@ void serialRxTask(void *param) {
if (linePos > 0) flushLineToQueues(lineBuf, linePos);
continue;
}
lineBuf[linePos++] = c;
// Only accept printable ASCII (0x20~0x7E) and TAB (0x09)
// Skip null bytes and noise (0x00, 0xFF, etc.)
if ((c >= 0x20 && c <= 0x7E) || c == '\t') {
lineBuf[linePos++] = c;
}
}
lastActivity = xTaskGetTickCount();
} else {

View File

@@ -194,18 +194,47 @@ tr:active{background:rgba(233,69,96,0.1);}
</div>
<div class="sgrp">
<h3>System Info</h3>
<div class="fr"><label>IP Address</label><input id="sIp" readonly></div>
<div class="fr"><label>RSSI</label><input id="sRssi" readonly></div>
<div class="fr"><label>AP IP</label><input id="sApIp" readonly></div>
<div class="fr"><label>AP Clients</label><input id="sApCli" readonly></div>
<div class="fr"><label>Free Heap</label><input id="sHeap" readonly></div>
<div class="fr"><label>Uptime</label><input id="sUp" readonly></div>
<button class="abtn" onclick="sendC('sysinfo')" style="background:var(--accent);">Refresh</button>
</div>
<div class="sgrp">
<h3>WiFi STA ( )</h3>
<div class="fr" style="flex-direction:row;align-items:center;gap:10px;">
<label>STA </label>
<label style="position:relative;display:inline-block;width:48px;height:26px;cursor:pointer;">
<input type="checkbox" id="staChk" style="opacity:0;width:0;height:0;" onchange="toggleSTA()">
<span style="position:absolute;inset:0;background:var(--border);border-radius:13px;transition:.3s;"></span>
<span id="staSlider" style="position:absolute;left:2px;top:2px;width:22px;height:22px;background:#fff;border-radius:50%;transition:.3s;"></span>
</label>
<span id="staLabel" style="font-size:12px;color:#888;">OFF</span>
</div>
<div id="staForm" style="display:none;">
<div class="fr"><label>SSID</label><input id="staSSID" type="text" placeholder="WiFi SSID" autocomplete="off"></div>
<div class="fr"><label>Password</label><input id="staPW" type="password" placeholder="WiFi Password" autocomplete="off"></div>
<button class="abtn" onclick="connectSTA()" id="staBtn" style="background:var(--ok);color:#000;">Connect</button>
</div>
<div id="staInfo" style="display:none;margin-top:6px;font-size:12px;">
<div class="fr"><label>STA Status</label><span id="staStatus" style="font-size:13px;">--</span></div>
<div class="fr"><label>STA IP</label><input id="staIpDisp" readonly style="font-size:12px;"></div>
<div class="fr"><label>RSSI</label><input id="staRssiDisp" readonly style="font-size:12px;"></div>
</div>
</div>
<div class="sgrp">
<h3>DS3231 RTC</h3>
<div class="fr"><label>RTC Status</label><span id="rtcSt" style="font-size:13px;color:#888;">--</span></div>
<div class="fr"><label>Time Synced</label><span id="rtcSynced" style="font-size:13px;">--</span></div>
<div class="fr"><label>Sync Count</label><span id="rtcSyncs" style="font-size:13px;color:#aaa;">0</span></div>
<div class="fr"><label>RTC Temp</label><span id="rtcTemp" style="font-size:13px;color:#aaa;">--</span></div>
</div>
<div class="sgrp">
<h3>SD Logging</h3>
<div class="fr"><label>Status</label><span id="logSt" style="color:var(--ok);font-size:13px;">Active</span></div>
<div class="fr"><label>Status</label><span id="logSt" style="color:var(--btn);font-size:13px;">Stopped</span></div>
<div class="fr"><label>Log File</label><input id="logFile" readonly style="font-size:11px;"></div>
<div style="display:flex;gap:8px;margin-top:6px;flex-wrap:wrap;">
<button class="abtn" onclick="sendC('toggle_log')" id="logBtn">Stop Logging</button>
<button class="abtn" onclick="sendC('toggle_log')" id="logBtn">Start Logging</button>
<button class="abtn" onclick="sendC('new_log')" style="background:var(--warn);color:#000;">New File</button>
</div>
</div>
@@ -233,8 +262,8 @@ tr:active{background:rgba(233,69,96,0.1);}
<!-- STATUS BAR -->
<div class="sbar">
<span class="ld rec" id="lDot"></span>
<span id="lInfo">Logging...</span>
<span class="ld" id="lDot"></span>
<span id="lInfo">Log OFF</span>
<span id="serInfo">115200 8N1</span>
<span id="rxC">RX:0</span>
<span id="txC">TX:0</span>
@@ -265,14 +294,14 @@ function wsConn(){
document.getElementById('wsDot').classList.add('on');
document.getElementById('wsSt').textContent='ON';
addSys('[Connected]');
// Send browser time to ESP32 for clock sync
// Send browser time to ESP32 for clock sync (silent - no terminal message)
let now=new Date();
let epoch=Math.floor(now.getTime()/1000);
let ms=now.getMilliseconds();
ws.send(JSON.stringify({cmd:'set_time',epoch:epoch,ms:ms}));
addSys('[Time sync: '+now.toLocaleString()+']');
sendC('sysinfo');
sendC('get_serial_config');
sendC('get_wifi');
};
ws.onmessage=function(e){
try{
@@ -281,7 +310,7 @@ function wsConn(){
else if(m.type==='sysinfo') updSys(m);
else if(m.type==='serial_config') updSer(m);
else if(m.type==='log_status') updLog(m);
else if(m.type==='time_synced'&&m.ok) addSys('[ESP32 time synced OK]');
else if(m.type==='wifi_status') updWifi(m);
}catch(x){}
};
ws.onclose=function(){
@@ -406,11 +435,86 @@ function updSer(m){
// ===== System Info =====
function updSys(m){
document.getElementById('sIp').value=m.ip||'';
document.getElementById('sRssi').value=(m.rssi||'')+' dBm';
document.getElementById('sApIp').value=m.apIp||'';
document.getElementById('sApCli').value=m.apClients||0;
document.getElementById('sHeap').value=fB(m.heap||0);
document.getElementById('sUp').value=fUp(m.uptime||0);
document.getElementById('tInfo').textContent=m.time||'--';
// STA info from sysinfo
if(m.staOn!==undefined) updWifi(m);
// RTC info
let rs=document.getElementById('rtcSt');
if(m.rtcOk){rs.textContent='OK (DS3231)';rs.style.color='var(--ok)';}
else{rs.textContent='Not found';rs.style.color='var(--btn)';}
let ry=document.getElementById('rtcSynced');
if(m.rtcSync){ry.textContent='Yes';ry.style.color='var(--ok)';}
else{ry.textContent='No';ry.style.color='var(--warn)';}
document.getElementById('rtcSyncs').textContent=m.rtcSyncs||0;
let t=m.rtcTemp;
document.getElementById('rtcTemp').textContent=(t&&t>-100)?(t.toFixed(1)+' °C'):'--';
}
// ===== WiFi STA Control =====
function updWifi(m){
let chk=document.getElementById('staChk');
let slider=document.getElementById('staSlider');
let label=document.getElementById('staLabel');
let form=document.getElementById('staForm');
let info=document.getElementById('staInfo');
chk.checked=m.staOn;
slider.style.transform=m.staOn?'translateX(22px)':'';
slider.parentElement.previousElementSibling.nextElementSibling.style.background=m.staOn?'var(--ok)':'var(--border)';
label.textContent=m.staOn?'ON':'OFF';
label.style.color=m.staOn?'var(--ok)':'#888';
form.style.display=m.staOn?'block':'none';
info.style.display=m.staOn?'block':'none';
if(m.staOn){
if(m.staSSID) document.getElementById('staSSID').value=m.staSSID;
let ss=document.getElementById('staStatus');
if(m.staConn){ss.textContent='Connected';ss.style.color='var(--ok)';}
else{ss.textContent='Disconnected';ss.style.color='var(--btn)';}
document.getElementById('staIpDisp').value=m.staIp||'--';
document.getElementById('staRssiDisp').value=m.staRssi?(m.staRssi+' dBm'):'--';
document.getElementById('staBtn').textContent='Disconnect';
document.getElementById('staBtn').style.background='var(--btn)';
document.getElementById('staBtn').style.color='#fff';
document.getElementById('staBtn').onclick=disconnectSTA;
}else{
document.getElementById('staBtn').textContent='Connect';
document.getElementById('staBtn').style.background='var(--ok)';
document.getElementById('staBtn').style.color='#000';
document.getElementById('staBtn').onclick=connectSTA;
}
}
function toggleSTA(){
let chk=document.getElementById('staChk');
let slider=document.getElementById('staSlider');
let form=document.getElementById('staForm');
let label=document.getElementById('staLabel');
slider.style.transform=chk.checked?'translateX(22px)':'';
slider.parentElement.previousElementSibling.nextElementSibling.style.background=chk.checked?'var(--ok)':'var(--border)';
label.textContent=chk.checked?'ON':'OFF';
label.style.color=chk.checked?'var(--ok)':'#888';
form.style.display=chk.checked?'block':'none';
if(!chk.checked) disconnectSTA();
}
function connectSTA(){
let ssid=document.getElementById('staSSID').value.trim();
let pw=document.getElementById('staPW').value;
if(!ssid){alert('SSID를 ');return;}
let ss=document.getElementById('staStatus');
if(ss) {ss.textContent='Connecting...';ss.style.color='var(--warn)';}
document.getElementById('staInfo').style.display='block';
if(ws&&ws.readyState===1) ws.send(JSON.stringify({cmd:'wifi_sta_on',ssid:ssid,pw:pw}));
}
function disconnectSTA(){
if(ws&&ws.readyState===1) ws.send(JSON.stringify({cmd:'wifi_sta_off'}));
document.getElementById('staInfo').style.display='none';
document.getElementById('staChk').checked=false;
toggleSTA();
}
// ===== Log Status =====

View File

@@ -2,10 +2,19 @@
#include "web_html.h"
#include "serial_task.h"
#include "sdcard_task.h"
#include "rtc_task.h"
#include <ArduinoJson.h>
#include <time.h>
#include <sys/time.h>
// WiFi STA control (defined in .ino)
extern volatile bool staEnabled;
extern volatile bool staConnected;
extern char staSSID[];
extern char staPW[];
extern bool wifiEnableSTA(const char *ssid, const char *password);
extern void wifiDisableSTA();
// --- Web Server (port 80) & WebSocket (port 81) ---
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(WS_PORT);
@@ -50,28 +59,32 @@ void handleWsMessage(uint8_t num, const char *message) {
const char *cmd = doc["cmd"];
if (!cmd) return;
// --- Set Time from browser ---
// --- Set Time from browser → System Clock + RTC ---
if (strcmp(cmd, "set_time") == 0) {
uint32_t epoch = doc["epoch"] | 0;
uint16_t ms = doc["ms"] | 0;
if (epoch > 1700000000) { // Sanity check: after 2023
// 1) Set ESP32 system clock
struct timeval tv;
tv.tv_sec = (time_t)epoch;
tv.tv_usec = (suseconds_t)ms * 1000;
settimeofday(&tv, NULL);
// Verify
struct tm timeinfo;
localtime_r(&tv.tv_sec, &timeinfo);
Serial.printf("[Time] Synced from browser: %04d-%02d-%02d %02d:%02d:%02d\n",
Serial.printf("[Time] Browser sync: %04d-%02d-%02d %02d:%02d:%02d\n",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
// Broadcast confirmation
// 2) Also write to DS3231 RTC (if available)
bool rtcOk = rtcSyncFromSystem();
// 3) Broadcast confirmation
StaticJsonDocument<128> resp;
resp["type"] = "time_synced";
resp["ok"] = true;
resp["rtc"] = rtcOk;
String json;
serializeJson(resp, json);
webSocket.sendTXT(num, json);
@@ -122,7 +135,6 @@ void handleWsMessage(uint8_t num, const char *message) {
} else if (strcmp(lineEnd, "lf") == 0) {
entry->data[entry->dataLen++] = '\n';
}
// "none" - no line ending
}
entry->data[entry->dataLen] = '\0';
}
@@ -141,7 +153,6 @@ void handleWsMessage(uint8_t num, const char *message) {
reconfigureSerial(baud, dataBits, p[0], stopBits);
// Broadcast new config to all clients
StaticJsonDocument<256> resp;
resp["type"] = "serial_config";
resp["baud"] = baud;
@@ -166,17 +177,36 @@ void handleWsMessage(uint8_t num, const char *message) {
webSocket.sendTXT(num, json);
}
// --- System Info ---
// --- System Info (includes RTC + WiFi status) ---
else if (strcmp(cmd, "sysinfo") == 0) {
StaticJsonDocument<512> resp;
StaticJsonDocument<768> resp;
resp["type"] = "sysinfo";
resp["ip"] = WiFi.localIP().toString();
resp["rssi"] = WiFi.RSSI();
resp["heap"] = ESP.getFreeHeap();
resp["uptime"] = millis() / 1000;
char timeBuf[24];
getTimestamp(timeBuf, sizeof(timeBuf));
resp["time"] = timeBuf;
// WiFi AP info (always active)
resp["apIp"] = WiFi.softAPIP().toString();
resp["apSSID"] = WIFI_AP_SSID;
resp["apClients"] = WiFi.softAPgetStationNum();
// WiFi STA info
resp["staOn"] = (bool)staEnabled;
resp["staConn"] = (bool)staConnected;
if (staEnabled) {
resp["staSSID"] = staSSID;
resp["staIp"] = WiFi.localIP().toString();
resp["staRssi"] = WiFi.RSSI();
}
// RTC info
resp["rtcOk"] = rtcStatus.available;
resp["rtcSync"] = rtcStatus.timeSynced;
resp["rtcSyncs"] = rtcStatus.syncCount;
resp["rtcTemp"] = rtcStatus.temperature;
String json;
serializeJson(resp, json);
webSocket.sendTXT(num, json);
@@ -209,74 +239,114 @@ void handleWsMessage(uint8_t num, const char *message) {
serializeJson(resp, json);
webSocket.broadcastTXT(json);
}
// --- WiFi STA Enable ---
else if (strcmp(cmd, "wifi_sta_on") == 0) {
const char *ssid = doc["ssid"];
const char *pw = doc["pw"] | "";
if (!ssid || strlen(ssid) == 0) return;
bool ok = wifiEnableSTA(ssid, pw);
StaticJsonDocument<256> resp;
resp["type"] = "wifi_status";
resp["staOn"] = (bool)staEnabled;
resp["staConn"] = ok;
resp["staSSID"] = staSSID;
if (ok) {
resp["staIp"] = WiFi.localIP().toString();
resp["staRssi"] = WiFi.RSSI();
}
String json;
serializeJson(resp, json);
webSocket.broadcastTXT(json);
}
// --- WiFi STA Disable ---
else if (strcmp(cmd, "wifi_sta_off") == 0) {
wifiDisableSTA();
StaticJsonDocument<128> resp;
resp["type"] = "wifi_status";
resp["staOn"] = false;
resp["staConn"] = false;
String json;
serializeJson(resp, json);
webSocket.broadcastTXT(json);
}
// --- Get WiFi Status ---
else if (strcmp(cmd, "get_wifi") == 0) {
StaticJsonDocument<256> resp;
resp["type"] = "wifi_status";
resp["staOn"] = (bool)staEnabled;
resp["staConn"] = (bool)staConnected;
resp["staSSID"] = staSSID;
resp["apIp"] = WiFi.softAPIP().toString();
if (staConnected) {
resp["staIp"] = WiFi.localIP().toString();
resp["staRssi"] = WiFi.RSSI();
}
String json;
serializeJson(resp, json);
webSocket.sendTXT(num, json);
}
}
// ============================================================
// Setup Web Server Routes
// ============================================================
void setupWebRoutes() {
// Main page
server.on("/", HTTP_GET, []() {
server.send_P(200, "text/html", INDEX_HTML);
});
// API: File List
server.on("/api/files", HTTP_GET, []() {
String json = sdGetFileList();
server.send(200, "application/json", json);
});
// API: Delete Files (POST with JSON body)
server.on("/api/delete", HTTP_POST, []() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"error\":\"no body\"}");
return;
}
StaticJsonDocument<1024> doc;
DeserializationError err = deserializeJson(doc, server.arg("plain"));
if (err) {
server.send(400, "application/json", "{\"error\":\"invalid json\"}");
return;
}
JsonArray files = doc["files"];
int deleted = 0;
for (JsonVariant f : files) {
const char *fname = f.as<const char*>();
if (fname && sdDeleteFile(fname)) deleted++;
}
String resp = "{\"deleted\":" + String(deleted) + "}";
server.send(200, "application/json", resp);
});
// API: Download File
server.on("/download", HTTP_GET, []() {
if (!server.hasArg("file")) {
server.send(400, "text/plain", "Missing file parameter");
return;
}
String filename = server.arg("file");
// Security: prevent path traversal
if (filename.indexOf("..") >= 0) {
server.send(403, "text/plain", "Forbidden");
return;
}
String path = String(LOG_DIR) + "/" + filename;
if (!SD.exists(path)) {
server.send(404, "text/plain", "File not found");
return;
}
File file = SD.open(path, FILE_READ);
if (!file) {
server.send(500, "text/plain", "Cannot open file");
return;
}
size_t fileSize = file.size();
server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
server.sendHeader("Content-Length", String(fileSize));
@@ -286,29 +356,23 @@ void setupWebRoutes() {
}
// ============================================================
// Web Broadcast Task - Sends serial data to WebSocket clients
// Runs webSocket.loop() and broadcasts queued data
// Web Broadcast Task
// ============================================================
void webBroadcastTask(void *param) {
Serial.println("[Task] WebBroadcast started on core " + String(xPortGetCoreID()));
vTaskDelay(pdMS_TO_TICKS(500)); // Initial stabilization
vTaskDelay(pdMS_TO_TICKS(500));
while (true) {
// Process WebSocket events (REQUIRED for Links2004 library)
webSocket.loop();
// Process web queue entries and broadcast
if (webSocket.connectedClients() > 0) {
LogEntry *entry;
// Process up to 10 entries per cycle for responsiveness
int processed = 0;
while (processed < 10 && xQueueReceive(queueWeb, &entry, 0) == pdTRUE) {
StaticJsonDocument<768> doc;
doc["type"] = (entry->direction == 'T') ? "tx" : "rx";
doc["ts"] = entry->timestamp;
doc["data"] = entry->data;
String json;
serializeJson(doc, json);
webSocket.broadcastTXT(json);
@@ -316,13 +380,11 @@ void webBroadcastTask(void *param) {
processed++;
}
} else {
// No clients - drain web queue to prevent memory buildup
LogEntry *entry;
while (xQueueReceive(queueWeb, &entry, 0) == pdTRUE) {
vPortFree(entry);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
@@ -340,7 +402,6 @@ void webTaskInit() {
server.begin();
Serial.println("[Web] HTTP server started on port 80");
// Create broadcast task on core 0
xTaskCreatePinnedToCore(webBroadcastTask, "WebBC", TASK_STACK_WEB,
NULL, TASK_PRIORITY_WEB, NULL, 0);
}