Plumet Core is the central server component of the Plumet fall detection system. Built with NestJS, it provides a REST API, MQTT broker, and real-time alert orchestration for elderly care monitoring.
- MQTT Broker: Runs an embedded Aedes MQTT broker over WebSocket for device and mobile app communication
- Fall Event Pipeline: Receives fall detection events from ESP8266 devices and processes them in real-time
- User Alerting: Publishes fall alerts to mobile apps via MQTT, triggering push notifications
- Data Persistence: Stores fall events and device associations in MySQL database
- REST API: Provides endpoints for mobile apps to manage devices, users, and view fall history
flowchart TB
subgraph Clients["Clients"]
ESP["ESP8266<br/>Device"]
Mobile["Mobile App<br/>iOS/Android"]
end
subgraph Backend["Plumet Core"]
subgraph API["REST API<br/>NestJS"]
Auth["Auth Module<br/>JWT"]
Devices["Devices Module<br/>CRUD"]
Falls["Falls Module<br/>Statistics"]
Users["Users Module<br/>Profile"]
end
subgraph MQTT["MQTT Broker<br/>Aedes over WebSocket"]
Broker["Message Broker"]
AuthMQTT["Device/Auth<br/>Validator"]
Handler["Fall Handler<br/>Processor"]
end
DB[(MySQL<br/>Database)]
QR["QR Generator"]
end
ESP -->|"MQTT<br/>device/{MAC}/fall"| Broker
ESP -->|"Auth<br/>MAC Address"| AuthMQTT
Mobile -->|"MQTT<br/>plumet-mobile-{UID}"| Broker
Mobile -->|"REST<br/>HTTPS"| API
Broker -->|"Fall Event"| Handler
Handler -->|"Save Event"| DB
Handler -->|"Alert<br/>user/{UID}/alert"| Broker
API <-->|"Query"| DB
API -->|"Generate"| QR
Mobile -->|"Scan QR"| QR
style ESP fill:#EDD60A
style Mobile fill:#3b82f6
style Broker fill:#10b981
style DB fill:#6366f1
style QR fill:#ec4899
sequenceDiagram
participant ESP as ESP8266 Device
participant MQTT as MQTT Broker
participant Handler as Fall Handler
participant DB as Database
participant Mobile as Mobile App
Note over ESP,Mobile: Fall Detection Sequence
ESP->>+MQTT: Connect (MAC as Client ID)
MQTT-->>ESP: Authenticated
ESP->>MQTT: Publish to device/{MAC}/fall
Note right of ESP: { gForce: 3.2, baselineX: 0.1, ... }
MQTT->>+Handler: handleFallDetection()
Handler->>DB: Save fall event
Handler->>DB: Update device.lastFallAt
DB-->>Handler: Saved
Handler->>MQTT: Publish to user/{UID}/alert
Note right of Handler: For each associated user
MQTT->>+Mobile: Fall alert received
Mobile-->>MQTT: Acknowledge
Mobile-->>-Handler: Display notification
Handler-->>-MQTT: Complete
MQTT-->>-ESP: Disconnect (battery saving)
- Embedded MQTT Broker: Runs Aedes broker in-process for real-time messaging without external dependencies
- Dual Authentication Flow: Separate auth paths for ESP8266 devices (MAC address) and mobile apps (JWT)
- Event-Driven Architecture: Devices only connect when detecting falls, then disconnect immediately
- Eternal Association Tokens: QR codes printed on products use tokens that never expire and can be reused
- Graceful Shutdown: 10-second timeout ensures clean connection drain on container termination
- REST API - NestJS-based API with JWT authentication
- MQTT Broker - Embedded Aedes broker over WebSocket (port 8884)
- Device Management - Many-to-many user-device relationships with custom names
- Fall Statistics - Time-windowed aggregation (24h, 7d, 30d)
- QR Code Generation - Device provisioning via internal API
- Docker Support - Multi-arch images (amd64, arm64) for flexible deployment
- Quick Start
- Docker Deployment
- Environment Configuration
- API Documentation
- MQTT Protocol
- Database Schema
- Development
- Testing
- Node.js 24 or higher
- MySQL 8.0 or higher
- npm package manager
npm install cp .env.example .env npm run start:dev
The API will be available at http://localhost:3000/api/v1
docker pull ghcr.io/pcamposu/plumet-core:latest
docker run -d \
--name plumet-core \
-p 3000:3000 \
-p 8884:8884 \
-e DB_HOST=your-db-host \
-e DB_DATABASE=plumet \
-e DB_USERNAME=root \
-e DB_PASSWORD=your-password \
-e JWT_SECRET=your-secret-key \
ghcr.io/pcamposu/plumet-core:latestdocker build -t plumet-core .
docker run -d \
--name plumet-core \
-p 3000:3000 \
-p 8884:8884 \
--env-file .env \
plumet-coredocker-compose up -d
docker-compose logs -f
docker-compose down| Variable | Description | Example |
|---|---|---|
DB_HOST |
MySQL host | localhost |
DB_PORT |
MySQL port | 3306 |
DB_DATABASE |
Database name | plumet |
DB_USERNAME |
Database user | root |
DB_PASSWORD |
Database password | your-password |
JWT_SECRET |
JWT signing secret | random-secret-key |
JWT_EXPIRATION |
JWT TTL | 7d |
INTERNAL_API_SECRET |
Internal API auth | internal-secret-key |
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP server port |
API_PREFIX |
api/v1 |
API route prefix |
MQTT_WS_PORT |
8884 |
MQTT WebSocket port |
NODE_ENV |
development |
Environment mode |
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=plumet
DB_USERNAME=root
DB_PASSWORD=your-secure-password
JWT_SECRET=your-random-secret-key-min-32-chars
JWT_EXPIRATION=7d
INTERNAL_API_SECRET=your-internal-secret
PORT=3000
API_PREFIX=api/v1
MQTT_WS_PORT=8884
NODE_ENV=developmentPOST /api/v1/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!",
"name": "John Doe"
}
Response 201:
{
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "John Doe",
"isVerified": false,
"createdAt": "2025-01-25T10:00:00.000Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!"
}
Response 200:
{
"user": { ... },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}POST /api/v1/devices/associate
Authorization: Bearer <token>
Content-Type: application/json
{
"token": "ABC123XYZ",
"name": "Grandma"
}
Response 200:
{
"id": "association-uuid",
"device": {
"id": "device-uuid",
"macAddress": "AA:BB:CC:DD:EE:FF"
},
"name": "Grandma",
"associatedAt": "2025-01-25T10:00:00.000Z"
}GET /api/v1/devices
Authorization: Bearer <token>
Response 200:
[{
"id": "device-uuid",
"macAddress": "AA:BB:CC:DD:EE:FF",
"lastFallAt": "2025-01-25T09:30:00.000Z",
"customName": "Grandma"
}]GET /api/v1/falls/stats?deviceId=device-uuid
Authorization: Bearer <token>
Response 200:
{
"total": 42,
"last24h": 2,
"last7d": 8,
"last30d": 15,
"recent": [
{
"id": "fall-uuid",
"deviceId": "device-uuid",
"gForce": 3.2,
"detectedAt": "2025-01-25T09:30:00.000Z"
}
]
}| Property | Value |
|---|---|
| Protocol | MQTT over WebSocket |
| Host | localhost:8884 (or configured host) |
| Path | /mqtt |
| QoS | 1 |
Client ID Format: {MAC_ADDRESS} (uppercase, colon-separated)
No username/password required. Device is authenticated by MAC address existence in database.
const mqtt = require('mqtt');
const client = mqtt.connect('ws://localhost:8884/mqtt', {
clientId: 'AA:BB:CC:DD:EE:FF'
});
client.on('connect', () => {
});Client ID Format: plumet-mobile-{USER_ID}_{TIMESTAMP}
No username/password required. User is authenticated by JWT in REST API, then connects with user ID in client ID.
const client = mqtt.connect('ws://localhost:8884/mqtt', {
clientId: `plumet-mobile-${userId}_${Date.now()}`
});
client.subscribe(`user/${userId}/alert`);Topic: device/{MAC_ADDRESS}/fall
Payload:
{
"macAddress": "AA:BB:CC:DD:EE:FF",
"gForce": 3.2,
"baselineX": 0.1,
"baselineY": -0.2,
"baselineZ": 9.8
}Topic: user/{USER_ID}/alert
Payload:
{
"type": "fall_detected",
"deviceId": "device-uuid",
"deviceName": "Grandma",
"macAddress": "AA:BB:CC:DD:EE:FF",
"timestamp": "2025-01-25T09:30:00.000Z",
"data": {
"gForce": 3.2,
"baselineX": 0.1,
"baselineY": -0.2,
"baselineZ": 9.8
}
}Topic: device/{MAC_ADDRESS}/command
Used by backend to send commands to devices (future use).
erDiagram
Users ||--o{ DeviceUsers : "owns"
Devices ||--o{ DeviceUsers : "associated with"
Users ||--o{ Falls : "via device"
Devices ||--o{ Falls : "generates"
Users {
uuid id PK
string email UK
string password_hash
string name
boolean is_verified
timestamp created_at
timestamp updated_at
}
Devices {
uuid id PK
string mac_address UK
string association_token UK
timestamp last_fall_at
timestamp created_at
}
DeviceUsers {
uuid id PK
uuid device_id FK
uuid user_id FK
string name
timestamp associated_at
}
Falls {
uuid id PK
uuid device_id FK
float g_force
float baseline_x
float baseline_y
float baseline_z
timestamp detected_at
timestamp created_at
}
- Users ↔ Devices: Many-to-many via
DeviceUsersjoin table - Devices → Falls: One-to-many (one device can have many fall events)
- Cascade Delete: Deleting a user removes their device associations (not the devices)
src/
├── main.ts # Application entry point
├── app.module.ts # Root module configuration
├── common/ # Shared utilities
│ ├── decorators/ # Custom decorators (@CurrentUser, @Public)
│ ├── guards/ # Auth guards (JwtAuthGuard, InternalApiGuard)
│ ├── interceptors/ # Logging, response transformation
│ └── filters/ # Global exception filter
└── modules/ # Feature modules
├── auth/ # Authentication (JWT, local strategy)
├── users/ # User management
├── devices/ # Device CRUD & associations
├── falls/ # Fall events & statistics
├── mqtt/ # MQTT broker & handlers
└── qr/ # QR code generation
npm run test
npm run test:watch
npm run test:cov
npm run test:debugnpm run lint
npm run format
npm run buildnpm run test
npm run test auth.service.spec.ts
npm run test:cov- Fork the repository
- Create feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open Pull Request
git clone https://github.com/your-username/plumet-core.git
cd plumet-core
git remote add upstream https://github.com/pcamposu/plumet-core.git
git fetch upstream
git merge upstream/mainThis project is licensed under the GNU General Public License v3.0 - see the COPYING file for details.