сделан сервер на esp, терминалка имитирующая клинета и терминалка для считывания логов

сделан также клиент на esp, но не проверен
This commit is contained in:
Razvalyaev 2025-09-04 00:03:06 +03:00
commit 5cc802c3be
8 changed files with 7091 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.vscode/

289
ESP_WifiTest.ino Normal file
View File

@ -0,0 +1,289 @@
#include <WiFi.h>
#include "logs.h"
#include <Adafruit_NeoPixel.h>
// -------------------- НАСТРОЙКИ --------------------
char ssid[32] = "";
char password[32] = "";
char serverIP[32] = "198.168.0.1";
#define NEOPIXEL_PIN 48
#define NUMPIXELS 1
#define BRIGHTNESS 40
#define SERVER // раскомментировать для сервера
// -------------------- ФУНКЦИИ --------------------
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
uint8_t brightness = 0; // 0 = выкл, 255 = макс
bool greenOn = false;
LogModule logger;
void toggleGreen() {
greenOn = !greenOn; // меняем состояние
if (greenOn) {
pixels.setPixelColor(0, pixels.Color(0, BRIGHTNESS, 0)); // включаем зелёный
} else {
pixels.setPixelColor(0, pixels.Color(0, 0, 0)); // выключаем
}
pixels.show();
}
void setRed() {
pixels.setPixelColor(0, pixels.Color(BRIGHTNESS, 0, 0));
pixels.show();
}
void setYellow() {
pixels.setPixelColor(0, pixels.Color(BRIGHTNESS, BRIGHTNESS, 0));
pixels.show();
}
void clearLED() {
pixels.setPixelColor(0, pixels.Color(0, 0, 0));
pixels.show();
}
// -------------------- РЕЖИМЫ --------------------
#ifdef SERVER
WiFiServer server(1234);
#else
WiFiClient client;
#endif
// -------------------- Wi-Fi --------------------
bool wifiConnecting = false;
void startWiFi() {
WiFi.disconnect(true);
WiFi.begin(ssid, password);
wifiConnecting = true;
Serial.print("Connecting to WiFi");
}
void handleWiFi() {
static uint32_t lastCheck = 0;
const uint32_t interval = 500; // проверять каждые 500 мс
if (millis() - lastCheck < interval) return;
lastCheck = millis();
if (WiFi.status() == WL_CONNECTED) {
if (wifiConnecting) {
wifiConnecting = false;
clearLED();
Serial.println("\nWiFi connected");
Serial.print("IP: "); Serial.println(WiFi.localIP());
#ifdef SERVER
server.begin();
}
#else
else {
if (!client.connected()) {
if (WiFi.status() != WL_CONNECTED) return;
setYellow();
if (client.connect(serverIP, 1234)) {
clearLED();
Serial.println("Connected to server");
}
}
}
#endif
} else {
setRed();
if (!wifiConnecting) {
Serial.println("\nWiFi disconnected. Reconnecting...");
startWiFi();
} else {
Serial.print(".");
}
}
}
// -------------------- UART ДЛЯ НАСТРОЕК --------------------
void handleUARTNetwork() {
static String buffer;
while (Serial.available()) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
buffer.trim();
if (buffer.length() > 0) {
int sep = buffer.indexOf(' ');
if (sep > 0) {
String cmd = buffer.substring(0, sep);
String val = buffer.substring(sep + 1);
if (cmd == "SSID") {
val.toCharArray((char*)ssid, 32);
Serial.print("\nSSID set to: "); Serial.println(ssid);
startWiFi();
}
else if (cmd == "PASS") {
val.toCharArray((char*)password, 32);
Serial.print("\nPassword set to: "); Serial.println(password);
startWiFi();
}
else if (cmd == "IP") {
val.toCharArray((char*)serverIP, 16);
Serial.print("\nServer IP set to: "); Serial.println(serverIP);
}
else {
logger.handleUART(buffer[0]); // передаем неизвестные команды в логгер
}
} else {
logger.handleUART(buffer[0]); // нет пробела — считаем команду неизвестной
}
}
buffer = "";
}
else {
buffer += c;
}
}
}
void setup() {
Serial.begin(115200);
delay(1000);
logger.begin();
pixels.begin();
pixels.show();
#ifdef SERVER
Serial.println("SERVER MODE: Enter SSID and PASS via UART, e.g.:\nSSID MyWiFi\nPASS 12345678");
#else
Serial.println("CLIENT MODE: Enter server IP via UART, e.g.:\nIP 192.168.1.100");
#endif
}
void loop() {
handleUARTNetwork(); // UART-функция для настройки сети и получения логов
handleWiFi(); // проверка состояния Wi-Fi
if (WiFi.status() != WL_CONNECTED) return;
#ifdef SERVER
WiFiClient clientConn = server.available();
if (!clientConn) return;
Serial.println("Client connected");
while (clientConn.connected()) {
if (clientConn.available()) {
String msg = clientConn.readStringUntil('\n');
msg.trim();
if (msg.length() == 0) continue;
// Разбор входящего сообщения
LogEntry entry;
entry.seq = 0;
entry.ts = 0;
entry.event_type = 0; // RECEIVE
memset(entry.payload, 0, sizeof(entry.payload));
int seqIndex = msg.indexOf("SEQ:");
int tsIndex = msg.indexOf("TS:");
int payloadIndex = msg.indexOf("PAYLOAD:");
if (seqIndex >= 0 && tsIndex > seqIndex && payloadIndex > tsIndex) {
String seqStr = msg.substring(seqIndex + 4, tsIndex);
seqStr.trim();
String tsStr = msg.substring(tsIndex + 3, payloadIndex);
tsStr.trim();
String payloadStr = msg.substring(payloadIndex + 8);
payloadStr.trim();
entry.seq = seqStr.toInt();
entry.ts = strtoull(tsStr.c_str(), nullptr, 10);
int len = min((int)payloadStr.length(), 16);
payloadStr.toCharArray(entry.payload, len + 1);
}
// Сохраняем лог
logger.writeLog(entry);
Serial.print("Received: "); Serial.println(msg);
toggleGreen();
// Создаем SEND-запись
LogEntry sendEntry = entry;
sendEntry.ts = millis();
sendEntry.event_type = 1; // SEND
logger.writeLog(sendEntry);
// Echo для клиента
String echo = "SEQ:" + String(sendEntry.seq) +
" TS:" + String(sendEntry.ts) +
" EVT:SEND PAYLOAD:" + String(sendEntry.payload) + "\n";
if (clientConn.print(echo)) {
Serial.print("Sent: "); Serial.println(echo);
} else {
Serial.println("Error sending to client");
setRed();
}
}
}
clientConn.stop();
Serial.println("Client disconnected");
#else // CLIENT
static uint32_t lastSendMillis = 0; // время последней отправки
const uint32_t sendInterval = 500; // 500 мс между отправками
static uint16_t seqNum = 1;
if (!client.connected()) return;
// проверяем, пора ли отправлять сообщение
if (millis() - lastSendMillis >= sendInterval) {
lastSendMillis = millis(); // фиксируем время отправки
String msg = "SEQ:" + String(seqNum) + " TS:" + String(millis()) + " PAYLOAD:Hard!Text^?123\n";
if (client.print(msg)) {
Serial.print("Sent: "); Serial.println(msg);
toggleGreen();
// Сохраняем SEND-запись
LogEntry entry;
entry.seq = seqNum;
entry.ts = millis();
entry.event_type = 1; // SEND
msg.substring(msg.indexOf("PAYLOAD:") + 8).toCharArray(entry.payload, 16);
logger.writeLog(entry);
seqNum++;
} else {
Serial.println("Error sending");
setRed();
}
}
// Чтение ответа без блокировки
while (client.available()) {
String resp = client.readStringUntil('\n');
resp.trim();
if (resp.length() == 0) continue;
Serial.print("Received: "); Serial.println(resp);
// Сохраняем RECEIVE-запись
LogEntry entry;
entry.seq = resp.indexOf("SEQ:");
entry.ts = resp.indexOf("TS:");
entry.event_type = 0; // RECEIVE
int payloadIndex = resp.indexOf("PAYLOAD:");
if (payloadIndex >= 0) {
String payloadStr = resp.substring(payloadIndex + 8);
payloadStr.toCharArray(entry.payload, 16);
}
logger.writeLog(entry);
toggleGreen();
}
#endif
}

