init fully working core code

This commit is contained in:
Kostas Drakontidis
2026-05-24 00:19:09 +03:00
parent a3d28e8cd5
commit 565900c9d4
7 changed files with 377 additions and 7 deletions
+57
View File
@@ -0,0 +1,57 @@
interface Config {
api: {
address: string;
port: number;
};
mqtt: {
host: string;
port: number;
username: string;
password: string;
topic: string;
};
redis: {
host: string;
port: number;
};
core: {
hysteresis: number;
candidateHysteresis: number;
debounceMS: number;
minSamples: number;
transitionTTL: number;
transitionCleanupInterval: number;
lossThreshold: number;
userTTL: number;
};
}
const config: Config = {
api: {
address: "0.0.0.0",
port: 80,
},
mqtt: {
host: "192.168.1.2",
port: 1883,
username: "user",
password: "pass",
topic: "scanners/+",
},
redis: {
host: "192.168.1.2",
port: 6379,
},
core: {
hysteresis: 6,
candidateHysteresis: 3,
debounceMS: 3000,
minSamples: 4,
transitionTTL: 5 * 60 * 1000,
transitionCleanupInterval: 60 * 1000,
lossThreshold: 5000,
userTTL: 3 * 60 * 1000
},
}
export default config;
+122
View File
@@ -0,0 +1,122 @@
import { getRedis } from "./redis.js";
import { prisma } from "../lib/prisma.js";
import { RoomTransition, transitions } from "./transition.js";
import config from "../config/config.js";
const adjectives = [
"Crazy",
"Silent",
"Dark",
"Fast",
"Lucky",
"Wild",
"Epic"
];
const nouns = [
"Tiger",
"Wolf",
"Falcon",
"Shadow",
"Ninja",
"Dragon",
"Phoenix"
];
function generateNickname() {
const adjective =
adjectives[Math.floor(Math.random() * adjectives.length)];
const noun =
nouns[Math.floor(Math.random() * nouns.length)];
const number = Math.floor(Math.random() * 1000);
return `${adjective}${noun}${number}`;
}
function newUser(): { pseudoID: string, psuedoName: string } {
return {
pseudoID: crypto.randomUUID(),
psuedoName: generateNickname()
};
}
export async function analyzeData(roomID: string, metrics: {
tagID: string,
rssi: number
}) {
console.log(`\n=======================================================\n` +
`ROOM_ID: ${roomID}\nTAG_ID: ${metrics.tagID}\nRSSI: ${metrics.rssi}\n` +
`=======================================================\n`);
const userID = (
await prisma.tag.findUnique({
where: { id: metrics.tagID },
select: { userId: true },
})
)?.userId;
console.log(`[SQLite] Resolved USER_ID: ${userID}`);
if (!userID)
return;
const redis = getRedis();
const res = await redis.get(`user:${userID}`);
if (!res) {
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,
rssi: metrics.rssi,
room: roomID,
verified: false,
timestamp: Date.now()
}), { PX: config.core.userTTL });
console.log("[Redis] Stored new user in redis.");
return;
}
let resObj = JSON.parse(res);
console.log(`[Redis] User exists in redis in ROOM_ID: ${resObj["room"]}`);
if (roomID == resObj["room"]) {
console.log("[Redis] Same room, updating redis...");
resObj["rssi"] = metrics.rssi;
resObj["timestamp"] = Date.now()
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
return;
}
let transition = transitions.get(userID);
if (!transition) {
console.log("[!] Starting new transition");
transition = new RoomTransition();
transitions.set(userID, transition);
}
if (transition.shouldTransitionTo(roomID, metrics.rssi, resObj["rssi"], resObj["timestamp"])) {
// trigger camera
console.log(`[!] Transition done ${resObj["room"]} -> ${roomID}`);
resObj["rssi"] = metrics.rssi;
resObj["room"] = roomID;
resObj["timestamp"] = Date.now()
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
} else
console.log("[!] Transition declined");
}
+16
View File
@@ -0,0 +1,16 @@
import { initRedis } from "./redis.js";
import * as mqtt from "./mqtt.js";
import { cleanupWorker } from "./transition.js";
export async function bootstrap() {
try {
await initRedis();
await mqtt.start();
cleanupWorker();
console.log("Core started.");
} catch (err: any) {
console.log("Bootstrap failed:", err.message);
process.exit(1);
}
}
+48
View File
@@ -0,0 +1,48 @@
import mqtt, { MqttClient } from "mqtt";
import config from "../config/config.js";
import { analyzeData } from "./analyze.js";
function initMqtt(): Promise<MqttClient> {
return new Promise((resolve, reject) => {
const client = mqtt.connect(
`mqtt://${config.mqtt.host}:${config.mqtt.port}`,
{
username: config.mqtt.username,
password: config.mqtt.password
}
);
client.once("connect", () => {
console.log("MQTT connected.");
client.subscribe(config.mqtt.topic, (err) => {
if (err) {
reject(err);
} else {
console.log("MQTT subscribed.");
resolve(client);
}
});
});
client.once("error", (err) => {
reject(err);
});
});
}
export async function start() {
try {
const mqttClient = await initMqtt();
mqttClient.on("message", (topic, message) => {
analyzeData(topic.split("/").pop()!, JSON.parse(message.toString()));
});
mqttClient.on("error", (err) => {
console.log(err.message);
})
} catch (err: any) {
console.log("MQTT connection failed:", err.message);
}
}
+28
View File
@@ -0,0 +1,28 @@
import { createClient, type RedisClientType } from "redis";
import config from "../config/config.js";
let redisClient: RedisClientType;
export async function initRedis(): Promise<RedisClientType> {
redisClient = createClient({
url: `redis://${config.redis.host}:${config.redis.port}`
});
redisClient.on("error", (err) => {
console.log("Redis error:", err.message);
});
await redisClient.connect();
console.log("Redis connected.");
return redisClient;
}
export function getRedis(): RedisClientType {
if (!redisClient) {
throw new Error("Redis not initialized");
}
return redisClient;
}
+94
View File
@@ -0,0 +1,94 @@
import config from "../config/config.js";
export const transitions = new Map<string, RoomTransition>();
export function cleanupWorker() {
setInterval(() => {
const now = Date.now();
for (const [userID, transition] of transitions) {
const inactiveFor = now - transition.lastSeen;
if (inactiveFor > 5 * config.core.transitionTTL)
transitions.delete(userID);
}
}, config.core.transitionCleanupInterval);
}
type Candidate = {
roomID: string | null;
since: number | null;
samples: number;
rssi: number | null;
};
export class RoomTransition {
candidate: Candidate;
lastSeen: number;
constructor() {
this.lastSeen = Date.now();
this.candidate = { roomID: null, since: null, samples: 0, rssi: null };
}
shouldTransitionTo(roomID: string, candidateRSSI: number, currRSSI: number, lastCurrentSeen: number) {
const now = Date.now();
this.lastSeen = now;
const stronger = candidateRSSI > currRSSI + config.core.hysteresis;
const signalLost = now - lastCurrentSeen > config.core.lossThreshold;
console.log(`[TRANSITION] stronger = ${stronger}`);
console.log(`[TRANSITION] signalLost = ${signalLost}`);
if (!stronger && !signalLost) {
this.reset();
return false;
}
const noCandidate = this.candidate.roomID == null;
const betterCandidate = this.candidate.roomID != roomID &&
this.candidate.rssi !== null &&
candidateRSSI > this.candidate.rssi + config.core.candidateHysteresis;
console.log(`[TRANSITION] noCandidate = ${noCandidate}`);
console.log(`[TRANSITION] betterCandidate = ${betterCandidate}`);
if (noCandidate || betterCandidate) {
this.candidate.roomID = roomID;
this.candidate.since = now;
this.candidate.samples = 1;
this.candidate.rssi = candidateRSSI;
return false;
}
this.candidate.samples++;
this.candidate.rssi = candidateRSSI;
console.log(`[TRANSITION] samples = ${this.candidate.samples}`);
console.log(`[TRANSITION] candidateRSSI = ${this.candidate.rssi}`);
const enoughTime = now - (this.candidate.since ?? now) >= config.core.debounceMS;
const enoughConfirmations = this.candidate.samples >= config.core.minSamples;
console.log(`[TRANSITION] enoughTime = ${enoughTime}`);
console.log(`[TRANSITION] enoughConfirmations = ${enoughConfirmations}`);
console.log(`[TRANSITION] signalLost = ${signalLost}`);
if ((enoughTime && enoughConfirmations) || signalLost) {
this.reset();
return true;
}
return false;
}
reset() {
this.candidate.roomID = null;
this.candidate.since = null;
this.candidate.samples = 0;
this.candidate.rssi = null;
}
}
+9 -4
View File
@@ -1,12 +1,17 @@
import express from "express";
import lookupRoutes from "./api/lookup/lookup.routes.js";
import config from "./config/config.js";
import { bootstrap } from "./core/bootstrap.js";
(async () => {
const app = express();
const PORT = 80;
const ADDRESS = "0.0.0.0";
app.use("/", lookupRoutes);
app.listen(PORT, ADDRESS, () => {
console.log(`Server listening on ${ADDRESS}:${PORT}`);
app.listen(config.api.port, config.api.address, () => {
console.log(`Server listening on ${config.api.address}:${config.api.port}`);
});
bootstrap();
})()