75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
import {
|
|
Injectable,
|
|
Logger,
|
|
OnModuleDestroy,
|
|
OnModuleInit,
|
|
} from '@nestjs/common';
|
|
import { connect, MqttClient } from 'mqtt';
|
|
|
|
export type MqttMessageListener = (topic: string, payload: Buffer) => void;
|
|
|
|
@Injectable()
|
|
export class MqttClientService implements OnModuleInit, OnModuleDestroy {
|
|
private readonly logger = new Logger(MqttClientService.name);
|
|
private readonly listeners = new Set<MqttMessageListener>();
|
|
private client: MqttClient | null = null;
|
|
|
|
onModuleInit() {
|
|
const host = process.env.MQTT_HOST || 'mosquitto';
|
|
const port = Number(process.env.MQTT_PORT || 1883);
|
|
const username = process.env.MQTT_USERNAME;
|
|
const password = process.env.MQTT_PASSWORD;
|
|
if (!username || !password) {
|
|
throw new Error(
|
|
'MQTT_USERNAME and MQTT_PASSWORD are required; refusing to use a default broker credential.',
|
|
);
|
|
}
|
|
this.client = connect(`mqtt://${host}:${port}`, {
|
|
username,
|
|
password,
|
|
clientId: `ulhub-backend-${Math.random().toString(16).slice(2)}`,
|
|
});
|
|
|
|
this.client.on('connect', () => {
|
|
this.logger.log(`Connected to MQTT broker at ${host}:${port}`);
|
|
// devices/# = legacy device-direct namespace; ul/# = SRS §3.4.2 topic scheme
|
|
// (app + device-direct). QoS 1 so the broker redelivers durable log records the
|
|
// backend missed while disconnected (broker loss ≠ capture loss, SRS-SYN-1/7).
|
|
this.client!.subscribe(['devices/#', 'ul/#'], { qos: 1 }, (err) => {
|
|
if (err) {
|
|
this.logger.error('Failed to subscribe to devices/# + ul/#', err);
|
|
} else {
|
|
this.logger.log('Subscribed to devices/# and ul/#');
|
|
}
|
|
});
|
|
});
|
|
|
|
this.client.on('message', (topic, payload) => {
|
|
for (const listener of this.listeners) {
|
|
listener(topic, payload);
|
|
}
|
|
});
|
|
|
|
this.client.on('error', (error) => {
|
|
this.logger.error(`MQTT client error: ${error.message}`);
|
|
});
|
|
}
|
|
|
|
async onModuleDestroy() {
|
|
await this.client?.endAsync();
|
|
}
|
|
|
|
onMessage(listener: MqttMessageListener): () => void {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
}
|
|
|
|
publish(topic: string, payload: object) {
|
|
this.client?.publish(topic, JSON.stringify(payload), { qos: 1 }, (err) => {
|
|
if (err) {
|
|
this.logger.error(`Failed to publish to ${topic}: ${err.message}`);
|
|
}
|
|
});
|
|
}
|
|
}
|