init fully working core code
This commit is contained in:
@@ -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;
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import lookupRoutes from "./api/lookup/lookup.routes.js";
|
import lookupRoutes from "./api/lookup/lookup.routes.js";
|
||||||
|
import config from "./config/config.js";
|
||||||
|
import { bootstrap } from "./core/bootstrap.js";
|
||||||
|
|
||||||
const app = express();
|
(async () => {
|
||||||
const PORT = 80;
|
const app = express();
|
||||||
const ADDRESS = "0.0.0.0";
|
|
||||||
|
|
||||||
app.use("/", lookupRoutes);
|
app.use("/", lookupRoutes);
|
||||||
|
|
||||||
app.listen(PORT, ADDRESS, () => {
|
app.listen(config.api.port, config.api.address, () => {
|
||||||
console.log(`Server listening on ${ADDRESS}:${PORT}`);
|
console.log(`Server listening on ${config.api.address}:${config.api.port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
|
|
||||||
|
})()
|
||||||
|
|||||||
Reference in New Issue
Block a user