4c1dc4082e
Build and Push Images / build-and-push (push) Has been cancelled
이전 세션들에서 작업된 아래 범위를 모두 포함: Fleet 서브시스템 (src/fleet/) - fleetDeviceService / fleetCommandService / fleetDeploymentService / fleetReleaseService - fleetMetricsService, fleetScriptService, fleetEdgeConfigService - Edge 디바이스 관리, 커맨드 발행, 배포/릴리스, 스크립트 동기화 Collector 확장 - centralMqttForwarder / centralForwarderConfigService - equipmentStateService, pythonHookRunner, scriptCache - Modbus/OPC-UA/S7/XGT 프로토콜 클라이언트 - targetDbIntrospection (저장 DB 조회) Routes / API - automationDashboardRoutes, centralForwarderRoutes, equipmentStateRoutes DB - importEdgeConfig (Python cached config → Pipeline DB) - seedDataSources (external_db_connections 초기 시드) 엣지 배포 리소스 - docker/edge/Dockerfile.backend.prod, Dockerfile.frontend.prod - docker/edge/docker-compose.edge.yml 프론트엔드 - admin/automaticMng (centralForwarder, dashboard, equipmentState) - admin/fleet (commands, devices, deployments, releases, scripts, alerts) - admin/pipeline-device 개선 (저장 DB 드롭다운, 태그 매핑 등) - ExternalDbConnectionModal, ScriptsManagerDialog 등 신규 컴포넌트 - lib/api: automationDashboard, centralForwarder, equipmentState, fleet docs/ - EDGE_SERVER_STRUCTURE, FLEET_COMPLETE, FLEET_EDGE_INTEGRATION, FLEET_HOOK_INTEGRATION Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
177 lines
5.5 KiB
TypeScript
177 lines
5.5 KiB
TypeScript
/**
|
|
* 내장 MQTT 브로커 (aedes)
|
|
* - Pipeline이 MQTT 브로커 역할까지 수행
|
|
* - aedes 내부 이벤트로 직접 publish 감지 (내부 client 불필요)
|
|
*
|
|
* 토픽 규칙 (vexplor_fleet 호환):
|
|
* vexplor/devices/{deviceId}/status - 디바이스 → 서버
|
|
* vexplor/devices/{deviceId}/metrics - 디바이스 → 서버
|
|
* vexplor/devices/{deviceId}/commands - 서버 → 디바이스
|
|
* vexplor/devices/{deviceId}/responses - 디바이스 → 서버
|
|
* vexplor/devices/{deviceId}/data - 디바이스 → 서버
|
|
*/
|
|
|
|
import Aedes from "aedes";
|
|
import { createServer } from "aedes-server-factory";
|
|
import { logger } from "../utils/logger";
|
|
|
|
const MQTT_PORT = parseInt(process.env.MQTT_PORT || "1883", 10);
|
|
const MQTT_WS_PORT = parseInt(process.env.MQTT_WS_PORT || "8083", 10);
|
|
|
|
type MessageHandler = (topic: string, payload: Buffer) => void | Promise<void>;
|
|
|
|
export class FleetMqttBroker {
|
|
private aedes: any;
|
|
private tcpServer: any;
|
|
private wsServer: any;
|
|
private started = false;
|
|
|
|
private messageHandlers = new Map<string, MessageHandler[]>();
|
|
|
|
constructor() {
|
|
this.aedes = (Aedes as any)({ id: "pipeline-mqtt-broker" });
|
|
|
|
this.aedes.on("client", (client) => {
|
|
logger.info(`[MQTT] 클라이언트 연결: ${client.id}`);
|
|
});
|
|
|
|
this.aedes.on("clientDisconnect", (client) => {
|
|
logger.info(`[MQTT] 클라이언트 연결 해제: ${client.id}`);
|
|
});
|
|
|
|
this.aedes.on("subscribe", (subscriptions, client) => {
|
|
logger.debug(
|
|
`[MQTT] 구독: ${subscriptions.map((s) => s.topic).join(", ")} (by ${client?.id})`,
|
|
);
|
|
});
|
|
|
|
// 모든 publish 감지
|
|
this.aedes.on("publish", (packet, client) => {
|
|
if (!packet.topic || packet.topic.startsWith("$SYS")) return;
|
|
// 내부에서 aedes.publish()로 보낸 것도 이 이벤트로 잡힘 - client가 null이면 내부 발행
|
|
if (client) {
|
|
this.dispatchMessage(packet.topic, packet.payload as Buffer);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 브로커 시작 (TCP + WebSocket)
|
|
*/
|
|
async start(): Promise<void> {
|
|
if (this.started) return;
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.tcpServer = createServer(this.aedes as any);
|
|
this.tcpServer.once("error", reject);
|
|
this.tcpServer.listen(MQTT_PORT, "0.0.0.0", () => {
|
|
logger.info(`[MQTT] TCP 브로커 시작: mqtt://0.0.0.0:${MQTT_PORT}`);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.wsServer = createServer(this.aedes as any, { ws: true });
|
|
this.wsServer.once("error", reject);
|
|
this.wsServer.listen(MQTT_WS_PORT, "0.0.0.0", () => {
|
|
logger.info(`[MQTT] WebSocket 브로커 시작: ws://0.0.0.0:${MQTT_WS_PORT}`);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
this.started = true;
|
|
logger.info("[MQTT] 브로커 완전 기동 (내부 publish 가능)");
|
|
}
|
|
|
|
/**
|
|
* 토픽 패턴 등록 (MQTT 와일드카드 지원: +, #)
|
|
*/
|
|
on(topicPattern: string, handler: MessageHandler): void {
|
|
if (!this.messageHandlers.has(topicPattern)) {
|
|
this.messageHandlers.set(topicPattern, []);
|
|
}
|
|
this.messageHandlers.get(topicPattern)!.push(handler);
|
|
}
|
|
|
|
private dispatchMessage(topic: string, payload: Buffer): void {
|
|
for (const [pattern, handlers] of this.messageHandlers) {
|
|
if (this.topicMatches(pattern, topic)) {
|
|
for (const handler of handlers) {
|
|
Promise.resolve(handler(topic, payload)).catch((e) =>
|
|
logger.error(`[MQTT] 핸들러 에러 (${pattern}):`, e),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private topicMatches(pattern: string, topic: string): boolean {
|
|
const pParts = pattern.split("/");
|
|
const tParts = topic.split("/");
|
|
|
|
for (let i = 0; i < pParts.length; i++) {
|
|
if (pParts[i] === "#") return true;
|
|
if (i >= tParts.length) return false;
|
|
if (pParts[i] === "+") continue;
|
|
if (pParts[i] !== tParts[i]) return false;
|
|
}
|
|
return pParts.length === tParts.length;
|
|
}
|
|
|
|
/**
|
|
* 서버 → 디바이스 메시지 발행 (aedes.publish 직접 사용)
|
|
*/
|
|
publish(topic: string, message: string | object, qos: 0 | 1 | 2 = 1): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.started) return reject(new Error("MQTT 브로커가 시작되지 않았습니다."));
|
|
const payload =
|
|
typeof message === "string" ? message : JSON.stringify(message);
|
|
(this.aedes as any).publish(
|
|
{
|
|
cmd: "publish",
|
|
qos,
|
|
topic,
|
|
payload: Buffer.from(payload),
|
|
retain: false,
|
|
},
|
|
(err: Error | null) => {
|
|
if (err) reject(err);
|
|
else resolve();
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 디바이스에 커맨드 발행
|
|
*/
|
|
sendCommandToDevice(deviceId: string, command: object): Promise<void> {
|
|
const topic = `vexplor/devices/${deviceId}/commands`;
|
|
return this.publish(topic, command, 1);
|
|
}
|
|
|
|
/**
|
|
* 토픽에서 deviceId 추출 (vexplor/devices/{deviceId}/...)
|
|
*/
|
|
static extractDeviceId(topic: string): string | null {
|
|
const m = topic.match(/^vexplor\/devices\/([^/]+)\//);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
if (this.tcpServer) this.tcpServer.close();
|
|
if (this.wsServer) this.wsServer.close();
|
|
this.aedes.close();
|
|
this.started = false;
|
|
}
|
|
}
|
|
|
|
let brokerInstance: FleetMqttBroker | null = null;
|
|
|
|
export function getFleetMqttBroker(): FleetMqttBroker {
|
|
if (!brokerInstance) {
|
|
brokerInstance = new FleetMqttBroker();
|
|
}
|
|
return brokerInstance;
|
|
}
|