сделан сервер на 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