сделан сервер на esp, терминалка имитирующая клинета и терминалка для считывания логов
сделан также клиент на esp, но не проверен
This commit is contained in:
commit
5cc802c3be
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/.vscode/
|
289
ESP_WifiTest.ino
Normal file
289
ESP_WifiTest.ino
Normal 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
131
esp_client_emu.py
Normal 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
224
getLogs.py
Normal 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
371
logs.cpp
Normal 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
61
logs.h
Normal 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
6009
logs_stat_example.csv
Normal file
File diff suppressed because it is too large
Load Diff
5
partitions.csv
Normal file
5
partitions.csv
Normal 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,
|
|
Loading…
Reference in New Issue
Block a user