From c28d7285227cb4a5fafff0b8aa4744e9a26ef865 Mon Sep 17 00:00:00 2001 From: vlapa Date: Wed, 17 Jun 2026 03:07:13 +0300 Subject: First --- mqtt_log.py | 120 +++++++++++++ sensors.py | 151 ++++++++++++++++ templates/sensors.html | 474 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 745 insertions(+) create mode 100644 mqtt_log.py create mode 100644 sensors.py create mode 100644 templates/sensors.html diff --git a/mqtt_log.py b/mqtt_log.py new file mode 100644 index 0000000..cac4a01 --- /dev/null +++ b/mqtt_log.py @@ -0,0 +1,120 @@ +import os +import json +import time +from pathlib import Path +import requests +import paho.mqtt.client as mqtt +from dotenv import load_dotenv + +load_dotenv() + +def file_print(data_string, data_data): + """ Обработчик данных от сервера MQTT. Передаем: топик, данные """ + try: + # Извлекаем имя файла из топика + if "/" in data_string: + file_name = data_string[1:data_string.rfind("/")] + # Получаем расширение или вторую часть + value_part = data_string[data_string.rfind("/") + 1:] + else: + file_name = data_string + value_part = "" + + # Формируем пути + csv_path = f"{file_name}.csv" + timestamp = time.strftime('%Y/%m/%d - %H:%M:%S', time.localtime()) + formatted_data = f"/{timestamp}_{data_data}" + + # Запись в файл + fileWrite(os.getenv('FOLDER_ARHIVE_NAME'), csv_path, formatted_data) + + # Обновление кэша JSON + update_json_cache(file_name, f"{data_data}_{time.strftime('%H:%M:%S_%d/%m/%Y', time.localtime())}") + + except Exception as e: + print(f"Ошибка в file_print: {e}") + +def update_json_cache(file_name, value): + """Обновление JSON кэша""" + path = os.getenv('FILE_CACHE_JSON_NAME') + data_json = {file_name: value} + + try: + if Path(path).exists(): + with open(path, 'r', encoding='utf-8') as json_file: + data = json.load(json_file) + data.update(data_json) + else: + data = data_json + + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"Ошибка при обновлении JSON: {e}") + +def connect_mqtt(): + """ Подключение к серверу MQTT """ + try: + # Попытка для новых версий + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + except AttributeError: + # Для старых версий + client = mqtt.Client() + + client.username_pw_set(os.getenv('USERNAME'), os.getenv('PASSWORD')) + client.connect( + os.getenv('SERVER_IP'), + int(os.getenv('PORT_MQTT')) + ) + return client + +def subscribe(client: mqtt): + """ Подписываемся на топики """ + def on_message(client, userdata, msg): + try: + file_print(msg.topic, msg.payload.decode()) + except Exception as error: + print(f"Ошибка в on_message: {error}") + + client.subscribe(os.getenv('TOPIC')) + client.on_message = on_message + +def fileWrite(folderArhiveName, path, myFile): + """ Запись в файл. Передаем: название папки архива, название файла (топика), данные """ + try: + # Извлекаем имя папки + space_pos = myFile.find(' ') + if space_pos == -1: + folder_name = folderArhiveName + myFile + else: + folder_name = folderArhiveName + myFile[:space_pos] + + # Создаем путь + path_temp = Path.cwd() / folder_name + path_temp.mkdir(parents=True, exist_ok=True) + + # Записываем данные + file_path = path_temp / path + with open(file_path, "a", encoding='utf-8') as f: + # Извлекаем данные после пробела + data_part = myFile[myFile.rfind(' ') + 1:] if ' ' in myFile else myFile + f.write(data_part + '\n') + + except Exception as e: + print(f"Ошибка записи файла: {e}") + +def run(): + try: + print("Подключение к MQTT...") + client = connect_mqtt() + subscribe(client) + print("Ожидание сообщений...") + client.loop_forever() + except KeyboardInterrupt: + print("\nПрограмма остановлена") + except Exception as e: + print(f"Критическая ошибка: {e}") + +if __name__ == "__main__": + run() + diff --git a/sensors.py b/sensors.py new file mode 100644 index 0000000..d454a13 --- /dev/null +++ b/sensors.py @@ -0,0 +1,151 @@ +import os +import json +import time +import threading +from flask import Flask, jsonify, request, render_template +from dotenv import load_dotenv +from datetime import datetime + +load_dotenv() + +app = Flask(__name__) + +# Создаем папку для логов при запуске +UPLOAD_FOLDER = 'data/battery_logs' +os.makedirs(UPLOAD_FOLDER, exist_ok=True) + +# Глобальная переменная для хранения данных +cached_data = {} + +def file_read(filename, interval=1.0): + """Функция для постоянного чтения файла в отдельном потоке""" + global cached_data + while True: + try: + with open(filename, 'r', encoding='utf-8') as file: + cached_data = json.load(file) + except FileNotFoundError: + print(f"Файл {filename} не найден. Ожидание...") + except json.JSONDecodeError as e: + print(f"Ошибка декодирования JSON в файле {filename}: {e}") + except Exception as e: + print(f"Неожиданная ошибка при чтении файла {filename}: {e}") + + time.sleep(interval) + +@app.route('/upload', methods=['POST']) +def upload_file(): + """ + Принимаем CSV файл от ESP32-C3 + """ + try: + device_id = request.headers.get('X-Device-ID', 'unknown') + file_name = request.headers.get('X-File-Name', 'log.csv') + + csv_data = request.get_data(as_text=True) + + if not csv_data: + return jsonify({'error': 'No data received'}), 400 + + device_folder = os.path.join(UPLOAD_FOLDER, device_id) + os.makedirs(device_folder, exist_ok=True) + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + safe_filename = f"{timestamp}_{file_name}" + file_path = os.path.join(device_folder, safe_filename) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(csv_data) + + print(f"✅ Получен файл от {device_id}: {len(csv_data)} байт") + print(f" Сохранен как: {file_path}") + + return jsonify({ + 'status': 'success', + 'message': 'File saved', + 'filename': safe_filename, + 'size': len(csv_data) + }), 200 + + except Exception as e: + print(f"❌ Ошибка: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/', methods=['POST']) +def receive_string(): + """Прием простой текстовой строки""" + text_data = request.data.decode('utf-8') + clean_list = [item.strip() for item in text_data.split(',')] + + if not cached_data: + return '*no data*' + + result_values = [] + for key in clean_list: + if key in cached_data: + strValue = str(cached_data[key]) + if '_' in strValue: + result_values.append(strValue[:strValue.index('_')]) + else: + result_values.append(strValue) + else: + result_values.append(f'? {key} ?') + + return '*' + '*'.join(result_values) + '*' + + +@app.route('/', methods=['GET']) +def sample(): + global cached_data + if cached_data: + return jsonify(cached_data) + else: + test_data = { + "Temp": 23.5, + "Hum": 45, + "Pres": 750, + "status": "no_file" + } + return jsonify(test_data) + +@app.route('/sensors', methods=['GET']) +def get_sensors(): + """Красивое отображение данных датчиков в браузере""" + global cached_data + + # Если запрошен JSON формат + if request.args.get('format') == 'json': + if cached_data: + return jsonify(cached_data) + else: + test_data = { + "Temp": 23.5, + "Hum": 45, + "Pres": 750, + "status": "no_data" + } + return jsonify(test_data) + + # Иначе возвращаем красивую HTML страницу + return render_template('sensors.html', data=cached_data) + +if __name__ == '__main__': + filename = os.getenv('FILE_CACHE_JSON_NAME') + + if not filename: + print("❌ Ошибка: переменная FILE_CACHE_JSON_NAME не задана в .env") + filename = 'sensors_cache.json' + + print(f"📁 Файлы будут сохраняться в: {os.path.abspath(UPLOAD_FOLDER)}") + print("🌐 Доступ по адресу: http://localhost:5000") + print("📤 Endpoint для загрузки: http://localhost:5000/upload") + print("📊 Endpoint для датчиков: http://localhost:5000/sensors") + print("-" * 50) + + data_thread = threading.Thread(target=file_read, args=(filename, 2), daemon=True) + data_thread.start() + + host = os.getenv('SERVER_IP', '0.0.0.0') + port = int(os.getenv('PORT_HTTP', 5000)) + + app.run(host=host, port=port) diff --git a/templates/sensors.html b/templates/sensors.html new file mode 100644 index 0000000..3bc21a0 --- /dev/null +++ b/templates/sensors.html @@ -0,0 +1,474 @@ + + + + + + Данные датчиков + + + +
+
+

📊 Данные датчиков

+
+ Всего датчиков: 0 + | + Обновлено: -- +
+
+ +
+ 📄 Показать как JSON +
+ +
+ + + + + + + + + + + + + +
📌 Датчик📈 Значение⚡ Статус
+
+ 🔄 +

Загрузка данных...

+
Ожидайте поступления данных от датчиков
+
+
+
+ + +
+ + + + -- cgit v1.2.3