@cef-ai/cli 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1273 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +66 -0
- package/dist/commands/deploy/clone.d.ts +13 -0
- package/dist/commands/deploy/clone.js +143 -0
- package/dist/commands/deploy/delete.d.ts +14 -0
- package/dist/commands/deploy/delete.js +134 -0
- package/dist/commands/deploy/deploy.d.ts +24 -0
- package/dist/commands/deploy/deploy.js +654 -0
- package/dist/commands/deploy/index.d.ts +2 -0
- package/dist/commands/deploy/index.js +216 -0
- package/dist/commands/deploy/status.d.ts +14 -0
- package/dist/commands/deploy/status.js +61 -0
- package/dist/commands/dev/index.d.ts +2 -0
- package/dist/commands/dev/index.js +134 -0
- package/dist/commands/dev/server.d.ts +14 -0
- package/dist/commands/dev/server.js +167 -0
- package/dist/commands/dev/ui.d.ts +1 -0
- package/dist/commands/dev/ui.js +984 -0
- package/dist/commands/playground/index.d.ts +2 -0
- package/dist/commands/playground/index.js +75 -0
- package/dist/commands/playground/test.d.ts +15 -0
- package/dist/commands/playground/test.js +166 -0
- package/dist/commands/service/index.d.ts +2 -0
- package/dist/commands/service/index.js +75 -0
- package/dist/lib/api.d.ts +146 -0
- package/dist/lib/api.js +330 -0
- package/dist/lib/config.d.ts +12 -0
- package/dist/lib/config.js +120 -0
- package/dist/lib/runtime/agent-router.d.ts +12 -0
- package/dist/lib/runtime/agent-router.js +78 -0
- package/dist/lib/runtime/context-builder.d.ts +27 -0
- package/dist/lib/runtime/context-builder.js +166 -0
- package/dist/lib/runtime/cubby-manager.d.ts +20 -0
- package/dist/lib/runtime/cubby-manager.js +161 -0
- package/dist/lib/runtime/index.d.ts +51 -0
- package/dist/lib/runtime/index.js +183 -0
- package/dist/lib/runtime/log-collector.d.ts +17 -0
- package/dist/lib/runtime/log-collector.js +61 -0
- package/dist/lib/runtime/stream-manager.d.ts +10 -0
- package/dist/lib/runtime/stream-manager.js +77 -0
- package/dist/lib/runtime/types.d.ts +76 -0
- package/dist/lib/runtime/types.js +2 -0
- package/dist/lib/typecheck.d.ts +31 -0
- package/dist/lib/typecheck.js +175 -0
- package/dist/lib/types.d.ts +246 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/utils.js +33 -0
- package/example/.env.example +6 -0
- package/example/agents/object-detection/tasks/yolo.ts +28 -0
- package/example/agents/parking-violation-detector/tasks/detect.ts +326 -0
- package/example/cef.config.yaml +91 -0
- package/example/engagements/engagement.ts +902 -0
- package/package.json +69 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
// RAFT: Multi-Stream Synchronization for Drone Data
|
|
2
|
+
// Handles: DRONE_TELEMETRY_DATA, VIDEO_STREAM_DATA, THERMAL_STREAM_DATA, VIDEO_KLV_DATA
|
|
3
|
+
|
|
4
|
+
const SYNC_WINDOW_MS = 3000;
|
|
5
|
+
const BUFFER_TTL_MS = 300000;
|
|
6
|
+
const VIOLATION_WINDOW_MS = 30000; //120000; // 2 minutes – aggregation flush window
|
|
7
|
+
const VIOLATION_GRID_SIZE = 50; // px – bbox quantization
|
|
8
|
+
const MAX_FRAME_REFS = 5;
|
|
9
|
+
const VIOLATION_STALE_MS = 60000; // 60s – max gap to consider "same sighting"
|
|
10
|
+
const DRONE_POS_PRECISION = 4; // decimal places ≈ ~11m grid
|
|
11
|
+
const MIN_DETECTION_CONFIDENCE = 0.80;
|
|
12
|
+
const VEHICLE_LABELS = new Set(['car', 'truck', 'van', 'bus', 'vehicle']);
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────────────────────────────────────
|
|
15
|
+
// Mission Registry
|
|
16
|
+
// ─────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
async function registerMission(missionId, droneName, timestamp, cubby, context) {
|
|
19
|
+
const t0 = Date.now();
|
|
20
|
+
const listKey = 'list_missions';
|
|
21
|
+
let missions = [];
|
|
22
|
+
try {
|
|
23
|
+
missions = await cubby.json.get(listKey);
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
if (!Array.isArray(missions)) missions = [];
|
|
26
|
+
|
|
27
|
+
const idx = missions.findIndex(m => m.missionId === missionId);
|
|
28
|
+
|
|
29
|
+
if (idx === -1) {
|
|
30
|
+
missions.push({
|
|
31
|
+
missionId,
|
|
32
|
+
droneName,
|
|
33
|
+
firstSeen: timestamp,
|
|
34
|
+
lastSeen: timestamp,
|
|
35
|
+
});
|
|
36
|
+
context.log(`📝 Registered new mission: ${missionId}`);
|
|
37
|
+
} else {
|
|
38
|
+
missions[idx].lastSeen = timestamp;
|
|
39
|
+
if (droneName) missions[idx].droneName = droneName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await cubby.json.set(listKey, missions);
|
|
43
|
+
context.log(`⏱ registerMission ${Date.now() - t0}ms`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// ─────────────────────────────────────────────────────────────
|
|
48
|
+
// Timestamp Index Helpers
|
|
49
|
+
// ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
async function appendTimestamp(missionId, streamType, timestamp, cubby) {
|
|
52
|
+
const indexKey = `mission/${missionId}/${streamType}/_index`;
|
|
53
|
+
let timestamps = []
|
|
54
|
+
try{
|
|
55
|
+
timestamps = await cubby.json.get(indexKey);
|
|
56
|
+
}
|
|
57
|
+
catch(error){
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
const cutoff = Date.now() - BUFFER_TTL_MS;
|
|
61
|
+
if (timestamps.length > 0 && timestamps[0] < cutoff) {
|
|
62
|
+
timestamps = timestamps.filter(ts => ts > cutoff);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
timestamps.push(timestamp);
|
|
66
|
+
await cubby.json.set(indexKey, timestamps);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function getTimestamps(missionId, streamType, cubby, context?) {
|
|
70
|
+
let data = []
|
|
71
|
+
try{
|
|
72
|
+
data = await cubby.json.get(`mission/${missionId}/${streamType}/_index`)
|
|
73
|
+
}
|
|
74
|
+
catch(error){
|
|
75
|
+
if (context) context.log(`⚠️ getTimestamps(${streamType}) failed: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(data)) data = [];
|
|
78
|
+
return data
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────
|
|
82
|
+
// Storage Functions
|
|
83
|
+
// ─────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
async function storeTelemetry(missionId, timestamp, data, context, cubby) {
|
|
86
|
+
const t0 = Date.now();
|
|
87
|
+
const key = `mission/${missionId}/telemetry/${timestamp}`;
|
|
88
|
+
const value = { ...data, storedAt: Date.now() };
|
|
89
|
+
await Promise.all([
|
|
90
|
+
cubby.json.set(key, value),
|
|
91
|
+
appendTimestamp(missionId, 'telemetry', timestamp, cubby),
|
|
92
|
+
]);
|
|
93
|
+
context.log(`✅ Stored telemetry for mission ${missionId} at ${timestamp} (${Date.now() - t0}ms)`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function storeRgbFrame(missionId, timestamp, data, context, cubby) {
|
|
97
|
+
const t0 = Date.now();
|
|
98
|
+
const key = `mission/${missionId}/rgb/${timestamp}`;
|
|
99
|
+
|
|
100
|
+
let detectedObjects = null;
|
|
101
|
+
const tYolo = Date.now();
|
|
102
|
+
try {
|
|
103
|
+
const result = await context.agents.objectDetection.yolo({
|
|
104
|
+
image: data.image,
|
|
105
|
+
objectsToFind: ["a car", "a vehicle", "a truck"],
|
|
106
|
+
minConfidence: 0.03,
|
|
107
|
+
});
|
|
108
|
+
if(result?.detections){
|
|
109
|
+
detectedObjects = result?.detections.filter(d =>
|
|
110
|
+
d.confidence >= MIN_DETECTION_CONFIDENCE && VEHICLE_LABELS.has(d.label.toLowerCase())
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
context.log(`ObjectDetectionFailedWithError: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
context.log(`⏱ YOLO detection ${Date.now() - tYolo}ms | detections: ${detectedObjects?.length ?? 0}`);
|
|
117
|
+
|
|
118
|
+
const value = {
|
|
119
|
+
detectedObjects: detectedObjects,
|
|
120
|
+
index: data.index,
|
|
121
|
+
frameId: data.frameId,
|
|
122
|
+
framePath: data.framePath,
|
|
123
|
+
frameWidth: data.frameWidth,
|
|
124
|
+
timestamp: timestamp,
|
|
125
|
+
droneName: data.droneName,
|
|
126
|
+
storedAt: Date.now(),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
await Promise.all([
|
|
130
|
+
cubby.json.set(key, value),
|
|
131
|
+
appendTimestamp(missionId, 'rgb', timestamp, cubby),
|
|
132
|
+
]);
|
|
133
|
+
context.log(`✅ Stored RGB frame #${data.index} for mission ${missionId} (${Date.now() - t0}ms)`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function storeThermalFrame(missionId, timestamp, data, context, cubby) {
|
|
137
|
+
const t0 = Date.now();
|
|
138
|
+
const key = `mission/${missionId}/thermal/${timestamp}`;
|
|
139
|
+
const value = {
|
|
140
|
+
frameId: data.frameId,
|
|
141
|
+
framePath: data.framePath,
|
|
142
|
+
frameWidth: data.frameWidth,
|
|
143
|
+
index: data.index,
|
|
144
|
+
timestamp: timestamp,
|
|
145
|
+
droneName: data.droneName,
|
|
146
|
+
storedAt: Date.now(),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
await Promise.all([
|
|
150
|
+
cubby.json.set(key, value),
|
|
151
|
+
appendTimestamp(missionId, 'thermal', timestamp, cubby),
|
|
152
|
+
]);
|
|
153
|
+
context.log(`✅ Stored thermal frame #${data.index} for mission ${missionId} (${Date.now() - t0}ms)`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function storeKlvData(missionId, timestamp, data, context, cubby) {
|
|
157
|
+
const t0 = Date.now();
|
|
158
|
+
const key = `mission/${missionId}/klv/${timestamp}`;
|
|
159
|
+
const value = {
|
|
160
|
+
klv: data.klv,
|
|
161
|
+
index: data.index,
|
|
162
|
+
timestamp: timestamp,
|
|
163
|
+
droneName: data.droneName,
|
|
164
|
+
storedAt: Date.now(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
await Promise.all([
|
|
168
|
+
cubby.json.set(key, value),
|
|
169
|
+
appendTimestamp(missionId, 'klv', timestamp, cubby),
|
|
170
|
+
]);
|
|
171
|
+
context.log(`✅ Stored KLV data #${data.index} for mission ${missionId} (${Date.now() - t0}ms)`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─────────────────────────────────────────────────────────────
|
|
175
|
+
// Query Functions
|
|
176
|
+
// ─────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
async function getLatestSynced(missionId, context, cubby) {
|
|
179
|
+
const key = `mission/${missionId}/synced/latest`;
|
|
180
|
+
const data = await cubby.json.get(key);
|
|
181
|
+
return { success: true, data: data || null };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function getAllSynced(missionId, context, cubby) {
|
|
185
|
+
const pattern = `mission/${missionId}/synced/*`;
|
|
186
|
+
const keys = await cubby.json.keys(pattern);
|
|
187
|
+
|
|
188
|
+
if (!keys || keys.length === 0) {
|
|
189
|
+
return { success: true, data: [] };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const dataKeys = keys.filter(k => !k.endsWith('/latest'));
|
|
193
|
+
if (dataKeys.length === 0) {
|
|
194
|
+
return { success: true, data: [] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const allData = await cubby.json.mget(dataKeys);
|
|
198
|
+
const dataArray = Object.values(allData)
|
|
199
|
+
.filter(item => item !== null)
|
|
200
|
+
//@ts-ignore
|
|
201
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
202
|
+
|
|
203
|
+
return { success: true, data: dataArray };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function getSyncedRange(missionId, startTime, endTime, context, cubby) {
|
|
207
|
+
const pattern = `mission/${missionId}/synced/*`;
|
|
208
|
+
const keys = await cubby.json.keys(pattern);
|
|
209
|
+
|
|
210
|
+
if (!keys || keys.length === 0) {
|
|
211
|
+
return { success: true, data: [] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const filteredKeys = keys.filter(k => {
|
|
215
|
+
if (k.endsWith('/latest')) return false;
|
|
216
|
+
const ts = parseInt(k.split('/').pop(), 10);
|
|
217
|
+
return ts >= startTime && ts <= endTime;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (filteredKeys.length === 0) {
|
|
221
|
+
return { success: true, data: [] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const allData = await cubby.json.mget(filteredKeys);
|
|
225
|
+
const dataArray = Object.values(allData)
|
|
226
|
+
.filter(item => item !== null)
|
|
227
|
+
//@ts-ignore
|
|
228
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
229
|
+
|
|
230
|
+
return { success: true, data: dataArray };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─────────────────────────────────────────────────────────────
|
|
234
|
+
// Violation Helpers
|
|
235
|
+
// ─────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function computeViolationGridKey(bbox) {
|
|
238
|
+
const cx = bbox.x + bbox.w / 2;
|
|
239
|
+
const cy = bbox.y + bbox.h / 2;
|
|
240
|
+
const gridX = Math.floor(cx / VIOLATION_GRID_SIZE);
|
|
241
|
+
const gridY = Math.floor(cy / VIOLATION_GRID_SIZE);
|
|
242
|
+
return `${gridX}_${gridY}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function quantizeDronePosition(lat, lng) {
|
|
246
|
+
const factor = Math.pow(10, DRONE_POS_PRECISION);
|
|
247
|
+
return `${Math.floor(lat * factor)}_${Math.floor(lng * factor)}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildDedupKey(classLabel, bbox, dronePosition) {
|
|
251
|
+
const bboxKey = computeViolationGridKey(bbox);
|
|
252
|
+
const posKey = dronePosition
|
|
253
|
+
? quantizeDronePosition(dronePosition.lat, dronePosition.lng)
|
|
254
|
+
: "nopos";
|
|
255
|
+
return `${posKey}/${classLabel}/${bboxKey}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─────────────────────────────────────────────────────────────
|
|
259
|
+
// Violation Buffer + Windowed Flush
|
|
260
|
+
// ─────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async function bufferViolations(missionId, processedRGB, dronePosition, cubby, context) {
|
|
263
|
+
const t0 = Date.now();
|
|
264
|
+
const newEntries = [];
|
|
265
|
+
|
|
266
|
+
for (const rgb of processedRGB) {
|
|
267
|
+
const violations = rgb.violationDetection?.value;
|
|
268
|
+
if (!violations || !violations.length) continue;
|
|
269
|
+
|
|
270
|
+
for (const v of violations) {
|
|
271
|
+
if (v.violationType !== "outside_slot") continue;
|
|
272
|
+
|
|
273
|
+
const classLabel = v.detection?.classLabel;
|
|
274
|
+
const bbox = v.detection?.bbox;
|
|
275
|
+
if (!classLabel || !bbox) continue;
|
|
276
|
+
|
|
277
|
+
newEntries.push({
|
|
278
|
+
classLabel,
|
|
279
|
+
bbox,
|
|
280
|
+
confidence: v.confidence,
|
|
281
|
+
trackingId: v.detection.trackingId,
|
|
282
|
+
timestamp: v.timestamp,
|
|
283
|
+
framePath: rgb.framePath || null,
|
|
284
|
+
frameId: rgb.frameId || null,
|
|
285
|
+
dronePosition: dronePosition
|
|
286
|
+
? { lat: dronePosition.lat, lng: dronePosition.lng }
|
|
287
|
+
: null,
|
|
288
|
+
violationLocation:
|
|
289
|
+
v.location?.lat != null && v.location?.lng != null
|
|
290
|
+
? { lat: v.location.lat, lng: v.location.lng }
|
|
291
|
+
: null,
|
|
292
|
+
// Store the original YOLO detection for traceability
|
|
293
|
+
sourceDetection: rgb.detectedObjects?.find((d) => {
|
|
294
|
+
const bw = d.box.xmax - d.box.xmin;
|
|
295
|
+
const bh = d.box.ymax - d.box.ymin;
|
|
296
|
+
return (
|
|
297
|
+
Math.abs(d.box.xmin - bbox.x) < 1 &&
|
|
298
|
+
Math.abs(d.box.ymin - bbox.y) < 1 &&
|
|
299
|
+
Math.abs(bw - bbox.w) < 1 &&
|
|
300
|
+
Math.abs(bh - bbox.h) < 1
|
|
301
|
+
);
|
|
302
|
+
}) || null,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (newEntries.length === 0) return;
|
|
308
|
+
|
|
309
|
+
const bufKey = `mission/${missionId}/violation_buf`;
|
|
310
|
+
let buf = null;
|
|
311
|
+
try {
|
|
312
|
+
buf = await cubby.json.get(bufKey);
|
|
313
|
+
} catch (error) {}
|
|
314
|
+
|
|
315
|
+
if (!buf || !Array.isArray(buf.entries)) {
|
|
316
|
+
buf = { startedAt: Date.now(), entries: [] };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
buf.entries = buf.entries.concat(newEntries);
|
|
320
|
+
await cubby.json.set(bufKey, buf);
|
|
321
|
+
|
|
322
|
+
context.log(
|
|
323
|
+
`📥 Buffered ${newEntries.length} violations (total in window: ${buf.entries.length}) (${Date.now() - t0}ms)`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function checkAndFlushViolations(missionId, cubby, context) {
|
|
328
|
+
const t0 = Date.now();
|
|
329
|
+
const bufKey = `mission/${missionId}/violation_buf`;
|
|
330
|
+
let buf = null;
|
|
331
|
+
try {
|
|
332
|
+
buf = await cubby.json.get(bufKey);
|
|
333
|
+
} catch (error) {}
|
|
334
|
+
|
|
335
|
+
if (!buf || !Array.isArray(buf.entries) || buf.entries.length === 0) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const elapsed = Date.now() - buf.startedAt;
|
|
340
|
+
if (elapsed < VIOLATION_WINDOW_MS) {
|
|
341
|
+
return null; // Window not expired yet
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
context.log(
|
|
345
|
+
`⏱️ Violation window expired (${Math.round(elapsed / 1000)}s, ${buf.entries.length} entries). Flushing...`
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// ── 1. Deduplicate buffer entries in-memory ──────────────
|
|
349
|
+
const grouped = {};
|
|
350
|
+
|
|
351
|
+
for (const e of buf.entries) {
|
|
352
|
+
const dedupKey = buildDedupKey(e.classLabel, e.bbox, e.dronePosition);
|
|
353
|
+
|
|
354
|
+
if (!grouped[dedupKey]) {
|
|
355
|
+
grouped[dedupKey] = {
|
|
356
|
+
missionId,
|
|
357
|
+
vehicleType: e.classLabel,
|
|
358
|
+
violationType: "outside_slot",
|
|
359
|
+
dedupKey,
|
|
360
|
+
bboxGridKey: computeViolationGridKey(e.bbox),
|
|
361
|
+
sightingsCount: 0,
|
|
362
|
+
bestConfidence: 0,
|
|
363
|
+
bestDetection: null,
|
|
364
|
+
bestSourceDetection: null,
|
|
365
|
+
firstSeenAt: e.timestamp,
|
|
366
|
+
lastSeenAt: e.timestamp,
|
|
367
|
+
dronePosition: e.dronePosition,
|
|
368
|
+
violationLocation: e.violationLocation || e.dronePosition,
|
|
369
|
+
representativeFramePath: null,
|
|
370
|
+
lastFrameReferences: [],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const rec = grouped[dedupKey];
|
|
375
|
+
rec.sightingsCount += 1;
|
|
376
|
+
if (e.timestamp < rec.firstSeenAt) rec.firstSeenAt = e.timestamp;
|
|
377
|
+
if (e.timestamp > rec.lastSeenAt) rec.lastSeenAt = e.timestamp;
|
|
378
|
+
|
|
379
|
+
if (e.violationLocation) {
|
|
380
|
+
rec.violationLocation = e.violationLocation;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (e.confidence > rec.bestConfidence) {
|
|
384
|
+
rec.bestConfidence = e.confidence;
|
|
385
|
+
rec.bestDetection = {
|
|
386
|
+
classLabel: e.classLabel,
|
|
387
|
+
trackingId: e.trackingId,
|
|
388
|
+
confidence: e.confidence,
|
|
389
|
+
bbox: e.bbox,
|
|
390
|
+
};
|
|
391
|
+
rec.bestSourceDetection = e.sourceDetection || null;
|
|
392
|
+
rec.representativeFramePath = e.framePath;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
rec.lastFrameReferences.push({
|
|
396
|
+
framePath: e.framePath,
|
|
397
|
+
frameId: e.frameId,
|
|
398
|
+
timestamp: e.timestamp,
|
|
399
|
+
confidence: e.confidence,
|
|
400
|
+
violationBbox: e.bbox,
|
|
401
|
+
location: e.violationLocation || e.dronePosition,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Cap frame references
|
|
406
|
+
const dedupKeys = Object.keys(grouped);
|
|
407
|
+
for (const dk of dedupKeys) {
|
|
408
|
+
grouped[dk].lastFrameReferences = grouped[dk].lastFrameReferences.slice(
|
|
409
|
+
-MAX_FRAME_REFS
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── 2. Merge with existing Redis aggregation records ─────
|
|
414
|
+
const redisKeys = dedupKeys.map(
|
|
415
|
+
(dk) => `mission/${missionId}/violation_agg/${dk}`
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
let existingMap = {};
|
|
419
|
+
try {
|
|
420
|
+
existingMap = await cubby.json.mget(redisKeys);
|
|
421
|
+
} catch (error) {}
|
|
422
|
+
|
|
423
|
+
const toWrite = {};
|
|
424
|
+
|
|
425
|
+
for (const dk of dedupKeys) {
|
|
426
|
+
const incoming = grouped[dk];
|
|
427
|
+
const redisKey = `mission/${missionId}/violation_agg/${dk}`;
|
|
428
|
+
const existing = existingMap[redisKey] || null;
|
|
429
|
+
|
|
430
|
+
if (existing) {
|
|
431
|
+
const gap = incoming.firstSeenAt - existing.lastSeenAt;
|
|
432
|
+
if (gap > VIOLATION_STALE_MS) {
|
|
433
|
+
toWrite[redisKey] = incoming;
|
|
434
|
+
context.log(
|
|
435
|
+
`🔄 Reset stale violation ${dk} (gap ${Math.round(gap / 1000)}s)`
|
|
436
|
+
);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
existing.sightingsCount += incoming.sightingsCount;
|
|
441
|
+
if (incoming.bestConfidence > existing.bestConfidence) {
|
|
442
|
+
existing.bestConfidence = incoming.bestConfidence;
|
|
443
|
+
existing.bestDetection = incoming.bestDetection;
|
|
444
|
+
existing.bestSourceDetection = incoming.bestSourceDetection;
|
|
445
|
+
existing.representativeFramePath = incoming.representativeFramePath;
|
|
446
|
+
}
|
|
447
|
+
if (incoming.firstSeenAt < existing.firstSeenAt) {
|
|
448
|
+
existing.firstSeenAt = incoming.firstSeenAt;
|
|
449
|
+
}
|
|
450
|
+
if (incoming.lastSeenAt > existing.lastSeenAt) {
|
|
451
|
+
existing.lastSeenAt = incoming.lastSeenAt;
|
|
452
|
+
}
|
|
453
|
+
if (incoming.dronePosition) {
|
|
454
|
+
existing.dronePosition = incoming.dronePosition;
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
incoming.violationLocation &&
|
|
458
|
+
(!incoming.dronePosition ||
|
|
459
|
+
incoming.violationLocation.lat !== incoming.dronePosition.lat ||
|
|
460
|
+
incoming.violationLocation.lng !== incoming.dronePosition.lng)
|
|
461
|
+
) {
|
|
462
|
+
existing.violationLocation = incoming.violationLocation;
|
|
463
|
+
} else if (!existing.violationLocation && incoming.violationLocation) {
|
|
464
|
+
existing.violationLocation = incoming.violationLocation;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
existing.lastFrameReferences = existing.lastFrameReferences
|
|
468
|
+
.concat(incoming.lastFrameReferences)
|
|
469
|
+
.slice(-MAX_FRAME_REFS);
|
|
470
|
+
|
|
471
|
+
toWrite[redisKey] = existing;
|
|
472
|
+
} else {
|
|
473
|
+
toWrite[redisKey] = incoming;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── 3. Batch write aggregated records ────────────────────
|
|
478
|
+
await cubby.json.mset(toWrite);
|
|
479
|
+
|
|
480
|
+
// ── 4. Update violation_agg index ────────────────────────
|
|
481
|
+
const indexKey = `mission/${missionId}/violation_agg/_index`;
|
|
482
|
+
let indexArr = [];
|
|
483
|
+
try {
|
|
484
|
+
indexArr = await cubby.json.get(indexKey);
|
|
485
|
+
} catch (error) {}
|
|
486
|
+
if (!Array.isArray(indexArr)) indexArr = [];
|
|
487
|
+
|
|
488
|
+
const existingSet = new Set(indexArr);
|
|
489
|
+
let indexChanged = false;
|
|
490
|
+
for (const dk of dedupKeys) {
|
|
491
|
+
if (!existingSet.has(dk)) {
|
|
492
|
+
indexArr.push(dk);
|
|
493
|
+
indexChanged = true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (indexChanged) {
|
|
497
|
+
await cubby.json.set(indexKey, indexArr);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ── 5. Find closest synced packet and patch it ───────────
|
|
501
|
+
const lastSeenAt = Math.max(...buf.entries.map((e) => e.timestamp));
|
|
502
|
+
const syncedTimestamps = await getTimestamps(missionId, "telemetry", cubby, context);
|
|
503
|
+
|
|
504
|
+
let closestSyncTs = null;
|
|
505
|
+
let closestDelta = Infinity;
|
|
506
|
+
for (const ts of syncedTimestamps) {
|
|
507
|
+
const delta = Math.abs(ts - lastSeenAt);
|
|
508
|
+
if (delta < closestDelta) {
|
|
509
|
+
closestSyncTs = ts;
|
|
510
|
+
closestDelta = delta;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const aggregatedSummary = Object.values(toWrite).map((r:any) => ({
|
|
515
|
+
vehicleType: r.vehicleType,
|
|
516
|
+
dedupKey: r.dedupKey,
|
|
517
|
+
bboxGridKey: r.bboxGridKey,
|
|
518
|
+
sightingsCount: r.sightingsCount,
|
|
519
|
+
bestConfidence: r.bestConfidence,
|
|
520
|
+
firstSeenAt: r.firstSeenAt,
|
|
521
|
+
lastSeenAt: r.lastSeenAt,
|
|
522
|
+
violationLocation: r.violationLocation,
|
|
523
|
+
dronePosition: r.dronePosition,
|
|
524
|
+
representativeFramePath: r.representativeFramePath,
|
|
525
|
+
}));
|
|
526
|
+
|
|
527
|
+
if (closestSyncTs !== null) {
|
|
528
|
+
const syncedKey = `mission/${missionId}/synced/${closestSyncTs}`;
|
|
529
|
+
let syncedPacket = null;
|
|
530
|
+
try {
|
|
531
|
+
syncedPacket = await cubby.json.get(syncedKey);
|
|
532
|
+
} catch (error) {}
|
|
533
|
+
|
|
534
|
+
if (syncedPacket) {
|
|
535
|
+
syncedPacket.aggregatedViolations = aggregatedSummary;
|
|
536
|
+
syncedPacket.violationFlushedAt = Date.now();
|
|
537
|
+
await cubby.json.set(syncedKey, syncedPacket);
|
|
538
|
+
context.log(
|
|
539
|
+
`📌 Attached ${dedupKeys.length} aggregated violations to synced packet at ${closestSyncTs}`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ── 6. Append to flush log ───────────────────────────────
|
|
545
|
+
const flushLogKey = `mission/${missionId}/violation_flush_log/_index`;
|
|
546
|
+
let flushLog = [];
|
|
547
|
+
try {
|
|
548
|
+
flushLog = await cubby.json.get(flushLogKey);
|
|
549
|
+
} catch (error) {}
|
|
550
|
+
if (!Array.isArray(flushLog)) flushLog = [];
|
|
551
|
+
|
|
552
|
+
flushLog.push({
|
|
553
|
+
flushedAt: Date.now(),
|
|
554
|
+
syncedTimestamp: closestSyncTs,
|
|
555
|
+
violationCount: dedupKeys.length,
|
|
556
|
+
totalSightings: buf.entries.length,
|
|
557
|
+
windowDurationMs: elapsed,
|
|
558
|
+
});
|
|
559
|
+
await cubby.json.set(flushLogKey, flushLog);
|
|
560
|
+
|
|
561
|
+
// ── 7. Clear the buffer ──────────────────────────────────
|
|
562
|
+
await cubby.json.set(bufKey, { startedAt: Date.now(), entries: [] });
|
|
563
|
+
|
|
564
|
+
context.log(
|
|
565
|
+
`📊 Flushed ${dedupKeys.length} unique violations (${buf.entries.length} raw sightings) over ${Math.round(elapsed / 1000)}s window (flush took ${Date.now() - t0}ms)`
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
return aggregatedSummary;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Retrieve all aggregated violations for a mission.
|
|
573
|
+
*/
|
|
574
|
+
async function getAggregatedViolations(missionId, cubby) {
|
|
575
|
+
const indexKey = `mission/${missionId}/violation_agg/_index`;
|
|
576
|
+
let indexArr = [];
|
|
577
|
+
try {
|
|
578
|
+
indexArr = await cubby.json.get(indexKey);
|
|
579
|
+
} catch (error) {}
|
|
580
|
+
if (!Array.isArray(indexArr) || indexArr.length === 0) {
|
|
581
|
+
return { success: true, data: [] };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const keys = indexArr.map(
|
|
585
|
+
(dk) => `mission/${missionId}/violation_agg/${dk}`
|
|
586
|
+
);
|
|
587
|
+
const records = await cubby.json.mget(keys);
|
|
588
|
+
|
|
589
|
+
const dataArray = Object.values(records)
|
|
590
|
+
.filter((r) => r !== null)
|
|
591
|
+
//@ts-ignore
|
|
592
|
+
.sort((a, b) => b.bestConfidence - a.bestConfidence);
|
|
593
|
+
|
|
594
|
+
return { success: true, data: dataArray };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ─────────────────────────────────────────────────────────────
|
|
598
|
+
// Synchronization Logic
|
|
599
|
+
// ─────────────────────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
async function detectParkingViloations(mergedRgb, telemetryTimestamp, telemetry, context) {
|
|
602
|
+
const t0 = Date.now();
|
|
603
|
+
const processedRGB = await Promise.all(
|
|
604
|
+
mergedRgb.map(async (rgb) => {
|
|
605
|
+
const data = { ...rgb };
|
|
606
|
+
if (!rgb.detectedObjects?.length || rgb.violationDetectionProcessed) {
|
|
607
|
+
return data;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
context.log(`Detected RGB object Detection`);
|
|
611
|
+
const objectsToDetect = rgb.detectedObjects.filter((d) =>
|
|
612
|
+
VEHICLE_LABELS.has(d.label.toLowerCase())
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
if (!objectsToDetect.length) return data;
|
|
616
|
+
|
|
617
|
+
context.log(`needProcess:${objectsToDetect.length}`);
|
|
618
|
+
try {
|
|
619
|
+
context.log(`Processing violation detection...`);
|
|
620
|
+
const violationDetection =
|
|
621
|
+
await context.agents.parkingViolationDetector.detect({
|
|
622
|
+
timestamp: telemetryTimestamp,
|
|
623
|
+
detections: rgb.detectedObjects.map((item) => ({
|
|
624
|
+
classLabel: item.label,
|
|
625
|
+
bbox: {
|
|
626
|
+
x: item.box.xmin,
|
|
627
|
+
y: item.box.ymin,
|
|
628
|
+
w: item.box.xmax - item.box.xmin,
|
|
629
|
+
h: item.box.ymax - item.box.ymin,
|
|
630
|
+
},
|
|
631
|
+
trackingId: item.class_id,
|
|
632
|
+
confidence: item.confidence,
|
|
633
|
+
})),
|
|
634
|
+
telemetry: {
|
|
635
|
+
timestamp: telemetryTimestamp,
|
|
636
|
+
position: { lat: telemetry.lat, lng: telemetry.lon },
|
|
637
|
+
altitudeAGL: 65,
|
|
638
|
+
heading: 75,
|
|
639
|
+
pitch: 0,
|
|
640
|
+
roll: 0,
|
|
641
|
+
gimbalPitch: -85,
|
|
642
|
+
hfov: 84,
|
|
643
|
+
vfov: 63,
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
// context.log(
|
|
647
|
+
// `ProcessedViolationDetection:${JSON.stringify(violationDetection)}`
|
|
648
|
+
// );
|
|
649
|
+
data.violationDetection = violationDetection;
|
|
650
|
+
data.violationDetectionProcessed = true;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
context.log(`[ERROR]: Failed detect violation: ${error.message}`);
|
|
653
|
+
data.violationDetectionProcessed = true;
|
|
654
|
+
data.violationDetectionError = error.message;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return data;
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
context.log(`⏱ detectParkingViolations ${Date.now() - t0}ms | ${mergedRgb.length} frames`);
|
|
662
|
+
return processedRGB;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ─── Optimized: index lookup + mget instead of keys() + N×get ───
|
|
666
|
+
|
|
667
|
+
async function findAllFramesInWindow(missionId, streamType, targetTimestamp, cubby, context) {
|
|
668
|
+
const t0 = Date.now();
|
|
669
|
+
const timestamps = await getTimestamps(missionId, streamType, cubby, context);
|
|
670
|
+
|
|
671
|
+
if (timestamps.length === 0) return [];
|
|
672
|
+
|
|
673
|
+
const matchingTimestamps = timestamps.filter(
|
|
674
|
+
ts => Math.abs(ts - targetTimestamp) <= SYNC_WINDOW_MS
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
if (matchingTimestamps.length === 0) return [];
|
|
678
|
+
|
|
679
|
+
const keys = matchingTimestamps.map(ts => `mission/${missionId}/${streamType}/${ts}`);
|
|
680
|
+
const framesMap = await cubby.json.mget(keys);
|
|
681
|
+
|
|
682
|
+
const matches = [];
|
|
683
|
+
for (const key of keys) {
|
|
684
|
+
const frame = framesMap[key];
|
|
685
|
+
if (frame) {
|
|
686
|
+
const ts = parseInt(key.split('/').pop(), 10);
|
|
687
|
+
frame.timeDelta = ts - targetTimestamp;
|
|
688
|
+
matches.push(frame);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
matches.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
693
|
+
context.log(`⏱ findFrames(${streamType}) ${Date.now() - t0}ms | ${matches.length} matches`);
|
|
694
|
+
return matches;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ─── Optimized: parallel fetches in attemptSync ───
|
|
698
|
+
|
|
699
|
+
async function attemptSync(missionId, telemetryTimestamp, context, cubby) {
|
|
700
|
+
const t0 = Date.now();
|
|
701
|
+
try {
|
|
702
|
+
const telemetryKey = `mission/${missionId}/telemetry/${telemetryTimestamp}`;
|
|
703
|
+
const syncedKey = `mission/${missionId}/synced/${telemetryTimestamp}`;
|
|
704
|
+
let telemetry
|
|
705
|
+
let existing = null
|
|
706
|
+
try{
|
|
707
|
+
telemetry = await cubby.json.get(telemetryKey)
|
|
708
|
+
}
|
|
709
|
+
catch(error){
|
|
710
|
+
|
|
711
|
+
}
|
|
712
|
+
try{
|
|
713
|
+
existing = await cubby.json.get(syncedKey)
|
|
714
|
+
} catch(error){
|
|
715
|
+
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const [rgbFrames, thermalFrames, klvPackets] = await Promise.all([
|
|
719
|
+
findAllFramesInWindow(missionId, "rgb", telemetryTimestamp, cubby, context),
|
|
720
|
+
findAllFramesInWindow(missionId, "thermal", telemetryTimestamp, cubby, context),
|
|
721
|
+
findAllFramesInWindow(missionId, "klv", telemetryTimestamp, cubby, context),
|
|
722
|
+
]);
|
|
723
|
+
|
|
724
|
+
if (!telemetry) return;
|
|
725
|
+
|
|
726
|
+
const mergedRgb = rgbFrames.length > 0 ? rgbFrames : (existing?.rgb || []);
|
|
727
|
+
const mergedThermal = thermalFrames.length > 0 ? thermalFrames : (existing?.thermal || []);
|
|
728
|
+
const mergedKlv = klvPackets.length > 0 ? klvPackets : (existing?.klv || []);
|
|
729
|
+
|
|
730
|
+
const hasRgb = mergedRgb.length > 0;
|
|
731
|
+
const hasThermal = mergedThermal.length > 0;
|
|
732
|
+
const hasKlv = mergedKlv.length > 0;
|
|
733
|
+
const streamCount = [hasRgb, hasThermal, hasKlv].filter(Boolean).length;
|
|
734
|
+
|
|
735
|
+
let syncQuality;
|
|
736
|
+
if (streamCount === 3) syncQuality = "full";
|
|
737
|
+
else if (streamCount === 0) syncQuality = "telemetry-only";
|
|
738
|
+
else syncQuality = "partial";
|
|
739
|
+
|
|
740
|
+
const processedRGB = await detectParkingViloations(mergedRgb, telemetryTimestamp, telemetry, context);
|
|
741
|
+
|
|
742
|
+
// ── Buffer violations (append only, no aggregation yet) ─
|
|
743
|
+
const telData = telemetry.payload || telemetry;
|
|
744
|
+
const dronePosition =
|
|
745
|
+
telData.lat != null && telData.lon != null
|
|
746
|
+
? { lat: telData.lat, lng: telData.lon }
|
|
747
|
+
: null;
|
|
748
|
+
|
|
749
|
+
await bufferViolations(missionId, processedRGB, dronePosition, cubby, context);
|
|
750
|
+
|
|
751
|
+
// ── Check if 2-minute window expired → flush ────────────
|
|
752
|
+
const flushed = await checkAndFlushViolations(missionId, cubby, context);
|
|
753
|
+
|
|
754
|
+
const syncedPacket = {
|
|
755
|
+
timestamp: telemetryTimestamp,
|
|
756
|
+
missionId,
|
|
757
|
+
droneName: telemetry.payload?.name || telemetry.droneName,
|
|
758
|
+
syncQuality,
|
|
759
|
+
syncedAt: Date.now(),
|
|
760
|
+
telemetry: telData,
|
|
761
|
+
rgb: processedRGB,
|
|
762
|
+
thermal: mergedThermal,
|
|
763
|
+
klv: mergedKlv.map(f =>
|
|
764
|
+
f.data ? f : { data: f.klv, index: f.index, timestamp: f.timestamp, timeDelta: f.timeDelta }
|
|
765
|
+
),
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// Only attach aggregatedViolations when a flush actually happened
|
|
769
|
+
if (flushed) {
|
|
770
|
+
//@ts-ignore
|
|
771
|
+
syncedPacket.aggregatedViolations = flushed;
|
|
772
|
+
//@ts-ignore
|
|
773
|
+
syncedPacket.violationFlushedAt = Date.now();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const latestKey = `mission/${missionId}/synced/latest`;
|
|
777
|
+
await cubby.json.set(syncedKey, syncedPacket)
|
|
778
|
+
let currentLatest = null
|
|
779
|
+
try{
|
|
780
|
+
currentLatest = cubby.json.get(latestKey)
|
|
781
|
+
} catch(error){
|
|
782
|
+
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const qualityRank = { "telemetry-only": 0, partial: 1, full: 2 };
|
|
786
|
+
if (
|
|
787
|
+
!currentLatest ||
|
|
788
|
+
syncedPacket.timestamp >= currentLatest.timestamp ||
|
|
789
|
+
qualityRank[syncQuality] >= qualityRank[currentLatest.syncQuality || "telemetry-only"]
|
|
790
|
+
) {
|
|
791
|
+
await cubby.json.set(latestKey, syncedPacket);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
context.log(
|
|
795
|
+
`🔗 Synced for mission ${missionId} [${syncQuality}] ` +
|
|
796
|
+
`RGB: ${mergedRgb.length}, Thermal: ${mergedThermal.length}, KLV: ${mergedKlv.length} (${Date.now() - t0}ms)`
|
|
797
|
+
);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
context.log(`❌ Sync failed for mission ${missionId}: ${error.message}`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ─── Optimized: index lookup instead of keys() scan ───
|
|
804
|
+
|
|
805
|
+
async function attemptSyncFromFrame(missionId, frameTimestamp, context, cubby) {
|
|
806
|
+
const t0 = Date.now();
|
|
807
|
+
try {
|
|
808
|
+
const timestamps = await getTimestamps(missionId, 'telemetry', cubby, context);
|
|
809
|
+
if (timestamps.length === 0) {
|
|
810
|
+
context.log(`⏱ attemptSyncFromFrame: no telemetry timestamps yet for ${missionId} (${Date.now() - t0}ms)`);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let closestTs = null;
|
|
815
|
+
let closestDelta = Infinity;
|
|
816
|
+
|
|
817
|
+
for (const ts of timestamps) {
|
|
818
|
+
const delta = Math.abs(ts - frameTimestamp);
|
|
819
|
+
if (delta <= SYNC_WINDOW_MS && delta < closestDelta) {
|
|
820
|
+
closestTs = ts;
|
|
821
|
+
closestDelta = delta;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (closestTs !== null) {
|
|
825
|
+
context.log(`⏱ attemptSyncFromFrame: matched telemetry ts=${closestTs} (delta ${closestDelta}ms) from ${timestamps.length} candidates`);
|
|
826
|
+
await attemptSync(missionId, closestTs, context, cubby);
|
|
827
|
+
} else {
|
|
828
|
+
const nearest = timestamps.reduce((best, ts) => {
|
|
829
|
+
const d = Math.abs(ts - frameTimestamp);
|
|
830
|
+
return d < best.delta ? { ts, delta: d } : best;
|
|
831
|
+
}, { ts: null, delta: Infinity });
|
|
832
|
+
context.log(`⏱ attemptSyncFromFrame: no telemetry within ${SYNC_WINDOW_MS}ms window | nearest delta=${nearest.delta}ms (ts=${nearest.ts}), frameTs=${frameTimestamp}, ${timestamps.length} candidates`);
|
|
833
|
+
}
|
|
834
|
+
context.log(`⏱ attemptSyncFromFrame ${Date.now() - t0}ms`);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
context.log(`❌ Sync from frame failed: ${error.message} (${Date.now() - t0}ms)`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ─────────────────────────────────────────────────────────────
|
|
841
|
+
// Main Handler
|
|
842
|
+
// ─────────────────────────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
function bytesToString(bytes) {
|
|
845
|
+
var str = "";
|
|
846
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
847
|
+
str += String.fromCharCode(bytes[i]);
|
|
848
|
+
}
|
|
849
|
+
return str;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function handle(event, context) {
|
|
853
|
+
const cubby = await context.cubby('syncMission');
|
|
854
|
+
const streamId = event.payload?.streamId || event.streamId;
|
|
855
|
+
|
|
856
|
+
if (!streamId) {
|
|
857
|
+
context.log('❌ Missing streamId in event payload');
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const stream = await context.streams.subscribe(streamId);
|
|
862
|
+
context.log('✅ Successfully subscribed to Stream');
|
|
863
|
+
|
|
864
|
+
for await (const packet of stream) {
|
|
865
|
+
const seqNum = packet.sequenceNum;
|
|
866
|
+
const ctx = Object.create(context);
|
|
867
|
+
ctx.log = (msg) => context.log(`[seq:${seqNum}] ${msg}`);
|
|
868
|
+
|
|
869
|
+
try {
|
|
870
|
+
const payloadStr = bytesToString(packet.payload);
|
|
871
|
+
const data = JSON.parse(payloadStr);
|
|
872
|
+
const eventType = data.event_type;
|
|
873
|
+
const missionId = data.missionId || data.payload?.mission || "unknown";
|
|
874
|
+
const droneName = data.name || data.payload?.name
|
|
875
|
+
let timestamp = data.timestamp || Date.now();
|
|
876
|
+
if (typeof timestamp === 'string') {
|
|
877
|
+
timestamp = new Date(timestamp).getTime();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const tEvent = Date.now();
|
|
881
|
+
|
|
882
|
+
if (eventType === "DRONE_TELEMETRY_DATA") {
|
|
883
|
+
await registerMission(missionId, droneName, timestamp, cubby, ctx);
|
|
884
|
+
await storeTelemetry(missionId, timestamp, data, ctx, cubby);
|
|
885
|
+
await attemptSync(missionId, timestamp, ctx, cubby);
|
|
886
|
+
} else if (eventType === "VIDEO_STREAM_DATA") {
|
|
887
|
+
await storeRgbFrame(missionId, timestamp, data, ctx, cubby);
|
|
888
|
+
await attemptSyncFromFrame(missionId, timestamp, ctx, cubby);
|
|
889
|
+
} else if (eventType === "THERMAL_STREAM_DATA") {
|
|
890
|
+
await storeThermalFrame(missionId, timestamp, data, ctx, cubby);
|
|
891
|
+
await attemptSyncFromFrame(missionId, timestamp, ctx, cubby);
|
|
892
|
+
} else if (eventType === "VIDEO_KLV_DATA") {
|
|
893
|
+
await storeKlvData(missionId, timestamp, data, ctx, cubby);
|
|
894
|
+
await attemptSyncFromFrame(missionId, timestamp, ctx, cubby);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
ctx.log(`⏱ [${eventType}] total pipeline ${Date.now() - tEvent}ms`);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
ctx.log(`Failed to process data: ${error.message}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|