cameraSession integration

This commit is contained in:
Kostas Drakontidis
2026-06-01 02:24:33 +03:00
parent c7a0d921ea
commit 98bffc4117
5 changed files with 141 additions and 14 deletions
@@ -10,8 +10,6 @@ const execPath = '../../../../bin';
const binaryPath = path.join(__dirname, execPath, 'extractEmbeddings') const binaryPath = path.join(__dirname, execPath, 'extractEmbeddings')
async function extractEmbedding(base64: string): Promise<string> { async function extractEmbedding(base64: string): Promise<string> {
console.log(base64);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const proc = spawn(binaryPath, [], { const proc = spawn(binaryPath, [], {
cwd: path.join(__dirname, execPath) cwd: path.join(__dirname, execPath)
@@ -22,9 +20,7 @@ async function extractEmbedding(base64: string): Promise<string> {
proc.stdout.on('data', (d) => stdout += d) proc.stdout.on('data', (d) => stdout += d)
proc.stderr.on('data', (d) => stderr += d) proc.stderr.on('data', (d) => stderr += d)
proc.stdin.on('error', (err) => { proc.stdin.on('error', (err) => { });
// ignore EPIPE here, we'll catch it in close handler
})
proc.on('error', (err) => { proc.on('error', (err) => {
reject(new Error(`Failed to start binary: ${err.message}`)) reject(new Error(`Failed to start binary: ${err.message}`))
+3 -1
View File
@@ -23,6 +23,7 @@ interface Config {
transitionCleanupInterval: number; transitionCleanupInterval: number;
lossThreshold: number; lossThreshold: number;
userTTL: number; userTTL: number;
verifyTimeout: number;
}; };
} }
@@ -50,7 +51,8 @@ const config: Config = {
transitionTTL: 5 * 60 * 1000, transitionTTL: 5 * 60 * 1000,
transitionCleanupInterval: 60 * 1000, transitionCleanupInterval: 60 * 1000,
lossThreshold: 5000, lossThreshold: 5000,
userTTL: 3 * 60 * 1000 userTTL: 3 * 60 * 1000,
verifyTimeout: 30 * 1000
}, },
} }
+2 -8
View File
@@ -38,7 +38,7 @@ export async function updateRoomOccupancyListener(message: string, channel: stri
const redis = getRedis(); const redis = getRedis();
if (!lastRoomID) { if (!lastRoomID) {
console.log("[!] Key not found in local mapι") console.log("[!] Key not found in local map")
} else { } else {
await redis.decr(`room:${lastRoomID}`); await redis.decr(`room:${lastRoomID}`);
} }
@@ -103,8 +103,6 @@ export async function analyzeData(roomID: string, metrics: {
const user = newUser(); const user = newUser();
console.log(`[Redis] Created USER: ${user.psuedoName} USER_ID: ${user.pseudoID}`); console.log(`[Redis] Created USER: ${user.psuedoName} USER_ID: ${user.pseudoID}`);
// trigger camera
await redis.set(`user:${userID}`, JSON.stringify({ await redis.set(`user:${userID}`, JSON.stringify({
userID: user.pseudoID, userID: user.pseudoID,
name: user.psuedoName, name: user.psuedoName,
@@ -141,8 +139,6 @@ export async function analyzeData(roomID: string, metrics: {
let transition = transitions.get(userID); let transition = transitions.get(userID);
// update room occupancy
if (!transition) { if (!transition) {
console.log("[!] Starting new transition"); console.log("[!] Starting new transition");
transition = new RoomTransition(); transition = new RoomTransition();
@@ -150,8 +146,6 @@ export async function analyzeData(roomID: string, metrics: {
} }
if (transition.shouldTransitionTo(roomID, metrics.rssi, resObj["rssi"], resObj["timestamp"])) { if (transition.shouldTransitionTo(roomID, metrics.rssi, resObj["rssi"], resObj["timestamp"])) {
// trigger camera
console.log(`[!] Transition done ${resObj["room"]} -> ${roomID}`); console.log(`[!] Transition done ${resObj["room"]} -> ${roomID}`);
await redis.decr(`room:${resObj["room"]}`); await redis.decr(`room:${resObj["room"]}`);
@@ -162,7 +156,7 @@ export async function analyzeData(roomID: string, metrics: {
resObj["rssi"] = metrics.rssi; resObj["rssi"] = metrics.rssi;
resObj["room"] = roomID; resObj["room"] = roomID;
resObj["timestamp"] = Date.now() resObj["timestamp"] = Date.now()
resObj["verified"] = false;
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL }); await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
} else } else
+3
View File
@@ -1,5 +1,6 @@
import { getRoomIDs } from "../api/livedata/livedata.repository.js"; import { getRoomIDs } from "../api/livedata/livedata.repository.js";
import { initRedis, initSubRedis } from "../redis/redis.js"; import { initRedis, initSubRedis } from "../redis/redis.js";
import { initCamera } from "./camera.js";
import * as mqtt from "./mqtt.js"; import * as mqtt from "./mqtt.js";
import { cleanupWorker } from "./transition.js"; import { cleanupWorker } from "./transition.js";
@@ -19,6 +20,8 @@ export async function bootstrap() {
cleanupWorker(); cleanupWorker();
await initCamera();
console.log("[!] Core started."); console.log("[!] Core started.");
} catch (err: any) { } catch (err: any) {
console.log("[!] Bootstrap failed:", err.message); console.log("[!] Bootstrap failed:", err.message);
+132
View File
@@ -0,0 +1,132 @@
import { fileURLToPath } from 'url'
import path from 'path'
import { spawn } from "child_process";
import { getRedis } from '../redis/redis.js';
import { prisma } from '../lib/prisma.js';
import config from '../config/config.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const execPath = '../../bin';
const binaryPath = path.join(__dirname, execPath, 'cameraSession')
type InputPacket = {
uuid: string
descriptor: number[]
}
type ResultPacket = {
uuid: string
verified: boolean
}
export async function getUnverifiedUserKeys(): Promise<string[]> {
const redis = getRedis()
const keys = await redis.keys('user:*')
const unverified: string[] = []
await Promise.all(keys.map(async (key) => {
const raw = await redis.get(key)
if (!raw) return
const session = JSON.parse(raw)
if (!session.verified) unverified.push(key.replace('user:', ''))
}))
return unverified
}
export async function getUnverifiedUsersWithEmbeddings(): Promise<{ uuid: string, descriptor: number[] }[]> {
const userIds = await getUnverifiedUserKeys()
const users = await prisma.user.findMany({
where: {
id: { in: userIds },
faceEmbedding: { not: null },
},
select: { id: true, faceEmbedding: true },
})
return users.map((user) => ({
uuid: user.id,
descriptor: JSON.parse(user.faceEmbedding!),
}))
}
async function cameraSession(packets: InputPacket[]): Promise<ResultPacket[] | null> {
return new Promise((resolve, reject) => {
const proc = spawn(binaryPath, [], {
cwd: path.join(__dirname, execPath)
})
let stdout = ''
let stderr = ''
proc.stdout.on('data', (d) => stdout += d)
proc.stderr.on('data', (d) => stderr += d)
proc.stdin.on('error', () => { })
proc.on('error', (err) => {
reject(new Error(`Failed to start binary: ${err.message}`))
})
proc.on('close', (code) => {
console.log('cameraSession exited with code', code)
const trimmed = stdout.trim()
if (trimmed === 'no face found.') return resolve(null)
if (code !== 0) return reject(new Error(stderr || `exited with code ${code}`))
try {
const results: ResultPacket[] = JSON.parse(trimmed)
resolve(results)
} catch (e) {
reject(new Error(`Failed to parse output: ${trimmed}`))
}
})
for (const packet of packets) {
proc.stdin.write(JSON.stringify(packet) + '\n')
}
proc.stdin.end()
})
}
const setUnverified = async (uuid: string) => {
const redis = getRedis()
const key = `user:${uuid}`
const raw = await redis.get(key)
if (!raw) return
const session = JSON.parse(raw)
session.verified = false
await redis.set(key, JSON.stringify(session))
console.log(`[Camera] Unverified user ${uuid} due to timeout`)
}
const run = async () => {
const redis = getRedis();
try {
const packets = await getUnverifiedUsersWithEmbeddings()
if (packets.length > 0) {
const results = await cameraSession(packets)
if (results) {
await Promise.all(results.map(async (r) => {
if (!r.verified) return
const key = `user:${r.uuid}`
const raw = await redis.get(key)
if (!raw) return
const session = JSON.parse(raw)
session.verified = true
await redis.set(key, JSON.stringify(session))
setTimeout(() => setUnverified(r.uuid), config.core.verifyTimeout)
console.log(`[Camera] Verified user ${r.uuid}`)
}))
}
}
} catch (e) {
console.error('camera run error:', e)
}
setTimeout(run, 1000)
}
export { run as initCamera };