SalexAI commited on
Commit
78dc7b6
·
verified ·
1 Parent(s): 34c8969

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -157
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # Code, updated to require user interaction to start streaming, request camera and GPS permissions with high accuracy, and fix dashboard map centering on first real telemetry.
2
 
3
  from fastapi import FastAPI, WebSocket
4
  from fastapi.responses import HTMLResponse, StreamingResponse
@@ -8,162 +8,189 @@ import csv
8
  from io import StringIO
9
 
10
  app = FastAPI()
11
- telemetry_data = []
12
- dashboard_connections = []
13
 
14
  @app.get("/stream")
15
  async def stream():
16
  html = """
17
- <!DOCTYPE html>
18
- <html lang="en">
19
- <head>
20
- <meta charset="UTF-8">
21
- <title>Emerson Telemetry Stream</title>
22
- <style>
23
- body{margin:0;padding:20px;background:#eef6fc;font-family:sans-serif;display:flex;flex-direction:column;align-items:center;}
24
- h1{color:#034f84;}
25
- #status{font-weight:bold;color:#fc3d03;margin:10px 0;}
26
- #streams{display:flex;gap:10px;width:100%;max-width:680px;margin-top:10px;}
27
- video{border:2px solid #034f84;border-radius:8px;width:50%;height:auto;}
28
- button{padding:10px 20px;font-size:1rem;border:none;border-radius:5px;background:#034f84;color:white;cursor:pointer;}
29
- button:disabled{background:#ccc;cursor:default;}
30
- </style>
31
- </head>
32
- <body>
33
- <h1>Emerson Telemetry Stream</h1>
34
- <button id="start">Start Streaming</button>
35
- <p id="status">Click "Start Streaming" to begin.</p>
36
- <div id="streams" style="display:none;">
37
- <video id="video-back" autoplay muted playsinline></video>
38
- <video id="video-front" autoplay muted playsinline></video>
39
- </div>
40
- <script>
41
- const startBtn = document.getElementById('start');
42
- const statusEl = document.getElementById('status');
43
- let ws, buffer = [];
44
- function initWS() {
45
- ws = new WebSocket(`wss://${location.host}/ws`);
46
- ws.onopen = () => { statusEl.textContent='Streaming'; flushBuffer(); };
47
- ws.onclose = () => statusEl.textContent='Disconnected';
48
- ws.onerror = () => statusEl.textContent='WebSocket error';
49
- }
50
- function flushBuffer() {
51
- while(buffer.length && ws.readyState===WebSocket.OPEN) {
52
- ws.send(buffer.shift());
53
- }
54
- }
55
- function sendTelemetry() {
56
- const backV = document.getElementById('video-back');
57
- const frontV = document.getElementById('video-front');
58
- if(!backV.videoWidth || !frontV.videoWidth) return;
59
- // capture images
60
- const cb = document.createElement('canvas'); cb.width=backV.videoWidth; cb.height=backV.videoHeight;
61
- cb.getContext('2d').drawImage(backV,0,0);
62
- const cf = document.createElement('canvas'); cf.width=frontV.videoWidth; cf.height=frontV.videoHeight;
63
- cf.getContext('2d').drawImage(frontV,0,0);
64
- // request high-accuracy GPS
65
- navigator.geolocation.getCurrentPosition(pos => {
66
- const data = { timestamp: Date.now(), gps: { latitude: pos.coords.latitude, longitude: pos.coords.longitude, altitude: pos.coords.altitude, speed: pos.coords.speed }, images: { back: cb.toDataURL('image/jpeg'), front: cf.toDataURL('image/jpeg') } };
67
- const msg = JSON.stringify(data);
68
- if(ws.readyState===WebSocket.OPEN) ws.send(msg); else buffer.push(msg);
69
- }, err => {
70
- console.error('GPS error', err);
71
- statusEl.textContent='GPS permission needed';
72
- }, { enableHighAccuracy: true });
73
- }
74
- async function startStreaming() {
75
- startBtn.disabled = true;
76
- try {
77
- // ask for camera permission
78
- const [backStream, frontStream] = await Promise.all([
79
- navigator.mediaDevices.getUserMedia({ video:{ facingMode:'environment' } }),
80
- navigator.mediaDevices.getUserMedia({ video:{ facingMode:'user' } })
81
- ]);
82
- document.getElementById('video-back').srcObject = backStream;
83
- document.getElementById('video-front').srcObject = frontStream;
84
- document.getElementById('streams').style.display = 'flex';
85
- statusEl.textContent = 'Initializing...';
86
- // ask for GPS permission
87
- navigator.geolocation.getCurrentPosition(() => {}, err => { console.error('GPS permission needed', err); statusEl.textContent='GPS permission needed'; }, { enableHighAccuracy: true });
88
- initWS();
89
- window.addEventListener('online', () => flushBuffer());
90
- window.addEventListener('offline', () => statusEl.textContent='Waiting for Emerson...');
91
- setInterval(sendTelemetry, 1000);
92
- } catch (err) {
93
- console.error('Permission error', err);
94
- statusEl.textContent = 'Camera permission needed';
95
- }
96
- }
97
- startBtn.addEventListener('click', startStreaming);
98
- </script>
99
- </body>
100
- </html>
101
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  return HTMLResponse(html)
103
 
104
  @app.get("/dashboard")
105
  async def dashboard():
106
  html = """
