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 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();
|
||||
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user