From 98bffc4117d788614df8db6d9da3e67b408ce96d Mon Sep 17 00:00:00 2001 From: Kostas Drakontidis Date: Mon, 1 Jun 2026 02:24:33 +0300 Subject: [PATCH] cameraSession integration --- .../resources/adminjs.user.resource.ts | 6 +- src/officesense_pi/src/config/config.ts | 4 +- src/officesense_pi/src/core/analyze.ts | 10 +- src/officesense_pi/src/core/bootstrap.ts | 3 + src/officesense_pi/src/core/camera.ts | 132 ++++++++++++++++++ 5 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 src/officesense_pi/src/core/camera.ts diff --git a/src/officesense_pi/src/api/management/resources/adminjs.user.resource.ts b/src/officesense_pi/src/api/management/resources/adminjs.user.resource.ts index 0a62b77..a7d8b70 100644 --- a/src/officesense_pi/src/api/management/resources/adminjs.user.resource.ts +++ b/src/officesense_pi/src/api/management/resources/adminjs.user.resource.ts @@ -10,8 +10,6 @@ const execPath = '../../../../bin'; const binaryPath = path.join(__dirname, execPath, 'extractEmbeddings') async function extractEmbedding(base64: string): Promise { - console.log(base64); - return new Promise((resolve, reject) => { const proc = spawn(binaryPath, [], { cwd: path.join(__dirname, execPath) @@ -22,9 +20,7 @@ async function extractEmbedding(base64: string): Promise { proc.stdout.on('data', (d) => stdout += d) proc.stderr.on('data', (d) => stderr += d) - proc.stdin.on('error', (err) => { - // ignore EPIPE here, we'll catch it in close handler - }) + proc.stdin.on('error', (err) => { }); proc.on('error', (err) => { reject(new Error(`Failed to start binary: ${err.message}`)) diff --git a/src/officesense_pi/src/config/config.ts b/src/officesense_pi/src/config/config.ts index f45d078..1cdd242 100644 --- a/src/officesense_pi/src/config/config.ts +++ b/src/officesense_pi/src/config/config.ts @@ -23,6 +23,7 @@ interface Config { transitionCleanupInterval: number; lossThreshold: number; userTTL: number; + verifyTimeout: number; }; } @@ -50,7 +51,8 @@ const config: Config = { transitionTTL: 5 * 60 * 1000, transitionCleanupInterval: 60 * 1000, lossThreshold: 5000, - userTTL: 3 * 60 * 1000 + userTTL: 3 * 60 * 1000, + verifyTimeout: 30 * 1000 }, } diff --git a/src/officesense_pi/src/core/analyze.ts b/src/officesense_pi/src/core/analyze.ts index 5159c1b..51e4176 100644 --- a/src/officesense_pi/src/core/analyze.ts +++ b/src/officesense_pi/src/core/analyze.ts @@ -38,7 +38,7 @@ export async function updateRoomOccupancyListener(message: string, channel: stri const redis = getRedis(); if (!lastRoomID) { - console.log("[!] Key not found in local mapι") + console.log("[!] Key not found in local map") } else { await redis.decr(`room:${lastRoomID}`); } @@ -103,8 +103,6 @@ export async function analyzeData(roomID: string, metrics: { const user = newUser(); console.log(`[Redis] Created USER: ${user.psuedoName} USER_ID: ${user.pseudoID}`); - // trigger camera - await redis.set(`user:${userID}`, JSON.stringify({ userID: user.pseudoID, name: user.psuedoName, @@ -141,8 +139,6 @@ export async function analyzeData(roomID: string, metrics: { let transition = transitions.get(userID); - // update room occupancy - if (!transition) { console.log("[!] Starting new transition"); transition = new RoomTransition(); @@ -150,8 +146,6 @@ export async function analyzeData(roomID: string, metrics: { } if (transition.shouldTransitionTo(roomID, metrics.rssi, resObj["rssi"], resObj["timestamp"])) { - // trigger camera - console.log(`[!] Transition done ${resObj["room"]} -> ${roomID}`); await redis.decr(`room:${resObj["room"]}`); @@ -162,7 +156,7 @@ export async function analyzeData(roomID: string, metrics: { resObj["rssi"] = metrics.rssi; resObj["room"] = roomID; resObj["timestamp"] = Date.now() - + resObj["verified"] = false; await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL }); } else diff --git a/src/officesense_pi/src/core/bootstrap.ts b/src/officesense_pi/src/core/bootstrap.ts index 551a67d..0748b87 100644 --- a/src/officesense_pi/src/core/bootstrap.ts +++ b/src/officesense_pi/src/core/bootstrap.ts @@ -1,5 +1,6 @@ import { getRoomIDs } from "../api/livedata/livedata.repository.js"; import { initRedis, initSubRedis } from "../redis/redis.js"; +import { initCamera } from "./camera.js"; import * as mqtt from "./mqtt.js"; import { cleanupWorker } from "./transition.js"; @@ -19,6 +20,8 @@ export async function bootstrap() { cleanupWorker(); + await initCamera(); + console.log("[!] Core started."); } catch (err: any) { console.log("[!] Bootstrap failed:", err.message); diff --git a/src/officesense_pi/src/core/camera.ts b/src/officesense_pi/src/core/camera.ts new file mode 100644 index 0000000..9ea4a4e --- /dev/null +++ b/src/officesense_pi/src/core/camera.ts @@ -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 { + 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 { + 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 };