|
|
@@ -29,11 +29,10 @@ import {
|
|
|
MessageBody,
|
|
|
ConnectedSocket,
|
|
|
} from '@nestjs/websockets';
|
|
|
-import { Logger, OnModuleInit } from '@nestjs/common';
|
|
|
+import { Logger } from '@nestjs/common';
|
|
|
import { Server, Socket } from 'socket.io';
|
|
|
import { PalmOilService } from './palm-oil.service';
|
|
|
import { AnalysisResponse } from './interfaces/palm-analysis.interface';
|
|
|
-import { SurveillanceService, SystemMetrics } from '../surveillance/surveillance.service';
|
|
|
|
|
|
// ─── FIS Protocol Envelope ────────────────────────────────────────────────────
|
|
|
|
|
|
@@ -59,7 +58,6 @@ interface FisAppResponse {
|
|
|
operation: string;
|
|
|
complete: boolean;
|
|
|
message?: string; // JSON-encoded payload for data packets (complete: false)
|
|
|
- payload?: any; // Used only for push notifications (surveillance)
|
|
|
error?: string;
|
|
|
}
|
|
|
|
|
|
@@ -79,6 +77,16 @@ interface HistoryDeletePayload {
|
|
|
archiveId: string;
|
|
|
}
|
|
|
|
|
|
+interface EdgeResultPayload {
|
|
|
+ frame: string;
|
|
|
+ filename?: string;
|
|
|
+ batchId?: string;
|
|
|
+ detections: any[];
|
|
|
+ industrial_summary: Record<string, number>;
|
|
|
+ inference_ms: number;
|
|
|
+ processing_ms?: number;
|
|
|
+}
|
|
|
+
|
|
|
// ─── Env ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
const N8N_WEBHOOK_URL = process.env['N8N_WEBHOOK_URL'] ?? '';
|
|
|
@@ -89,35 +97,17 @@ const N8N_WEBHOOK_URL = process.env['N8N_WEBHOOK_URL'] ?? '';
|
|
|
cors: { origin: '*' },
|
|
|
})
|
|
|
export class VisionGateway
|
|
|
- implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, OnModuleInit
|
|
|
+ implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
|
|
|
{
|
|
|
@WebSocketServer()
|
|
|
private server!: Server;
|
|
|
|
|
|
private readonly logger = new Logger(VisionGateway.name);
|
|
|
|
|
|
- constructor(
|
|
|
- private readonly palmOilService: PalmOilService,
|
|
|
- private readonly surveillanceService: SurveillanceService,
|
|
|
- ) {}
|
|
|
+ constructor(private readonly palmOilService: PalmOilService) {}
|
|
|
|
|
|
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
- onModuleInit() {
|
|
|
- this.surveillanceService.registerMetricsCallback((metrics: SystemMetrics) => {
|
|
|
- const id = crypto.randomUUID();
|
|
|
- const packet: FisAppResponse = {
|
|
|
- id,
|
|
|
- messageID: id,
|
|
|
- serviceId: 'Surveillance',
|
|
|
- operation: 'metricsUpdate',
|
|
|
- complete: true,
|
|
|
- payload: metrics,
|
|
|
- };
|
|
|
- this.server?.to('telemetry-room').emit('response', packet);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
afterInit() {
|
|
|
this.logger.log('🔌 VisionGateway initialized on root namespace');
|
|
|
}
|
|
|
@@ -125,24 +115,9 @@ export class VisionGateway
|
|
|
handleConnection(client: Socket) {
|
|
|
client.data.sessionId = crypto.randomUUID();
|
|
|
this.logger.log(`📡 Vision client connected: ${client.id} (session: ${client.data.sessionId})`);
|
|
|
-
|
|
|
- const snapshot = this.surveillanceService.getLatestMetrics();
|
|
|
- if (snapshot) {
|
|
|
- const snapId = crypto.randomUUID();
|
|
|
- const packet: FisAppResponse = {
|
|
|
- id: snapId,
|
|
|
- messageID: snapId,
|
|
|
- serviceId: 'Surveillance',
|
|
|
- operation: 'metricsUpdate',
|
|
|
- complete: true,
|
|
|
- payload: snapshot,
|
|
|
- };
|
|
|
- client.emit('response', packet);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
handleDisconnect(client: Socket) {
|
|
|
- client.leave('telemetry-room');
|
|
|
this.logger.log(`🔌 Vision client disconnected: ${client.id}`);
|
|
|
}
|
|
|
|
|
|
@@ -155,15 +130,14 @@ export class VisionGateway
|
|
|
* Routing key: `${serviceId}:${operation}`
|
|
|
*
|
|
|
* Supported routes:
|
|
|
- * PalmVision:analyze — decode Base64 frame, run ONNX, persist, reply
|
|
|
- * History:getAll — fetch last 50 history records
|
|
|
- * History:delete — delete one record by archiveId
|
|
|
- * History:clearAll — wipe all records and archived images
|
|
|
- * Chat:send — proxy message to n8n webhook
|
|
|
- * Chat:clear — reset session UUID
|
|
|
- * PalmHistory:GetImage — stream archived image as Base64 data URL
|
|
|
- * Surveillance:SubscribeTelemetry — join telemetry-room for metric push
|
|
|
- * Surveillance:UnsubscribeTelemetry — leave telemetry-room
|
|
|
+ * PalmVision:analyze — decode Base64 frame, run ONNX, persist, reply
|
|
|
+ * History:getAll — fetch last 50 history records
|
|
|
+ * History:delete — delete one record by archiveId
|
|
|
+ * History:clearAll — wipe all records and archived images
|
|
|
+ * Chat:send — proxy message to n8n webhook
|
|
|
+ * Chat:clear — reset session UUID
|
|
|
+ * PalmHistory:GetImage — stream archived image as Base64 data URL
|
|
|
+ * PalmHistory:SaveExternalResult — persist edge-computed inference result to SQLite without re-running ONNX
|
|
|
*/
|
|
|
@SubscribeMessage('request')
|
|
|
async handleMessage(
|
|
|
@@ -316,6 +290,30 @@ export class VisionGateway
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
+ // ── PalmHistory:SaveExternalResult ─────────────────────────────────
|
|
|
+ case 'PalmHistory:SaveExternalResult': {
|
|
|
+ const edgePayload = (payload ?? {}) as EdgeResultPayload;
|
|
|
+
|
|
|
+ if (!edgePayload.frame) {
|
|
|
+ replyError('No frame data received');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const saved = await this.palmOilService.saveExternalResult(edgePayload);
|
|
|
+ this.logger.log(
|
|
|
+ `💾 PalmHistory:SaveExternalResult — SQLite write OK: ${saved.archive_id} | detections: ${saved.total_count} | edge_inference_ms: ${saved.inference_ms}`,
|
|
|
+ );
|
|
|
+
|
|
|
+ reply({
|
|
|
+ status: 'success',
|
|
|
+ archive_id: saved.archive_id,
|
|
|
+ total_count: saved.total_count,
|
|
|
+ inference_ms: saved.inference_ms,
|
|
|
+ created_at: saved.created_at,
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
// ── PalmHistory:GetImage ───────────────────────────────────────────
|
|
|
case 'PalmHistory:GetImage': {
|
|
|
const { archiveId } = (payload ?? {}) as HistoryDeletePayload;
|
|
|
@@ -343,31 +341,6 @@ export class VisionGateway
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- // ── Surveillance:SubscribeTelemetry ────────────────────────────────
|
|
|
- case 'Surveillance:SubscribeTelemetry': {
|
|
|
- await client.join('telemetry-room');
|
|
|
- this.logger.log(`📊 ${client.id} joined telemetry-room`);
|
|
|
- const snapshot = this.surveillanceService.getLatestMetrics();
|
|
|
- if (snapshot) {
|
|
|
- const snapId = crypto.randomUUID();
|
|
|
- client.emit('response', {
|
|
|
- id: snapId, messageID: snapId,
|
|
|
- serviceId: 'Surveillance', operation: 'metricsUpdate',
|
|
|
- complete: true, payload: snapshot,
|
|
|
- } satisfies FisAppResponse);
|
|
|
- }
|
|
|
- reply({ status: 'subscribed' });
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- // ── Surveillance:UnsubscribeTelemetry ──────────────────────────────
|
|
|
- case 'Surveillance:UnsubscribeTelemetry': {
|
|
|
- await client.leave('telemetry-room');
|
|
|
- this.logger.log(`📊 ${client.id} left telemetry-room`);
|
|
|
- reply({ status: 'unsubscribed' });
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
// ── Unknown route ──────────────────────────────────────────────────
|
|
|
default: {
|
|
|
this.logger.warn(`⚠️ Unknown route: ${route}`);
|