107
- <!DOCTYPE html>
108
- <html lang="en">
109
- <head>
110
- <meta charset="UTF-8">
111
- <title>Emerson Dashboard</title>
112
- <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
113
- <style>
114
- body{margin:0;padding:20px;background:#f8f9fa;font-family:sans-serif;}
115
- h1{color:#343a40;}
116
- #status{font-weight:bold;color:#dc3545;}
117
- #metrics{display:flex;gap:20px;margin:10px 0;}
118
- .metric{flex:1;background:white;padding:10px;border-radius:5px;box-shadow:0 1px 4px rgba(0,0,0,0.1);text-align:center;}
119
- #map{height:300px;border:1px solid #ccc;border-radius:5px;}
120
- #streams{display:flex;gap:10px;margin-top:10px;}
121
- #streams img{width:50%;border:2px solid #034f84;border-radius:5px;}
122
- a{display:inline-block;margin-top:10px;color:white;background:#007bff;padding:8px 12px;text-decoration:none;border-radius:4px;}
123
- </style>
124
- </head>
125
- <body>
126
- <h1>Emerson Dashboard</h1>
127
- <p id="status">Waiting for Emerson...</p>
128
- <div id="metrics">
129
- <div class="metric" id="speed">Speed:<br>-- m/s</div>
130
- <div class="metric" id="altitude">Altitude:<br>-- m</div>
131
- </div>
132
- <div id="map"></div>
133
- <div id="streams">
134
- <img id="img-back" src="" alt="Back Camera">
135
- <img id="img-front" src="" alt="Front Camera">
136
- </div>
137
- <a href="/download-csv" download="telemetry.csv">Download CSV</a>
138
- <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
139
- <script>
140
- const statusEl = document.getElementById('status');
141
- const speedEl = document.getElementById('speed');
142
- const altEl = document.getElementById('altitude');
143
- const imgBack = document.getElementById('img-back');
144
- const imgFront = document.getElementById('img-front');
145
- const map = L.map('map');
146
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(map);
147
- const marker = L.marker([0,0]).addTo(map);
148
- let first = true;
149
- const ws = new WebSocket(`wss://${location.host}/ws`);
150
- ws.onopen = ()=> statusEl.textContent='Connected';
151
- ws.onclose = ()=> statusEl.textContent='Disconnected';
152
- ws.onmessage = evt => {
153
- const d = JSON.parse(evt.data);
154
- statusEl.textContent = 'Streaming';
155
- speedEl.innerHTML = `Speed:<br>${d.gps.speed!=null?d.gps.speed.toFixed(2):'--'} m/s`;
156
- altEl.innerHTML = `Altitude:<br>${d.gps.altitude!=null?d.gps.altitude.toFixed(1):'--'} m`;
157
- const lat = d.gps.latitude, lon = d.gps.longitude;
158
- marker.setLatLng([lat,lon]);
159
- if(first){ map.setView([lat,lon],13); first = false;} else map.setView([lat,lon],map.getZoom());
160
- imgBack.src = d.images.back;
161
- imgFront.src = d.images.front;
162
- };
163
- </script>
164
- </body>
165
- </html>
166
- """
 
 
 
167
  return HTMLResponse(html)
168
 
169
  @app.get("/download-csv")
@@ -174,23 +201,31 @@ async def download_csv():
174
  writer.writerow(["timestamp","latitude","longitude","altitude","speed","back_image","front_image"])
175
  for d in telemetry_data:
176
  writer.writerow([
177
- d["timestamp"],d["gps"]["latitude"],d["gps"]["longitude"],
178
- d["gps"]["altitude"],d["gps"]["speed"],
179
- d["images"]["back"],d["images"]["front"]
 
 
 
 
180
  ])
181
  yield si.getvalue(); si.seek(0); si.truncate(0)
182
- return StreamingResponse(iter_csv(),media_type="text/csv")
183
 
184
  @app.websocket("/ws")
185
- async def websocket_endpoint(ws:WebSocket):
186
- await ws.accept(); dashboard_connections.append(ws)
 
187
  try:
188
  while True:
189
- msg = await ws.receive_text(); obj = json.loads(msg)
 
190
  telemetry_data.append(obj)
191
  for conn in dashboard_connections:
192
- try: await conn.send_text(json.dumps(obj))
193
- except: dashboard_connections.remove(conn)
 
 
194
  except:
195
  dashboard_connections.remove(ws)
196
 
 
1
+ # Sir, complete app.py with full code for /stream, /dashboard, CSV download, and WebSocket handling.
2
 
3
  from fastapi import FastAPI, WebSocket
4
  from fastapi.responses import HTMLResponse, StreamingResponse
 
8
  from io import StringIO
9
 
10
  app = FastAPI()
11
+ telemetry_data = [] # list to hold telemetry data
12
+ dashboard_connections = [] # active dashboard WebSocket connections
13
 
14
  @app.get("/stream")
15
  async def stream():
16
  html = """
17
+ <!DOCTYPE html>
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="UTF-8">
21
+ <title>Emerson Telemetry Stream</title>
22
+ <style>
23
+ body { margin:0; padding:20px; background:#eef6fc; font-family:sans-serif; display:flex; flex-direction:column; align-items:center; }
24
+ h1 { color:#034f84; margin-top:20px; }
25
+ #status { font-weight:bold; color:#fc3d03; margin:10px 0; }
26
+ #streams { display:none; gap:10px; width:100%; max-width:680px; margin-top:10px; }
27
+ video { border:2px solid #034f84; border-radius:8px; width:50%; height:auto; }
28
+ button { padding:10px 20px; font-size:1rem; border:none; border-radius:5px; background:#034f84; color:white; cursor:pointer; }
29
+ button:disabled { background:#ccc; cursor:default; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <h1>Emerson Telemetry Stream</h1>
34
+ <button id="start">Start Streaming</button>
35
+ <p id="status">Click "Start Streaming" to begin.</p>
36
+ <div id="streams">
37
+ <video id="video-back" autoplay muted playsinline></video>
38
+ <video id="video-front" autoplay muted playsinline></video>
39
+ </div>
40
+ <script>
41
+ const startBtn = document.getElementById('start');
42
+ const statusEl = document.getElementById('status');
43
+ const streamsEl = document.getElementById('streams');
44
+ let ws, buffer = [];
45
+
46
+ function initWS() {
47
+ ws = new WebSocket(`wss://${location.host}/ws`);
48
+ ws.onopen = () => statusEl.textContent = 'Streaming';
49
+ ws.onclose = () => statusEl.textContent = 'Disconnected';
50
+ ws.onerror = () => statusEl.textContent = 'WebSocket error';
51
+ }
52
+
53
+ function flushBuffer() {
54
+ while (buffer.length && ws.readyState === WebSocket.OPEN) {
55
+ ws.send(buffer.shift());
56
+ }
57
+ }
58
+
59
+ function sendTelemetry() {
60
+ const backV = document.getElementById('video-back');
61
+ const frontV = document.getElementById('video-front');
62
+ // capture available frames
63
+ let backImg = '', frontImg = '';
64
+ if (backV.videoWidth) {
65
+ const c1 = document.createElement('canvas'); c1.width = backV.videoWidth; c1.height = backV.videoHeight;
66
+ c1.getContext('2d').drawImage(backV, 0, 0);
67
+ backImg = c1.toDataURL('image/jpeg');
68
+ }
69
+ if (frontV.videoWidth) {
70
+ const c2 = document.createElement('canvas'); c2.width = frontV.videoWidth; c2.height = frontV.videoHeight;
71
+ c2.getContext('2d').drawImage(frontV, 0, 0);
72
+ frontImg = c2.toDataURL('image/jpeg');
73
+ }
74
+ navigator.geolocation.getCurrentPosition(pos => {
75
+ const data = {
76
+ timestamp: Date.now(),
77
+ gps: {
78
+ latitude: pos.coords.latitude,
79
+ longitude: pos.coords.longitude,
80
+ altitude: pos.coords.altitude,
81
+ speed: pos.coords.speed
82
+ },
83
+ images: { back: backImg, front: frontImg }
84
+ };
85
+ const msg = JSON.stringify(data);
86
+ if (ws.readyState === WebSocket.OPEN) ws.send(msg);
87
+ else buffer.push(msg);
88
+ }, err => {
89
+ console.error('GPS error', err);
90
+ statusEl.textContent = 'GPS permission needed';
91
+ }, { enableHighAccuracy: true });
92
+ }
93
+
94
+ async function startStreaming() {
95
+ startBtn.disabled = true;
96
+ statusEl.textContent = 'Requesting camera access...';
97
+ // request both cameras
98
+ let backStream, frontStream;
99
+ try { backStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); }
100
+ catch (e) { console.warn('Back camera error', e); }
101
+ try { frontStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } }); }
102
+ catch (e) { console.warn('Front camera error', e); }
103
+ if (!backStream && !frontStream) {
104
+ statusEl.textContent = 'No camera available or permission denied';
105
+ return;
106
+ }
107
+ if (backStream) document.getElementById('video-back').srcObject = backStream;
108
+ if (frontStream) document.getElementById('video-front').srcObject = frontStream;
109
+ streamsEl.style.display = 'flex';
110
+ statusEl.textContent = 'Initializing GPS & WebSocket...';
111
+ navigator.geolocation.getCurrentPosition(() => {}, err => {
112
+ console.error('GPS permission needed', err);
113
+ statusEl.textContent = 'GPS permission needed';
114
+ }, { enableHighAccuracy: true });
115
+ initWS();
116
+ window.addEventListener('online', flushBuffer);
117
+ window.addEventListener('offline', () => statusEl.textContent = 'Offline: buffering');
118
+ setInterval(sendTelemetry, 1000);
119
+ }
120
+
121
+ startBtn.addEventListener('click', startStreaming);
122
+ </script>
123
+ </body>
124
+ </html>
125
+ """
126
  return HTMLResponse(html)
127
 
128
  @app.get("/dashboard")
129
  async def dashboard():
130
  html = """
131
+ <!DOCTYPE html>
132
+ <html lang="en">
133
+ <head>
134
+ <meta charset="UTF-8">
135
+ <title>Emerson Dashboard</title>
136
+ <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
137
+ <style>
138
+ body { margin:0; padding:20px; background:#f8f9fa; font-family:sans-serif; }
139
+ h1 { color:#343a40; }
140
+ #status { font-weight:bold; color:#dc3545; }
141
+ #metrics { display:flex; gap:20px; margin:10px 0; }
142
+ .metric { flex:1; background:white; padding:10px; border-radius:5px; box-shadow:0 1px 4px rgba(0,0,0,0.1); text-align:center; }
143
+ #map { height:300px; border:1px solid #ccc; border-radius:5px; }
144
+ #streams { display:flex; gap:10px; margin-top:10px; }
145
+ #streams img { width:50%; border:2px solid #034f84; border-radius:5px; }
146
+ a { display:inline-block; margin-top:10px; color:white; background:#007bff; padding:8px 12px; text-decoration:none; border-radius:4px; }
147
+ </style>
148
+ </head>
149
+ <body>
150
+ <h1>Emerson Dashboard</h1>
151
+ <p id="status">Waiting for Emerson...</p>
152
+ <div id="metrics">
153
+ <div class="metric" id="speed">Speed:<br>-- m/s</div>
154
+ <div class="metric" id="altitude">Altitude:<br>-- m</div>
155
+ </div>
156
+ <div id="map"></div>
157
+ <div id="streams">
158
+ <img id="img-back" src="" alt="Back Camera">
159
+ <img id="img-front" src="" alt="Front Camera">
160
+ </div>
161
+ <a href="/download-csv" download="telemetry.csv">Download CSV</a>
162
+ <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
163
+ <script>
164
+ const statusEl = document.getElementById('status');
165
+ const speedEl = document.getElementById('speed');
166
+ const altEl = document.getElementById('altitude');
167
+ const imgBack = document.getElementById('img-back');
168
+ const imgFront = document.getElementById('img-front');
169
+ const map = L.map('map');
170
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
171
+ const marker = L.marker([0, 0]).addTo(map);
172
+ let first = true;
173
+ const ws = new WebSocket(`wss://${location.host}/ws`);
174
+ ws.onopen = () => statusEl.textContent = 'Connected';
175
+ ws.onclose = () => statusEl.textContent = 'Disconnected';
176
+ ws.onerror = () => statusEl.textContent = 'WebSocket error';
177
+ ws.onmessage = evt => {
178
+ const d = JSON.parse(evt.data);
179
+ statusEl.textContent = 'Streaming';
180
+ const sp = d.gps.speed != null ? d.gps.speed.toFixed(2) : '--';
181
+ const alt = d.gps.altitude != null ? d.gps.altitude.toFixed(1) : '--';
182
+ speedEl.innerHTML = `Speed:<br>${sp} m/s`;
183
+ altEl.innerHTML = `Altitude:<br>${alt} m`;
184
+ const lat = d.gps.latitude, lon = d.gps.longitude;
185
+ marker.setLatLng([lat, lon]);
186
+ if (first) { map.setView([lat, lon], 13); first = false; }
187
+ imgBack.src = d.images.back;
188
+ imgFront.src = d.images.front;
189
+ };
190
+ </script>
191
+ </body>
192
+ </html>
193
+ """
194
  return HTMLResponse(html)
195
 
196
  @app.get("/download-csv")
 
201
  writer.writerow(["timestamp","latitude","longitude","altitude","speed","back_image","front_image"])
202
  for d in telemetry_data:
203
  writer.writerow([
204
+ d["timestamp"],
205
+ d["gps"]["latitude"],
206
+ d["gps"]["longitude"],
207
+ d["gps"]["altitude"],
208
+ d["gps"]["speed"],
209
+ d["images"]["back"],
210
+ d["images"]["front"]
211
  ])
212
  yield si.getvalue(); si.seek(0); si.truncate(0)
213
+ return StreamingResponse(iter_csv(), media_type="text/csv")
214
 
215
  @app.websocket("/ws")
216
+ async def websocket_endpoint(ws: WebSocket):
217
+ await ws.accept()
218
+ dashboard_connections.append(ws)
219
  try:
220
  while True:
221
+ msg = await ws.receive_text()
222
+ obj = json.loads(msg)
223
  telemetry_data.append(obj)
224
  for conn in dashboard_connections:
225
+ try:
226
+ await conn.send_text(json.dumps(obj))
227
+ except:
228
+ dashboard_connections.remove(conn)
229
  except:
230
  dashboard_connections.remove(ws)
231