Spaces:
Sleeping
Sleeping
| from flask import Flask, request, jsonify, send_file, g, Response | |
| from flask_cors import CORS | |
| from datetime import datetime | |
| import sqlite3 | |
| import time | |
| import os | |
| import csv | |
| import io | |
| # βββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HOST = "0.0.0.0" | |
| PORT = 7860 | |
| DB_PATH = os.path.join(os.path.dirname(__file__), "telemetry.db") | |
| app = Flask(__name__) | |
| CORS(app) | |
| # βββ DATABASE βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_db(): | |
| if "db" not in g: | |
| g.db = sqlite3.connect(DB_PATH, detect_types=sqlite3.PARSE_DECLTYPES) | |
| g.db.row_factory = sqlite3.Row | |
| g.db.execute("PRAGMA journal_mode=WAL") | |
| g.db.execute("PRAGMA synchronous=NORMAL") | |
| return g.db | |
| def close_db(exc=None): | |
| db = g.pop("db", None) | |
| if db: | |
| db.close() | |
| def init_db(): | |
| with sqlite3.connect(DB_PATH) as db: | |
| db.execute("PRAGMA journal_mode=WAL") | |
| db.execute(""" | |
| CREATE TABLE IF NOT EXISTS telemetry ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| device_id TEXT NOT NULL, | |
| server_time TEXT NOT NULL, | |
| server_unix REAL NOT NULL, | |
| uptime_sec INTEGER DEFAULT 0, | |
| -- Motion | |
| speed_pct REAL NOT NULL, | |
| speed_ups REAL NOT NULL, | |
| pos_x REAL NOT NULL, | |
| pos_y REAL NOT NULL DEFAULT 0, | |
| pos_z REAL NOT NULL DEFAULT 0, | |
| total_distance REAL NOT NULL, | |
| -- Power | |
| voltage_V REAL NOT NULL, | |
| current_mA REAL NOT NULL, | |
| current_A REAL NOT NULL, | |
| power_mW REAL NOT NULL, | |
| power_W REAL NOT NULL, | |
| drawn_mW REAL NOT NULL DEFAULT 0, | |
| generated_mW REAL NOT NULL DEFAULT 0, | |
| samples INTEGER NOT NULL DEFAULT 25, | |
| interval_sec INTEGER NOT NULL DEFAULT 5 | |
| ) | |
| """) | |
| db.execute(""" | |
| CREATE INDEX IF NOT EXISTS idx_device_time | |
| ON telemetry (device_id, server_unix DESC) | |
| """) | |
| db.commit() | |
| print(f"[DB] SQLite ready β {DB_PATH}") | |
| # π΄ IMPORTANT FIX (Gunicorn compatible) | |
| with app.app_context(): | |
| init_db() | |
| def row_to_dict(row): | |
| return dict(row) | |
| # βββ HELPERS ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def query(sql, params=(), one=False): | |
| cur = get_db().execute(sql, params) | |
| rows = cur.fetchall() | |
| result = [row_to_dict(r) for r in rows] | |
| return result[0] if (one and result) else (None if one else result) | |
| def compute_stats(rows): | |
| if not rows: | |
| return {} | |
| n = len(rows) | |
| return { | |
| "speed_avg_pct": round(sum(r["speed_pct"] for r in rows) / n, 2), | |
| "speed_max_pct": round(max(r["speed_pct"] for r in rows), 2), | |
| "power_avg_mW": round(sum(r["power_mW"] for r in rows) / n, 2), | |
| "power_max_mW": round(max(r["power_mW"] for r in rows), 2), | |
| "voltage_avg_V": round(sum(r["voltage_V"] for r in rows) / n, 3), | |
| "pos_x_latest": round(rows[-1]["pos_x"], 3), | |
| "pos_x_range": round( | |
| max(r["pos_x"] for r in rows) - min(r["pos_x"] for r in rows), 3 | |
| ), | |
| } | |
| def safe_float(data, key, default=0.0): | |
| try: | |
| return float(data.get(key, default)) | |
| except (TypeError, ValueError): | |
| return default | |
| # βββ ROUTES βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def receive_telemetry(): | |
| data = request.get_json(force=True, silent=True) | |
| if not data: | |
| return jsonify({"error": "Invalid JSON"}), 400 | |
| required = ["device_id", "speed_pct", "speed_ups", "position", "total_distance", "power"] | |
| missing = [k for k in required if k not in data] | |
| if missing: | |
| return jsonify({"error": f"Missing fields: {missing}"}), 400 | |
| pos = data["position"] | |
| pwr = data["power"] | |
| now = datetime.now() | |
| server_time = now.isoformat(timespec="milliseconds") | |
| server_unix = time.time() | |
| db = get_db() | |
| db.execute(""" | |
| INSERT INTO telemetry ( | |
| device_id, server_time, server_unix, uptime_sec, | |
| speed_pct, speed_ups, | |
| pos_x, pos_y, pos_z, total_distance, | |
| voltage_V, current_mA, current_A, | |
| power_mW, power_W, drawn_mW, generated_mW, | |
| samples, interval_sec | |
| ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) | |
| """, ( | |
| data["device_id"], | |
| server_time, | |
| server_unix, | |
| data.get("uptime_sec", 0), | |
| float(data["speed_pct"]), | |
| float(data["speed_ups"]), | |
| safe_float(pos, "x"), | |
| safe_float(pos, "y"), | |
| safe_float(pos, "z"), | |
| float(data["total_distance"]), | |
| float(pwr["voltage_V"]), | |
| float(pwr["current_mA"]), | |
| float(pwr["current_A"]), | |
| float(pwr["power_mW"]), | |
| float(pwr["power_W"]), | |
| safe_float(pwr, "drawn_mW"), | |
| safe_float(pwr, "generated_mW"), | |
| int(pwr.get("samples", 25)), | |
| int(pwr.get("interval_sec", 5)), | |
| )) | |
| db.commit() | |
| return jsonify({"status": "ok", "server_time": server_time}), 201 | |
| def get_latest(): | |
| row = query( | |
| "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT 1", | |
| one=True | |
| ) | |
| if not row: | |
| return jsonify({"error": "No data yet"}), 404 | |
| total = query("SELECT COUNT(*) AS n FROM telemetry", one=True)["n"] | |
| return jsonify({ | |
| "latest": row, | |
| "total_stored": total | |
| }) | |
| def get_history(): | |
| limit = min(int(request.args.get("limit", 200)), 1000) | |
| rows = query( | |
| "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT ?", | |
| (limit,) | |
| ) | |
| rows = list(reversed(rows)) | |
| return jsonify({ | |
| "readings": rows, | |
| "count": len(rows), | |
| "stats": compute_stats(rows) | |
| }) | |
| def get_devices(): | |
| rows = query(""" | |
| SELECT device_id, | |
| COUNT(*) AS total_packets, | |
| MAX(server_time) AS last_seen, | |
| MAX(pos_x) AS max_x, | |
| MAX(speed_pct) AS max_speed, | |
| AVG(voltage_V) AS avg_voltage, | |
| AVG(power_mW) AS avg_power | |
| FROM telemetry | |
| GROUP BY device_id | |
| """) | |
| devices = {r["device_id"]: r for r in rows} | |
| return jsonify({ | |
| "devices": devices, | |
| "count": len(devices) | |
| }) | |
| def export_csv(): | |
| rows = query("SELECT * FROM telemetry ORDER BY server_unix ASC") | |
| if not rows: | |
| return jsonify({"error": "No data"}), 404 | |
| out = io.StringIO() | |
| writer = csv.DictWriter(out, fieldnames=rows[0].keys()) | |
| writer.writeheader() | |
| writer.writerows(rows) | |
| return Response( | |
| out.getvalue(), | |
| mimetype="text/csv", | |
| headers={"Content-Disposition": "attachment; filename=telemetry_all.csv"} | |
| ) | |
| def clear_data(): | |
| db = get_db() | |
| db.execute("DELETE FROM telemetry") | |
| db.commit() | |
| return jsonify({"status": "ok", "message": "All data cleared"}) | |
| def health(): | |
| row = query("SELECT COUNT(*) AS n FROM telemetry", one=True) | |
| return jsonify({ | |
| "status": "ok", | |
| "db": DB_PATH, | |
| "total_records": row["n"] if row else 0, | |
| "server_time": datetime.now().isoformat(timespec="milliseconds") | |
| }) | |
| def dashboard(): | |
| html_path = os.path.join(os.path.dirname(__file__), "dashboard.html") | |
| return send_file(html_path) | |
| # βββ MAIN βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| init_db() | |
| print(f"\n[SERVER] Flask running β http://{HOST}:{PORT}") | |
| print(f"[SERVER] Dashboard β http://localhost:{PORT}") | |
| print(f"[SERVER] API telemetry β POST http://localhost:{PORT}/api/telemetry") | |
| print(f"[SERVER] DB path β {DB_PATH}\n") | |
| app.run(host=HOST, port=PORT, debug=True) |