NitinBot001 commited on
Commit
afca6eb
Β·
verified Β·
1 Parent(s): 4e75900

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +439 -0
app.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Flask + SQLite Telemetry Server v2.0
3
+ ESP32 (INA219 + Potentiometer) β†’ Flask API β†’ SQLite DB β†’ Dashboard
4
+
5
+ Install: pip install flask flask-cors
6
+ Run: python server.py
7
+ Dashboard: http://localhost:8000
8
+
9
+ DB file: telemetry.db (auto-created on first run)
10
+
11
+ POST /api/telemetry ← ESP32 ΰ€―ΰ€Ήΰ€Ύΰ€ data ΰ€­ΰ₯‡ΰ€œΰ€€ΰ€Ύ ΰ€Ήΰ₯ˆ
12
+ GET /api/latest ← latest 1 record
13
+ GET /api/history ← last N records + stats
14
+ GET /api/track ← position + speed only (lightweight)
15
+ GET /api/stats ← session summary + energy
16
+ GET /api/devices ← all registered devices
17
+ GET /api/export ← CSV download
18
+ DELETE /api/clear ← data clear
19
+ GET /health ← server health check
20
+ """
21
+
22
+ from flask import Flask, request, jsonify, send_file, g, Response
23
+ from flask_cors import CORS
24
+ from datetime import datetime
25
+ import sqlite3
26
+ import time
27
+ import os
28
+ import csv
29
+ import io
30
+
31
+ # ─── CONFIG ──────────────────────────────────────────────────
32
+ HOST = "0.0.0.0"
33
+ PORT = 8000
34
+ DB_PATH = os.path.join(os.path.dirname(__file__), "telemetry.db")
35
+
36
+ app = Flask(__name__)
37
+ CORS(app)
38
+
39
+ # ─── DATABASE ─────────────────────────────────────────────────
40
+
41
+ def get_db():
42
+ if "db" not in g:
43
+ g.db = sqlite3.connect(DB_PATH, detect_types=sqlite3.PARSE_DECLTYPES)
44
+ g.db.row_factory = sqlite3.Row
45
+ g.db.execute("PRAGMA journal_mode=WAL")
46
+ g.db.execute("PRAGMA synchronous=NORMAL")
47
+ return g.db
48
+
49
+
50
+ @app.teardown_appcontext
51
+ def close_db(exc=None):
52
+ db = g.pop("db", None)
53
+ if db:
54
+ db.close()
55
+
56
+
57
+ def init_db():
58
+ with sqlite3.connect(DB_PATH) as db:
59
+ db.execute("PRAGMA journal_mode=WAL")
60
+ db.execute("""
61
+ CREATE TABLE IF NOT EXISTS telemetry (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ device_id TEXT NOT NULL,
64
+ server_time TEXT NOT NULL,
65
+ server_unix REAL NOT NULL,
66
+ uptime_sec INTEGER DEFAULT 0,
67
+
68
+ -- Motion
69
+ speed_pct REAL NOT NULL,
70
+ speed_ups REAL NOT NULL,
71
+ pos_x REAL NOT NULL,
72
+ pos_y REAL NOT NULL DEFAULT 0,
73
+ pos_z REAL NOT NULL DEFAULT 0,
74
+ total_distance REAL NOT NULL,
75
+
76
+ -- Power (from INA219)
77
+ voltage_V REAL NOT NULL,
78
+ current_mA REAL NOT NULL,
79
+ current_A REAL NOT NULL,
80
+ power_mW REAL NOT NULL,
81
+ power_W REAL NOT NULL,
82
+ drawn_mW REAL NOT NULL DEFAULT 0,
83
+ generated_mW REAL NOT NULL DEFAULT 0,
84
+ samples INTEGER NOT NULL DEFAULT 25,
85
+ interval_sec INTEGER NOT NULL DEFAULT 5
86
+ )
87
+ """)
88
+ db.execute("""
89
+ CREATE INDEX IF NOT EXISTS idx_device_time
90
+ ON telemetry (device_id, server_unix DESC)
91
+ """)
92
+ db.commit()
93
+ print(f"[DB] SQLite ready β†’ {DB_PATH}")
94
+
95
+
96
+ def row_to_dict(row):
97
+ return dict(row)
98
+
99
+
100
+ # ─── HELPERS ──────────────────────────────────────────────────
101
+
102
+ def query(sql, params=(), one=False):
103
+ cur = get_db().execute(sql, params)
104
+ rows = cur.fetchall()
105
+ result = [row_to_dict(r) for r in rows]
106
+ return result[0] if (one and result) else (None if one else result)
107
+
108
+
109
+ def compute_stats(rows):
110
+ if not rows:
111
+ return {}
112
+ n = len(rows)
113
+ return {
114
+ "speed_avg_pct": round(sum(r["speed_pct"] for r in rows) / n, 2),
115
+ "speed_max_pct": round(max(r["speed_pct"] for r in rows), 2),
116
+ "power_avg_mW": round(sum(r["power_mW"] for r in rows) / n, 2),
117
+ "power_max_mW": round(max(r["power_mW"] for r in rows), 2),
118
+ "voltage_avg_V": round(sum(r["voltage_V"] for r in rows) / n, 3),
119
+ "pos_x_latest": round(rows[-1]["pos_x"], 3),
120
+ "pos_x_range": round(
121
+ max(r["pos_x"] for r in rows) - min(r["pos_x"] for r in rows), 3
122
+ ),
123
+ }
124
+
125
+
126
+ def safe_float(data, key, default=0.0):
127
+ """Safely extract float from dict, return default if missing/invalid."""
128
+ try:
129
+ return float(data.get(key, default))
130
+ except (TypeError, ValueError):
131
+ return default
132
+
133
+
134
+ # ─── ROUTES ───────────────────────────────────────────────────
135
+
136
+ @app.post("/api/telemetry")
137
+ def receive_telemetry():
138
+ """
139
+ ESP32 POSTs JSON here every interval.
140
+
141
+ Required body:
142
+ {
143
+ "device_id" : "esp32-tracker-01",
144
+ "uptime_sec" : 120, ← optional (default 0)
145
+ "speed_pct" : 73.4, ← 0–100
146
+ "speed_ups" : 7.34, ← speed_pct / 10
147
+ "position" : { "x": 182.5, "y": 0, "z": 0 },
148
+ "total_distance": 182.5,
149
+ "power": {
150
+ "voltage_V" : 3.712,
151
+ "current_mA" : 245.6,
152
+ "current_A" : 0.2456,
153
+ "power_mW" : 912.5,
154
+ "power_W" : 0.9125,
155
+ "drawn_mW" : 912.5,
156
+ "generated_mW" : 0.0,
157
+ "samples" : 25,
158
+ "interval_sec" : 5
159
+ }
160
+ }
161
+ """
162
+ data = request.get_json(force=True, silent=True)
163
+ if not data:
164
+ return jsonify({"error": "Invalid JSON"}), 400
165
+
166
+ # Required fields validation
167
+ required = ["device_id", "speed_pct", "speed_ups", "position", "total_distance", "power"]
168
+ missing = [k for k in required if k not in data]
169
+ if missing:
170
+ return jsonify({"error": f"Missing fields: {missing}"}), 400
171
+
172
+ pos = data["position"]
173
+ pwr = data["power"]
174
+
175
+ # Required power fields
176
+ required_pwr = ["voltage_V", "current_mA", "current_A", "power_mW", "power_W"]
177
+ missing_pwr = [k for k in required_pwr if k not in pwr]
178
+ if missing_pwr:
179
+ return jsonify({"error": f"Missing power fields: {missing_pwr}"}), 400
180
+
181
+ now = datetime.now()
182
+ server_time = now.isoformat(timespec="milliseconds")
183
+ server_unix = time.time()
184
+
185
+ db = get_db()
186
+ db.execute("""
187
+ INSERT INTO telemetry (
188
+ device_id, server_time, server_unix, uptime_sec,
189
+ speed_pct, speed_ups,
190
+ pos_x, pos_y, pos_z, total_distance,
191
+ voltage_V, current_mA, current_A,
192
+ power_mW, power_W, drawn_mW, generated_mW,
193
+ samples, interval_sec
194
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
195
+ """, (
196
+ data["device_id"],
197
+ server_time,
198
+ server_unix,
199
+ data.get("uptime_sec", 0),
200
+ float(data["speed_pct"]),
201
+ float(data["speed_ups"]),
202
+ safe_float(pos, "x"),
203
+ safe_float(pos, "y"),
204
+ safe_float(pos, "z"),
205
+ float(data["total_distance"]),
206
+ float(pwr["voltage_V"]),
207
+ float(pwr["current_mA"]),
208
+ float(pwr["current_A"]),
209
+ float(pwr["power_mW"]),
210
+ float(pwr["power_W"]),
211
+ safe_float(pwr, "drawn_mW"),
212
+ safe_float(pwr, "generated_mW"),
213
+ int(pwr.get("samples", 25)),
214
+ int(pwr.get("interval_sec", 5)),
215
+ ))
216
+ db.commit()
217
+
218
+ print(
219
+ f"[{server_time[:19]}] {data['device_id']} | "
220
+ f"Spd={float(data['speed_pct']):.1f}% | "
221
+ f"X={safe_float(pos,'x'):.3f} | "
222
+ f"V={float(pwr['voltage_V']):.3f}V | "
223
+ f"P={float(pwr['power_mW']):.2f}mW"
224
+ )
225
+
226
+ return jsonify({"status": "ok", "server_time": server_time}), 201
227
+
228
+
229
+ @app.get("/api/latest")
230
+ def get_latest():
231
+ device_id = request.args.get("device_id")
232
+ if device_id:
233
+ row = query(
234
+ "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix DESC LIMIT 1",
235
+ (device_id,), one=True
236
+ )
237
+ else:
238
+ row = query(
239
+ "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT 1",
240
+ one=True
241
+ )
242
+
243
+ if not row:
244
+ return jsonify({"error": "No data yet"}), 404
245
+
246
+ total = query("SELECT COUNT(*) AS n FROM telemetry", one=True)["n"]
247
+ return jsonify({"latest": row, "total_stored": total})
248
+
249
+
250
+ @app.get("/api/history")
251
+ def get_history():
252
+ limit = min(int(request.args.get("limit", 200)), 1000) # max 1000
253
+ device_id = request.args.get("device_id")
254
+
255
+ if device_id:
256
+ rows = query(
257
+ "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix DESC LIMIT ?",
258
+ (device_id, limit)
259
+ )
260
+ else:
261
+ rows = query(
262
+ "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT ?",
263
+ (limit,)
264
+ )
265
+
266
+ rows = list(reversed(rows))
267
+ return jsonify({
268
+ "readings": rows,
269
+ "count": len(rows),
270
+ "stats": compute_stats(rows),
271
+ })
272
+
273
+
274
+ @app.get("/api/track")
275
+ def get_track():
276
+ """Position + speed only β€” lightweight for real-time visualization."""
277
+ limit = min(int(request.args.get("limit", 500)), 2000)
278
+ device_id = request.args.get("device_id")
279
+
280
+ if device_id:
281
+ rows = query(
282
+ "SELECT server_unix, pos_x, pos_y, speed_pct FROM telemetry "
283
+ "WHERE device_id=? ORDER BY server_unix DESC LIMIT ?",
284
+ (device_id, limit)
285
+ )
286
+ else:
287
+ rows = query(
288
+ "SELECT server_unix, pos_x, pos_y, speed_pct FROM telemetry "
289
+ "ORDER BY server_unix DESC LIMIT ?",
290
+ (limit,)
291
+ )
292
+
293
+ rows = list(reversed(rows))
294
+ track = [{
295
+ "t": r["server_unix"],
296
+ "x": r["pos_x"],
297
+ "y": r["pos_y"],
298
+ "spd": r["speed_pct"]
299
+ } for r in rows]
300
+ return jsonify({"track": track, "points": len(track)})
301
+
302
+
303
+ @app.get("/api/stats")
304
+ def get_stats():
305
+ """Full session summary with energy calculation."""
306
+ device_id = request.args.get("device_id")
307
+
308
+ if device_id:
309
+ rows = query(
310
+ "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix ASC",
311
+ (device_id,)
312
+ )
313
+ else:
314
+ rows = query("SELECT * FROM telemetry ORDER BY server_unix ASC")
315
+
316
+ if not rows:
317
+ return jsonify({"error": "No data"}), 404
318
+
319
+ n = len(rows)
320
+ interval = rows[-1]["interval_sec"] or 5
321
+ total_h = (n * interval) / 3600.0
322
+ energy_mWh = sum(r["power_mW"] for r in rows) * (interval / 3600.0)
323
+
324
+ return jsonify({
325
+ "total_packets": n,
326
+ "total_time_minutes": round(total_h * 60, 2),
327
+ "device_id": rows[-1]["device_id"],
328
+ "speed": {
329
+ "avg_pct": round(sum(r["speed_pct"] for r in rows) / n, 2),
330
+ "max_pct": round(max(r["speed_pct"] for r in rows), 2),
331
+ "avg_ups": round(sum(r["speed_ups"] for r in rows) / n, 3),
332
+ },
333
+ "position": {
334
+ "current_x": round(rows[-1]["pos_x"], 3),
335
+ "total_distance": round(rows[-1]["total_distance"], 3),
336
+ },
337
+ "power": {
338
+ "voltage_avg_V": round(sum(r["voltage_V"] for r in rows) / n, 3),
339
+ "current_avg_mA": round(sum(r["current_mA"] for r in rows) / n, 2),
340
+ "power_avg_mW": round(sum(r["power_mW"] for r in rows) / n, 2),
341
+ "power_max_mW": round(max(r["power_mW"] for r in rows), 2),
342
+ "total_energy_mWh": round(energy_mWh, 4),
343
+ "drawn_total_mWh": round(
344
+ sum(r["drawn_mW"] for r in rows) * (interval / 3600.0), 4
345
+ ),
346
+ },
347
+ "first_packet": rows[0]["server_time"],
348
+ "last_packet": rows[-1]["server_time"],
349
+ })
350
+
351
+
352
+ @app.get("/api/devices")
353
+ def get_devices():
354
+ """List all devices with last-seen info and packet count."""
355
+ rows = query("""
356
+ SELECT device_id,
357
+ COUNT(*) AS total_packets,
358
+ MAX(server_time) AS last_seen,
359
+ MAX(pos_x) AS max_x,
360
+ MAX(speed_pct) AS max_speed,
361
+ AVG(voltage_V) AS avg_voltage,
362
+ AVG(power_mW) AS avg_power
363
+ FROM telemetry
364
+ GROUP BY device_id
365
+ """)
366
+ devices = {r["device_id"]: r for r in rows}
367
+ return jsonify({"devices": devices, "count": len(devices)})
368
+
369
+
370
+ @app.get("/api/export")
371
+ def export_csv():
372
+ """Download all data as CSV."""
373
+ device_id = request.args.get("device_id")
374
+
375
+ if device_id:
376
+ rows = query(
377
+ "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix ASC",
378
+ (device_id,)
379
+ )
380
+ filename = f"telemetry_{device_id}.csv"
381
+ else:
382
+ rows = query("SELECT * FROM telemetry ORDER BY server_unix ASC")
383
+ filename = "telemetry_all.csv"
384
+
385
+ if not rows:
386
+ return jsonify({"error": "No data"}), 404
387
+
388
+ out = io.StringIO()
389
+ writer = csv.DictWriter(out, fieldnames=rows[0].keys())
390
+ writer.writeheader()
391
+ writer.writerows(rows)
392
+
393
+ return Response(
394
+ out.getvalue(),
395
+ mimetype="text/csv",
396
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
397
+ )
398
+
399
+
400
+ @app.delete("/api/clear")
401
+ def clear_data():
402
+ """Delete all records (or by device_id)."""
403
+ device_id = request.args.get("device_id")
404
+ db = get_db()
405
+ if device_id:
406
+ db.execute("DELETE FROM telemetry WHERE device_id=?", (device_id,))
407
+ msg = f"Cleared data for '{device_id}'"
408
+ else:
409
+ db.execute("DELETE FROM telemetry")
410
+ msg = "All data cleared"
411
+ db.commit()
412
+ return jsonify({"status": "ok", "message": msg})
413
+
414
+
415
+ @app.get("/health")
416
+ def health():
417
+ row = query("SELECT COUNT(*) AS n FROM telemetry", one=True)
418
+ return jsonify({
419
+ "status": "ok",
420
+ "db": DB_PATH,
421
+ "total_records": row["n"] if row else 0,
422
+ "server_time": datetime.now().isoformat(timespec="milliseconds"),
423
+ })
424
+
425
+
426
+ @app.get("/")
427
+ def dashboard():
428
+ html_path = os.path.join(os.path.dirname(__file__), "dashboard.html")
429
+ return send_file(html_path)
430
+
431
+
432
+ # ─── MAIN ─────────────────────────────────────────────────────
433
+ if __name__ == "__main__":
434
+ init_db()
435
+ print(f"\n[SERVER] Flask running β†’ http://{HOST}:{PORT}")
436
+ print(f"[SERVER] Dashboard β†’ http://localhost:{PORT}")
437
+ print(f"[SERVER] API telemetry β†’ POST http://localhost:{PORT}/api/telemetry")
438
+ print(f"[SERVER] DB path β†’ {DB_PATH}\n")
439
+ app.run(host=HOST, port=PORT, debug=True)