feat: restructure backend with Elysia framework and add MQTT adapter
- Updated .gitignore to exclude generated files and database - Modified package.json to change dev script and add new dependencies - Removed src/index.ts and created app.ts for Elysia server initialization - Added environment variable configuration in config/env.ts - Implemented MQTT adapter in adapter/mqtt.ts for message handling - Created Prisma client in prisma/client.ts and defined schema in prisma/schema.prisma - Added seeding script in prisma/seed.ts for measurement points - Established logging utility in utils/logger.ts for structured logging - Created bed router in routes/bed.ts for handling bed-related routes
This commit is contained in:
parent
b76d6b99ee
commit
a767dc3635
14 changed files with 801 additions and 14 deletions
7
.env
Normal file
7
.env
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Environment variables declared in this file are automatically made available to Prisma.
|
||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
||||
|
||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
DATABASE_URL="file:./data.db"
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -40,3 +40,10 @@ yarn-error.log*
|
|||
**/*.log
|
||||
package-lock.json
|
||||
**/*.bun
|
||||
/generated/prisma
|
||||
|
||||
/generated/prisma
|
||||
|
||||
/generated/prisma
|
||||
|
||||
/data.db
|
160
adapter/mqtt.ts
Normal file
160
adapter/mqtt.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
import mqtt, { type MqttClient } from "mqtt";
|
||||
import type { QoS } from "mqtt-packet";
|
||||
|
||||
export interface MQTTConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface MQTTSubscription {
|
||||
topic: string;
|
||||
callback: (topic: string, message: string) => void;
|
||||
}
|
||||
|
||||
export interface MQTTMessage {
|
||||
topic: string;
|
||||
payload: string;
|
||||
qos?: QoS;
|
||||
retain?: boolean;
|
||||
}
|
||||
|
||||
class MQTT {
|
||||
private client: MqttClient | null = null;
|
||||
private config: MQTTConfig | null = null;
|
||||
private readonly subscriptions: MQTTSubscription[] = [];
|
||||
private isConnected: boolean = false;
|
||||
private keepAliveTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
async keepAlive(): Promise<void> {
|
||||
if (this.isConnected) {
|
||||
return;
|
||||
}
|
||||
console.log("MQTT client is not connected, attempting to reconnect...");
|
||||
await this.connectMQTT();
|
||||
}
|
||||
|
||||
async initialize(config: MQTTConfig): Promise<void> {
|
||||
this.config = config;
|
||||
this.connectMQTT();
|
||||
|
||||
// Start keep-alive timer
|
||||
this.keepAliveTimer = setInterval(() => {
|
||||
this.keepAlive();
|
||||
}, 5000); // Run every 5 seconds
|
||||
}
|
||||
|
||||
async connectMQTT(): Promise<void> {
|
||||
if (!this.config) {
|
||||
throw new Error("MQTT configuration is not set.");
|
||||
}
|
||||
try {
|
||||
this.client = mqtt.connect(`mqtt://${this.config.host}:${this.config.port}`, {
|
||||
username: this.config?.username,
|
||||
password: this.config?.password
|
||||
});
|
||||
await this.setupHandler();
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to MQTT broker:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async publish(message: MQTTMessage): Promise<void> {
|
||||
if (!this.client || !this.isConnected) {
|
||||
console.error("MQTT client is not connected, cannot publish message.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { topic, payload, qos = 0, retain = false } = message;
|
||||
try {
|
||||
await this.client.publishAsync(topic, payload, { qos, retain });
|
||||
} catch (error) {
|
||||
console.error(`Failed to publish message to ${topic}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async subscribe(topic: string, callback: (topic: string, message: string) => void): Promise<void> {
|
||||
console.log(`Subscribing to topic: ${topic}`);
|
||||
const subscription: MQTTSubscription = { topic, callback };
|
||||
this.subscriptions.push(subscription);
|
||||
|
||||
if (this.client && this.isConnected) {
|
||||
try {
|
||||
await this.client.subscribeAsync(topic);
|
||||
} catch (error) {
|
||||
console.error(`Failed to subscribe to ${topic}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setupHandler(): Promise<void> {
|
||||
if (!this.client) {
|
||||
console.error("MQTT client is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.on("connect", this.onConnect.bind(this));
|
||||
this.client.on("message", this.onMessage.bind(this));
|
||||
this.client.on("disconnect", this.onDisconnect.bind(this));
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.keepAliveTimer) {
|
||||
clearInterval(this.keepAliveTimer);
|
||||
this.keepAliveTimer = null;
|
||||
}
|
||||
|
||||
if (this.client) {
|
||||
await this.client.endAsync();
|
||||
this.client = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
private async onConnect(): Promise<void> {
|
||||
console.log("MQTT connected successfully.");
|
||||
for (const sub of this.subscriptions) {
|
||||
if (this.client) {
|
||||
try {
|
||||
await this.client.subscribeAsync(sub.topic);
|
||||
} catch (error) {
|
||||
console.error(`Failed to subscribe to ${sub.topic}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.isConnected = true;
|
||||
}
|
||||
|
||||
private onDisconnect(): void {
|
||||
console.log("MQTT disconnected.");
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
private onMessage(topic: string, message: Buffer): void {
|
||||
const msg = message.toString();
|
||||
console.log(`Received message on topic ${topic}: ${msg}`);
|
||||
this.subscriptions.forEach(sub => {
|
||||
if (this.matchTopic(sub.topic, topic)) {
|
||||
sub.callback(topic, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private matchTopic(subscriptionTopic: string, publishTopic: string): boolean {
|
||||
// Exact match
|
||||
if (subscriptionTopic === publishTopic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert MQTT wildcards to regex
|
||||
const regexPattern = subscriptionTopic
|
||||
.replace(/\+/g, '[^/]+') // + matches single level
|
||||
.replace(/#$/, '.*'); // # matches multi-level (only at end)
|
||||
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(publishTopic);
|
||||
}
|
||||
}
|
||||
|
||||
export default MQTT;
|
39
app.ts
Normal file
39
app.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Elysia } from 'elysia';
|
||||
import { cors } from '@elysiajs/cors';
|
||||
|
||||
import { createTopicLogger } from '~/utils/logger';
|
||||
import env from './config/env';
|
||||
|
||||
async function initialize() {
|
||||
const logger = createTopicLogger({ topic: 'Initializer' });
|
||||
try {
|
||||
await initializeElysia();
|
||||
} catch (error) {
|
||||
logger.error(`Initialization error: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function initializeElysia() {
|
||||
const logger = createTopicLogger({ topic: 'Elysia' });
|
||||
const PORT = env.BACKEND_PORT;
|
||||
|
||||
// Instantiate and configure Elysia
|
||||
const app = new Elysia()
|
||||
// Logging Messages
|
||||
.onStart(() => {
|
||||
logger.info(`API server starting on port ${PORT}`);
|
||||
})
|
||||
.onStop(() => {
|
||||
logger.info('API server shutting down');
|
||||
})
|
||||
|
||||
// Core Components
|
||||
.use(cors())
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT);
|
||||
}
|
||||
|
||||
initialize();
|
122
bun.lock
Normal file
122
bun.lock
Normal file
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "elysia",
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.3.3",
|
||||
"elysia": "latest",
|
||||
"envalid": "^8.0.0",
|
||||
"winston": "^3.17.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.2.17",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||
|
||||
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
||||
|
||||
"@elysiajs/cors": ["@elysiajs/cors@1.3.3", "", { "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-mYIU6PyMM6xIJuj7d27Vt0/wuzVKIEnFPjcvlkyd7t/m9xspAG37cwNjFxVOnyvY43oOd2I/oW2DB85utXpA2Q=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.35", "", {}, "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
|
||||
|
||||
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||
|
||||
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
||||
|
||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"elysia": ["elysia@1.3.5", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="],
|
||||
|
||||
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||
|
||||
"envalid": ["envalid@8.0.0", "", { "dependencies": { "tslib": "2.6.2" } }, "sha512-PGeYJnJB5naN0ME6SH8nFcDj9HVbLpYIfg1p5lAyM9T4cH2lwtu2fLbozC/bq+HUUOIFxhX/LP0/GmlqPHT4tQ=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
|
||||
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
|
||||
|
||||
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||
|
||||
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="],
|
||||
|
||||
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||
|
||||
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
||||
|
||||
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||
|
||||
"tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||
|
||||
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
|
||||
}
|
||||
}
|
58
config/env.ts
Normal file
58
config/env.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { cleanEnv, str, bool, port, url, host } from "envalid";
|
||||
|
||||
const env = cleanEnv(process.env, {
|
||||
BACKEND_PORT: port({
|
||||
desc: "The port for the backend server",
|
||||
default: 3000,
|
||||
}),
|
||||
|
||||
DATABASE_URL: url({
|
||||
desc: "The URL for the database connection",
|
||||
}),
|
||||
|
||||
// Redis configuration
|
||||
REDIS_HOST: host({
|
||||
desc: "The host for the Redis server",
|
||||
default: "localhost",
|
||||
}),
|
||||
REDIS_PORT: port({
|
||||
desc: "The port for the Redis server",
|
||||
default: 6379,
|
||||
}),
|
||||
REDIS_PASSWORD: str({
|
||||
desc: "The password for the Redis server",
|
||||
}),
|
||||
|
||||
// S3 configuration
|
||||
S3_ENDPOINT: host({
|
||||
desc: "The endpoint for the S3 service",
|
||||
default: "localhost",
|
||||
}),
|
||||
S3_BUCKET: str({
|
||||
desc: "The name of the S3 bucket",
|
||||
default: "my-bucket",
|
||||
}),
|
||||
S3_PORT: port({
|
||||
desc: "The port for the S3 service",
|
||||
default: 9000,
|
||||
}),
|
||||
S3_USE_SSL: bool({
|
||||
desc: "Use SSL for S3 service",
|
||||
default: false,
|
||||
}),
|
||||
S3_ACCESS_KEY: str({
|
||||
desc: "Access key for the S3 service",
|
||||
}),
|
||||
S3_SECRET_KEY: str({
|
||||
desc: "Secret key for the S3 service",
|
||||
}),
|
||||
|
||||
// Log Level configuration
|
||||
LOG_LEVEL: str({
|
||||
desc: "The log level for the application",
|
||||
choices: ["debug", "info", "warn", "error", "silent"],
|
||||
default: "info",
|
||||
}),
|
||||
});
|
||||
|
||||
export default env;
|
12
package.json
12
package.json
|
@ -3,13 +3,19 @@
|
|||
"version": "1.0.50",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bun run --watch src/index.ts"
|
||||
"dev": "bun run --watch app.ts",
|
||||
"start": "bun run app.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"elysia": "latest"
|
||||
"@elysiajs/cors": "^1.3.3",
|
||||
"@prisma/client": "^6.10.1",
|
||||
"elysia": "latest",
|
||||
"envalid": "^8.0.0",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
"bun-types": "^1.2.17",
|
||||
"prisma": "^6.10.1"
|
||||
},
|
||||
"module": "src/index.js"
|
||||
}
|
5
prisma/client.ts
Normal file
5
prisma/client.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { PrismaClient } from "~/generated/prisma";
|
||||
|
||||
const db = new PrismaClient()
|
||||
|
||||
export default db;
|
81
prisma/schema.prisma
Normal file
81
prisma/schema.prisma
Normal file
|
@ -0,0 +1,81 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model MeasurementPoint {
|
||||
id String @id @default(cuid())
|
||||
sensorId String @unique // e.g., "head-1", "back-2"
|
||||
label String // e.g., "Head Left", "Upper Back Center"
|
||||
zone String // e.g., "head", "back", "shoulders"
|
||||
x Int // X coordinate on bed layout
|
||||
y Int // Y coordinate on bed layout
|
||||
pin Int // Hardware pin number
|
||||
|
||||
// Threshold configuration
|
||||
warningThreshold Int // Pressure value that triggers warning
|
||||
alarmThreshold Int // Pressure value that triggers alarm
|
||||
warningDelayMs Int // Delay before warning escalates to alarm
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
data MeasurementPointData[]
|
||||
alerts Alert[]
|
||||
}
|
||||
|
||||
model MeasurementPointData {
|
||||
id String @id @default(cuid())
|
||||
measurementPointId String
|
||||
|
||||
// Sensor reading data
|
||||
value Int // Analog sensor value (0-4095)
|
||||
|
||||
// Timestamps
|
||||
timestamp DateTime @default(now())
|
||||
time String // Formatted time string
|
||||
|
||||
// Relations
|
||||
measurementPoint MeasurementPoint @relation(fields: [measurementPointId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([measurementPointId, timestamp])
|
||||
}
|
||||
|
||||
enum AlertType {
|
||||
WARNING
|
||||
ALARM
|
||||
}
|
||||
|
||||
model Alert {
|
||||
id String @id @default(cuid())
|
||||
measurementPointId String
|
||||
|
||||
// Alert details
|
||||
type AlertType
|
||||
value Int // Sensor value that triggered alert
|
||||
threshold Int // Threshold that was exceeded
|
||||
|
||||
// Alert state
|
||||
acknowledged Boolean @default(false)
|
||||
silenced Boolean @default(false)
|
||||
|
||||
// Timing
|
||||
startTime DateTime @default(now())
|
||||
endTime DateTime?
|
||||
|
||||
// Relations
|
||||
measurementPoint MeasurementPoint @relation(fields: [measurementPointId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([measurementPointId, startTime])
|
||||
@@index([type, acknowledged])
|
||||
}
|
219
prisma/seed.ts
Normal file
219
prisma/seed.ts
Normal file
|
@ -0,0 +1,219 @@
|
|||
import db from "./client";
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding MeasurementPoints...');
|
||||
|
||||
// Delete existing measurement points
|
||||
await db.measurementPoint.deleteMany();
|
||||
|
||||
// Create measurement points based on API sensor configuration
|
||||
const measurementPoints = [
|
||||
// Head area - Higher thresholds due to critical nature, faster escalation
|
||||
{
|
||||
sensorId: "head-1",
|
||||
label: "Head Left",
|
||||
zone: "head",
|
||||
x: 45,
|
||||
y: 15,
|
||||
pin: 2,
|
||||
warningThreshold: 3000,
|
||||
alarmThreshold: 3500,
|
||||
warningDelayMs: 30000 // 30 seconds
|
||||
},
|
||||
{
|
||||
sensorId: "head-2",
|
||||
label: "Head Right",
|
||||
zone: "head",
|
||||
x: 55,
|
||||
y: 15,
|
||||
pin: 3,
|
||||
warningThreshold: 3000,
|
||||
alarmThreshold: 3500,
|
||||
warningDelayMs: 30000 // 30 seconds
|
||||
},
|
||||
|
||||
// Shoulder area - Moderate thresholds, medium escalation time
|
||||
{
|
||||
sensorId: "shoulder-1",
|
||||
label: "Left Shoulder",
|
||||
zone: "shoulders",
|
||||
x: 35,
|
||||
y: 25,
|
||||
pin: 4,
|
||||
warningThreshold: 2800,
|
||||
alarmThreshold: 3200,
|
||||
warningDelayMs: 45000 // 45 seconds
|
||||
},
|
||||
{
|
||||
sensorId: "shoulder-2",
|
||||
label: "Right Shoulder",
|
||||
zone: "shoulders",
|
||||
x: 65,
|
||||
y: 25,
|
||||
pin: 5,
|
||||
warningThreshold: 2800,
|
||||
alarmThreshold: 3200,
|
||||
warningDelayMs: 45000 // 45 seconds
|
||||
},
|
||||
|
||||
// Upper back - Moderate thresholds, 1 minute escalation
|
||||
{
|
||||
sensorId: "back-1",
|
||||
label: "Upper Back Left",
|
||||
zone: "back",
|
||||
x: 40,
|
||||
y: 35,
|
||||
pin: 6,
|
||||
warningThreshold: 2500,
|
||||
alarmThreshold: 3000,
|
||||
warningDelayMs: 60000 // 1 minute
|
||||
},
|
||||
{
|
||||
sensorId: "back-2",
|
||||
label: "Upper Back Center",
|
||||
zone: "back",
|
||||
x: 50,
|
||||
y: 35,
|
||||
pin: 7,
|
||||
warningThreshold: 2500,
|
||||
alarmThreshold: 3000,
|
||||
warningDelayMs: 60000 // 1 minute
|
||||
},
|
||||
{
|
||||
sensorId: "back-3",
|
||||
label: "Upper Back Right",
|
||||
zone: "back",
|
||||
x: 60,
|
||||
y: 35,
|
||||
pin: 8,
|
||||
warningThreshold: 2500,
|
||||
alarmThreshold: 3000,
|
||||
warningDelayMs: 60000 // 1 minute
|
||||
},
|
||||
|
||||
// Lower back/Hip area - Lower thresholds, 90 second escalation
|
||||
{
|
||||
sensorId: "hip-1",
|
||||
label: "Left Hip",
|
||||
zone: "hips",
|
||||
x: 35,
|
||||
y: 50,
|
||||
pin: 9,
|
||||
warningThreshold: 2200,
|
||||
alarmThreshold: 2800,
|
||||
warningDelayMs: 90000 // 90 seconds
|
||||
},
|
||||
{
|
||||
sensorId: "hip-2",
|
||||
label: "Lower Back",
|
||||
zone: "hips",
|
||||
x: 50,
|
||||
y: 50,
|
||||
pin: 10,
|
||||
warningThreshold: 2200,
|
||||
alarmThreshold: 2800,
|
||||
warningDelayMs: 90000 // 90 seconds
|
||||
},
|
||||
{
|
||||
sensorId: "hip-3",
|
||||
label: "Right Hip",
|
||||
zone: "hips",
|
||||
x: 65,
|
||||
y: 50,
|
||||
pin: 11,
|
||||
warningThreshold: 2200,
|
||||
alarmThreshold: 2800,
|
||||
warningDelayMs: 90000 // 90 seconds
|
||||
},
|
||||
|
||||
// Thigh area - Lower thresholds, 2 minute escalation
|
||||
{
|
||||
sensorId: "thigh-1",
|
||||
label: "Left Thigh",
|
||||
zone: "legs",
|
||||
x: 40,
|
||||
y: 65,
|
||||
pin: 12,
|
||||
warningThreshold: 2000,
|
||||
alarmThreshold: 2500,
|
||||
warningDelayMs: 120000 // 2 minutes
|
||||
},
|
||||
{
|
||||
sensorId: "thigh-2",
|
||||
label: "Right Thigh",
|
||||
zone: "legs",
|
||||
x: 60,
|
||||
y: 65,
|
||||
pin: 13,
|
||||
warningThreshold: 2000,
|
||||
alarmThreshold: 2500,
|
||||
warningDelayMs: 120000 // 2 minutes
|
||||
},
|
||||
|
||||
// Calf area (mock sensors - no pin) - Lower thresholds, 2.5 minute escalation
|
||||
{
|
||||
sensorId: "calf-1",
|
||||
label: "Left Calf",
|
||||
zone: "legs",
|
||||
x: 40,
|
||||
y: 75,
|
||||
pin: null,
|
||||
warningThreshold: 1800,
|
||||
alarmThreshold: 2200,
|
||||
warningDelayMs: 150000 // 2.5 minutes
|
||||
},
|
||||
{
|
||||
sensorId: "calf-2",
|
||||
label: "Right Calf",
|
||||
zone: "legs",
|
||||
x: 60,
|
||||
y: 75,
|
||||
pin: null,
|
||||
warningThreshold: 1800,
|
||||
alarmThreshold: 2200,
|
||||
warningDelayMs: 150000 // 2.5 minutes
|
||||
},
|
||||
|
||||
// Feet (mock sensors - no pin) - Lowest thresholds, 3 minute escalation
|
||||
{
|
||||
sensorId: "feet-1",
|
||||
label: "Left Foot",
|
||||
zone: "feet",
|
||||
x: 45,
|
||||
y: 85,
|
||||
pin: null,
|
||||
warningThreshold: 1500,
|
||||
alarmThreshold: 1800,
|
||||
warningDelayMs: 180000 // 3 minutes
|
||||
},
|
||||
{
|
||||
sensorId: "feet-2",
|
||||
label: "Right Foot",
|
||||
zone: "feet",
|
||||
x: 55,
|
||||
y: 85,
|
||||
pin: null,
|
||||
warningThreshold: 1500,
|
||||
alarmThreshold: 1800,
|
||||
warningDelayMs: 180000 // 3 minutes
|
||||
}
|
||||
];
|
||||
|
||||
// Insert all measurement points
|
||||
for (const point of measurementPoints) {
|
||||
await db.measurementPoint.create({
|
||||
data: point
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Created ${measurementPoints.length} measurement points`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await db.$disconnect();
|
||||
});
|
5
routes/bed.ts
Normal file
5
routes/bed.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Elysia from "elysia";
|
||||
|
||||
const bedRouter = new Elysia()
|
||||
|
||||
export default bedRouter;
|
|
@ -1,7 +0,0 @@
|
|||
import { Elysia } from "elysia";
|
||||
|
||||
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
|
@ -28,8 +28,10 @@
|
|||
"module": "ES2022", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
|
|
83
utils/logger.ts
Normal file
83
utils/logger.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { createLogger, format, transports, Logger, LeveledLogMethod } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Define log directory
|
||||
const LOG_DIR = path.join(process.cwd(), 'logs');
|
||||
|
||||
// Ensure log directory exists
|
||||
if (!fs.existsSync(LOG_DIR)) {
|
||||
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Interface for topic logger options
|
||||
interface TopicLoggerOptions {
|
||||
topic: string;
|
||||
level?: string;
|
||||
}
|
||||
|
||||
// Create the base logger
|
||||
const createBaseLogger = (level: string = 'debug') => {
|
||||
return createLogger({
|
||||
level,
|
||||
format: format.combine(
|
||||
format.timestamp(),
|
||||
format.json()
|
||||
),
|
||||
transports: [
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.printf(({ timestamp, level, message, topic, ...meta }) => {
|
||||
const topicStr = topic ? `[${topic}] ` : '';
|
||||
return `${timestamp} ${level}: ${topicStr}${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`;
|
||||
})
|
||||
)
|
||||
}),
|
||||
new transports.File({
|
||||
filename: path.join(LOG_DIR, 'combined.log')
|
||||
})
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
// Main logger instance
|
||||
const logger = createBaseLogger();
|
||||
|
||||
// Create a topic-specific logger
|
||||
const createTopicLogger = (options: TopicLoggerOptions): Logger => {
|
||||
const topicLogger = createBaseLogger(options.level);
|
||||
|
||||
// Create a wrapper that adds topic to all log messages
|
||||
const wrappedLogger = {
|
||||
...topicLogger,
|
||||
};
|
||||
|
||||
// Wrap each log level method to include topic
|
||||
(['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'] as const).forEach((level) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
wrappedLogger[level] = ((...args: any[]) => {
|
||||
// Handle different call patterns
|
||||
if (typeof args[0] === 'string') {
|
||||
const message = args[0];
|
||||
const meta = args[1] || {};
|
||||
return topicLogger[level]({
|
||||
message,
|
||||
topic: options.topic,
|
||||
...meta
|
||||
});
|
||||
} else {
|
||||
// If first argument is an object, add topic to it
|
||||
return topicLogger[level]({
|
||||
...args[0],
|
||||
topic: options.topic
|
||||
});
|
||||
}
|
||||
}) as LeveledLogMethod;
|
||||
});
|
||||
|
||||
return wrappedLogger as Logger;
|
||||
};
|
||||
|
||||
export { logger, createTopicLogger };
|
||||
export default logger;
|
Loading…
Add table
Add a link
Reference in a new issue