generated from Grigo/AndroidTemplate
Initial commit: LoraTester Android + server
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""Primary LoraTester server: REST API + web UI."""
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, jsonify, request, send_from_directory
|
||||
from flask_cors import CORS
|
||||
|
||||
from core.auth import ANDROID_CLIENT_HEADER, is_android_client
|
||||
from core.config import HOST, PORT
|
||||
from core.models import ChatIn, TelemetryIn
|
||||
from core import storage
|
||||
from core.telemetry_body import telemetry_from_body
|
||||
|
||||
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
||||
|
||||
app = Flask(__name__, static_folder=str(STATIC_DIR), static_url_path="/static")
|
||||
CORS(app)
|
||||
|
||||
storage.init_db()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
resp = send_from_directory(STATIC_DIR, "index.html")
|
||||
resp.headers["Cache-Control"] = "no-store"
|
||||
return resp
|
||||
|
||||
|
||||
@app.post("/api/telemetry")
|
||||
def post_telemetry():
|
||||
if not is_android_client(request.headers):
|
||||
return jsonify({
|
||||
"error": "telemetry only from Android app",
|
||||
"hint": f"send header {ANDROID_CLIENT_HEADER}: android",
|
||||
}), 403
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
device_id = body.get("device_id")
|
||||
if not device_id:
|
||||
return jsonify({"error": "device_id required"}), 400
|
||||
try:
|
||||
data = telemetry_from_body({**body, "device_id": device_id})
|
||||
return jsonify(storage.record_telemetry(data))
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.get("/api/devices")
|
||||
def get_devices():
|
||||
return jsonify(storage.list_devices())
|
||||
|
||||
|
||||
@app.get("/api/telemetry")
|
||||
def get_telemetry_history():
|
||||
device_id = request.args.get("device_id")
|
||||
limit = int(request.args.get("limit", 100))
|
||||
since = _float_or_none(request.args.get("since"))
|
||||
until = _float_or_none(request.args.get("until"))
|
||||
role = request.args.get("role")
|
||||
return jsonify(storage.get_telemetry(device_id, limit, since, until, role))
|
||||
|
||||
|
||||
@app.get("/api/stats/history")
|
||||
def get_stats_history():
|
||||
device_id = request.args.get("device_id")
|
||||
if not device_id:
|
||||
return jsonify({"error": "device_id required"}), 400
|
||||
limit = int(request.args.get("limit", 50))
|
||||
since = _float_or_none(request.args.get("since"))
|
||||
until = _float_or_none(request.args.get("until"))
|
||||
role = request.args.get("role")
|
||||
return jsonify(storage.get_telemetry(device_id, limit, since, until, role))
|
||||
|
||||
|
||||
@app.post("/api/tracks/start")
|
||||
def tracks_start():
|
||||
if not is_android_client(request.headers):
|
||||
return jsonify({"error": "Android only"}), 403
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
device_id = body.get("device_id")
|
||||
if not device_id:
|
||||
return jsonify({"error": "device_id required"}), 400
|
||||
try:
|
||||
return jsonify(storage.start_track(str(device_id), body.get("label")))
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.post("/api/tracks/<int:track_id>/points")
|
||||
def tracks_points(track_id: int):
|
||||
if not is_android_client(request.headers):
|
||||
return jsonify({"error": "Android only"}), 403
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
points = body.get("points") or []
|
||||
try:
|
||||
return jsonify(storage.add_track_points(track_id, points))
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.post("/api/tracks/<int:track_id>/finish")
|
||||
def tracks_finish(track_id: int):
|
||||
if not is_android_client(request.headers):
|
||||
return jsonify({"error": "Android only"}), 403
|
||||
try:
|
||||
return jsonify(storage.finish_track(track_id))
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.get("/api/tracks")
|
||||
def tracks_list():
|
||||
device_id = request.args.get("device_id")
|
||||
limit = int(request.args.get("limit", 50))
|
||||
return jsonify(storage.list_tracks(device_id, limit))
|
||||
|
||||
|
||||
@app.get("/api/tracks/<int:track_id>")
|
||||
def tracks_get(track_id: int):
|
||||
try:
|
||||
return jsonify(storage.get_track(track_id))
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 404
|
||||
|
||||
|
||||
@app.post("/api/chat")
|
||||
def post_chat():
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
device_id = body.get("device_id")
|
||||
text = (body.get("text") or "").strip()
|
||||
if not device_id or not text:
|
||||
return jsonify({"error": "device_id and text required"}), 400
|
||||
data = ChatIn(
|
||||
device_id=str(device_id),
|
||||
text=text,
|
||||
ts=_float_or_none(body.get("ts")),
|
||||
)
|
||||
return jsonify(storage.add_chat(data))
|
||||
|
||||
|
||||
@app.get("/api/chat")
|
||||
def get_chat():
|
||||
since = float(request.args.get("since", 0))
|
||||
limit = int(request.args.get("limit", 200))
|
||||
return jsonify(storage.get_chat(since, limit))
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
status = storage.db_status()
|
||||
return jsonify({"ok": status["db_ok"], "ts": time.time(), **status})
|
||||
|
||||
|
||||
def _float_or_none(value):
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host=HOST, port=PORT, debug=True)
|
||||
Reference in New Issue
Block a user