Add live transmitter position (status) alongside logged points
devices/<serial>/log messages now carry a "type": "log" or "status". "log" persists a LocatePoint as before; "status" carries the same position + telemetry shape but is broadcast live over the job's realtime channel without touching locate_points — it's a current- position update, not a recorded point. The job map renders the latest status as a blue "you are here" marker (with a soft accuracy-radius halo) that moves in place as new updates arrive, separate from the colored polyline of logged points. Clicking it shows the same detail readout as a logged point. The simulator gained a message-type toggle (Log point / Status update) so both paths can be exercised from /sim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
@@ -139,10 +140,18 @@ export class MqttPointsMessageDto {
|
||||
points: MqttPointDto[];
|
||||
}
|
||||
|
||||
// Single-reading log entry for a locator that publishes directly (or is
|
||||
// relayed) to devices/<serial>/log — the serial comes from the topic itself,
|
||||
// so identity is job-anchored rather than publisher-credential-anchored.
|
||||
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/<serial>/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 {
|
||||
@IsIn(MQTT_LOG_MESSAGE_TYPES)
|
||||
type: MqttLogMessageType;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
jobId: string;
|
||||
|
||||
@@ -20,6 +20,9 @@ export class LogIngestService {
|
||||
// devices/<serial>/log carries the locator's identity in the topic itself
|
||||
// (no publisher device needs to be pre-registered); the job named in the
|
||||
// payload supplies the org, since the topic alone doesn't identify one.
|
||||
// `type` decides what happens to the reading: "log" persists a LocatePoint;
|
||||
// "status" is an ephemeral current-position update, broadcast live but
|
||||
// never written to locate_points.
|
||||
async handle(serial: string, rawPayload: string) {
|
||||
const msg = plainToInstance(MqttLogMessageDto, JSON.parse(rawPayload) as object);
|
||||
const errors = await validate(msg, { whitelist: true });
|
||||
@@ -36,6 +39,35 @@ export class LogIngestService {
|
||||
|
||||
const locator = await this.locatorRegistry.resolve(job.orgId, serial);
|
||||
|
||||
if (msg.type === 'status') {
|
||||
this.realtime.publish(`job:${job.id}`, {
|
||||
type: 'status',
|
||||
jobId: job.id,
|
||||
deviceId: locator.id,
|
||||
serial,
|
||||
lat: msg.lat,
|
||||
lng: msg.lng,
|
||||
altitude: msg.alt ?? null,
|
||||
fixType: msg.fix ?? 'NONE',
|
||||
utilityType: msg.utility ?? 'UNKNOWN',
|
||||
hAccuracy: msg.hAcc ?? null,
|
||||
vAccuracy: msg.vAcc ?? null,
|
||||
satellites: msg.sats ?? null,
|
||||
hdop: msg.hdop ?? null,
|
||||
depth: msg.depth ?? null,
|
||||
frequencyHz: msg.freqHz ?? null,
|
||||
currentMa: msg.currentMa ?? null,
|
||||
signalDb: msg.signalDb ?? null,
|
||||
gainDb: msg.gainDb ?? null,
|
||||
locateMode: msg.mode ?? null,
|
||||
phaseDeg: msg.phaseDeg ?? null,
|
||||
compassDeg: msg.compassDeg ?? null,
|
||||
distortionPct: msg.distortionPct ?? null,
|
||||
recordedAt: msg.ts,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const point = await this.prisma.locatePoint.create({
|
||||
data: {
|
||||
jobId: job.id,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||
import { MqttPointDto } from '../../ingest/dto/mqtt-messages.dto';
|
||||
import { MqttLogMessageDto } from '../../ingest/dto/mqtt-messages.dto';
|
||||
|
||||
// What the simulator UI sends the backend. Mirrors a devices/<serial>/log
|
||||
// payload plus the two fields that live in the topic/routing rather than the
|
||||
// wire payload itself (serial, jobId) so this DTO doubles as validation for
|
||||
// both the REST call and the outgoing MQTT message.
|
||||
export class SimPublishPointDto extends MqttPointDto {
|
||||
// What the simulator UI sends the backend: a devices/<serial>/log payload
|
||||
// (type: "log" persists a point, "status" is a live-only position update)
|
||||
// plus "serial", which lives in the topic rather than the wire payload.
|
||||
export class SimPublishPointDto extends MqttLogMessageDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(64)
|
||||
serial: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
jobId: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user