131
esp_client_emu.py Normal file
View File

@ -0,0 +1,131 @@
import sys
import socket
import threading
import time
from PySide2.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QLabel, QLineEdit,
QPushButton, QMessageBox, QSpinBox
)
from PySide2.QtCore import Qt, Signal, QObject
class LogSignal(QObject):
log_msg = Signal(str)
class ESPClientTerminal(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("ESP32 TCP Client")
self.resize(400, 200)
self.layout = QVBoxLayout(self)
self.layout.addWidget(QLabel("Server IP:"))
self.ip_input = QLineEdit("192.168.0.96")
self.layout.addWidget(self.ip_input)
self.layout.addWidget(QLabel("Server port:"))
self.port_input = QLineEdit("1234")
self.layout.addWidget(self.port_input)
self.layout.addWidget(QLabel("Packet interval (ms):"))
self.interval_input = QSpinBox()
self.interval_input.setRange(50, 10000)
self.interval_input.setValue(500)
self.layout.addWidget(self.interval_input)
self.start_btn = QPushButton("Start Sending")
self.start_btn.clicked.connect(self.start_sending)
self.layout.addWidget(self.start_btn)
self.stop_btn = QPushButton("Stop Sending")
self.stop_btn.clicked.connect(self.stop_sending)
self.stop_btn.setEnabled(False)
self.layout.addWidget(self.stop_btn)
self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.status_label)
self.running = False
self.thread = None
self.log_signal = LogSignal()
self.log_signal.log_msg.connect(self.update_status)
def update_status(self, msg):
self.status_label.setText(msg)
def start_sending(self):
server_ip = self.ip_input.text().strip()
try:
server_port = int(self.port_input.text().strip())
except ValueError:
QMessageBox.warning(self, "Error", "Invalid port")
return
interval = self.interval_input.value() / 1000.0 # ms -> sec
if not server_ip:
QMessageBox.warning(self, "Error", "Enter server IP")
return
self.running = True
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.thread = threading.Thread(target=self.send_loop, args=(server_ip, server_port, interval), daemon=True)
self.thread.start()
def stop_sending(self):
self.running = False
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.status_label.setText("Stopped sending")
def send_loop(self, ip, port, interval):
seq = 0
sock = None
while self.running:
try:
# Если сокета нет пытаемся подключиться
if sock is None:
self.log_signal.log_msg.emit(f"Connecting to {ip}:{port}...")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((ip, port))
self.log_signal.log_msg.emit(f"Connected to {ip}:{port}")
# Отправляем данные
seq += 1
ts = int(time.time() * 1000)
payload = "PING"
msg = f"SEQ:{seq} TS:{ts} PAYLOAD:{payload}\n"
sock.sendall(msg.encode())
# Читаем ответ
data = sock.recv(1024).decode()
self.log_signal.log_msg.emit(f"Sent SEQ:{seq} | Echo: {data.strip()}")
time.sleep(interval)
except Exception as e:
self.log_signal.log_msg.emit(f"Connection lost: {e}")
if sock:
try:
sock.close()
except:
pass
sock = None
time.sleep(2) # ждём перед реконнектом
if sock:
try:
sock.close()
except:
pass
self.log_signal.log_msg.emit("Stopped / Disconnected")
if __name__ == "__main__":
app = QApplication(sys.argv)
w = ESPClientTerminal()
w.show()
sys.exit(app.exec_())

