|
|
|
|
|
from fastapi import FastAPI, WebSocket |
|
from fastapi.responses import HTMLResponse, StreamingResponse |
|
import uvicorn |
|
import json |
|
import csv |
|
from io import StringIO |
|
|
|
app = FastAPI() |
|
telemetry_data = [] |
|
|
|
dashboard_connections = [] |
|
|
|
@app.get("/stream") |
|
async def stream(): |
|
html = """ |
|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>Emerson Telemetry Stream</title> |
|
<style> |
|
body { margin:0; padding:0; display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; background:#eef6fc; font-family:sans-serif; } |
|
h1 { margin-bottom:10px; color:#034f84; } |
|
#status { font-weight:bold; margin-bottom:20px; color:#fc3d03; } |
|
video { border:2px solid #034f84; border-radius:8px; width:100%; max-width:320px; } |
|
</style> |
|
</head> |
|
<body> |
|
<h1>Emerson Telemetry Stream</h1> |
|
<p id="status">Initializing...</p> |
|
<video id="video" autoplay muted playsinline></video> |
|
<script> |
|
const statusEl = document.getElementById('status'); |
|
let buffer = []; |
|
let ws; |
|
|
|
// Initialize WebSocket |
|
function initWS() { |
|
ws = new WebSocket(`wss://${location.host}/ws`); |
|
ws.onopen = () => { statusEl.textContent = 'Connected'; flushBuffer(); }; |
|
ws.onclose = () => statusEl.textContent = 'Disconnected'; |
|
ws.onerror = () => statusEl.textContent = 'WebSocket error'; |
|
} |
|
|
|
// Send telemetry data |
|
function sendTelemetry() { |
|
if (!videoStream) return; |
|
const video = document.getElementById('video'); |
|
const canvas = document.createElement('canvas'); |
|
canvas.width = video.videoWidth; |
|
canvas.height = video.videoHeight; |
|
canvas.getContext('2d').drawImage(video, 0, 0); |
|
const image = canvas.toDataURL('image/jpeg'); |
|
navigator.geolocation.getCurrentPosition(pos => { |
|
const data = { timestamp: Date.now(), gps: pos.coords, image }; |
|
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(data)); else buffer.push(data); |
|
}); |
|
} |
|
|
|
function flushBuffer() { |
|
while (buffer.length && ws.readyState === WebSocket.OPEN) { |
|
ws.send(JSON.stringify(buffer.shift())); |
|
} |
|
} |
|
|
|
// Prompt for camera, mobile compatible |
|
let videoStream; |
|
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }) |
|
.then(stream => { |
|
videoStream = stream; |
|
const video = document.getElementById('video'); |
|
video.srcObject = stream; |
|
statusEl.textContent = navigator.onLine ? 'Connected' : 'Waiting for Emerson...'; |
|
initWS(); |
|
window.addEventListener('online', () => flushBuffer()); |
|
window.addEventListener('offline', () => statusEl.textContent = 'Waiting for Emerson...'); |
|
setInterval(sendTelemetry, 1000); |
|
}) |
|
.catch(err => { |
|
console.error('Camera permission error:', err); |
|
statusEl.textContent = 'Camera permission needed'; |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
return HTMLResponse(html) |
|
|
|
@app.get("/dashboard") |
|
async def dashboard(): |
|
html = """ |
|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>Emerson Dashboard</title> |
|
<style> |
|
body { margin:0; padding:20px; background:#f8f9fa; font-family:sans-serif; } |
|
h1 { margin-bottom:10px; color:#343a40; } |
|
#status { font-weight:bold; margin-bottom:15px; color:#dc3545; } |
|
#data { display:grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap:20px; max-height: calc(100vh - 140px); overflow-y:auto; } |
|
.entry { background:white; border-radius:8px; padding:15px; box-shadow:0 2px 6px rgba(0,0,0,0.1); display:flex; flex-direction:column; } |
|
.entry p { margin:0 0 10px; white-space:pre-line; color:#495057; font-size:0.9rem; } |
|
.entry img { width:100%; height:auto; border-radius:4px; flex-shrink:0; } |
|
a { display:inline-block; margin-top:15px; color:white; background:#007bff; padding:10px 15px; text-decoration:none; border-radius:5px; } |
|
</style> |
|
</head> |
|
<body> |
|
<h1>Emerson Dashboard</h1> |
|
<p id="status">Waiting for Emerson...</p> |
|
<div id="data"></div> |
|
<a href="/download-csv" download="telemetry.csv">Download CSV</a> |
|
<script> |
|
const ws = new WebSocket(`wss://${location.host}/ws`); |
|
const statusEl = document.getElementById('status'); |
|
const dataEl = document.getElementById('data'); |
|
ws.onopen = () => statusEl.textContent = 'Connected'; |
|
ws.onclose = () => statusEl.textContent = 'Disconnected'; |
|
ws.onmessage = evt => { |
|
const d = JSON.parse(evt.data); |
|
statusEl.textContent = 'Streaming'; |
|
const card = document.createElement('div'); card.className = 'entry'; |
|
const text = document.createElement('p'); |
|
text.textContent = `Time: ${new Date(d.timestamp).toLocaleTimeString()}\nGPS: ${d.gps.latitude.toFixed(5)}, ${d.gps.longitude.toFixed(5)}`; |
|
const img = document.createElement('img'); img.src = d.image; |
|
card.append(text, img); |
|
dataEl.prepend(card); |
|
}; |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
return HTMLResponse(html) |
|
|
|
@app.get("/download-csv") |
|
async def download_csv(): |
|
def iter_csv(): |
|
si = StringIO() |
|
writer = csv.writer(si) |
|
writer.writerow(["timestamp","latitude","longitude","image"]) |
|
for d in telemetry_data: |
|
writer.writerow([d["timestamp"],d["gps"]["latitude"],d["gps"]["longitude"],d["image"]]) |
|
yield si.getvalue(); si.seek(0); si.truncate(0) |
|
return StreamingResponse(iter_csv(), media_type="text/csv") |
|
|
|
@app.websocket("/ws") |
|
async def websocket_endpoint(ws:WebSocket): |
|
await ws.accept(); dashboard_connections.append(ws) |
|
try: |
|
while True: |
|
msg = await ws.receive_text() |
|
obj = json.loads(msg); telemetry_data.append(obj) |
|
for conn in dashboard_connections: |
|
try: await conn.send_text(json.dumps(obj)) |
|
except: dashboard_connections.remove(conn) |
|
except: |
|
dashboard_connections.remove(ws) |
|
|
|
if __name__ == "__main__": |
|
uvicorn.run(app, host="0.0.0.0", port=7860) |
|
|