-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathflows.json
More file actions
447 lines (447 loc) · 130 KB
/
Copy pathflows.json
File metadata and controls
447 lines (447 loc) · 130 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
[
{
"id": "gcs-tab",
"type": "tab",
"label": "Argus GCS",
"disabled": false,
"info": ""
},
{
"id": "ui-gcs-tab",
"type": "ui_tab",
"name": "Argus GCS",
"icon": "dashboard",
"order": 1,
"disabled": false,
"hidden": false
},
{
"id": "ui-gcs-group",
"type": "ui_group",
"name": "Telemetry Control",
"tab": "ui-gcs-tab",
"order": 1,
"disp": false,
"width": "12",
"collapse": false,
"disabled": false,
"hidden": false
},
{
"id": "0293d823d8a0c5b2",
"type": "ui_base",
"theme": {
"name": "theme-light",
"lightTheme": {
"default": "#0094CE",
"baseColor": "#0094CE",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
"edited": false
},
"darkTheme": {
"default": "#097479",
"baseColor": "#097479",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
"edited": false
},
"customTheme": {
"name": "Untitled Theme 1",
"default": "#4B7930",
"baseColor": "#4B7930",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
},
"themeState": {
"base-color": {
"default": "#0094CE",
"value": "#0094CE",
"edited": false
},
"page-titlebar-backgroundColor": {
"value": "#0094CE",
"edited": false
},
"page-backgroundColor": {
"value": "#fafafa",
"edited": false
},
"page-sidebar-backgroundColor": {
"value": "#ffffff",
"edited": false
},
"group-textColor": {
"value": "#1bbfff",
"edited": false
},
"group-borderColor": {
"value": "#ffffff",
"edited": false
},
"group-backgroundColor": {
"value": "#ffffff",
"edited": false
},
"widget-textColor": {
"value": "#111111",
"edited": false
},
"widget-backgroundColor": {
"value": "#0094ce",
"edited": false
},
"widget-borderColor": {
"value": "#ffffff",
"edited": false
},
"base-font": {
"value": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
}
},
"angularTheme": {
"primary": "indigo",
"accents": "blue",
"warn": "red",
"background": "grey",
"palette": "light"
}
},
"site": {
"name": "Node-RED Dashboard",
"hideToolbar": "false",
"allowSwipe": "false",
"lockMenu": "false",
"allowTempTheme": "true",
"dateFormat": "DD/MM/YYYY",
"sizes": {
"sx": 48,
"sy": 48,
"gx": 6,
"gy": 6,
"cx": 6,
"cy": 6,
"px": 0,
"py": 0
}
}
},
{
"id": "argus-mqtt-broker",
"type": "mqtt-broker",
"name": "Argus Local MQTT",
"broker": "localhost",
"port": "1883",
"clientid": "nodered-argus",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
},
{
"id": "ui-gcs-template",
"type": "ui_template",
"z": "gcs-tab",
"group": "ui-gcs-group",
"name": "Dashboard UI",
"order": 1,
"width": "12",
"height": "12",
"format": "<style>\n@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;700;800&display=swap');\n.gcs-container { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; margin: 0; padding: 0; z-index: 9999; font-family: 'Outfit', sans-serif; overflow: hidden; background-color: #1a1c1e; color: #e5e7eb; }\n.gcs-map-frame { width: 100%; height: 100%; border: none; position: absolute; top: 0; left: 0; z-index: 1; }\n.leaflet-control-attribution { filter: invert(1) opacity(0.3); background: transparent !important; color: #fff !important; font-size: 10px !important; }\n\n.gcs-header { position: absolute; top: 0; left: 0; width: 100%; height: 55px; z-index: 100; background: rgba(22, 28, 36, 0.85); backdrop-filter: blur(8px); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }\n.gcs-title { position: absolute; left: 20px; top: 12px; font-size: 26px; font-weight: 800; letter-spacing: 1px; color: #4facfe; text-shadow: none; display: flex; align-items: center; gap: 10px; }\n.gcs-tabs { display: flex; gap: 40px; position: absolute; left: 50%; transform: translateX(-50%); top: 0; height: 100%; }\n.gcs-tab { color: #94a3b8; font-size: 13px; font-weight: 700; cursor: pointer; padding: 18px 0; border-bottom: 3px solid transparent; transition: all 0.2s; letter-spacing: 1px; }\n.gcs-tab:hover { color: #f8fafc; }\n.gcs-tab.active { color: #00f2fe; border-bottom: 3px solid #00f2fe; }\n\n.gcs-pills { display: flex; gap: 10px; align-items: center; position: absolute; right: 20px; top: 13px; }\n.status-pill { display: flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 700; color: #fff; }\n.transition-pill { transition: background 0.5s ease, border 0.5s ease, color 0.5s ease; }\n.pill-red { background: #dc2626; border: 1px solid #dc2626; color: #ffffff; }\n.pill-cyan { background: #0ea5e9; border: 1px solid #0ea5e9; color: #ffffff; }\n.pill-green { background: #16a34a; border: 1px solid #16a34a; color: #ffffff; }\n.pill-amber { background: #d97706; border: 1px solid #d97706; color: #ffffff; }\n.rescue-counter { animation: pulse-red 2s infinite; cursor: pointer; }\n@keyframes pulse-red { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } }\n\n.gcs-glass-panel { background: rgba(22, 28, 36, 0.85); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px; padding: 12px; box-sizing: border-box; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); }\n.gcs-side-panel { position: absolute; top: 70px; left: 15px; width: 280px; z-index: 10; display: flex; flex-direction: column; gap: 12px; }\n.gcs-side-panel .gcs-glass-panel { border-left: 3px solid #00f2fe; }\n.gcs-right-panel { position: absolute; top: 70px; right: 15px; width: 340px; z-index: 10; display: flex; flex-direction: column; gap: 12px; max-height: calc(100vh - 160px); }\n.panel-title { font-size: 12px; font-weight: 700; letter-spacing: 0.5px; color: #64748b; border-bottom: 1px solid rgba(255, 255, 255, 0.05); padding-bottom: 6px; margin-bottom: 10px; text-transform: uppercase; }\n.metric-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; background: rgba(0,0,0,0.1); padding: 6px 10px; border-radius: 6px; }\n.metric-row:last-child { margin-bottom: 0; }\n.metric-label { color: #94a3b8; font-size: 11px; text-transform: uppercase; font-weight: 600; }\n.metric-value { font-size: 18px; font-weight: 800; color: #f8fafc; }\n.metric-value-small { font-size: 13px; font-weight: 600; color: #cbd5e1; }\n.batt-container { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }\n.batt-bar-bg { width: 60px; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }\n.batt-bar-fill { height: 100%; background: #2ecc71; }\n#attitude-canvas { width: 100%; height: 130px; background: rgba(0,0,0,0.3); border-radius: 6px; border: 1px solid rgba(255,255,255,0.03); overflow: hidden; }\n.sensor-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }\n.sensor-box { background: rgba(0, 0, 0, 0.2); padding: 6px; border-radius: 6px; text-align: center; }\n.sensor-val { font-size: 13px; font-weight: 700; color: #2ecc71; margin-top: 4px; }\n.sensor-val.warn { color: #ef4444; }\n.trust-meter-bg { height: 6px; background: rgba(0,0,0,0.4); border-radius: 3px; margin-top: 8px; overflow: hidden; }\n.trust-meter-fill { height: 100%; background: #4facfe; transition: width 0.3s; }\n.link-row { display: flex; align-items: center; margin-bottom: 6px; }\n.link-name { width: 100px; font-size: 11px; color: #94a3b8; font-weight: 600; }\n.link-bar-bg { flex-grow: 1; height: 6px; background: rgba(0,0,0,0.4); border-radius: 3px; margin-right: 10px; overflow: hidden; }\n.link-bar-fill { height: 100%; width: 100%; }\n.link-val { width: 35px; text-align: right; font-size: 12px; font-weight: 700; color: #f8fafc; }\n.event-log { height: 120px; overflow-y: auto; font-family: monospace; font-size: 11px; background: rgba(0,0,0,0.3); border-radius: 6px; padding: 8px; }\n.log-entry { margin-bottom: 4px; color: #cbd5e1; min-height: 16px; line-height: 1.4; word-wrap: break-word; }\n.log-time { color: #64748b; margin-right: 8px; }\n.log-warn { color: #fbbf24; }\n.log-err { color: #ef4444; font-weight: bold; }\n.log-ok { color: #4ade80; }\n.gcs-bottom-bar { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 10; background: rgba(22, 28, 36, 0.95); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 10px 15px; display: flex; align-items: center; gap: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); }\n.control-btn { border: none !important; padding: 12px 20px; font-size: 14px; font-weight: 700; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 8px; color: white !important; transition: all 0.2s ease; text-transform: uppercase; }\n.control-btn:disabled { opacity: 0.4 !important; cursor: not-allowed; }\n.md-button.control-btn { margin: 0 !important; }\n.btn-arm { background: #dc2626 !important; box-shadow: 0 0 10px rgba(220, 38, 38, 0.3); }\n.btn-takeoff, .md-button.btn-takeoff { background: #16a34a !important; color: white !important; }\n.btn-land, .md-button.btn-land { background: #2563eb !important; color: white !important; }\n.btn-rtl, .md-button.btn-rtl { background: #f59e0b !important; color: #000 !important; }\n\n.broadcast-btn { position: absolute; bottom: 20px; left: 20px; z-index: 10; background: #dc2626 !important; padding: 12px 20px; font-size: 14px; font-weight: 800; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 8px; color: white; border: none; box-shadow: none; }\n.map-tools { position: absolute; top: 70px; left: 310px; z-index: 10; display: flex; flex-direction: column; gap: 8px; background: rgba(22, 28, 36, 0.85); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 10px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); }\n.map-tool-btn { background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.05); color: #fff; padding: 10px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; text-align: left; transition: all 0.2s; }\n.map-tool-btn:hover { background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.2); }\n.modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); z-index: 99999; display: flex; justify-content: center; align-items: center; }\n.checklist-modal { background: #1e293b; border: 1px solid #334155; border-radius: 12px; width: 350px; padding: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.8); }\n.checklist-title { font-size: 18px; font-weight: 800; color: #f8fafc; margin-bottom: 20px; text-align: center; }\n.check-item { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; font-size: 14px; color: #cbd5e1; font-weight: 600; }\n.check-item i.pending { color: #64748b; animation: spin 1s linear infinite; }\n.check-item i.ok { color: #2ecc71; }\n.check-item i.warn { color: #f59e0b; }\n@keyframes spin { 100% { transform: rotate(360deg); } }\n.compass-rose { position: relative; width: 40px; height: 40px; border-radius: 50%; border: 2px solid #334155; background: rgba(0,0,0,0.2); display: flex; justify-content: center; align-items: center; }\n.compass-arrow { position: absolute; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 12px solid #ef4444; top: 4px; transition: transform 0.3s ease-out; transform-origin: 50% 16px; }\n.compass-center { width: 4px; height: 4px; background: #fff; border-radius: 50%; }\n\n.tab-overlay { position: absolute; top: 55px; left: 0; width: 100vw; height: calc(100vh - 55px); background: rgba(15, 23, 42, 0.95); backdrop-filter: blur(15px); z-index: 50; display: flex; justify-content: center; align-items: flex-start; padding-top: 0px; }\n.mock-tab-panel { background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 30px; width: 1200px; max-width: 95vw; }\n.mock-feed { width: 240px; height: 180px; background: #000; border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; position: relative; display: flex; justify-content: center; align-items: center; }\n.feed-title { position: absolute; top: 10px; left: 10px; font-size: 10px; font-weight: bold; color: #fff; background: rgba(0,0,0,0.5); padding: 2px 6px; border-radius: 4px; }\n.feed-status { position: absolute; bottom: 10px; left: 10px; font-size: 10px; font-weight: bold; color: #2ecc71; }\n\n\n.cameras-fullscreen { width: 100%; height: 100%; max-width: calc(100vw - 30px); max-height: calc(100vh - 85px); }\n.cameras-grid { display: grid; grid-template-columns: 2fr 1fr; grid-template-rows: 1fr 1fr; gap: 15px; width: 100%; height: 100%; }\n\n.camera-feed { background: rgba(0, 0, 0, 0.6); border: 1px solid rgba(0, 242, 254, 0.2); border-radius: 8px; position: relative; display: flex; justify-content: center; align-items: center; box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.8); overflow: hidden; }\n.camera-feed::before { content: '\\f03d'; font-family: FontAwesome; font-size: 60px; color: rgba(255,255,255,0.05); position: absolute; }\n.camera-title { position: absolute; top: 12px; left: 12px; font-size: 11px; font-weight: 800; color: #00f2fe; background: rgba(15, 23, 42, 0.85); padding: 4px 10px; border-radius: 4px; border: 1px solid rgba(0, 242, 254, 0.3); letter-spacing: 1px; display: flex; align-items: center; gap: 6px; }\n.camera-status { position: absolute; bottom: 12px; left: 12px; font-size: 11px; font-weight: 800; color: #2ecc71; background: rgba(15, 23, 42, 0.85); padding: 4px 10px; border-radius: 4px; border: 1px solid rgba(46, 204, 113, 0.3); display: flex; align-items: center; gap: 6px; }\n.camera-rec { position: absolute; top: 12px; right: 12px; font-size: 11px; font-weight: 800; color: #ef4444; background: rgba(15, 23, 42, 0.85); padding: 4px 10px; border-radius: 4px; border: 1px solid rgba(239, 68, 68, 0.3); animation: blink-red 2s infinite; display: flex; align-items: center; gap: 6px; }\n@keyframes blink-red { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }\n.camera-rgb { grid-column: 1 / 2; grid-row: 1 / 3; }\n.camera-thermal { grid-column: 2 / 3; grid-row: 1 / 2; border-color: rgba(245, 158, 11, 0.3); }\n.camera-thermal .camera-title { color: #f59e0b; border-color: rgba(245, 158, 11, 0.3); }\n.camera-yolo { grid-column: 2 / 3; grid-row: 2 / 3; border-color: rgba(239, 68, 68, 0.3); }\n.camera-yolo .camera-title { color: #ef4444; border-color: rgba(239, 68, 68, 0.3); }\n\n.fullscreen-btn { position: absolute; bottom: 12px; right: 12px; font-size: 14px; color: #fff; background: rgba(15, 23, 42, 0.85); padding: 6px 10px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.3); cursor: pointer; transition: all 0.2s; z-index: 10; display: flex; align-items: center; justify-content: center; }\n.fullscreen-btn:hover { background: rgba(255,255,255,0.2); }\n.camera-rgb::before { display: none; }\n\n#mission-map-container:fullscreen {\n background: #000 !important;\n padding: 0 !important;\n border-radius: 0 !important;\n}\n#mission-map-container:fullscreen iframe {\n height: 100vh !important;\n border: none !important;\n border-radius: 0 !important;\n}\n#mission-map-container:fullscreen > div:first-child {\n display: none !important; /* Hide 'DRAW ROUTE HERE' title */\n}\n#mission-map-container:-webkit-full-screen {\n background: #000 !important;\n padding: 0 !important;\n border-radius: 0 !important;\n}\n#mission-map-container:-webkit-full-screen iframe {\n height: 100vh !important;\n border: none !important;\n border-radius: 0 !important;\n}\n#mission-map-container:-webkit-full-screen > div:first-child {\n display: none !important;\n}\n\n\n .toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; }\n .toggle-switch input { opacity: 0; width: 0; height: 0; }\n .toggle-switch .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #334155; transition: .4s; border-radius: 24px; }\n .toggle-switch .slider:before { position: absolute; content: \"\"; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }\n .toggle-switch input:checked + .slider { background-color: #0ea5e9; }\n .toggle-switch input:checked + .slider:before { transform: translateX(20px); }\n \n </style>\n\n<div class=\"gcs-container\" ng-init=\"init()\">\n <div class=\"gcs-header\">\n <div class=\"gcs-title\"><i class=\"fa fa-crosshairs\"></i> ARGUS GCS</div>\n <div class=\"gcs-tabs\">\n <div class=\"gcs-tab\" ng-class=\"{'active': activeTab === 'FLIGHT'}\" ng-click=\"activeTab = 'FLIGHT'\">FLIGHT</div>\n <div class=\"gcs-tab\" ng-class=\"{'active': activeTab === 'CAMERAS'}\" ng-click=\"activeTab = 'CAMERAS'\">CAMERAS</div>\n <div class=\"gcs-tab\" ng-class=\"{'active': activeTab === 'MISSION'}\" ng-click=\"activeTab = 'MISSION'\">MISSION</div>\n <div class=\"gcs-tab\" ng-class=\"{'active': activeTab === 'LOGS'}\" ng-click=\"activeTab = 'LOGS'\">LOGS</div>\n <div class=\"gcs-tab\" ng-class=\"{'active': activeTab === 'RECOVERY'}\" ng-click=\"activeTab = 'RECOVERY'\">RECOVERY</div>\n </div>\n <div class=\"gcs-pills\">\n <div class=\"status-pill transition-pill\" ng-class=\"distToHome > 100 ? 'pill-amber' : 'pill-green'\">\n <i class=\"fa fa-wifi\"></i> {{ distToHome > 100 ? 'FIELD MODE' : 'FULL MODE' }}\n </div>\n <div class=\"status-pill pill-red rescue-counter\" ng-click=\"simulateRescue()\"><i class=\"fa fa-user-circle\"></i> PERSONS DETECTED: {{ rescueCount }}</div>\n <div class=\"status-pill pill-green\"><i class=\"fa fa-fire\"></i> THERMAL: ACTIVE</div>\n <div style=\"position: relative;\">\n <div class=\"status-pill\" ng-click=\"showPanelMenu = !showPanelMenu\" style=\"background: #334155; border: 1px solid #475569; cursor: pointer; padding: 8px 16px; font-size: 16px; transition: background 0.2s;\" onmouseover=\"this.style.background='#475569'\" onmouseout=\"this.style.background='#334155'\">\n <i class=\"fa fa-bars\"></i>\n </div>\n <div class=\"panel-menu-dropdown\" ng-show=\"showPanelMenu\" style=\"position: absolute; right: 0; top: 35px; background: rgba(22, 28, 36, 0.95); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 20px; display: flex; flex-direction: column; gap: 14px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); z-index: 9999; min-width: 220px;\">\n <div style=\"font-size: 13px; margin-bottom: 8px; font-weight: 800; color: #64748b; margin-bottom: 5px;\">TOGGLE PANELS</div>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.mapTools\"> Map Tools</label>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.flightData\"> Flight Data</label>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.missionProgress\"> Mission Progress</label>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.attitude\"> Attitude</label>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.status\"> Status & Links</label>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.eventLog\"> Event Log</label>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.bottomBar\"> Bottom Controls</label>\n <label style=\"display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; cursor: pointer;\"><input type=\"checkbox\" ng-model=\"panels.rescueBtn\"> Rescue Broadcast</label>\n <hr style=\"border-color: rgba(255,255,255,0.1); margin: 5px 0; width: 100%;\">\n <div style=\"font-size: 11px; font-weight: 800; color: #64748b; margin-bottom: 5px; margin-top: 5px;\">SIMULATION PANEL</div>\n <label style=\"display: flex; align-items: center; justify-content: space-between; font-size: 15px; font-weight: 600; cursor: pointer; color: #0ea5e9; width: 100%;\">\n <span>ArduPilot SITL</span>\n <div class=\"toggle-switch\">\n <input type=\"checkbox\" ng-model=\"panels.sitl\" ng-change=\"toggleSITL()\">\n <span class=\"slider round\"></span>\n </div>\n </label>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Map is always in background -->\n <iframe class=\"gcs-map-frame\" src=\"/gcs-map\"></iframe>\n \n <div ng-show=\"activeTab === 'FLIGHT'\">\n <div class=\"map-tools\" ng-show=\"panels.mapTools\">\n <button class=\"map-tool-btn\" ng-click=\"recenterMap()\"><i class=\"fa fa-crosshairs\"></i> Re-center on Drone</button>\n <button class=\"map-tool-btn\" ng-click=\"toggleThermal()\"> \ud83c\udf21 Thermal Overlay</button>\n <button class=\"map-tool-btn\" ng-click=\"clearMission()\"><i class=\"fa fa-eraser\"></i> Clear Mission</button>\n <button class=\"map-tool-btn\" ng-click=\"clearDetections()\"><i class=\"fa fa-trash\"></i> Clear Detections</button>\n </div>\n <button class=\"broadcast-btn\" ng-show=\"panels.rescueBtn\" ng-click=\"broadcastRescue()\"><i class=\"fa fa-bolt\"></i> BROADCAST RESCUE</button>\n <button class=\"broadcast-btn\" ng-show=\"panels.sitl\" ng-click=\"stopSITL()\" style=\"bottom: 80px; background: #eab308; color: #000; box-shadow: 0 4px 15px rgba(234,179,8,0.5);\"><i class=\"fa fa-sign-out\"></i> CLOSE SITL</button>\n\n <div class=\"gcs-side-panel\">\n <div class=\"gcs-glass-panel\" ng-show=\"panels.flightData\">\n <div class=\"panel-title\">Flight Data</div>\n <div class=\"metric-row\"><div class=\"metric-label\">Altitude</div><div class=\"metric-value\" style=\"color:#4facfe\">{{ (telemetry.alt || 0) | number:1 }} m</div></div>\n <div class=\"metric-row\"><div class=\"metric-label\">Speed</div><div class=\"metric-value\">{{ (telemetry.speed || 0) | number:1 }} m/s</div></div>\n <div class=\"metric-row\">\n <div class=\"metric-label\">Battery</div>\n <div class=\"batt-container\">\n <div style=\"display:flex; align-items:baseline; gap:5px;\"><span class=\"metric-value\">{{ telemetry.battery || 100 }}%</span> <span style=\"font-size:10px; color:#94a3b8;\">(~{{ battTime }} min remaining)</span></div>\n <div class=\"batt-bar-bg\"><div class=\"batt-bar-fill\" ng-style=\"{width: telemetry.battery + '%', background: telemetry.battery > 30 ? '#2ecc71' : '#ef4444'}\"></div></div>\n </div>\n </div>\n <div class=\"metric-row\">\n <div class=\"metric-label\">Heading</div>\n <div style=\"display:flex; align-items:center; gap:10px;\">\n <div class=\"metric-value\">{{ telemetry.heading || 0 }}\u00b0</div>\n <div class=\"compass-rose\"><div class=\"compass-arrow\" ng-style=\"{'transform': 'rotate(' + (telemetry.heading || 0) + 'deg)'}\"></div><div class=\"compass-center\"></div></div>\n </div>\n </div>\n <div class=\"metric-row\" style=\"display:flex; flex-direction:column; align-items:flex-start;\">\n <div style=\"display:flex; justify-content:space-between; width:100%; margin-bottom:4px;\">\n <div class=\"metric-label\">GPS</div>\n <div class=\"metric-value-small\">{{ (telemetry.lat || 0.0) | number:5 }}, {{ (telemetry.lon || 0.0) | number:5 }}</div>\n </div>\n <div style=\"font-size:10px; color:#64748b; font-weight:700;\"><i class=\"fa fa-satellite\"></i> {{telemetry.satellites || 0}} SATS | HDOP: {{telemetry.hdop || 0.0}}</div>\n </div>\n </div>\n <div class=\"gcs-glass-panel\" ng-show=\"panels.missionProgress\">\n <div class=\"panel-title\">Mission Progress</div>\n <div class=\"metric-row\"><div class=\"metric-label\">Flight Time</div><div class=\"metric-value-small\" style=\"font-family:monospace; font-size:16px;\">{{ flightTimeFormatted }}</div></div>\n <div class=\"metric-row\"><div class=\"metric-label\">Dist to Home</div><div class=\"metric-value-small\">{{ distToHome | number:1 }} m</div></div>\n </div>\n </div>\n \n <div class=\"gcs-right-panel\">\n <div class=\"gcs-glass-panel\" ng-show=\"panels.attitude\">\n <div class=\"panel-title\">Attitude</div>\n <div id=\"attitude-canvas\"></div>\n <div style=\"display:flex; justify-content:space-around; margin-top:8px; font-size:11px; font-weight:700; color:#cbd5e1; font-family:monospace;\">\n <span>R: {{ telemetry.roll | number:1 }}\u00b0</span>\n <span>P: {{ telemetry.pitch | number:1 }}\u00b0</span>\n <span>Y: {{ telemetry.heading | number:1 }}\u00b0</span>\n </div>\n </div>\n <div class=\"gcs-glass-panel\" ng-show=\"panels.status\">\n <div class=\"panel-title\">Sensor Redundancy</div>\n <div class=\"sensor-grid\">\n <div class=\"sensor-box\"><div style=\"font-size:10px; color:#94a3b8; font-weight:700;\">IMU 1</div><div class=\"sensor-val\" id=\"imu1-val\">OK</div></div>\n <div class=\"sensor-box\"><div style=\"font-size:10px; color:#94a3b8; font-weight:700;\">BARO 1</div><div class=\"sensor-val\" style=\"color: #4facfe;\">OK</div></div>\n <div class=\"sensor-box\"><div style=\"font-size:10px; color:#94a3b8; font-weight:700;\">GPS 1</div><div class=\"sensor-val\">3D</div></div>\n <div class=\"sensor-box\"><div style=\"font-size:10px; color:#94a3b8; font-weight:700;\">IMU 2</div><div class=\"sensor-val\" id=\"imu2-val\">OK</div></div>\n <div class=\"sensor-box\"><div style=\"font-size:10px; color:#94a3b8; font-weight:700;\">BARO 2</div><div class=\"sensor-val\" style=\"color: #4facfe;\">OK</div></div>\n <div class=\"sensor-box\"><div style=\"font-size:10px; color:#94a3b8; font-weight:700;\">GPS 2</div><div class=\"sensor-val\">3D</div></div>\n </div>\n <div style=\"margin-top: 10px; font-size: 11px; color: #94a3b8; display: flex; justify-content: space-between; font-weight:600;\">\n <span>IMU Trust Score:</span><span id=\"trust-score-text\" style=\"color: #f8fafc;\">100%</span>\n </div>\n <div class=\"trust-meter-bg\"><div class=\"trust-meter-fill\" id=\"trust-meter\"></div></div>\n </div>\n <div class=\"gcs-glass-panel\" ng-show=\"panels.status\">\n <div class=\"panel-title\">Data Links</div>\n <div class=\"link-row\"><div class=\"link-name\">RC Link</div><div class=\"link-bar-bg\"><div class=\"link-bar-fill\" id=\"link-rc-fill\"></div></div><div class=\"link-val\" id=\"val-rc\">98%</div></div>\n <div class=\"link-row\"><div class=\"link-name\">LoRa Telemetry</div><div class=\"link-bar-bg\"><div class=\"link-bar-fill\" id=\"link-lora-fill\"></div></div><div class=\"link-val\" id=\"val-lora\">100%</div></div>\n <div class=\"link-row\"><div class=\"link-name\">OpenHD Video</div><div class=\"link-bar-bg\"><div class=\"link-bar-fill\" id=\"link-video-fill\"></div></div><div class=\"link-val\" id=\"val-video\">95%</div></div>\n </div>\n <div class=\"gcs-glass-panel\" ng-show=\"panels.eventLog\">\n <div class=\"panel-title\">Event Log</div>\n <div class=\"event-log\" id=\"event-log-container\">\n <div class=\"log-entry\" ng-repeat=\"log in eventLogs track by $index\">\n <span class=\"log-time\">[{{log.time}}]</span><span ng-class=\"log.cls\">{{log.msg}}</span>\n </div>\n </div>\n </div>\n </div>\n \n \n <div ng-show=\"showSitlPopup\" style=\"position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); z-index: 999999; display: flex; justify-content: center; align-items: center; flex-direction: column;\">\n <i class=\"fa fa-spinner fa-spin\" style=\"font-size: 40px; color: #0ea5e9; margin-bottom: 20px;\"></i>\n <div style=\"font-size: 24px; font-weight: 800; color: #fff;\">Booting ArduPilot SITL...</div>\n <div style=\"font-size: 14px; color: #94a3b8; margin-top: 10px;\">Please wait while simulation services initialize</div>\n </div>\n <div class=\"gcs-bottom-bar\" ng-show=\"panels.bottomBar\">\n <button class=\"control-btn btn-arm\" ng-style=\"{'background': telemetry.armed ? '#16a34a' : '#dc2626'}\" ng-click=\"startArmSequence()\">\n <i class=\"fa\" ng-class=\"telemetry.armed ? 'fa-lock' : 'fa-unlock'\"></i> {{ telemetry.armed ? 'DISARM' : 'ARM SYSTEM' }}\n </button>\n <button class=\"control-btn btn-takeoff\" ng-disabled=\"!telemetry.armed\" ng-click=\"takeoff()\"><i class=\"fa fa-arrow-up\"></i> TAKE OFF</button>\n <button class=\"control-btn btn-land\" ng-disabled=\"!telemetry.armed\" ng-click=\"land()\"><i class=\"fa fa-arrow-down\"></i> LAND</button>\n <button class=\"control-btn btn-rtl\" ng-disabled=\"!telemetry.armed\" ng-click=\"rtl()\"><i class=\"fa fa-home\"></i> RTL</button>\n </div>\n </div>\n \n <!-- Tab Overlays -->\n <div class=\"tab-overlay\" ng-if=\"activeTab !== 'FLIGHT'\" ng-style=\"activeTab === 'CAMERAS' || activeTab === 'MISSION' ? {'padding-top': '0px', 'padding-bottom': '0px'} : {'padding-top': '50px'}\">\n <div ng-if=\"activeTab === 'CAMERAS'\" class=\"cameras-fullscreen\">\n <div class=\"cameras-grid\">\n <div class=\"camera-feed camera-rgb\" id=\"rgb-container\">\n <!-- Apply scaling via a wrapper and standard CSS to ensure cross browser support -->\n <video id=\"rgb-video\" autoplay playsinline style=\"width: 100%; height: 100%; object-fit: cover; -webkit-transform: scaleX(-1); transform: scaleX(-1);\"></video>\n <div class=\"camera-title\"><i class=\"fa fa-video-camera\"></i> RGB LIVE FEED</div>\n \n <!-- Camera Power Button -->\n <div style=\"position: absolute; top: 12px; left: 160px; font-size: 11px; font-weight: 800; color: #fff; background: rgba(15, 23, 42, 0.85); padding: 4px 10px; border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.3); cursor: pointer; z-index: 20;\" ng-click=\"stopWebcam()\">\n <i class=\"fa fa-power-off\"></i> STOP\n </div>\n \n <div class=\"camera-rec\"><i class=\"fa fa-circle\"></i> REC</div>\n <div class=\"camera-status\"><i class=\"fa fa-check-circle\"></i> 1080p | 60 FPS</div>\n <div class=\"fullscreen-btn\" ng-click=\"toggleFullScreen('rgb-container')\" style=\"z-index: 20;\"><i class=\"fa fa-expand\"></i></div>\n </div>\n <div class=\"camera-feed camera-thermal\">\n <div class=\"camera-title\"><i class=\"fa fa-thermometer-half\"></i> THERMAL (FLIR)</div>\n <div class=\"camera-rec\"><i class=\"fa fa-circle\"></i> REC</div>\n <div class=\"camera-status\"><i class=\"fa fa-check-circle\"></i> 640x512 | 30 FPS</div>\n </div>\n <div class=\"camera-feed camera-yolo\">\n <div class=\"camera-title\"><i class=\"fa fa-crosshairs\"></i> YOLO INFERENCE</div>\n <div class=\"camera-status\"><i class=\"fa fa-cogs\"></i> AI RUNNING | 12 FPS</div>\n </div>\n </div>\n </div>\n <div ng-if=\"activeTab === 'MISSION'\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column;\">\n \n \n <div style=\"display: flex; gap: 20px; flex: 1; min-height: 0;\">\n <!-- Left: Mini Map & Global Settings -->\n <div style=\"flex: 0 0 400px; display: flex; flex-direction: column; gap: 15px;\">\n <!-- Embedded Map -->\n <div id=\"mission-map-container\" style=\"background: rgba(0,0,0,0.5); padding: 10px; border-radius: 8px; display: flex; flex-direction: column; flex-shrink: 0; position: relative;\">\n <div style=\"font-weight: bold; color: #f8fafc; margin-bottom: 10px; font-size: 13px;\"><i class=\"fa fa-crosshairs\"></i> DRAW ROUTE HERE</div>\n <iframe src=\"/planner-map\" style=\"width: 100%; height: 350px; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px;\"></iframe>\n <div class=\"fullscreen-btn\" ng-click=\"toggleFullScreen('mission-map-container')\" style=\"z-index: 100;\">\n <i class=\"fa fa-expand\"></i>\n </div>\n </div>\n\n <div style=\"background: rgba(0,0,0,0.5); padding: 20px; border-radius: 8px; display: flex; flex-direction: column; gap: 15px; flex: 1; overflow-y: auto;\">\n <div style=\"font-weight: bold; color: #f8fafc; font-size: 13px; margin-bottom: 5px;\">GLOBAL DEFAULTS</div>\n <div>\n <label style=\"color: #94a3b8; font-size: 11px; text-transform: uppercase;\">Default Altitude (m)</label>\n <input type=\"range\" ng-model=\"gAlt\" min=\"5\" max=\"120\" step=\"5\" style=\"width: 100%;\">\n <div style=\"color: #f8fafc; font-weight: bold;\">{{ gAlt }}m</div>\n </div>\n <div>\n <label style=\"color: #94a3b8; font-size: 11px; text-transform: uppercase;\">Default Speed (m/s)</label>\n <input type=\"range\" ng-model=\"gSpeed\" min=\"1\" max=\"15\" step=\"1\" style=\"width: 100%;\">\n <div style=\"color: #f8fafc; font-weight: bold;\">{{ gSpeed }} m/s</div>\n </div>\n <div>\n <label style=\"color: #94a3b8; font-size: 11px; text-transform: uppercase;\">Default Action</label>\n <select ng-model=\"gAction\" style=\"width: 100%; padding: 10px; background: rgba(0,0,0,0.5); color: white; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; font-family: 'Outfit', sans-serif; font-size: 13px; cursor: pointer;\">\n <optgroup label=\"Navigation\">\n <option value=\"waypoint\">NAV: Waypoint</option>\n <option value=\"spline_waypoint\">NAV: Spline WP</option>\n <option value=\"takeoff\">NAV: Takeoff</option>\n <option value=\"land\">NAV: Land</option>\n <option value=\"rtl\">NAV: Return to Launch</option>\n </optgroup>\n <optgroup label=\"Loiter\">\n <option value=\"loiter_unlim\">Loiter: Unlimited</option>\n <option value=\"loiter_turns\">Loiter: Turns</option>\n <option value=\"loiter_time\">Loiter: Timed</option>\n <option value=\"loiter_to_alt\">Loiter: To Alt</option>\n </optgroup>\n <optgroup label=\"Condition\">\n <option value=\"delay\">Condition: Delay</option>\n <option value=\"change_yaw\">Condition: Yaw</option>\n </optgroup>\n <optgroup label=\"Do Command\">\n <option value=\"change_speed\">Do: Change Speed</option>\n <option value=\"set_home\">Do: Set Home</option>\n <option value=\"set_roi\">Do: Set ROI</option>\n <option value=\"cam_trigg\">Do: Camera Trigger</option>\n <option value=\"digicam_ctrl\">Do: Digicam Control</option>\n <option value=\"mount_ctrl\">Do: Gimbal Control</option>\n <option value=\"set_servo\">Do: Set Servo</option>\n <option value=\"set_relay\">Do: Set Relay</option>\n <option value=\"gripper\">Do: Gripper</option>\n <option value=\"drop_payload\">Do: Drop Payload</option>\n </optgroup>\n </select>\n </div>\n <button class=\"control-btn\" style=\"background:#334155 !important; justify-content: center; margin-top: 5px;\" ng-click=\"applyGlobal()\">APPLY TO ALL</button>\n </div>\n </div>\n\n <!-- Right: Waypoint List -->\n <div style=\"flex: 1; background: rgba(0,0,0,0.5); border-radius: 8px; display: flex; flex-direction: column; overflow: hidden;\">\n <div style=\"padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.3);\">\n <div style=\"font-weight: 800; color: #f8fafc; font-size: 16px; letter-spacing: 1px;\"><i class=\"fa fa-map-signs\" style=\"color: #00f2fe; margin-right: 8px;\"></i>WAYPOINTS</div>\n <div style=\"display: flex; gap: 10px;\">\n <button class=\"control-btn\" style=\"background:#dc2626 !important; padding: 6px 12px; font-size: 12px;\" ng-click=\"clearMissionData()\"><i class=\"fa fa-trash\"></i> CLEAR</button>\n <button class=\"control-btn btn-takeoff\" style=\"padding: 6px 12px; font-size: 12px;\" ng-click=\"uploadMission()\"><i class=\"fa fa-upload\"></i> UPLOAD MISSION ({{ waypoints.length }})</button>\n </div>\n </div>\n \n <div style=\"flex: 1; overflow-y: auto; padding: 15px;\">\n <div ng-if=\"waypoints.length === 0\" style=\"text-align: center; color: #64748b; padding: 40px 20px;\">\n <i class=\"fa fa-map-marker\" style=\"font-size: 32px; margin-bottom: 10px; opacity: 0.5;\"></i>\n <div>No waypoints yet.<br><small>Draw a route on the mini-map.</small></div>\n </div>\n \n <div ng-repeat=\"wp in waypoints track by $index\" style=\"background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-left: 3px solid #00f2fe; border-radius: 10px; margin-bottom: 12px; overflow: hidden; display: flex; flex-direction: column; transition: all 0.2s ease;\" onmouseenter=\"this.style.background='rgba(255,255,255,0.08)'; this.style.borderColor='rgba(0,242,254,0.3)'\" onmouseleave=\"this.style.background='rgba(255,255,255,0.04)'; this.style.borderColor='rgba(255,255,255,0.08)'; this.style.borderLeftColor='#00f2fe'\">\n <!-- Waypoint Header & Quick Actions -->\n <div style=\"display: flex; align-items: center; padding: 14px 18px; background: rgba(0,0,0,0.15);\">\n <div style=\"font-family: monospace; font-size: 20px; font-weight: 800; color: #00f2fe; width: 42px; text-shadow: 0 0 10px rgba(0,242,254,0.3);\">{{ ($index+1 < 10 ? '0' : '') + ($index+1) }}</div>\n <div style=\"flex: 1; display: flex; align-items: center; gap: 18px; flex-wrap: wrap;\">\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <label style=\"color: #94a3b8; font-size: 11px; text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px;\">Action:</label>\n <select ng-model=\"wp.action\" style=\"padding: 6px 8px; background: rgba(0,0,0,0.5); color: white; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; font-size: 13px; font-family: 'Outfit', sans-serif; cursor: pointer;\">\n <optgroup label=\"Navigation\">\n <option value=\"waypoint\">NAV: Waypoint</option>\n <option value=\"spline_waypoint\">NAV: Spline WP</option>\n <option value=\"takeoff\">NAV: Takeoff</option>\n <option value=\"land\">NAV: Land</option>\n <option value=\"rtl\">NAV: Return to Launch</option>\n </optgroup>\n <optgroup label=\"Loiter\">\n <option value=\"loiter_unlim\">Loiter: Unlimited</option>\n <option value=\"loiter_turns\">Loiter: Turns</option>\n <option value=\"loiter_time\">Loiter: Timed</option>\n <option value=\"loiter_to_alt\">Loiter: To Alt</option>\n </optgroup>\n <optgroup label=\"Condition\">\n <option value=\"delay\">Condition: Delay</option>\n <option value=\"change_yaw\">Condition: Yaw</option>\n </optgroup>\n <optgroup label=\"Do Command\">\n <option value=\"change_speed\">Do: Change Speed</option>\n <option value=\"set_home\">Do: Set Home</option>\n <option value=\"set_roi\">Do: Set ROI</option>\n <option value=\"cam_trigg\">Do: Camera Trigger</option>\n <option value=\"digicam_ctrl\">Do: Digicam Control</option>\n <option value=\"mount_ctrl\">Do: Gimbal Control</option>\n <option value=\"set_servo\">Do: Set Servo</option>\n <option value=\"set_relay\">Do: Set Relay</option>\n <option value=\"gripper\">Do: Gripper</option>\n <option value=\"drop_payload\">Do: Drop Payload</option>\n </optgroup>\n </select>\n </div>\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <label style=\"color: #94a3b8; font-size: 11px; text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px;\">Alt:</label>\n <input type=\"number\" ng-model=\"wp.altitude\" style=\"width: 58px; padding: 6px 8px; background: rgba(0,0,0,0.5); color: white; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; font-size: 13px; font-family: 'Outfit', sans-serif;\">\n <span style=\"font-size: 12px; color:#cbd5e1; font-weight: 600;\">m</span>\n </div>\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <label style=\"color: #94a3b8; font-size: 11px; text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px;\">Speed:</label>\n <input type=\"number\" ng-model=\"wp.speed\" style=\"width: 58px; padding: 6px 8px; background: rgba(0,0,0,0.5); color: white; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; font-size: 13px; font-family: 'Outfit', sans-serif;\">\n <span style=\"font-size: 12px; color:#cbd5e1; font-weight: 600;\">m/s</span>\n </div>\n \n <span style=\"font-family: monospace; font-size: 12px; color: #94a3b8; margin-left: auto; background: rgba(0,0,0,0.2); padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.05);\">\n <i class=\"fa fa-map-marker\" style=\"color: #ef4444;\"></i> {{ wp.lat | number:5 }}, {{ wp.lon | number:5 }}\n </span>\n </div>\n <div style=\"display: flex; gap: 6px; margin-left: 15px;\">\n <button class=\"control-btn\" style=\"background: rgba(255,255,255,0.08) !important; padding: 6px 10px; font-size: 12px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1) !important; transition: all 0.2s;\" ng-disabled=\"$first\" ng-click=\"mvUp($index)\"><i class=\"fa fa-arrow-up\"></i></button>\n <button class=\"control-btn\" style=\"background: rgba(255,255,255,0.08) !important; padding: 6px 10px; font-size: 12px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1) !important; transition: all 0.2s;\" ng-disabled=\"$last\" ng-click=\"mvDn($index)\"><i class=\"fa fa-arrow-down\"></i></button>\n <button class=\"control-btn\" style=\"background: rgba(239,68,68,0.15) !important; color: #ef4444 !important; padding: 6px 10px; font-size: 12px; border-radius: 6px; border: 1px solid rgba(239,68,68,0.2) !important; transition: all 0.2s;\" ng-click=\"rmWP($index)\"><i class=\"fa fa-trash\"></i></button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div ng-if=\"activeTab === 'LOGS'\" style=\"display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 100%; height: calc(100vh - 90px); padding: 5px 15px 15px 15px; overflow-y: auto; box-sizing: border-box;\">\n \n\n\n <!-- Chain Visualization -->\n <div ng-if=\"logState.loaded\" style=\"background: rgba(0,0,0,0.3); padding: 8px 15px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; overflow: hidden; height: 40px; box-sizing: border-box;\">\n <div style=\"display: flex; align-items: center; gap: 0; width: 100%;\">\n <div ng-repeat=\"block in logState.chainBlocks track by $index\" style=\"display: flex; align-items: center; flex: 1;\">\n <!-- Block -->\n <div style=\"min-width: 20px; height: 20px; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.3s;\"\n ng-style=\"{'background': block.status === 'valid' ? '#2ecc71' : (block.status === 'invalid' ? '#ef4444' : '#64748b'), 'box-shadow': block.status === 'invalid' ? '0 0 10px #ef4444' : 'none'}\">\n <i class=\"fa fa-cube\" style=\"color: rgba(0,0,0,0.5); font-size: 10px;\"></i>\n </div>\n <!-- Link -->\n <div ng-if=\"!$last\" style=\"flex: 1; height: 3px; transition: all 0.3s;\"\n ng-style=\"{'background': block.linkBroken ? '#ef4444' : (block.status === 'valid' && logState.chainBlocks[$index+1].status === 'valid' ? '#2ecc71' : '#64748b')}\">\n </div>\n </div>\n </div>\n </div>\n\n <div style=\"display: flex; gap: 15px; flex: 1; overflow: hidden; height: 0;\">\n <!-- Left Panel: Table -->\n <div style=\"flex: 1; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; display: flex; flex-direction: column; overflow: hidden;\">\n <div style=\"padding: 12px 16px; background: rgba(0,0,0,0.5); border-bottom: 1px solid rgba(255,255,255,0.1); font-weight: bold; color: #94a3b8; font-size: 14px; letter-spacing: 1px;\">\n FLIGHT LOG TABLE\n </div>\n \n <div ng-if=\"!logState.loaded\" style=\"flex: 1; display: flex; align-items: center; justify-content: center; color: #64748b;\">\n Load a log to view records\n </div>\n\n <div ng-if=\"logState.loaded\" style=\"flex: 1; overflow-y: auto;\">\n <table style=\"width: 100%; border-collapse: collapse; font-family: monospace; font-size: 12px; text-align: left;\">\n <thead style=\"position: sticky; top: 0; background: #0f172a; z-index: 10;\">\n <tr style=\"color: #94a3b8; border-bottom: 1px solid rgba(255,255,255,0.1);\">\n <th style=\"padding: 10px;\">REC#</th>\n <th style=\"padding: 10px;\">TIME</th>\n <th style=\"padding: 10px;\">GPS</th>\n <th style=\"padding: 10px;\">ALT</th>\n <th style=\"padding: 10px;\">SPD</th>\n <th style=\"padding: 10px;\">HASH</th>\n <th style=\"padding: 10px;\">STS</th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat=\"r in logState.records track by $index\" \n style=\"border-bottom: 1px solid rgba(255,255,255,0.05); cursor: pointer; transition: background 0.2s;\"\n ng-style=\"{'background': (r.status === 'invalid' ? 'rgba(239,68,68,0.1)' : 'transparent')}\"\n onmouseover=\"this.style.background='rgba(255,255,255,0.05)'\" \n onmouseout=\"this.style.background=this.getAttribute('data-bg') || 'transparent'\"\n data-bg=\"{{r.status === 'invalid' ? 'rgba(239,68,68,0.1)' : 'transparent'}}\">\n <td style=\"padding: 10px; color: #00f2fe;\">{{r.id}}</td>\n <td style=\"padding: 10px; color: #e2e8f0;\">{{r.time}}</td>\n <td style=\"padding: 10px; font-weight: {{r.tampered ? 'bold' : 'normal'}};\" ng-style=\"{'color': r.tampered ? '#ef4444' : '#94a3b8'}\">{{r.gps}}</td>\n <td style=\"padding: 10px; color: #e2e8f0;\">{{r.alt}}m</td>\n <td style=\"padding: 10px; color: #e2e8f0;\">{{r.spd}}m/s</td>\n <td style=\"padding: 10px; color: #cbd5e1;\">{{r.hashTrunc}}</td>\n <td style=\"padding: 10px; font-weight: bold; font-size: 14px;\" ng-style=\"{'color': r.status==='valid'?'#2ecc71':(r.status==='invalid'?'#ef4444':'#64748b')}\">\n {{r.status==='valid'?'\u2713':(r.status==='invalid'?'\u2717':'-')}}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- Right Panel: Verifier -->\n <div style=\"flex: 1; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; display: flex; flex-direction: column; overflow: hidden;\">\n <div style=\"padding: 8px 16px; background: rgba(0,0,0,0.5); border-bottom: 1px solid rgba(255,255,255,0.1); display: flex; justify-content: space-between; align-items: center;\">\n <div style=\"font-weight: bold; color: #94a3b8; font-size: 14px; letter-spacing: 1px;\">VERIFICATION ENGINE</div>\n <div style=\"display: flex; gap: 10px;\">\n <button class=\"control-btn\" style=\"background: rgba(46, 204, 113, 0.15) !important; color: #2ecc71 !important; border: 1px solid rgba(46, 204, 113, 0.5) !important; padding: 6px 12px; border-radius: 6px; font-weight: bold; font-size: 12px; transition: all 0.2s;\" onmouseover=\"this.style.background='rgba(46, 204, 113, 0.25)'\" onmouseout=\"this.style.background='rgba(46, 204, 113, 0.15)'\" ng-click=\"loadMockLog()\">\n <i class=\"fa fa-folder-open\"></i> LOAD LOG\n </button>\n <button class=\"control-btn\" style=\"background: rgba(239, 68, 68, 0.15) !important; color: #ef4444 !important; border: 1px solid rgba(239, 68, 68, 0.5) !important; padding: 6px 12px; border-radius: 6px; font-weight: bold; font-size: 12px; transition: all 0.2s;\" onmouseover=\"this.style.background='rgba(239, 68, 68, 0.25)'\" onmouseout=\"this.style.background='rgba(239, 68, 68, 0.15)'\" ng-click=\"simulateTampering()\" ng-disabled=\"!logState.loaded || logState.verifying\">\n <i class=\"fa fa-warning\"></i> TAMPER DEMO\n </button>\n </div>\n </div>\n\n <div ng-if=\"!logState.loaded\" style=\"flex: 1; display: flex; align-items: center; justify-content: center; color: #64748b;\">\n Load a log to begin verification\n </div>\n\n <div ng-if=\"logState.loaded\" style=\"flex: 1; display: flex; flex-direction: column; padding: 15px; overflow: hidden;\">\n \n <!-- Progress Bar -->\n <div style=\"margin-bottom: 15px;\">\n <div style=\"display: flex; justify-content: space-between; font-family: monospace; font-size: 12px; margin-bottom: 5px; font-weight: bold;\"\n ng-style=\"{'color': logState.tampered ? '#ef4444' : (logState.verifying ? '#00f2fe' : '#2ecc71')}\">\n <span>{{logState.verifying ? 'VERIFYING CHAIN...' : (logState.tampered ? 'VERIFICATION HALTED' : 'VERIFICATION COMPLETE')}}</span>\n <span>{{logState.progressPct}}%</span>\n </div>\n <div style=\"height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;\">\n <div style=\"height: 100%; transition: width 0.1s linear;\"\n ng-style=\"{'width': logState.progressPct + '%', 'background': logState.tampered ? '#ef4444' : (logState.verifying ? '#00f2fe' : '#2ecc71')}\"></div>\n </div>\n <div style=\"font-family: monospace; font-size: 10px; color: #64748b; margin-top: 5px;\">\n Checking record {{logState.verifiedCount}} of {{logState.totalRecords}}...\n </div>\n </div>\n\n <!-- Live Output -->\n <div id=\"verify-console\" style=\"flex: 1; background: #0f172a; border: 1px solid rgba(255,255,255,0.05); border-radius: 6px; padding: 12px; font-family: monospace; font-size: 13px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px;\">\n <div ng-repeat=\"line in logState.consoleLines track by $index\" \n ng-style=\"{'color': line.type === 'valid' ? '#2ecc71' : (line.type === 'invalid' ? '#ef4444' : '#cbd5e1')}\">\n {{line.text}}\n </div>\n </div>\n\n <!-- Status Banner -->\n <div style=\"margin-top: 15px; padding: 12px; border-radius: 6px; text-align: center; font-family: monospace; font-weight: bold; font-size: 14px; transition: all 0.3s;\"\n ng-style=\"{'background': logState.tampered ? 'rgba(239,68,68,0.2)' : (logState.verifying ? 'rgba(0,242,254,0.1)' : 'rgba(46,204,113,0.2)'), 'color': logState.tampered ? '#ef4444' : (logState.verifying ? '#00f2fe' : '#2ecc71'), 'border': '1px solid ' + (logState.tampered ? 'rgba(239,68,68,0.5)' : (logState.verifying ? 'rgba(0,242,254,0.3)' : 'rgba(46,204,113,0.5)'))}\">\n {{logState.tampered ? '[ \u26a0 TAMPERING DETECTED \u2014 LOG INTEGRITY COMPROMISED ]' : (logState.verifying ? '[ VERIFYING CRYPTOGRAPHIC SIGNATURES... ]' : '[ SYSTEM SECURE: ZERO TAMPERING DETECTED ]')}}\n </div>\n </div>\n </div>\n </div>\n\n <!-- Footer Panels -->\n <div ng-if=\"logState.loaded\" style=\"display: flex; gap: 20px; height: 100px; flex-shrink: 0;\">\n <!-- Metadata -->\n <div style=\"flex: 2; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 12px 16px; font-family: monospace; font-size: 13px; color: #94a3b8; display: flex; flex-direction: column; justify-content: center; gap: 6px;\">\n <div style=\"color: #00f2fe; font-weight: bold; font-size: 14px; margin-bottom: 4px;\">FLIGHT LOG: mission_2026-07-02_1315.log</div>\n <div style=\"display: flex; justify-content: space-between;\">\n <span>Duration: <span style=\"color: #e2e8f0;\">00:12:34</span></span>\n <span>Records: <span style=\"color: #e2e8f0;\">{{logState.totalRecords}}</span></span>\n </div>\n <div style=\"display: flex; justify-content: space-between;\">\n <span>Start GPS: <span style=\"color: #e2e8f0;\">30.7071, 76.7689</span></span>\n <span>End GPS: <span style=\"color: #e2e8f0;\">30.7103, 76.7699</span></span>\n </div>\n <div style=\"display: flex; justify-content: space-between;\">\n <span>Encryption: <span style=\"color: #e2e8f0;\">AES-256</span></span>\n <span>Hash: <span style=\"color: #e2e8f0;\">SHA-256</span></span>\n </div>\n </div>\n <!-- Export -->\n <div style=\"flex: 1; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 12px; display: flex; flex-direction: column; justify-content: center; gap: 10px;\">\n <button class=\"control-btn\" style=\"background: rgba(255,255,255,0.08) !important; font-size: 12px; padding: 8px; border-radius: 6px;\"><i class=\"fa fa-file-pdf-o\" style=\"color: #ef4444;\"></i> Export Verification Report (PDF)</button>\n <button class=\"control-btn\" style=\"background: rgba(255,255,255,0.08) !important; font-size: 12px; padding: 8px; border-radius: 6px;\"><i class=\"fa fa-download\" style=\"color: #00f2fe;\"></i> Export Raw Log (Encrypted)</button>\n </div>\n </div>\n\n </div>\n <div ng-if=\"activeTab === 'RECOVERY'\" style=\"display: grid; grid-template-rows: auto 1fr auto; gap: 8px; width: 100%; height: calc(100vh - 138px); padding: 6px 15px 10px 15px; box-sizing: border-box; overflow: hidden;\">\n\n <style>\n @keyframes rp-green { 0%{box-shadow:0 0 0 0 rgba(46,204,113,.8)} 70%{box-shadow:0 0 0 8px rgba(46,204,113,0)} 100%{box-shadow:0 0 0 0 rgba(46,204,113,0)} }\n @keyframes rp-red { 0%{box-shadow:0 0 0 0 rgba(239,68,68,.8)} 70%{box-shadow:0 0 0 10px rgba(239,68,68,0)} 100%{box-shadow:0 0 0 0 rgba(239,68,68,0)} }\n @keyframes rp-ring { 0%{opacity:.6;transform:scale(1)} 50%{opacity:1;transform:scale(1.15)} 100%{opacity:.6;transform:scale(1)} }\n @keyframes rp-dot-pulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.4);opacity:.7} }\n @keyframes rp-radar { 0%{transform:scale(0.5);opacity:.8} 100%{transform:scale(2.5);opacity:0} }\n .rv-card { background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.09); border-radius:10px; padding:12px 16px; }\n .rv-label { font-size:10px; letter-spacing:1.5px; color:#64748b; text-transform:uppercase; margin-bottom:4px; }\n .rv-val { font-family:monospace; font-size:13px; color:#e2e8f0; font-weight:600; }\n .rv-btn { display:flex; align-items:center; gap:8px; padding:9px 14px; border-radius:7px; font-size:11px; font-weight:700; letter-spacing:.8px; cursor:pointer; border:none; transition:all .2s; width:100%; }\n .rv-btn-blue { background:rgba(59,130,246,.15); color:#60a5fa; border:1px solid rgba(59,130,246,.4) !important; }\n .rv-btn-blue:hover { background:rgba(59,130,246,.3) !important; }\n .rv-btn-ghost { background:rgba(255,255,255,.05); color:#94a3b8; border:1px solid rgba(255,255,255,.12) !important; }\n .rv-btn-ghost:hover { background:rgba(255,255,255,.1) !important; }\n .rv-btn-broadcast { background:linear-gradient(135deg,rgba(239,68,68,.25),rgba(220,38,38,.15)); color:#fca5a5; border:1px solid rgba(239,68,68,.5) !important; }\n .rv-btn-broadcast:hover { background:linear-gradient(135deg,rgba(239,68,68,.4),rgba(220,38,38,.3)) !important; }\n .rv-map-grid {\n position:absolute; inset:0;\n background-image: linear-gradient(rgba(0,242,254,.04) 1px,transparent 1px), linear-gradient(90deg,rgba(0,242,254,.04) 1px,transparent 1px);\n background-size:50px 50px; background-color:#020617;\n }\n .rv-tl-event { display:flex; align-items:center; gap:6px; white-space:nowrap; flex-shrink:0; font-family:monospace; font-size:12px; }\n \n .toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; }\n .toggle-switch input { opacity: 0; width: 0; height: 0; }\n .toggle-switch .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #334155; transition: .4s; border-radius: 24px; }\n .toggle-switch .slider:before { position: absolute; content: \"\"; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }\n .toggle-switch input:checked + .slider { background-color: #0ea5e9; }\n .toggle-switch input:checked + .slider:before { transform: translateX(20px); }\n \n </style>\n\n <!-- \u2550\u2550\u2550 ROW 1: TOP STATUS BAR \u2550\u2550\u2550 -->\n <div style=\"display:grid; grid-template-columns: auto auto 1fr auto; gap:10px; align-items:stretch;\">\n\n <!-- Beacon Status -->\n <div class=\"rv-card\" style=\"display:flex; flex-direction:column; justify-content:center; gap:6px; min-width:180px;\">\n <div style=\"display:flex; align-items:center; gap:8px;\">\n <div style=\"width:10px; height:10px; border-radius:50%; background:#2ecc71; animation:rp-green 1.5s infinite, rp-dot-pulse 2s ease-in-out infinite; flex-shrink:0;\"></div>\n <span style=\"font-size:13px; font-weight:700; letter-spacing:1px; color:#2ecc71;\">BEACON ACTIVE</span>\n </div>\n <div style=\"font-size:12px; color:#64748b;\">Signal \u00b7 every 2s | Last packet: 3s ago</div>\n </div>\n\n <!-- Incident Summary -->\n <div class=\"rv-card\" style=\"border-color:rgba(245,158,11,.3); background:rgba(245,158,11,.04);\">\n <div style=\"display:flex; align-items:center; gap:6px; margin-bottom:6px;\">\n <i class=\"fa fa-warning\" style=\"color:#f59e0b; font-size:11px;\"></i>\n <span style=\"font-size:12px; font-weight:700; letter-spacing:1.5px; color:#f59e0b;\">INCIDENT</span>\n <span style=\"margin-left:auto; font-size:12px; color:#64748b; font-family:monospace;\">13:47:22</span>\n </div>\n <div style=\"display:grid; grid-template-columns:1fr 1fr; gap:5px 16px; font-size:12px; font-family:monospace;\">\n <div style=\"color:#64748b;\">Alt</div><div style=\"color:#e2e8f0; text-align:right;\">24.3m</div>\n <div style=\"color:#64748b;\">Spd</div><div style=\"color:#e2e8f0; text-align:right;\">3.1m/s</div>\n <div style=\"color:#64748b;\">Batt</div><div style=\"color:#f87171; text-align:right;\">34%</div>\n <div style=\"color:#64748b;\">Time</div><div style=\"color:#e2e8f0; text-align:right;\">00:08:14</div>\n </div>\n </div>\n\n <!-- Last GPS -->\n <div class=\"rv-card\" style=\"border-color:rgba(239,68,68,.3); background:rgba(239,68,68,.04); display:flex; flex-direction:column; justify-content:center;\">\n <div style=\"font-size:12px; letter-spacing:1px; color:#f87171; margin-bottom:4px;\"><i class=\"fa fa-map-marker\"></i> LAST KNOWN POSITION</div>\n <div style=\"font-family:monospace; font-size:18px; font-weight:700; color:#fff; letter-spacing:.5px;\">30.70689\u00b0N 76.76887\u00b0E</div>\n <div style=\"font-size:12px; color:#64748b; margin-top:3px;\">Dist from home: <span style=\"color:#00f2fe; font-weight:600;\">412m</span> \u00b7 Backup LiPo: <span style=\"color:#2ecc71; font-weight:600;\">3.8V (95%) \u00b7 ~4.2hr</span></div>\n </div>\n\n <!-- Action Buttons -->\n <div style=\"display:flex; flex-direction:column; gap:6px; justify-content:center; min-width:190px;\">\n <button class=\"rv-btn rv-btn-blue\" onclick=\"alert('Opening in Google Maps...')\"><i class=\"fa fa-map-marker\"></i> OPEN IN GOOGLE MAPS</button>\n <button class=\"rv-btn rv-btn-ghost\"><i class=\"fa fa-copy\"></i> COPY COORDINATES</button>\n <button class=\"rv-btn rv-btn-broadcast\" onclick=\"alert('Broadcasting via LoRa...')\"><i class=\"fa fa-bolt\"></i> BROADCAST CRASH LOCATION</button>\n </div>\n </div>\n\n <!-- \u2550\u2550\u2550 ROW 2: MAP \u2550\u2550\u2550 -->\n <div style=\"position:relative; border-radius:10px; overflow:hidden; border:1px solid rgba(255,255,255,.08);\">\n <div class=\"rv-map-grid\"></div>\n\n <!-- Top-left map badge -->\n <div style=\"position:absolute; top:12px; left:12px; z-index:10; display:flex; gap:8px;\">\n <div style=\"background:rgba(0,0,0,.75); backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,.1); border-radius:6px; padding:6px 12px; font-size:12px; font-family:monospace; color:#cbd5e1;\">\n <i class=\"fa fa-crosshairs\" style=\"color:#00f2fe;\"></i> LIVE RECOVERY TRACKING\n </div>\n <div style=\"background:rgba(239,68,68,.15); border:1px solid rgba(239,68,68,.4); border-radius:6px; padding:6px 12px; font-size:12px; font-family:monospace; color:#fca5a5; display:flex; align-items:center; gap:5px;\">\n <div style=\"width:7px; height:7px; background:#ef4444; border-radius:50%; animation:rp-red 1s infinite;\"></div> CRASH DETECTED <span style=\"color:rgba(252,165,165,.5); margin-left:3px;\">\u00b7 13:47:22</span>\n </div>\n </div>\n\n <!-- HOME marker -->\n <div style=\"position:absolute; bottom:28%; left:25%; z-index:5; display:flex; flex-direction:column; align-items:center; gap:4px;\">\n <div style=\"background:rgba(0,0,0,.75); border:1px solid rgba(59,130,246,.4); border-radius:4px; padding:2px 7px; font-size:12px; font-family:monospace; color:#93c5fd; letter-spacing:1px;\">HOME</div>\n <i class=\"fa fa-home\" style=\"color:#3b82f6; font-size:18px; filter:drop-shadow(0 0 8px rgba(59,130,246,.7));\"></i>\n </div>\n\n <!-- Dashed line SVG -->\n <svg style=\"position:absolute; inset:0; width:100%; height:100%; z-index:2; pointer-events:none;\">\n <line x1=\"25%\" y1=\"72%\" x2=\"65%\" y2=\"30%\" stroke=\"rgba(255,255,255,.35)\" stroke-width=\"1.5\" stroke-dasharray=\"7 5\"/>\n </svg>\n\n <!-- CRASH marker -->\n <div style=\"position:absolute; top:30%; left:65%; transform:translate(-50%,-50%); z-index:8; display:flex; flex-direction:column; align-items:center;\">\n <div style=\"background:rgba(0,0,0,.85); border:1px solid rgba(239,68,68,.6); border-radius:5px; padding:4px 10px; font-size:11px; font-family:monospace; font-weight:700; color:#fca5a5; letter-spacing:1px; margin-bottom:6px; white-space:nowrap;\"><i class=\"fa fa-map-marker\" style=\"margin-right:4px;\"></i>LAST KNOWN POSITION</div>\n <i class=\"fa fa-map-marker\" style=\"color:#ef4444; font-size:40px; filter:drop-shadow(0 0 12px rgba(239,68,68,.9));\"></i>\n <!-- Radar ping rings -->\n <div style=\"position:absolute; top:10px; left:50%; transform:translateX(-50%); width:46px; height:46px; border:1.5px solid rgba(239,68,68,.6); border-radius:50%; animation:rp-radar 1.8s ease-out infinite;\"></div>\n <div style=\"position:absolute; top:10px; left:50%; transform:translateX(-50%); width:46px; height:46px; border:1.5px solid rgba(239,68,68,.4); border-radius:50%; animation:rp-radar 1.8s ease-out infinite; animation-delay:.6s;\"></div>\n <div style=\"position:absolute; top:10px; left:50%; transform:translateX(-50%); width:46px; height:46px; border:1.5px solid rgba(239,68,68,.2); border-radius:50%; animation:rp-radar 1.8s ease-out infinite; animation-delay:1.2s;\"></div>\n <!-- Dead drone -->\n <i class=\"fa fa-plane\" style=\"color:#64748b; font-size:18px; position:absolute; top:48px; left:28px; transform:rotate(40deg); opacity:.6;\"></i>\n </div>\n\n <!-- Bottom-right distance badge -->\n <div style=\"position:absolute; bottom:12px; right:12px; z-index:10; background:rgba(0,0,0,.75); backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,.1); border-radius:6px; padding:6px 14px; font-family:monospace; font-size:13px; color:#cbd5e1;\">\n Dist from Home: <strong style=\"color:#00f2fe;\">412m</strong>\n </div>\n </div>\n\n <!-- \u2550\u2550\u2550 ROW 3: TIMELINE \u2550\u2550\u2550 -->\n <div style=\"position:relative; border-radius:8px; overflow:hidden; border:1px solid rgba(255,255,255,.08);\">\n <div style=\"background:rgba(0,0,0,.4); padding:7px 14px; display:flex; align-items:center; gap:12px; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.1) transparent;\">\n <span style=\"font-size:11px; letter-spacing:1.5px; color:#475569; font-weight:700; white-space:nowrap; flex-shrink:0;\">RECOVERY TIMELINE</span>\n <div style=\"width:1px; height:20px; background:rgba(255,255,255,.1); flex-shrink:0;\"></div>\n <div class=\"rv-tl-event\" style=\"color:#f59e0b;\"><span style=\"color:#475569;\">[13:47:19]</span> \u26a0 Signal degrading</div>\n <div style=\"color:#334155; flex-shrink:0;\">\u25b8</div>\n <div class=\"rv-tl-event\" style=\"color:#f59e0b;\"><span style=\"color:#475569;\">[13:47:21]</span> \u26a0 Main link lost</div>\n <div style=\"color:#334155; flex-shrink:0;\">\u25b8</div>\n <div class=\"rv-tl-event\" style=\"color:#ef4444; font-weight:700;\"><span style=\"color:#475569;\">[13:47:22]</span> \ud83d\udd34 CRASH DETECTED \u2014 beacon mode</div>\n <div style=\"color:#334155; flex-shrink:0;\">\u25b8</div>\n <div class=\"rv-tl-event\" style=\"color:#2ecc71;\"><span style=\"color:#475569;\">[13:47:22]</span> \u2713 LoRa beacon activated</div>\n <div style=\"color:#334155; flex-shrink:0;\">\u25b8</div>\n <div class=\"rv-tl-event\" style=\"color:#2ecc71;\"><span style=\"color:#475569;\">[13:47:24]</span> \u2713 First packet transmitted</div>\n <div style=\"color:#334155; flex-shrink:0;\">\u25b8</div>\n <div class=\"rv-tl-event\" style=\"color:#00f2fe;\"><span style=\"color:#475569;\">[13:50:01]</span> \u2713 Beacon 95% battery \u2014 still transmitting</div>\n </div>\n <!-- fade-out mask on right -->\n <div style=\"position:absolute; right:0; top:0; bottom:0; width:60px; background:linear-gradient(to right, transparent, rgba(10,14,23,.95)); pointer-events:none; border-radius:0 8px 8px 0;\"></div>\n </div>\n\n </div>\n </div>\n </div>\n \n <!-- Modals -->\n <div class=\"modal-overlay\" ng-if=\"showChecklist\">\n <div class=\"checklist-modal\">\n <div class=\"checklist-title\">PRE-FLIGHT CHECKS</div>\n <div class=\"check-item\"><i class=\"fa\" ng-class=\"checkStatus.gps ? 'fa-check-circle ok' : 'fa-circle-o-notch pending'\"></i> GPS Lock <span style=\"margin-left:auto;\">{{gpsSats}} satellites</span></div>\n <div class=\"check-item\"><i class=\"fa\" ng-class=\"checkStatus.imu ? 'fa-check-circle ok' : 'fa-circle-o-notch pending'\"></i> IMU Agreement <span style=\"margin-left:auto;\">{{trustScore}}%</span></div>\n <div class=\"check-item\"><i class=\"fa\" ng-class=\"checkStatus.batt ? 'fa-check-circle ok' : 'fa-circle-o-notch pending'\"></i> Battery <span style=\"margin-left:auto;\">{{telemetry.battery || 100}}%</span></div>\n <div class=\"check-item\"><i class=\"fa\" ng-class=\"checkStatus.link ? 'fa-check-circle ok' : 'fa-circle-o-notch pending'\"></i> LoRa Link <span style=\"margin-left:auto;\">Active</span></div>\n <div class=\"check-item\"><i class=\"fa\" ng-class=\"checkStatus.link ? 'fa-check-circle ok' : 'fa-circle-o-notch pending'\"></i> RC Link <span style=\"margin-left:auto;\">Active</span></div>\n <div class=\"check-item\"><i class=\"fa\" ng-class=\"checkStatus.fence ? 'fa-exclamation-triangle warn' : 'fa-circle-o-notch pending'\"></i> Geofence <span style=\"margin-left:auto;\">Not set</span></div>\n <div style=\"margin-top: 20px; display: flex; justify-content: space-between; gap: 10px;\">\n <button class=\"control-btn\" style=\"background:#334155 !important; padding:8px 12px; font-size:12px; flex:1; justify-content:center;\" ng-click=\"cancelArm()\">CANCEL</button>\n <button class=\"control-btn\" ng-style=\"{'background': !checkStatus.fence ? '#16a34a' : '#f59e0b'}\" style=\"padding:8px 12px; font-size:12px; flex:1; justify-content:center;\" ng-disabled=\"!checkComplete\" ng-click=\"proceedArm()\">PROCEED TO ARM</button>\n </div>\n </div>\n </div>\n \n <div class=\"modal-overlay\" ng-if=\"showRescueSummary\">\n <div class=\"checklist-modal\" style=\"width: 450px;\">\n <div class=\"checklist-title\" style=\"color: #2ecc71;\">RESCUE BROADCAST SENT \u2713</div>\n <div style=\"border-top: 1px solid #334155; border-bottom: 1px solid #334155; padding: 15px 0; margin-bottom: 15px; max-height: 200px; overflow-y: auto;\">\n <div ng-repeat=\"pin in rescuePins\" style=\"font-family: monospace; color: #e5e7eb; margin-bottom: 5px; font-size: 13px;\">\n {{pin.name}} \u2014 {{pin.lat | number:5}}\u00b0N {{pin.lon | number:5}}\u00b0E \u2014 {{pin.time}}\n </div>\n <div ng-if=\"rescuePins.length === 0\" style=\"color: #64748b; text-align: center;\">No detections yet.</div>\n </div>\n <div style=\"color: #94a3b8; font-size: 12px; margin-bottom: 15px; text-align: center;\">Transmitted over LoRa | {{rescuePins.length}} survivors located</div>\n <div style=\"display: flex; justify-content: space-between; gap: 10px;\">\n <button class=\"control-btn\" style=\"background:#334155 !important; flex:1; justify-content:center;\" ng-click=\"showRescueSummary = false\">CLOSE</button>\n <button class=\"control-btn\" style=\"background:#2563eb !important; flex:1; justify-content:center;\" onclick=\"alert('Exporting to CSV...')\">EXPORT AS CSV</button>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n(function(scope) {\n scope.toggleFullScreen = function(elementId) {\n var elem = document.getElementById(elementId);\n if (!elem) return;\n \n var isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;\n \n if (isFullscreen) {\n if (document.exitFullscreen) document.exitFullscreen();\n else if (document.webkitExitFullscreen) document.webkitExitFullscreen();\n else if (document.mozCancelFullScreen) document.mozCancelFullScreen();\n else if (document.msExitFullscreen) document.msExitFullscreen();\n } else {\n if (elem.requestFullscreen) elem.requestFullscreen();\n else if (elem.webkitRequestFullscreen) elem.webkitRequestFullscreen();\n else if (elem.mozRequestFullScreen) elem.mozRequestFullScreen();\n else if (elem.msRequestFullscreen) elem.msRequestFullscreen();\n }\n };\n\n scope.initWebcam = function() {\n var video = document.getElementById('rgb-video');\n if (!video) return;\n \n // Try to use modern API\n if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {\n navigator.mediaDevices.getUserMedia({ video: true })\n .then(function(stream) {\n scope.currentWebcamStream = stream;\n video.srcObject = stream;\n video.play();\n })\n .catch(function(err) {\n console.log(\"Error accessing webcam (mediaDevices): \" + err);\n fallbackWebcam(video);\n });\n } else {\n fallbackWebcam(video);\n }\n \n function fallbackWebcam(vid) {\n // Older browsers might use this\n var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;\n if (getUserMedia) {\n getUserMedia.call(navigator, {video: true}, function(stream) {\n scope.currentWebcamStream = stream;\n if (\"srcObject\" in vid) {\n vid.srcObject = stream;\n } else {\n vid.src = window.URL.createObjectURL(stream);\n }\n vid.play();\n }, function(err) {\n console.log(\"Fallback webcam error: \" + err);\n });\n } else {\n console.log(\"WebRTC not supported on this browser/connection (Requires localhost or HTTPS)\");\n }\n }\n };\n\n scope.stopWebcam = function() {\n if (scope.currentWebcamStream) {\n var tracks = scope.currentWebcamStream.getTracks();\n tracks.forEach(function(track) {\n track.stop();\n });\n scope.currentWebcamStream = null;\n }\n // Also try to stop from video element if it still exists\n var video = document.getElementById('rgb-video');\n if (video && video.srcObject) {\n var stream = video.srcObject;\n var tracks2 = stream.getTracks();\n tracks2.forEach(function(track) {\n track.stop();\n });\n video.srcObject = null;\n }\n };\n\n scope.$watch('activeTab', function(newVal) {\n if (newVal === 'CAMERAS') {\n setTimeout(scope.initWebcam, 800); // Wait for DOM to render the video tag\n } else {\n if (scope.stopWebcam) {\n scope.stopWebcam();\n }\n }\n });\n\n scope.waypoints = [];\n scope.wpOpen = {};\n scope.gAlt = 20;\n scope.gSpeed = 5;\n scope.gAction = 'waypoint';\n\n scope.applyGlobal = function() {\n scope.waypoints.forEach(wp => {\n wp.altitude = scope.gAlt;\n wp.speed = scope.gSpeed;\n wp.action = scope.gAction;\n });\n addLog(\"Defaults applied to all waypoints\", \"ok\");\n };\n\n scope.rmWP = function(i) {\n scope.waypoints.splice(i, 1);\n scope.wpOpen = {}; \n };\n\n scope.mvUp = function(i) {\n if (i > 0) {\n var temp = scope.waypoints[i];\n scope.waypoints.splice(i, 1);\n scope.waypoints.splice(i-1, 0, temp);\n scope.wpOpen = {};\n }\n };\n\n scope.mvDn = function(i) {\n if (i < scope.waypoints.length - 1) {\n var temp = scope.waypoints[i];\n scope.waypoints.splice(i, 1);\n scope.waypoints.splice(i+1, 0, temp);\n scope.wpOpen = {};\n }\n };\n\n scope.dupWP = function(i) {\n var wp = angular.copy(scope.waypoints[i]);\n scope.waypoints.splice(i+1, 0, wp);\n scope.wpOpen = {};\n };\n\n scope.uploadMission = function() {\n if (scope.waypoints.length === 0) {\n addLog(\"No waypoints to upload\", \"err\");\n return;\n }\n scope.send({ payload: { command: 'upload', waypoints: angular.copy(scope.waypoints) } });\n addLog(\"Uploading \" + scope.waypoints.length + \" waypoints...\", \"ok\");\n };\n\n scope.clearMissionData = function() {\n scope.waypoints = [];\n scope.wpOpen = {};\n scope.send({ payload: { command: 'clear' } });\n addLog(\"Mission cleared\", \"warn\");\n };\n\n scope.$watch('msg', function(msg) {\n if (msg && msg.payload) {\n var p = msg.payload;\n\n // \u2500\u2500 ARDUPILOT SITL LIVE TELEMETRY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (p.type === 'telemetry' && p.data) {\n var d = p.data;\n var wasArmed = scope.telemetry.armed;\n scope._sitlConnected = true; // Mark as live \u2014 disables mock overrides\n // Merge into existing telemetry object\n Object.assign(scope.telemetry, d);\n scope.battTime = Math.max(0, Math.floor((scope.telemetry.battery || 0) * 0.22));\n // Update GPS / trust score\n if (d.satellites !== undefined) {\n scope.gpsSats = d.satellites;\n scope.trustScore = Math.min(100, Math.round((d.satellites / 14) * 100));\n }\n // 3D drone model attitude\n if (typeof targetPitch !== 'undefined') {\n targetPitch = (d.pitch || 0) * (Math.PI / 180);\n targetRoll = -(d.roll || 0) * (Math.PI / 180);\n }\n // Arm detect\n if (!wasArmed && d.armed) {\n scope.flightTimeSecs = 0;\n scope.flightTimeFormatted = \"00:00:00\";\n addLog(\"\ud83d\ude80 SITL Armed \u2014 drone airborne.\", \"ok\");\n }\n // Distance to home\n if (d.lat && d.home_lat) {\n scope.distToHome = getDistance(d.lat, d.lon, d.home_lat, d.home_lon);\n }\n scope.$applyAsync();\n return;\n }\n\n // \u2500\u2500 GPS POSITION UPDATE (map marker) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (p.type === 'gps' && p.data) {\n var gd = p.data;\n scope.telemetry.lat = gd.lat;\n scope.telemetry.lon = gd.lon;\n scope.telemetry.heading = gd.heading || scope.telemetry.heading;\n scope.telemetry.alt = gd.alt || scope.telemetry.alt;\n scope.$applyAsync();\n return;\n }\n\n // \u2500\u2500 GPS STATUS (satellites, HDOP) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (p.type === 'gps_status') {\n scope.gpsSats = p.gpsSats !== undefined ? p.gpsSats : scope.gpsSats;\n scope.trustScore = p.trustScore !== undefined ? p.trustScore : scope.trustScore;\n scope.$applyAsync();\n return;\n }\n\n // \u2500\u2500 DRAW EVENTS (from Node-RED Worldmap) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (p.action === 'draw') {\n var pts = [];\n if (p.line) pts = p.line;\n else if (p.area) pts = p.area;\n \n // Flatten if necessary\n if (pts.length > 0 && Array.isArray(pts[0])) pts = pts[0];\n \n if (pts.length > 0) {\n scope.waypoints = pts.map(wp => ({\n lat: wp.lat || wp[0], \n lon: wp.lng !== undefined ? wp.lng : (wp.lon !== undefined ? wp.lon : wp[1]),\n action: scope.gAction, \n altitude: scope.gAlt,\n speed: scope.gSpeed, \n loiter_time: 0, \n heading: -1, \n radius: 10, \n note: ''\n }));\n addLog(pts.length + \" waypoints loaded from map\", \"ok\");\n }\n }\n }\n });\n\n scope.telemetry = { armed: false, mode: 'standby', alt: 0, speed: 0, battery: 100, pitch: 0, roll: 0, heading: 0, lat: 0, lon: 0, home_lat: 0, home_lon: 0 };\n scope.rescueCount = 0; scope.rescuePins = []; scope.gpsSats = 14; scope.gpsHdop = 0.9; scope.flightTimeSecs = 0; scope.flightTimeFormatted = \"00:00:00\"; scope.distToHome = 0; scope.trustScore = 100; scope.battTime = 30; scope.thermalOn = false;\n scope.showChecklist = false; scope.checkComplete = false; scope.checkStatus = { gps: false, imu: false, batt: false, link: false, fence: false };\n scope.activeTab = 'FLIGHT'; scope.showRescueSummary = false;\n\n // --- LOGS TAB LOGIC ---\n scope.logState = {\n loaded: false,\n verifying: false,\n tampered: false,\n records: [],\n chainBlocks: [],\n consoleLines: [],\n totalRecords: 24, // keep it small for visual chain\n verifiedCount: 0,\n progressPct: 0\n };\n\n function generateHash() {\n return Array.from({length: 64}, () => Math.floor(Math.random()*16).toString(16).toUpperCase()).join('');\n }\n\n scope.loadMockLog = function() {\n scope.logState.loaded = true;\n scope.logState.verifying = true;\n scope.logState.tampered = false;\n scope.logState.records = [];\n scope.logState.chainBlocks = [];\n scope.logState.consoleLines = [];\n scope.logState.verifiedCount = 0;\n scope.logState.progressPct = 0;\n\n // Generate mock records\n let lat = 30.7071, lon = 76.7689, alt = 0, spd = 0;\n for (let i = 1; i <= scope.logState.totalRecords; i++) {\n let hash = generateHash();\n scope.logState.records.push({\n id: i.toString().padStart(3, '0'),\n time: '13:15:' + (11+i).toString().padStart(2,'0'),\n gps: lat.toFixed(4) + ',' + lon.toFixed(4),\n alt: alt.toFixed(1),\n spd: spd.toFixed(1),\n hashFull: hash,\n hashTrunc: hash.substring(0,4) + '...' + hash.substring(60),\n status: 'pending',\n tampered: false\n });\n scope.logState.chainBlocks.push({ status: 'pending', linkBroken: false });\n \n lat += 0.0001; lon += 0.0001;\n if(i > 3) { alt += 1.5; spd = 4.2; }\n }\n\n runVerification(false);\n };\n\n function runVerification(isTampered) {\n scope.logState.verifying = true;\n scope.logState.consoleLines = [];\n scope.logState.verifiedCount = 0;\n scope.logState.progressPct = 0;\n scope.logState.tampered = false;\n \n // Reset statuses\n scope.logState.records.forEach(r => r.status = 'pending');\n scope.logState.chainBlocks.forEach(b => { b.status = 'pending'; b.linkBroken = false; });\n\n let i = 0;\n let interval = setInterval(() => {\n if (i >= scope.logState.totalRecords || (isTampered && i === 12)) {\n clearInterval(interval);\n scope.logState.verifying = false;\n \n if (isTampered) {\n scope.logState.tampered = true;\n // Cascade failure\n for(let j = i; j < scope.logState.totalRecords; j++) {\n scope.logState.records[j].status = 'invalid';\n scope.logState.chainBlocks[j].status = 'invalid';\n if(j > i) {\n scope.logState.consoleLines.push({ type: 'invalid', text: '\u2717 Record ' + scope.logState.records[j].id + ' cascade INVALID (chain broken from ' + scope.logState.records[12].id + ')'});\n }\n }\n scope.logState.consoleLines.push({ type: 'invalid', text: '[ \u26a0 CHAIN BROKEN AT RECORD ' + scope.logState.records[12].id + ' ]' });\n scope.logState.chainBlocks[11].linkBroken = true;\n } else {\n scope.logState.consoleLines.push({ type: 'info', text: '[ CHAIN INTACT \u2014 ' + scope.logState.totalRecords + '/' + scope.logState.totalRecords + ' RECORDS VERIFIED ]' });\n }\n \n scope.$applyAsync();\n setTimeout(scrollToBottom, 50);\n return;\n }\n\n // Verify current record\n let r = scope.logState.records[i];\n r.status = 'valid';\n scope.logState.chainBlocks[i].status = 'valid';\n scope.logState.consoleLines.push({ type: 'valid', text: '\u2713 Record ' + r.id + ' ' + r.hashTrunc + ' VALID' });\n \n scope.logState.verifiedCount = i + 1;\n scope.logState.progressPct = Math.round((scope.logState.verifiedCount / scope.logState.totalRecords) * 100);\n \n if (isTampered && i === 11) {\n // The next loop iteration (i=12) will fail\n }\n\n if (isTampered && i === 12) {\n // Actually it breaks at i=12, handled above by the condition\n }\n\n i++;\n scope.$applyAsync();\n setTimeout(scrollToBottom, 50);\n }, 150); // pace it slightly slower for better visual effect\n }\n\n function scrollToBottom() {\n let el = document.getElementById('verify-console');\n if(el) el.scrollTop = el.scrollHeight;\n }\n\n scope.simulateTampering = function() {\n if(!scope.logState.loaded || scope.logState.verifying) return;\n \n // Corrupt record 13 (index 12)\n scope.logState.records[12].gps = \"32.1111,74.9999\";\n scope.logState.records[12].tampered = true;\n \n runVerification(true);\n };\n // --- END LOGS TAB LOGIC ---\n\n \n scope.eventLogs = [];\n let lastLog = {msg: '', count: 1, time: 0, index: -1};\n function addLog(msg, type) {\n const now = Date.now();\n if (msg === lastLog.msg && scope.eventLogs.length > 0) {\n lastLog.count++; lastLog.time = now;\n scope.eventLogs[0].msg = msg + ` (\u00d7${lastLog.count})`;\n scope.$applyAsync(); return;\n }\n const d = new Date(); const timeStr = d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0') + ':' + d.getSeconds().toString().padStart(2,'0');\n let cls = ''; if(type==='err') cls='log-err'; else if(type==='warn') cls='log-warn'; else if(type==='ok') cls='log-ok';\n scope.eventLogs.unshift({time: timeStr, msg: msg, cls: cls});\n if(scope.eventLogs.length > 30) scope.eventLogs.pop();\n lastLog = {msg: msg, count: 1, time: now, index: 0};\n scope.$applyAsync();\n }\n \n function getDistance(lat1, lon1, lat2, lon2) {\n if (!lat1 || !lon1 || !lat2 || !lon2) return 0;\n const R = 6371e3; const p1 = lat1 * Math.PI/180; const p2 = lat2 * Math.PI/180;\n const dp = (lat2-lat1) * Math.PI/180; const dl = (lon2-lon1) * Math.PI/180;\n const a = Math.sin(dp/2) * Math.sin(dp/2) + Math.cos(p1) * Math.cos(p2) * Math.sin(dl/2) * Math.sin(dl/2);\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n return R * c;\n }\n \n scope.simulateRescue = function() {\n scope.rescueCount++; let pLat = (scope.telemetry.lat || 30.70689) + (Math.random()-0.5)*0.001; let pLon = (scope.telemetry.lon || 76.76887) + (Math.random()-0.5)*0.001;\n const d = new Date(); const timeStr = d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0') + ':' + d.getSeconds().toString().padStart(2,'0');\n scope.rescuePins.push({name: 'Pin '+scope.rescueCount, lat: pLat, lon: pLon, time: timeStr});\n addLog(`\ud83d\udd34 PERSON DETECTED \u2014 ${pLat.toFixed(5)}\u00b0N ${pLon.toFixed(5)}\u00b0E`, 'err');\n scope.send({ payload: { type: \"add_pin\", data: { lat: pLat, lon: pLon, num: scope.rescueCount } } });\n };\n\n scope.broadcastRescue = function() {\n if (scope.rescueCount === 0) { addLog(\"\u26a0 No detections to broadcast.\", \"warn\"); } else {\n addLog(`\u26a1 BROADCASTING ${scope.rescueCount} RESCUE PINS VIA LORA...`, \"ok\");\n setTimeout(() => { addLog(\"\u2713 Broadcast confirmed received by base station.\", \"ok\"); scope.showRescueSummary = true; scope.$applyAsync(); }, 1500);\n }\n };\n\n scope.recenterMap = function() {\n if (scope.telemetry.lat) { scope.send({ payload: { type: \"recenter\", data: { lat: scope.telemetry.lat, lon: scope.telemetry.lon } } }); addLog(\"Map re-centered on drone.\", \"\"); }\n };\n \n scope.toggleThermal = function() {\n scope.thermalOn = !scope.thermalOn;\n scope.send({ payload: { type: \"thermal_overlay\", state: scope.thermalOn, lat: scope.telemetry.lat, lon: scope.telemetry.lon } });\n addLog(`Thermal overlay ${scope.thermalOn ? 'ENABLED' : 'DISABLED'}.`, scope.thermalOn ? \"warn\" : \"\");\n };\n \n scope.clearMission = function() {\n scope.send({ payload: { type: \"clear_mission\" } });\n addLog(\"Mission waypoints cleared.\", \"warn\");\n };\n \n scope.clearDetections = function() {\n scope.rescueCount = 0;\n scope.rescuePins = [];\n scope.send({ payload: { type: \"clear_detections\" } });\n addLog(\"Rescue detections cleared from map.\", \"warn\");\n };\n\n scope.startArmSequence = function() {\n if (scope.telemetry.armed) { addLog(\"\u2713 System disarmed.\", \"ok\"); scope.send({ payload: JSON.stringify({ action: \"arm\", state: false }) }); return; }\n scope.showChecklist = true; scope.checkComplete = false; scope.checkStatus = { gps: false, imu: false, batt: false, link: false, fence: false };\n setTimeout(() => { scope.checkStatus.gps = true; scope.$applyAsync(); }, 400);\n setTimeout(() => { scope.checkStatus.imu = true; scope.$applyAsync(); }, 800);\n setTimeout(() => { scope.checkStatus.batt = true; scope.$applyAsync(); }, 1200);\n setTimeout(() => { scope.checkStatus.link = true; scope.$applyAsync(); }, 1600);\n setTimeout(() => { scope.checkStatus.fence = true; scope.checkComplete = true; scope.$applyAsync(); }, 2000);\n };\n \n scope.cancelArm = function() { scope.showChecklist = false; addLog(\"\u26a0 Arm sequence cancelled.\", \"warn\"); };\n scope.proceedArm = function() { scope.showChecklist = false; addLog(\"\u2713 Pre-flight checks passed. ARMING.\", \"ok\"); scope.send({ payload: JSON.stringify({ action: \"arm\", state: true }) }); };\n \n scope.takeoff = function() { addLog(\"\u2b06 Initiating Take Off.\", \"ok\"); scope.send({ payload: JSON.stringify({ action: \"takeoff\" }) }); };\n scope.land = function() { addLog(\"\u2b07 Initiating Landing.\", \"warn\"); scope.send({ payload: JSON.stringify({ action: \"land\" }) }); };\n scope.rtl = function() { addLog(\"\ud83c\udfe0 Returning to Launch.\", \"warn\"); scope.send({ payload: JSON.stringify({ action: \"rtl\" }) }); };\n\n let scene, camera, renderer, drone;\n let props = []; let targetPitch = 0, targetRoll = 0, targetYaw = 0; let targetQuaternion = null; let animationId = null;\n \n function init3D() {\n const container = document.getElementById('attitude-canvas');\n if (!container || container.clientWidth === 0) return setTimeout(init3D, 100);\n if (container.children.length > 0) container.innerHTML = '';\n if (animationId) cancelAnimationFrame(animationId);\n \n scene = new THREE.Scene(); scene.background = null; \n camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);\n camera.position.set(0, 0.4, 2.5); camera.lookAt(0, 0, 0); // Lowered camera so drone looks flat at 0 pitch\n renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n renderer.setClearColor( 0x000000, 0 ); renderer.setSize(container.clientWidth, container.clientHeight);\n container.appendChild(renderer.domElement);\n scene.add(new THREE.AmbientLight(0xffffff, 0.6));\n const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(5, 10, 5); scene.add(dirLight);\n \n drone = new THREE.Group(); props = [];\n const bodyMat = new THREE.MeshLambertMaterial({ color: 0x3b82f6 });\n const armMat = new THREE.MeshLambertMaterial({ color: 0x9ca3af });\n const motorMat = new THREE.MeshLambertMaterial({ color: 0x222222 });\n const propFrontMat = new THREE.MeshBasicMaterial({ color: 0x00f2fe, transparent: true, opacity: 0.6, side: THREE.DoubleSide });\n const propRearMat = new THREE.MeshBasicMaterial({ color: 0xff4444, transparent: true, opacity: 0.6, side: THREE.DoubleSide });\n \n const bodyGeo = new THREE.BoxGeometry(0.5, 0.15, 0.7); drone.add(new THREE.Mesh(bodyGeo, bodyMat));\n const arrowGeo = new THREE.ConeGeometry(0.1, 0.3, 3); const arrow = new THREE.Mesh(arrowGeo, new THREE.MeshLambertMaterial({ color: 0xffffff }));\n arrow.rotation.x = -Math.PI / 2; arrow.position.set(0, 0.15, -0.2); // Point forward (-Z) drone.add(arrow);\n\n const angles = [Math.PI/4, -Math.PI/4, 3*Math.PI/4, -3*Math.PI/4];\n angles.forEach((angle, i) => {\n const armG = new THREE.Group(); armG.rotation.y = angle;\n const arm = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.08, 1.0), armMat); arm.position.z = -0.5; armG.add(arm);\n const motor = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 0.2, 16), motorMat); motor.position.set(0, 0.1, -0.8); armG.add(motor);\n const prop = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.4, 0.01, 24), i < 2 ? propFrontMat : propRearMat); prop.position.set(0, 0.22, -0.8); armG.add(prop);\n props.push(prop); drone.add(armG);\n });\n scene.add(drone);\n if (window.THREE) targetQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0, 'YXZ'));\n \n function animate() {\n animationId = requestAnimationFrame(animate); const time = Date.now() * 0.001;\n if(props.length === 4) { props[0].rotation.y += 0.5; props[1].rotation.y -= 0.5; props[2].rotation.y -= 0.5; props[3].rotation.y += 0.5; }\n // Very subtle hovering effect\n drone.position.y = Math.sin(time * 2.0) * 0.03; \n // Smoothly interpolate rotation to target to fix low FPS robot movement\n if (targetQuaternion) {\n drone.quaternion.slerp(targetQuaternion, 0.025); // Very smooth interpolation across the 1Hz telemetry gap\n }\n renderer.render(scene, camera);\n }\n animate();\n }\n \n function loadThreeJS(cb) {\n if (window.THREE) return cb();\n const s = document.createElement('script'); s.src = \"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\";\n s.onload = cb; document.head.appendChild(s);\n }\n \n function getBarColor(val) { return val >= 70 ? '#2ecc71' : val >= 40 ? '#fbbf24' : '#ef4444'; }\n \n function updateSensorsAndLinks() {\n const rc = 90 + Math.random() * 10, lora = 95 + Math.random() * 5, video = 85 + Math.random() * 15;\n ['rc','lora','video'].forEach((l, i) => {\n const v = [rc, lora, video][i]; const eL = document.getElementById('link-'+l+'-fill'), eV = document.getElementById('val-'+l);\n if(eL) { eL.style.width = v + '%'; eL.style.background = getBarColor(v); }\n if(eV) eV.innerText = Math.round(v) + '%';\n });\n \n let imu1 = Math.random() * 2 - 1, imu2 = Math.random() > 0.95 ? imu1 + (Math.random() * 15 + 10) : imu1 + (Math.random() * 2 - 1);\n // trustScore now comes from real SITL GPS satellite count \u2014 only update visually\n let imuVariance = Math.max(0, 100 - (Math.abs(imu1 - imu2) * 5));\n // Don't override if we have real SITL data\n if (!scope._sitlConnected) { scope.trustScore = Math.round(imuVariance); }\n \n const m = document.getElementById('trust-meter'), a = document.getElementById('trust-alert'), b = document.getElementById('imu1-val'), t = document.getElementById('trust-score-text');\n if(m) m.style.width = trust + '%'; if(t) t.innerText = Math.round(trust) + '%';\n if (trust < 50) {\n if(m) m.style.background = '#ef4444'; if(b) { b.classList.add('warn'); b.innerText = 'ERR'; }\n } else {\n if(m) m.style.background = '#2ecc71'; if(b) { b.classList.remove('warn'); b.innerText = 'OK'; }\n }\n }\n \n setInterval(function() {\n if (scope.telemetry.armed) {\n scope.flightTimeSecs++;\n let h = Math.floor(scope.flightTimeSecs / 3600), m = Math.floor((scope.flightTimeSecs % 3600) / 60), s = scope.flightTimeSecs % 60;\n scope.flightTimeFormatted = h.toString().padStart(2,'0') + ':' + m.toString().padStart(2,'0') + ':' + s.toString().padStart(2,'0');\n scope.$applyAsync();\n }\n }, 1000);\n \n scope.init = function() {\n scope.panels = { mapTools: true, flightData: true, missionProgress: true, attitude: true, status: true, eventLog: true, bottomBar: true, rescueBtn: true };\n addLog(\"System initializing...\", \"\");\n loadThreeJS(init3D); setInterval(updateSensorsAndLinks, 1000);\n // GPS sats/HDOP now come from real ArduPilot SITL via MQTT \u2014 mock timer disabled\n // setInterval(() => { scope.gpsSats = 12 + Math.floor(Math.random() * 4); ... }, 5000);\n \n let locSent = false;\n function sendLoc(lat, lon) {\n if(locSent) return; locSent = true;\n scope.send({ payload: { type: \"init_location\", data: { lat: lat, lon: lon } } });\n }\n \n if (navigator.geolocation) {\n navigator.geolocation.getCurrentPosition(\n p => sendLoc(p.coords.latitude, p.coords.longitude),\n e => sendLoc(30.70689, 76.76887),\n {timeout: 1000}\n );\n }\n setTimeout(() => sendLoc(30.70689, 76.76887), 1500);\n \n scope.showSitlPopup = false;\n scope.panels.sitl = false; // default off in UI\n scope.stopSITL = function() {\n scope.panels.sitl = false;\n scope.send({ payload: { type: \"system\", action: \"stop_sitl\" } });\n };\n \n scope.toggleSITL = function() {\n if (scope.panels.sitl) {\n scope.showSitlPopup = true;\n scope.send({ payload: { type: \"system\", action: \"start_sitl\" } });\n setTimeout(function() {\n scope.$apply(function() { scope.showSitlPopup = false; });\n }, 4000);\n } else {\n scope.stopSITL();\n }\n };\n };\n \n scope.$watch('msg', function(msg) {\n if (msg && msg.payload) {\n if (msg.payload.type === 'telemetry') {\n let wasArmed = scope.telemetry.armed;\n scope.telemetry = msg.payload.data;\n scope.battTime = Math.max(0, Math.floor(scope.telemetry.battery * 0.22));\n \n if(!wasArmed && scope.telemetry.armed) {\n scope.flightTimeSecs = 0; scope.flightTimeFormatted = \"00:00:00\"; addLog(\"\\ud83d\\ude80 Drone Airborne.\", \"ok\");\n }\n \n let hLat = scope.telemetry.home_lat;\n let hLon = scope.telemetry.home_lon;\n if (scope.telemetry.lat && (!hLat || hLat === 0)) {\n if (!scope._fallbackHomeLat) {\n scope._fallbackHomeLat = scope.telemetry.lat;\n scope._fallbackHomeLon = scope.telemetry.lon;\n }\n hLat = scope._fallbackHomeLat;\n hLon = scope._fallbackHomeLon;\n }\n if(scope.telemetry.lat && hLat) {\n scope.distToHome = getDistance(scope.telemetry.lat, scope.telemetry.lon, hLat, hLon);\n }\n \n if (drone) {\n targetPitch = scope.telemetry.pitch * (Math.PI / 180); \n targetRoll = -scope.telemetry.roll * (Math.PI / 180);\n targetYaw = -(scope.telemetry.heading || 0) * (Math.PI / 180);\n if (window.THREE) {\n targetQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(targetPitch, targetYaw, targetRoll, 'YXZ'));\n }\n }\n }\n }\n });\n \n scope.$on('$destroy', function() { if (animationId) cancelAnimationFrame(animationId); });\n})(scope);\n</script>\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"x": 580,
"y": 180,
"wires": [
[
"gcs-handler",
"argus-cmd-out"
]
]
},
{
"id": "gcs-handler",
"type": "function",
"z": "gcs-tab",
"name": "GCS Handler",
"func": "\nif (actionMsg.type === \"system\") {\n if (actionMsg.action === \"start_sitl\") {\n return [null, null, { payload: \"start\" }];\n } else if (actionMsg.action === \"stop_sitl\") {\n return [null, null, { payload: \"stop\" }];\n }\n}\nvar actionMsg = msg.payload;\nif (!msg.payload || typeof msg.payload !== \"object\") return null;\n\nif (actionMsg.type === \"init_location\") {\n flow.set(\"home_lat\", actionMsg.data.lat);\n flow.set(\"home_lon\", actionMsg.data.lon);\n flow.set(\"lat\", actionMsg.data.lat);\n flow.set(\"lon\", actionMsg.data.lon);\n flow.set(\"mode\", \"standby\");\n flow.set(\"alt\", 0);\n flow.set(\"speed\", 0);\n flow.set(\"battery\", 100);\n flow.set(\"armed\", false);\n flow.set(\"heading\", 0);\n \n var gcsCmd = {\n payload: {\n command: {\n lat: actionMsg.data.lat, \n lon: actionMsg.data.lon, \n zoom: 17,\n map: {\n name: \"EsriS\",\n url: \"https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}\",\n opt: { maxZoom: 20, attribution: \"Google\" }\n },\n layer: \"EsriS\"\n }\n }\n };\n \n var wifiRing = { payload: { name: \"WiFi_Zone\", lat: actionMsg.data.lat, lon: actionMsg.data.lon, radius: 150, color: \"#00f2fe\", weight: 2, dashArray: \"5, 5\", layer: \"Range\", fillOpacity: 0, tooltip: \"WiFi Zone\" } };\n var loraRing = { payload: { name: \"LoRa_Zone\", lat: actionMsg.data.lat, lon: actionMsg.data.lon, radius: 1000, color: \"#eab308\", weight: 2, dashArray: \"10, 10\", layer: \"Range\", fillOpacity: 0, tooltip: \"LoRa Zone\" } };\n var homePin = { payload: { name: \"Home_Location\", lat: actionMsg.data.lat, lon: actionMsg.data.lon, icon: \"fa-home\", iconColor: \"#ef4444\", layer: \"Home\", tooltip: \"Home Base\" } };\n \n var plannerCmd = {\n payload: {\n command: {\n lat: actionMsg.data.lat, \n lon: actionMsg.data.lon, \n zoom: 15,\n map: {\n name: \"EsriS\",\n url: \"https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}\",\n opt: { maxZoom: 20, attribution: \"Google\" }\n },\n layer: \"EsriS\",\n showlayer: \"drawing\"\n }\n }\n };\n \n return [[gcsCmd, wifiRing, loraRing, homePin], plannerCmd];\n}\n\nif (actionMsg.type === \"clear_mission\") {\n var clearCmd = { payload: { command: { clear: \"Flight Path\" } } };\n return [[clearCmd], null];\n}\n\nif (actionMsg.type === \"clear_detections\") {\n var clearCmd = { payload: { command: { clear: \"Rescue Pins\" } } };\n return [[clearCmd], null];\n}\n\nif (actionMsg.type === \"recenter\") {\n return [[{ payload: { command: { lat: actionMsg.data.lat, lon: actionMsg.data.lon, zoom: 17 } } }], null];\n}\n\nif (actionMsg.type === \"add_pin\") {\n var pin = {\n payload: {\n name: \"Rescue_\" + actionMsg.data.num,\n lat: actionMsg.data.lat,\n lon: actionMsg.data.lon,\n icon: \"\",\n iconColor: \"#dc2626\",\n layer: \"Rescue Pins\",\n label: \"Pin \" + actionMsg.data.num,\n popup: `<b>\ud83d\udccd PERSON DETECTED</b><br>Time: ${new Date().toLocaleTimeString()}<br>Confidence: ${85 + Math.floor(Math.random()*10)}%<br>GPS: ${actionMsg.data.lat.toFixed(5)}\u00b0N ${actionMsg.data.lon.toFixed(5)}\u00b0E<br><button onclick=\"alert('Coords exported!')\">Export Coords</button>`\n }\n };\n return [[pin], null];\n}\n\nif (actionMsg.type === \"thermal_overlay\") {\n if (actionMsg.state) {\n var thermal = { payload: { name: \"Thermal_Overlay\", lat: actionMsg.lat, lon: actionMsg.lon, radius: 40, color: \"transparent\", fillColor: \"#ef4444\", fillOpacity: 0.5, layer: \"Thermal\", tooltip: \"Thermal FOV\" } };\n return [[thermal], null];\n } else {\n return [[{ payload: { deleted: true, name: \"Thermal_Overlay\" } }], null];\n }\n}\n\nif (actionMsg.action === \"arm\") {\n flow.set(\"armed\", actionMsg.state);\n if (!actionMsg.state) { flow.set(\"mode\", \"standby\"); flow.set(\"alt\", 0); flow.set(\"speed\", 0); }\n} else if (actionMsg.action === \"takeoff\") {\n if (flow.get(\"armed\")) flow.set(\"mode\", \"takeoff\");\n} else if (actionMsg.action === \"land\") {\n flow.set(\"mode\", \"land\");\n} else if (actionMsg.action === \"rtl\") {\n flow.set(\"mode\", \"rtl\");\n}\nreturn null;",
"outputs": 3,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 790,
"y": 240,
"wires": [
[
"gcs-map-node"
],
[
"planner-map-node"
],
[
"sitl-exec-node-d2d6bd4f"
]
]
},
{
"id": "gcs-map-node",
"type": "worldmap",
"z": "gcs-tab",
"name": "GCS Map",
"lat": "20.5937",
"lon": "78.9629",
"zoom": "4",
"layer": "EsriS",
"cluster": "",
"maxage": "",
"usermenu": "show",
"layers": "show",
"panit": "true",
"panlock": "false",
"zoomlock": "false",
"hiderightclick": "false",
"coords": "none",
"showgrid": "false",
"path": "gcs-map",
"overlist": "DR,CO,RA,DN,HM",
"maplist": "OSMG,OSMC,EsriC,EsriS,EsriT,EsriO,EsriDG,NatGeo,UKOS,OpTop",
"mapname": "",
"mapurl": "",
"mapopt": "",
"mapwms": false,
"x": 1060,
"y": 160,
"wires": []
},
{
"id": "planner-map-node",
"type": "worldmap",
"z": "gcs-tab",
"name": "Planner Map",
"lat": "20.5937",
"lon": "78.9629",
"zoom": "5",
"layer": "EsriS",
"cluster": "",
"maxage": "",
"usermenu": "show",
"layers": "show",
"panit": "true",
"panlock": "false",
"zoomlock": "false",
"hiderightclick": "false",
"coords": "none",
"showgrid": "false",
"path": "planner-map",
"overlist": "DR,CO,RA,DN,HM",
"maplist": "OSMG,OSMC,EsriC,EsriS,EsriT,EsriO,EsriDG,NatGeo,UKOS,OpTop",
"mapname": "",
"mapurl": "",
"mapopt": "",
"mapwms": false,
"x": 1070,
"y": 240,
"wires": []
},
{
"id": "sim-trigger",
"type": "inject",
"z": "gcs-tab",
"name": "1s Tick",
"props": [
{
"p": "payload"
}
],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 260,
"y": 300,
"wires": [
[
"sim-logic"
]
]
},
{
"id": "sim-logic",
"type": "function",
"z": "gcs-tab",
"name": "Telemetry Simulator",
"func": "var home_lat = flow.get(\"home_lat\");\nvar home_lon = flow.get(\"home_lon\");\n\nif (home_lat === undefined || home_lon === undefined) {\n return null;\n}\n\nvar armed = flow.get(\"armed\") || false;\nvar mode = flow.get(\"mode\") || \"standby\";\nvar alt = flow.get(\"alt\") || 0;\nvar speed = flow.get(\"speed\") || 0;\nvar battery = flow.get(\"battery\") === undefined ? 100 : flow.get(\"battery\");\nvar lat = flow.get(\"lat\") === undefined ? home_lat : flow.get(\"lat\");\nvar lon = flow.get(\"lon\") === undefined ? home_lon : flow.get(\"lon\");\nvar heading = flow.get(\"heading\") || 0;\n\nif (armed) {\n battery = Math.max(0, battery - 0.03);\n \n if (mode === \"takeoff\") {\n alt = Math.min(30, alt + 1.5);\n speed = Math.min(6, speed + 0.5);\n heading = 45;\n if (alt >= 30) {\n mode = \"hover\";\n }\n } else if (mode === \"hover\") {\n alt += (Math.random() - 0.5) * 0.2;\n alt = Math.max(28, Math.min(32, alt));\n speed = Math.max(0.5, speed + (Math.random() - 0.5) * 0.3);\n lat += 0.00002 * Math.cos(heading * Math.PI / 180);\n lon += 0.00002 * Math.sin(heading * Math.PI / 180);\n heading = (heading + (Math.random() - 0.5) * 8) % 360;\n } else if (mode === \"land\") {\n alt = Math.max(0, alt - 1.2);\n speed = Math.max(0, speed - 0.4);\n if (alt <= 0) {\n mode = \"standby\";\n armed = false;\n speed = 0;\n }\n } else if (mode === \"rtl\") {\n var dLat = home_lat - lat;\n var dLon = home_lon - lon;\n var dist = Math.sqrt(dLat * dLat + dLon * dLon);\n if (dist < 0.00005) {\n mode = \"land\";\n } else {\n lat += (dLat / dist) * 0.00008;\n lon += (dLon / dist) * 0.00008;\n heading = Math.atan2(dLon, dLat) * 180 / Math.PI;\n if (heading < 0) heading += 360;\n speed = 8;\n alt += (30 - alt) * 0.1;\n }\n }\n} else {\n mode = \"standby\";\n alt = 0;\n speed = 0;\n lat = home_lat;\n lon = home_lon;\n heading = 0;\n if (battery < 20) battery = 100;\n}\n\nflow.set(\"armed\", armed);\nflow.set(\"mode\", mode);\nflow.set(\"alt\", alt);\nflow.set(\"speed\", speed);\nflow.set(\"battery\", battery);\nflow.set(\"lat\", lat);\nflow.set(\"lon\", lon);\nflow.set(\"heading\", heading);\n\nvar telemetry = {\n payload: {\n type: \"telemetry\",\n data: {\n armed: armed,\n mode: mode,\n alt: alt,\n speed: speed,\n battery: Math.floor(battery),\n pitch: armed ? (mode === \"takeoff\" ? 12 : (mode === \"land\" ? -8 : (Math.random() - 0.5) * 3)) : 0,\n roll: armed ? (Math.random() - 0.5) * 5 : 0,\n heading: Math.floor(heading),\n lat: lat,\n lon: lon,\n home_lat: home_lat,\n home_lon: home_lon\n }\n }\n};\n\n// Use an arrow icon facing the heading direction\n\nvar isError = (battery < 20); // Basic error condition\n\nvar markerColor = \"#4facfe\"; // light blue when disarmed\nif (isError) {\n markerColor = \"#e74c3c\"; // red when error\n} else if (armed) {\n markerColor = \"#2ecc71\"; // green when armed\n}\n\nvar marker = {\n payload: {\n name: \"Drone\",\n lat: lat,\n lon: lon,\n icon: \"quad\",\n iconColor: markerColor,\n bearing: heading,\n iconSize: 48,\n layer: \"Drone Location\"\n }\n};\n\nreturn [telemetry, marker];\n",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 460,
"y": 300,
"wires": [
[],
[]
]
},
{
"id": "planner-map-in",
"type": "worldmap in",
"z": "gcs-tab",
"name": "Planner Map In",
"path": "planner-map",
"events": "connect,disconnect,point,layer,bounds,files,draw,other",
"x": 540,
"y": 380,
"wires": [
[
"planner-in-handler"
]
]
},
{
"id": "planner-in-handler",
"type": "function",
"z": "gcs-tab",
"name": "Planner Input Handler",
"func": "var event = msg.payload;\n\n// 1. Handle client connection\nif (event.action === \"connected\") {\n var home_lat = flow.get(\"home_lat\");\n var home_lon = flow.get(\"home_lon\");\n if (home_lat !== undefined && home_lon !== undefined) {\n msg.payload = {\n command: {\n lat: home_lat,\n lon: home_lon,\n zoom: 15,\n layer: \"OSM\",\n showlayer: \"drawing\"\n }\n };\n return [msg, null];\n }\n}\n\n// 2. Handle drawing actions\nif (event.action === \"draw\" || event.action === \"drawdelete\" || event.action === \"drawupdate\") {\n if (event.layer === \"_drawing\") {\n event.layer = \"Flight Path\";\n }\n\n // Extract waypoint coordinates\n var pts = [];\n if (event.line) pts = event.line;\n else if (event.area) pts = event.area;\n if (pts.length > 0 && Array.isArray(pts[0])) pts = pts[0];\n\n var outMsgs = [];\n\n // Clear old waypoint pins on both maps first\n outMsgs.push({ payload: { command: { clear: \"Waypoints\" } } });\n\n // Create a geo-tag pin for each waypoint\n // fa-map-marker icon is 32x52px, tip is at center-bottom \u2192 anchor [16, 52]\n pts.forEach(function(wp, i) {\n var lat = wp.lat !== undefined ? wp.lat : wp[0];\n var lon = wp.lng !== undefined ? wp.lng : (wp.lon !== undefined ? wp.lon : wp[1]);\n var num = i + 1;\n outMsgs.push({\n payload: {\n name: \"WP_\" + i,\n lat: lat,\n lon: lon,\n icon: \"fa-map-marker\",\n iconColor: \"#ef4444\",\n iconSize: [64, 104],\n iconAnchor: [32, 104],\n layer: \"Waypoints\",\n label: \"\" + num,\n tooltip: \"WP \" + num + \" (\" + lat.toFixed(5) + \", \" + lon.toFixed(5) + \")\"\n }\n });\n });\n\n var forwardMsg = { payload: event };\n\n // Output 1: planner-map (pins + draw line)\n // Output 2: gcs-map + ui-gcs-template (pins + draw line)\n return [outMsgs, [forwardMsg].concat(outMsgs)];\n}\n\nreturn null;\n",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 760,
"y": 380,
"wires": [
[
"planner-map-node"
],
[
"gcs-map-node",
"ui-gcs-template"
]
]
},
{
"id": "argus-mqtt-in",
"type": "mqtt in",
"z": "gcs-tab",
"name": "ArduPilot SITL Telemetry",
"topic": "argus/telemetry/all",
"qos": "0",
"datatype": "json",
"broker": "argus-mqtt-broker",
"nl": false,
"rap": false,
"rh": 0,
"inputs": 0,
"x": 210,
"y": 100,
"wires": [
[
"argus-telem-mapper"
]
]
},
{
"id": "argus-telem-mapper",
"type": "function",
"z": "gcs-tab",
"name": "MAVLink \u2192 GCS Telemetry",
"func": "\nconst d = msg.payload;\nif (!d || typeof d !== 'object') return null;\n\n// The template watches msg.payload.type === 'telemetry', msg.payload.data = {...}\nmsg.payload = {\n type: \"telemetry\",\n data: {\n // Position\n lat: d.lat || 0,\n lon: d.lon || 0,\n alt: d.rel_alt !== undefined ? parseFloat(d.rel_alt.toFixed(1)) : parseFloat((d.alt || 0).toFixed(1)),\n home_lat: d.home_lat || 0,\n home_lon: d.home_lon || 0,\n\n // Motion\n speed: parseFloat((d.groundspeed || 0).toFixed(1)),\n airspeed: parseFloat((d.airspeed || 0).toFixed(1)),\n groundspeed: parseFloat((d.groundspeed || 0).toFixed(1)),\n climb: parseFloat((d.climb || 0).toFixed(2)),\n throttle: d.throttle || 0,\n\n // Attitude\n roll: parseFloat((d.roll || 0).toFixed(1)),\n pitch: parseFloat((d.pitch || 0).toFixed(1)),\n yaw: parseFloat((d.yaw || 0).toFixed(1)),\n heading: d.heading !== undefined ? Math.round(d.heading) : 0,\n\n // Power\n battery: d.battery !== undefined ? d.battery : 100,\n voltage: parseFloat((d.voltage || 0).toFixed(2)),\n current: parseFloat((d.current || 0).toFixed(1)),\n\n // GPS\n satellites: d.satellites || 0,\n fix_type: d.fix_type || 0,\n hdop: d.hdop || 0,\n\n // Status\n armed: d.armed || false,\n mode: d.mode || 'UNKNOWN',\n }\n};\n\n// Also push GPS sats and trust score as top-level scope vars\nmsg.gpsSats = d.satellites || 0;\nmsg.trustScore = Math.min(100, Math.round(((d.satellites || 0) / 14) * 100));\nmsg.imuTrust = 95;\n\nreturn msg;\n",
"outputs": 1,
"x": 480,
"y": 100,
"wires": [
[
"ui-gcs-template",
"argus-gps-updater",
"argus-scope-updater"
]
]
},
{
"id": "argus-scope-updater",
"type": "function",
"z": "gcs-tab",
"name": "Update GPS/Trust Scope",
"func": "\nconst d = msg.payload && msg.payload.data;\nif (!d) return null;\n\n// Send a second message to update gpsSats and trustScore\nmsg.payload = {\n type: \"gps_status\",\n gpsSats: d.satellites || 0,\n trustScore: Math.min(100, Math.round(((d.satellites || 0) / 14) * 100)),\n imuTrust: 95,\n hdop: d.hdop || 0,\n fix_type: d.fix_type || 0,\n};\nreturn msg;\n",
"outputs": 1,
"x": 330,
"y": 180,
"wires": [
[
"ui-gcs-template"
]
]
},
{
"id": "argus-gps-updater",
"type": "function",
"z": "gcs-tab",
"name": "Move Map Marker",
"func": "\nconst d = msg.payload && msg.payload.data;\nif (!d || !d.lat || !d.lon) return null;\n\n// 1. To Template (Updates angular scope variables)\nvar templateMsg = {\n payload: {\n type: \"gps\",\n data: {\n lat: d.lat,\n lon: d.lon,\n heading: d.heading || 0,\n alt: d.alt || 0,\n }\n }\n};\n\n// 2. To Worldmap (Updates drone icon on map)\nvar isError = false; // Add battery check if needed\nvar armed = d.armed || false;\nvar markerColor = armed ? \"#dc2626\" : \"#16a34a\"; // Red if armed, Dark green if disarmed\n\nvar mapMsg = {\n payload: {\n name: \"Drone\",\n lat: d.lat,\n lon: d.lon,\n icon: \"quad\",\n iconColor: markerColor,\n bearing: d.heading || 0,\n iconSize: 48,\n layer: \"Drone Location\",\n tooltip: \"ArduCopter SITL\"\n }\n};\n\nreturn [templateMsg, mapMsg];\n",
"outputs": 2,
"x": 810,
"y": 120,
"wires": [
[
"ui-gcs-template"
],
[
"gcs-map-node",
"planner-map-node"
]
]
},
{
"id": "argus-cmd-out",
"type": "mqtt out",
"z": "gcs-tab",
"name": "Send Command",
"topic": "argus/command/drone",
"qos": "0",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "argus-mqtt-broker",
"x": 800,
"y": 180,
"wires": []
},
{
"id": "sitl-exec-node-d2d6bd4f",
"type": "exec",
"z": "gcs-tab",
"command": "bash -c 'if [ \"$1\" = \"start\" ]; then nohup bash \"/home/ritesh/Argus GCS/start_sitl.sh\" > /tmp/sitl_boot.log 2>&1 & else pkill -9 -f \"start_sitl.sh\" || true; pkill -9 -f arducopter || true; pkill -9 -f mavproxy.py || true; pkill -9 -f mavlink_bridge.py || true; fi' -- ",
"addpay": "payload",
"append": "",
"useSpawn": "false",
"timer": "",
"winHide": false,
"oldrc": false,
"name": "Manage SITL",
"x": 1000,
"y": 300,
"wires": [
[],
[],
[]
]
}
]