224
getLogs.py Normal file
View File

@ -0,0 +1,224 @@
import sys
import csv
import serial
import serial.tools.list_ports
import os
import subprocess
from PySide2.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QMessageBox, QComboBox, QHBoxLayout
)
from PySide2.QtCore import Qt
class ESPLoggerTerminal(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("ESP32 Log Downloader")
self.resize(500, 300)
layout = QVBoxLayout(self)
# --- COM-port selection ---
port_layout = QHBoxLayout()
port_layout.addWidget(QLabel("Serial port:"))
self.port_combo = QComboBox()
port_layout.addWidget(self.port_combo)
self.refresh_btn = QPushButton("Refresh")
self.refresh_btn.clicked.connect(self.refresh_ports)
port_layout.addWidget(self.refresh_btn)
layout.addLayout(port_layout)
layout.addWidget(QLabel("Baud rate:"))
self.baud_input = QLineEdit("115200")
layout.addWidget(self.baud_input)
layout.addWidget(QLabel("CSV separator:"))
self.sep_input = QLineEdit(";")
layout.addWidget(self.sep_input)
# --- File selection ---
file_layout = QHBoxLayout()
self.file_edit = QLineEdit()
file_layout.addWidget(self.file_edit)
self.browse_btn = QPushButton("Browse...")
self.browse_btn.clicked.connect(self.select_file)
file_layout.addWidget(self.browse_btn)
layout.addLayout(file_layout)
open_layout = QHBoxLayout()
self.open_file_btn = QPushButton("Open File")
self.open_file_btn.clicked.connect(self.open_file)
open_layout.addWidget(self.open_file_btn)
self.open_folder_btn = QPushButton("Open Folder")
self.open_folder_btn.clicked.connect(self.open_folder)
open_layout.addWidget(self.open_folder_btn)
layout.addLayout(open_layout)
# --- Buttons ---
self.get_log_btn = QPushButton("Get Log and Save CSV")
self.get_log_btn.clicked.connect(self.get_log)
layout.addWidget(self.get_log_btn)
self.clear_log_btn = QPushButton("Clear Logs")
self.clear_log_btn.clicked.connect(self.clear_logs)
layout.addWidget(self.clear_log_btn)
self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
# загрузка списка портов при запуске
self.refresh_ports()
def refresh_ports(self):
"""Обновить список доступных COM/tty"""
self.port_combo.clear()
ports = serial.tools.list_ports.comports()
for p in ports:
self.port_combo.addItem(f"{p.device} ({p.description})", p.device)
if not ports:
self.port_combo.addItem("No ports found", "")
def select_file(self):
fname, _ = QFileDialog.getSaveFileName(self, "Select CSV File", "", "CSV Files (*.csv)")
if fname:
self.file_edit.setText(fname)
def open_file(self):
fname = self.file_edit.text().strip()
if os.path.isfile(fname):
os.startfile(fname) # Windows only
else:
QMessageBox.warning(self, "Warning", "File does not exist")
def open_folder(self):
fname = self.file_edit.text().strip()
if os.path.isfile(fname):
subprocess.run(["explorer", f"/select,{os.path.abspath(fname)}"])
elif os.path.isdir(fname):
subprocess.run(["explorer", os.path.abspath(fname)])
else:
QMessageBox.warning(self, "Warning", "File does not exist")
def get_log(self):
port = self.port_combo.currentData()
try:
baud = int(self.baud_input.text().strip())
except ValueError:
QMessageBox.warning(self, "Error", "Invalid baud rate")
return
sep = self.sep_input.text() or ","
if not port:
QMessageBox.warning(self, "Error", "Select a serial port")
return
fname = self.file_edit.text().strip()
if not fname:
QMessageBox.warning(self, "Error", "Select a CSV file to save logs")
return
try:
self.status_label.setText("Connecting to ESP32...")
QApplication.processEvents()
ser = serial.Serial(port, baud, timeout=2) # короче таймаут
ser.reset_input_buffer()
ser.reset_output_buffer()
ser.write(b'd\n')
# Ждём первую реакцию от ESP32 (например "=== LOG DUMP START ===")
pre_line = ser.readline().decode(errors='ignore').strip()
first_line = ser.readline().decode(errors='ignore').strip()
if not first_line or "LOG DUMP" not in first_line:
ser.close()
QMessageBox.warning(self, "Error", "Selected port is not responding like ESP32")
return
self.status_label.setText("Receiving log...")
QApplication.processEvents()
import time
lines = []
start_time = time.time()
timeout_sec = 30
while True:
if time.time() - start_time > timeout_sec:
QMessageBox.warning(self, "Timeout", "No complete log received (timeout).")
break
line = ser.readline().decode(errors='ignore').strip()
if line:
start_time = time.time()
if not line:
continue
if line.startswith("=== LOG DUMP END ==="):
break
if line.startswith("Entry "):
lines.append(line)
self.status_label.setText(f"Received {len(lines)} entries...")
QApplication.processEvents()
except Exception as e:
QMessageBox.critical(self, "Error", f"Serial error: {e}")
return
finally:
if 'ser' in locals() and ser and ser.is_open:
ser.close()
if not lines:
QMessageBox.information(self, "Info", "No log data received")
return
try:
with open(fname, "w", newline="") as f:
writer = csv.writer(f, delimiter=sep)
writer.writerow(["SEQ", "TS", "EVT", "PAYLOAD"])
for line in lines:
parts = line.split()
seq = parts[2].split(":")[1]
ts = parts[3].split(":")[1]
evt = parts[4].split(":")[1]
payload = ""
if len(parts) > 5 and parts[5].startswith("PAYLOAD:"):
payload = line.split("PAYLOAD:", 1)[1]
writer.writerow([seq, ts, evt, payload])
except Exception as e:
QMessageBox.critical(self, "Error", f"CSV write error: {e}")
return
self.status_label.setText(f"Log saved to {fname}")
QMessageBox.information(self, "Success", f"CSV saved: {fname}")
def clear_logs(self):
"""Очистка логов на ESP32"""
port = self.port_combo.currentData()
try:
baud = int(self.baud_input.text().strip())
except ValueError:
QMessageBox.warning(self, "Error", "Invalid baud rate")
return
if not port:
QMessageBox.warning(self, "Error", "Select a serial port")
return
try:
ser = serial.Serial(port, baud, timeout=3)
ser.write(b'c\n')
response = ser.readline().decode(errors='ignore').strip()
ser.close()
if "CLEARED" in response:
self.status_label.setText("Logs cleared on ESP32")
QMessageBox.information(self, "Success", "Logs cleared on ESP32")
else:
QMessageBox.warning(self, "Warning", "No confirmation from ESP32")
except Exception as e:
QMessageBox.critical(self, "Error", f"Serial error: {e}")
if __name__ == "__main__":
app = QApplication(sys.argv)
w = ESPLoggerTerminal()
w.show()
sys.exit(app.exec_())

371
logs.cpp Normal file
View File

@ -0,0 +1,371 @@
#include "logs.h"
#include <esp_partition.h>
#include <esp_attr.h>
// CRC8 для проверки целостности
uint8_t LogModule::calculateCRC(const LogEntry &entry) {
uint8_t crc = 0;
const uint8_t* data = (const uint8_t*)&entry;
// Считаем CRC по всем байтам структуры КРОМЕ поля crc
for (size_t i = 0; i < offsetof(LogEntry, crc); i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
}
}
// Пропускаем поле crc и считаем остальные
for (size_t i = offsetof(LogEntry, crc) + 1; i < ENTRY_SIZE; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
}
}
return crc;
}
bool LogModule::verifyEntry(const LogEntry &entry) {
uint8_t calculated_crc = calculateCRC(entry);
bool valid = (entry.crc == calculated_crc);
if (!valid) {
Serial.printf("CRC mismatch: stored=0x%02X, calculated=0x%02X\n",
entry.crc, calculated_crc);
}
return valid;
}
LogModule::LogModule() : partition(nullptr), write_pos(0), total_entries(0), partition_size(0) {}
void LogModule::recoverLog() {
Serial.println("Starting log recovery...");
// Ищем последнюю валидную запись
uint32_t last_valid = findLastValidEntry();
if (last_valid == UINT32_MAX) {
// Не нашли ни одной валидной записи
Serial.println("No valid entries found, clearing log");
write_pos = 0;
total_entries = 0;
} else {
// Восстанавливаем метаданные
write_pos = (last_valid + 1) % ((partition_size - 8) / ENTRY_SIZE);
total_entries = last_valid + 1;
Serial.printf("Recovered %u valid entries\n", total_entries);
}
saveMetadata();
}
uint32_t LogModule::findLastValidEntry() {
uint32_t max_entries = (partition_size - 8) / ENTRY_SIZE;
uint32_t last_valid = UINT32_MAX;
for (uint32_t i = 0; i < max_entries; i++) {
uint32_t offset = 8 + (i * ENTRY_SIZE); // ← ИСПРАВЛЕНО!
LogEntry entry;
if (readRaw(offset, (uint8_t*)&entry, ENTRY_SIZE)) {
// Проверяем не пустая ли запись
bool is_erased = true;
for (size_t j = 0; j < ENTRY_SIZE; j++) {
if (((uint8_t*)&entry)[j] != 0xFF && ((uint8_t*)&entry)[j] != 0x00) {
is_erased = false;
break;
}
}
if (is_erased) {
Serial.printf("Empty entry at position %u, stopping scan\n", i);
break;
}
if (verifyEntry(entry)) {
last_valid = i;
Serial.printf("Valid entry at position %u: SEQ:%u\n", i, entry.seq);
} else {
Serial.printf("Corrupted entry at position %u\n", i);
break;
}
} else {
break;
}
}
return last_valid;
}
bool LogModule::validateAllEntries() {
if (total_entries == 0) return true;
uint32_t max_entries = (partition_size - 8) / ENTRY_SIZE;
for (uint32_t i = 0; i < total_entries; i++) {
uint32_t index = (write_pos - total_entries + i + max_entries) % max_entries;
uint32_t offset = 8 + (index * ENTRY_SIZE); // ← ИСПРАВЛЕНО!
LogEntry entry;
if (!readRaw(offset, (uint8_t*)&entry, ENTRY_SIZE)) {
Serial.printf("Read failed at index %u\n", index);
return false;
}
// Проверяем не пустая ли запись (все 0xFF или 0x00)
bool is_erased = true;
for (size_t j = 0; j < ENTRY_SIZE; j++) {
uint8_t byte = ((uint8_t*)&entry)[j];
if (byte != 0xFF && byte != 0x00) {
is_erased = false;
break;
}
}
if (is_erased) {
Serial.printf("Erased entry at index %u\n", index);
return false;
}
if (!verifyEntry(entry)) {
Serial.printf("CRC mismatch at index %u\n", index);
return false;
}
}
return true;
}
bool LogModule::begin() {
partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_ANY,
"log_storage");
if (!partition) {
Serial.println("Log partition not found!");
return false;
}
partition_size = partition->size;
// Проверяем что партиция имеет правильный размер (2 МБ)
if (partition_size != LOG_PARTITION_SIZE) {
Serial.printf("ERROR: Partition size mismatch! Expected: %u bytes, Got: %u bytes\n",
LOG_PARTITION_SIZE, partition_size);
Serial.println("Check partitions.csv file");
return false;
}
Serial.printf("Log partition: %u bytes (2 MB)\n", partition_size);
// Инициализируем Preferences для метаданных
if (!prefs.begin("log_storage", false)) {
Serial.println("Failed to initialize Preferences");
return false;
}
// Пытаемся загрузить метаданные
if (loadMetadata()) {
// Проверяем целостность
if (!validateAllEntries()) {
Serial.println("Log corruption detected, reformatting...");
esp_partition_erase_range(partition, 0, partition_size);
write_pos = 0;
total_entries = 0;
saveMetadata();
}
} else {
// Первый запуск или поврежденные метаданные
Serial.println("Initializing new log storage...");
esp_partition_erase_range(partition, 0, partition_size);
write_pos = 0;
total_entries = 0;
saveMetadata();
}
Serial.printf("Log system ready. Max entries: %u\n", (partition_size - 8) / ENTRY_SIZE);
return true;
}
bool LogModule::writeRaw(uint32_t offset, const uint8_t* data, size_t size) {
// Проверяем выравнивание
if (size % 4 != 0 || offset % 4 != 0) {
Serial.printf("FATAL: Alignment error: offset=0x%X, size=%u\n", offset, size);
return false;
}
esp_err_t err = esp_partition_write(partition, offset, data, size);
if (err != ESP_OK) {
Serial.printf("Write failed: err=0x%X\n", err);
return false;
}
return true;
}
bool LogModule::readRaw(uint32_t offset, uint8_t* data, size_t size) {
if (size % 4 != 0 || offset % 4 != 0) {
#ifdef FLASH_PRINT
Serial.printf("FATAL: Alignment error: offset=0x%X, size=%u\n", offset, size);
#endif
return false;
}
esp_err_t err = esp_partition_read(partition, offset, data, size);
return err == ESP_OK;
}
void LogModule::saveMetadata() {
prefs.putUInt("write_pos", write_pos);
prefs.putUInt("total_entries", total_entries);
#ifdef META_PRINT
Serial.printf("Metadata saved: write_pos=%u, total_entries=%u\n", write_pos, total_entries);
#endif
}
bool LogModule::loadMetadata() {
write_pos = prefs.getUInt("write_pos", 0);
total_entries = prefs.getUInt("total_entries", 0);
// Валидация загруженных метаданных
uint32_t max_entries = (partition_size - 8) / ENTRY_SIZE;
if (write_pos >= max_entries || total_entries > max_entries) {
Serial.printf("Invalid metadata, resetting: write_pos=%u, total_entries=%u\n",
write_pos, total_entries);
write_pos = 0;
total_entries = 0;
saveMetadata();
return false;
}
#ifdef META_PRINT
Serial.printf("Metadata loaded: write_pos=%u, total_entries=%u\n", write_pos, total_entries);
#endif
return true;
}
bool LogModule::writeLog(const LogEntry &entry) {
if (!partition) return false;
// Подготавливаем запись с CRC
LogEntry safe_entry = entry;
safe_entry.crc = calculateCRC(safe_entry);
memset(safe_entry.reserved, 0, sizeof(safe_entry.reserved)); // Обнуляем reserved
// Вычисляем позицию
uint32_t entry_offset = 8 + (write_pos * ENTRY_SIZE); // 8 байт метаданных
#ifdef FLASH_PRINT
Serial.printf("Writing to offset 0x%X: SEQ:%u Size:%u\n",
entry_offset, safe_entry.seq, ENTRY_SIZE);
#endif
// Записываем запись
if (writeRaw(entry_offset, (uint8_t*)&safe_entry, ENTRY_SIZE)) {
write_pos = (write_pos + 1) % ((partition_size - 8) / ENTRY_SIZE);
total_entries++;
// Сохраняем метаlанные
saveMetadata();
#ifdef LOGGED_PRINT
String tsStr = String(entry.ts);
Serial.println("Logged SEQ:" + String(entry.seq) +
" TS:" + tsStr +
" EVT:" + (entry.event_type == 0 ? "RECV" : "SEND") +
" PAYLOAD:" + String(entry.payload) +
" CRC:0x" + String(safe_entry.crc, HEX));
#endif
return true;
}
return false;
}
void LogModule::dumpLogs() {
if (!partition) return;
// БЛОКИРУЕМ ВЫПОЛНЕНИЕ ПОКА ВСЕ НЕ ОТПРАВИМ
Serial.println("\n=== LOG DUMP START ===");
Serial.flush(); // Ждем отправки
Serial.printf("Total entries: %u, Write pos: %u\n", total_entries, write_pos);
Serial.flush();
uint32_t max_entries = (partition_size - 8) / ENTRY_SIZE;
uint32_t entries_to_read = min(total_entries, max_entries);
// ПРАВИЛЬНАЯ логика для кольцевого буфера
uint32_t start_index = 0;
if (total_entries > max_entries) {
// Буфер переполнен - начинаем с позиции, где находятся самые старые данные
start_index = write_pos;
}
Serial.printf("Reading %u entries, start index: %u\n", entries_to_read, start_index);
Serial.flush();
for (uint32_t i = 0; i < entries_to_read; i++) {
uint32_t index = (start_index + i) % max_entries;
uint32_t offset = 8 + (index * ENTRY_SIZE);
LogEntry entry;
if (readRaw(offset, (uint8_t*)&entry, ENTRY_SIZE)) {
if (verifyEntry(entry)) {
Serial.printf("Entry %u: SEQ:%u TS:%llu EVT:%s PAYLOAD:%s\n",
i, entry.seq, entry.ts,
entry.event_type == 0 ? "RECV" : "SEND",
entry.payload);
} else {
Serial.printf("Entry %u: CORRUPTED - SEQ:%u CRC:0x%02X\n",
i, entry.seq, entry.crc);
}
// ПРИНУДИТЕЛЬНО ЖДЕМ ОТПРАВКИ КАЖДОЙ ЗАПИСИ
Serial.flush();
} else {
Serial.printf("Read failed at index %u, offset 0x%X\n", i, offset);
Serial.flush();
break;
}
}
// ФИНАЛЬНОЕ ОЖИДАНИЕ
Serial.println("=== LOG DUMP END ===");
Serial.flush();
}
void LogModule::clearLogs() {
Serial.println("\n=== LOG ERASING ===");
// Очищаем данные в партиции
esp_partition_erase_range(partition, 0, partition_size);
// Очищаем метаданные в Preferences
prefs.clear();
// Сбрасываем переменные
write_pos = 0;
total_entries = 0;
// Сохраняем начальные метаданные
saveMetadata();
Serial.println("=== LOG CLEARED ===");
}
void LogModule::handleUART(char cmd, char dumpCmd, char clearCmd) {
if (cmd == dumpCmd) dumpLogs();
else if (cmd == clearCmd) clearLogs();
}

