import { GpsFixType, LocateMode, UtilityType } from '@prisma/client'; import { ApiPropertyOptional, ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayMaxSize, ArrayMinSize, IsArray, IsDateString, IsEnum, IsIn, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, Max, MaxLength, Min, ValidateNested, } from 'class-validator'; // Field names here are terse (lat/lng/alt/hAcc/...) to keep MQTT payloads // small; this same shape is reused as the sim tool's REST request body. export class MqttPointDto { @ApiProperty({ minimum: -90, maximum: 90 }) @IsNumber() @Min(-90) @Max(90) lat: number; @ApiProperty({ minimum: -180, maximum: 180 }) @IsNumber() @Min(-180) @Max(180) lng: number; @ApiPropertyOptional({ description: 'Altitude, meters' }) @IsOptional() @IsNumber() alt?: number; @ApiPropertyOptional({ enum: GpsFixType }) @IsOptional() @IsEnum(GpsFixType) fix?: GpsFixType; @ApiPropertyOptional({ description: 'Horizontal accuracy, meters', minimum: 0 }) @IsOptional() @IsNumber() @Min(0) hAcc?: number; @ApiPropertyOptional({ description: 'Meters below grade', minimum: 0 }) @IsOptional() @IsNumber() @Min(0) depth?: number; @ApiPropertyOptional({ enum: UtilityType }) @IsOptional() @IsEnum(UtilityType) utility?: UtilityType; @ApiPropertyOptional({ description: 'Ordering within a locate run' }) @IsOptional() @IsInt() seq?: number; // GPS quality @ApiPropertyOptional({ description: 'Vertical accuracy, meters', minimum: 0 }) @IsOptional() @IsNumber() @Min(0) vAcc?: number; @ApiPropertyOptional({ minimum: 0 }) @IsOptional() @IsInt() @Min(0) sats?: number; @ApiPropertyOptional({ minimum: 0 }) @IsOptional() @IsNumber() @Min(0) hdop?: number; // Locator receiver telemetry @ApiPropertyOptional({ description: 'Locate frequency, Hz', minimum: 0 }) @IsOptional() @IsInt() @Min(0) freqHz?: number; @ApiPropertyOptional({ description: 'Signal current on the line, mA', minimum: 0 }) @IsOptional() @IsNumber() @Min(0) currentMa?: number; @ApiPropertyOptional({ description: 'Signal strength, dB' }) @IsOptional() @IsNumber() signalDb?: number; @ApiPropertyOptional({ description: 'Receiver gain, dB' }) @IsOptional() @IsNumber() gainDb?: number; @ApiPropertyOptional({ enum: LocateMode }) @IsOptional() @IsEnum(LocateMode) mode?: LocateMode; @ApiPropertyOptional({ description: 'Degrees' }) @IsOptional() @IsNumber() phaseDeg?: number; @ApiPropertyOptional({ description: 'Line direction, 0-360', minimum: 0, maximum: 360 }) @IsOptional() @IsNumber() @Min(0) @Max(360) compassDeg?: number; @ApiPropertyOptional({ minimum: 0, maximum: 100 }) @IsOptional() @IsNumber() @Min(0) @Max(100) distortionPct?: number; @ApiProperty({ format: 'date-time', description: 'Device GPS timestamp' }) @IsDateString() ts: string; } export class MqttPointsMessageDto { // Job is resolved by jobId when present, else by (device org, ticket) @IsOptional() @IsString() jobId?: string; @IsOptional() @IsString() @MaxLength(64) ticket?: string; // Locator receiver serial number; the publisher (MQTT credential) may be a // phone/gateway relaying for one or more locators. Auto-registered on first sight. @IsOptional() @IsString() @MaxLength(64) serial?: string; @IsArray() @ArrayMinSize(1) @ArrayMaxSize(500) @ValidateNested({ each: true }) @Type(() => MqttPointDto) points: MqttPointDto[]; } export const MQTT_LOG_MESSAGE_TYPES = ['log', 'status'] as const; export type MqttLogMessageType = (typeof MQTT_LOG_MESSAGE_TYPES)[number]; // Single reading for a locator that publishes directly (or is relayed) to // devices//log — the serial comes from the topic itself, so identity // is job-anchored rather than publisher-credential-anchored. `type` decides // what happens to it: "log" persists a LocatePoint; "status" is an ephemeral // current-position update, broadcast live but never written to the DB. export class MqttLogMessageDto extends MqttPointDto { @ApiProperty({ enum: MQTT_LOG_MESSAGE_TYPES, description: '"log" persists a point; "status" is live-only' }) @IsIn(MQTT_LOG_MESSAGE_TYPES) type: MqttLogMessageType; @ApiProperty({ description: 'Job this reading belongs to; also supplies the organization' }) @IsString() @IsNotEmpty() jobId: string; } export class MqttJobMessageDto { @IsString() @IsNotEmpty() @MaxLength(64) ticket: string; @IsOptional() @IsString() @MaxLength(200) title?: string; @IsOptional() @IsString() @MaxLength(4000) description?: string; @IsOptional() @IsString() @MaxLength(400) address?: string; }