61
logs.h Normal file
View File

@ -0,0 +1,61 @@
#ifndef LOG_MODULE_H
#define LOG_MODULE_H
#include <Arduino.h>
#include <esp_partition.h>
#include <Preferences.h>
#define PAYLOAD_SIZE 16
// Используем partition для логов
#define LOG_PARTITION_SUBTYPE 0x70
//#define META_PRINT
//#define FLASH_PRINT
//#define LOGGED_PRINT
// Размер партиции для логов - строго 2 МБ
#define LOG_PARTITION_SIZE 0x200000 // 2 MB = 2097152 bytes
struct LogEntry {
uint32_t seq;
uint64_t ts;
uint8_t event_type;
uint8_t crc;
char payload[PAYLOAD_SIZE];
uint8_t reserved[2];
} __attribute__((packed));
#define ENTRY_SIZE sizeof(LogEntry)
class LogModule {
public:
LogModule();
bool begin();
bool writeLog(const LogEntry &entry);
void dumpLogs();
void clearLogs();
void handleUART(char cmd, char dumpCmd = 'd', char clearCmd = 'c');
private:
const esp_partition_t* partition;
uint32_t write_pos;
uint32_t total_entries;
uint32_t partition_size;
Preferences prefs;
uint8_t calculateCRC(const LogEntry &entry);
bool verifyEntry(const LogEntry &entry);
void recoverLog();
uint32_t findLastValidEntry();
bool validateAllEntries();
bool readRaw(uint32_t offset, uint8_t* data, size_t size);
bool writeRaw(uint32_t offset, const uint8_t* data, size_t size);
void saveMetadata();
bool loadMetadata();
};
#endif

6009
logs_stat_example.csv Normal file

File diff suppressed because it is too large Load Diff

5
partitions.csv Normal file
View File

@ -0,0 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
phy_init, data, phy, 0xe000, 0x1000,
factory, app, factory, 0x10000, 1M,
log_storage, data, 0x70, , 2M,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 phy_init data phy 0xe000 0x1000
4 factory app factory 0x10000 1M
5 log_storage data 0x70 2M