Skip to content

Telemetry Server

BL-CZY edited this page Aug 29, 2026 · 4 revisions

The stack

NestJS

In Nest.js, everything is organized into modules, including the root AppModule itself. A module is a class annotated with @Module() that groups together related functionality. The decorator takes an object with four main fields:

  • imports — other modules whose exported providers are needed here
  • controllers — the set of controllers defined in this module (handle different HTTP requests)
  • providers — services/classes instantiated by Nest's DI container and usable within this module
  • exports — the subset of providers this module makes available to other modules that import it
@Module({
  imports: [SensorModule],
  controllers: [DashboardController],
  providers: [DashboardService],
  exports: [DashboardService],
})
export class DashboardModule {}

Docs

InfluxDB

InfluxDB is a time-series database optimized for storing and querying timestamped data that is commonly used for metrics, sensor readings, and IoT telemetry. Data is organized into buckets, written as points (measurement + tags + fields + timestamp), and queried with Flux or InfluxQL.

We use v2 here.

Example write script using the Node.js client:

const { InfluxDB, Point } = require('@influxdata/influxdb-client');

const url = 'http://localhost:8086';
const token = process.env.INFLUX_TOKEN;
const org = 'my-org';
const bucket = 'my-bucket';

const client = new InfluxDB({ url, token });
const writeApi = client.getWriteApi(org, bucket);

const point = new Point('temperature')
  .tag('sensor_id', 'sensor-01')
  .floatField('value', 23.5);

writeApi.writePoint(point);

writeApi.close()
  .then(() => console.log('Write finished'))
  .catch(e => console.error(e));

Docs

MQTT

MQTT is a lightweight publish/subscribe messaging protocol built for constrained devices and low-bandwidth networks. Clients publish messages to a topic (a hierarchical string like home/livingroom/temperature), and other clients subscribe to topics (optionally with wildcards + and #) to receive them via a central broker.

A basic MQTT message consists of:

  • Topic — the channel the message is published to
  • Payload — the actual message data (often JSON)
  • QoS level — 0 (at most once), 1 (at least once), 2 (exactly once)
  • Retain flag — whether the broker keeps the last message on that topic for new subscribers
  • Packet identifier — used internally for QoS 1/2 message tracking

Example payload:

{
  "sensor_id": "sensor-01",
  "value": 23.5,
  "timestamp": "2026-08-27T10:00:00Z"
}

Spec

Nest MQTT

We use the nest-mqtt module to read incoming MQTT messages from the pod

https://github.com/microud/nest-mqtt

MQTT messages

Header Content Subscriber Publisher Function
hyped/${podId}/controls/levitation_height height: number Pod Telemetry Server Sets levitation height for a pod
hyped/${podId}/controls/${control} control: string Pod Telemetry Server Sends Control Commands to pod
hyped/+/measurement/+ Params: rawParams: string[] where rawParams[0] is podId and rawParams[1] is measurementId; payload: rawValue: number Telemetry Server Pod Receives measurements and pipe it to /openmct/data/realtime; saves to the db
hyped/+/state Params: rawParams: string[] where rawParams[0] is podId; payload: rawValue: PodStateType Telemetry Server Pod Receives state and saves it to the db
hyped/+/logs Params: rawParams: string[] where rawParams[0] is podId; payload: rawValue: Buffer | string Telemetry Server Pod Receives logs from the pod and pipes it to /live-logs

Endpoints

The server is normally hosted at port ``. Here are the endpoints.

Path Type Params Return Function Module
/ping GET - "pong" - AppModule
/openmct/dictionary/pods GET - string Returns the IDs of all the pods. It reads the IDS from a config file in Appendix 1 OpenMCTModule
/openmct/dictionary/pods/:podId GET podId: string OpenMCTPod Returns the information of the pod at the moment, refer to Appendix 2 for the type OpenMCTModule
/openmct/dictionary/pods/:podId/measurements/:measurementKey GET podId: string, measurementKey: string OpenMctMeasurement Returns this specific measurement from this specific pod at the moment, refer to Appendix 2 for the type OpenMCTModule
/openmct/data/historical/pods/:podId/measurements/:measurementKey GET podId: string, measurementKey: string; queries: start: string, end: string {id: string, timestamp: string, value: OpenMctMeasurement}[] Returns the historical data of this specific measurement from this specific pod in the provided timeframe, refer to Appendix 2 and Appendix 3 for the type OpenMCTDataModule
/openmct/object-types GET - OpenMctObjectTypes Returns the obeject types, refer to Appendix 4 for the type OpenMctModule
/pods/:podId/controls/levitation-height POST podId: string; queries: height: number true Sets levitation height with hyped/${podId}/controls/levitation_height PodControlsModule
/pods/:podId/controls/:control POST podId: string, control: string true Publishes control commands to hyped/${podId}/controls/${control} PodControlsModule

Refer to Appendix 8 for endpoints on fault, public data, and remote logs

Websocket Gateways

Path Incoming Messages Outgoing Messages Function Module
/openmct/data/realtime SUBSCRIBE_TO_MEASUREMENT, UNSUBSCRIBE_FROM_MEASUREMENT MEASUREMENT_EVENT: MeasurementReading Subscribe/unsubscribe to measurements & receive specific measurements, refer to Appendix 5 and Appendix 6 for the types and messages OpenMCTDataModule
/openmct/faults/realtime SUBSCRIBE_TO_FAULTS, UNSUBSCRIBE_FROM_FAULTS FAULT_EVENT: {fault: OpenMCTFault} Subscribe/unsubscribe to fault & receive faults, refer to Appendix 7 for the types and messages FaultModule
/live-logs _ "log": string Subscribe to logs AppModule

Appendix 1

Exerpt from packages/constants/src/pods/pods-data.generated.ts

// Auto-generated - DO NOT EDIT
// Generated from config/pods.yaml
export const podsYamlContent = "pods:\n  the_podigal_son:\n    label: 'The Podigal Son'\n    mode: 'LEVITATION_ONLY'\n    measurements:\n      imd_iso_corrected:\n        label: 'IMD Corrected Resistance'\n        kind: 'resistance'\n        unit: 'Ω'\n        format: 'integer'\n        limits:\n          critical:\n            low: 500\n            high: 65535\n      accelerometer_1:\n        label: 'Accelerometer 1'\n        kind: 'acceleration'\n        unit: 'm/s²'\n        format: 'float'\n        limits:\n          critical:\n            low: -150\n            high: 150\n      accelerometer_2:\n        label: 'Accelerometer 2'\n        kind: 'acceleration'\n        unit: 'm/s²'\n        format: 'float'\n        limits:\n          critical:\n            low: -150\n            high: 150\n      accelerometer_3:\n        label: 'Accelerometer 3'\n        kind: 'acceleration'\n        unit: 'm/s²'\n        format: 'float'\n        limits:\n          critical:\n            low: -150\n            high: 150\n      accelerometer_4:\n        label: 'Accelerometer 4'\n        kind: 'acceleration'\n        unit: 'm/s²'\n        format: 'float'\n        limits:\n          critical:\n            low: -150\n            high: 150\n      accelerometer_avg:\n        label: 'Accelerometer Average'\n        kind: 'acceleration'\n        unit: 'm/s²'\n        format: 'float'\n        limits:\n          critical:\n            low: -150\n            high: 150\n      displacement:\n        label: 'Displacement'\n        kind: 'displacement'\n        unit: 'm'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      velocity:\n        label: 'Velocity'\n        kind: 'velocity'\n        unit: 'm/s'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 50\n      acceleration:\n        label: 'Acceleration'\n        kind: 'acceleration'\n        unit: 'm/s²'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 5\n      pressure_back_pull:\n        label: 'Pressure – Back Pull'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: -0.2\n            high: 5.5\n          warning:\n            low: -0.19\n            high: 5.2\n      pressure_front_pull:\n        label: 'Pressure – Front Pull'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: -0.2\n            high: 5.5\n          warning:\n            low: -0.19\n            high: 5.2\n      pressure_front_push:\n        label: 'Pressure – Front Push'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: -0.2\n            high: 5.5\n          warning:\n            low: -0.19\n            high: 5.2\n      pressure_back_push:\n        label: 'Pressure – Back Push'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: -0.2\n            high: 5.5\n          warning:\n            low: -0.19\n            high: 5.2\n      pressure_brakes_reservoir:\n        label: 'Pressure – Brakes Reservoir'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: 3\n            high: 7.4\n          warning:\n            low: 3.5\n            high: 6.9\n      pressure_active_suspension_reservoir:\n        label: 'Pressure – Active Suspension Reservoir'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: 3\n            high: 7.4\n          warning:\n            low: 3.5\n            high: 6.9\n      pressure_front_brake:\n        label: 'Pressure – Front Brake'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: -0.2\n            high: 4.2\n          warning:\n            low: -0.19\n            high: 4\n      pressure_back_brake:\n        label: 'Pressure – Back Brake'\n        kind: 'pressure'\n        unit: 'bar'\n        format: 'float'\n        limits:\n          critical:\n            low: -0.2\n            high: 4.2\n          warning:\n            low: -0.19\n            high: 4\n      thermistor_1:\n        label: 'Thermistor 1'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_2:\n        label: 'Thermistor 2'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_3:\n        label: 'Thermistor 3'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_4:\n        label: 'Thermistor 4'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_5:\n        label: 'Thermistor 5'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_6:\n        label: 'Thermistor 6'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_7:\n        label: 'Thermistor 7'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_8:\n        label: 'Thermistor 8'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_9:\n        label: 'Thermistor 9'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_10:\n        label: 'Thermistor 10'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_11:\n        label: 'Thermistor 11'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      thermistor_12:\n        label: 'Thermistor 12'\n        kind: 'temperature'\n        unit: '°C'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      hall_effect_1:\n        label: 'Hall Effect 1'\n        kind: 'magnetism'\n        unit: 'mT'\n        format: 'float'\n        limits:\n          critical:\n            low: -100\n            high: 100\n      hall_effect_2:\n        label: 'Hall Effect 2'\n        kind: 'magnetism'\n        unit: 'mT'\n        format: 'float'\n        limits:\n          critical:\n            low: -100\n            high: 100\n      keyence_1:\n        label: 'Keyence 1'\n        kind: 'keyence'\n        unit: 'number of stripes'\n        format: 'integer'\n        limits:\n          critical:\n            low: 0\n            high: 16\n          warning:\n            low: 5\n            high: 10\n      keyence_2:\n        label: 'Keyence 2'\n        kind: 'keyence'\n        unit: 'number of stripes'\n        format: 'integer'\n        limits:\n          critical:\n            low: 0\n            high: 16\n          warning:\n            low: 5\n            high: 10\n      power_line_resistance:\n        label: 'Power Line Resistance'\n        kind: 'resistance'\n        unit: 'kΩ'\n        format: 'integer'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      levitation_height_1:\n        label: 'Levitation Height 1'\n        kind: 'levitation'\n        unit: 'mm'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      levitation_height_2:\n        label: 'Levitation Height 2'\n        kind: 'levitation'\n        unit: 'mm'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      levitation_height_3:\n        label: 'Levitation Height 3'\n        kind: 'levitation'\n        unit: 'mm'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      levitation_height_4:\n        label: 'Levitation Height 4'\n        kind: 'levitation'\n        unit: 'mm'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      levitation_height_lateral_1:\n        label: 'Levitation Height Lateral 1'\n        kind: 'levitation'\n        unit: 'mm'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n      levitation_height_lateral_2:\n        label: 'Levitation Height Lateral 2'\n        kind: 'levitation'\n        unit: 'mm'\n        format: 'float'\n        limits:\n          critical:\n            low: 0\n            high: 100\n    statuses:\n      brake_clamp_status:\n        label: 'Brake Clamp Status'\n        kind: 'binary-status'\n        format: 'enum'\n        values:\n          - value: 1\n            label: 'CLAMPED'\n          - value: 0\n            label: 'UNCLAMPED'\n      pod_raised_status:\n        label: 'Pod Raised Status'\n        kind: 'binary-status'\n        format: 'enum'\n        values:\n          - value: 1\n            label: 'RAISED'\n          - value: 0\n            label: 'LOWERED'\n      battery_status:\n        label: 'Battery Status'\n        kind: 'binary-status'\n        format: 'enum'\n        values:\n          - value: 1\n            label: 'HEALTHY'\n          - value: 0\n            label: 'UNHEALTHY'\n      motor_controller_status:\n        label: 'Motor Controller Status'\n        kind: 'binary-status'\n        format: 'enum'\n        values:\n          - value: 1\n            label: 'HEALTHY'\n          - value: 0\n            label: 'UNHEALTHY'\n      high_power_status:\n        label: 'High Power Status'\n        kind: 'binary-status'\n        format: 'enum'\n        values:\n          - value: 1\n            label: 'ACTIVE'\n          - value: 0\n            label: 'OFF'\n";

Appendix 2

Excerpt from packages/types/src/openmct/openmct-dictionary.types.ts

/**
 * Type of an Open MCT measurement.
 */
export type OpenMctMeasurement = {
	name: string;
	key: string;
	type: string;
	values: {
		key: string;
		name: string;
		unit?: string;
		format: string;
		min?: number;
		max?: number;
		limits?: MeasurementLimits;
		enumerations?: {
			value: number;
			string: string;
		}[];
		hints?: {
			range?: number;
			domain?: number;
		};
		source?: string;
		units?: {
			domain: string;
		};
	}[];
};

/**
 * Type of an Open MCT pod.
 */
export type OpenMctPod = {
	id: string;
	name: string;
	measurements: OpenMctMeasurement[];
};

/**
 * Type of an Open MCT dictionary.
 */
export type OpenMctDictionary = Record<string, OpenMctPod>;

Appendix 3

Excerpt from packages/server/modules/openmct/data/historical/HistoricalTelemetryData.service.ts

public async getHistoricalReading(
    podId: string,
    measurementKey: string,
    startTimestamp: string,
    endTimestamp: string,
) {
    const fluxStart = fluxDateTime(
        new Date(Number.parseInt(startTimestamp)).toISOString(),
    );
    const fluxEnd = fluxDateTime(
        new Date(Number.parseInt(endTimestamp)).toISOString(),
    );

    const query = flux`
  from(bucket: "${INFLUX_TELEMETRY_BUCKET}")
    |> range(start: ${fluxStart}, stop: ${fluxEnd})
    |> filter(fn: (r) => r["measurementKey"] == "${measurementKey}")
    |> filter(fn: (r) => r["podId"] == "${podId}")`;

    try {
        const data =
            await this.influxService.query.collectRows<InfluxHistoricalRow>(query);

        return data.map((row) => ({
            id: row.measurementKey,
            timestamp: new Date(row._time).getTime(),
            value: row._value,
        }));
    } catch (e: unknown) {
        this.logger.error(
            `Failed to get historical reading for {${podId}/${measurementKey}}`,
            e,
            HistoricalTelemetryDataService.name,
        );
        throw new HttpException("Couldn't get historical reading", 500);
    }
}

Appendix 4

Excerpt from packages/types/src/openmct/openmct-object-types.types.ts

export type OpenMctObjectType = {
	id: string;
	name: string;
	description?: string;
	icon: string;
};

export type OpenMctObjectTypes = OpenMctObjectType[];

Appendix 5

Excerpt from packages/server/src/modules/telemetry/MeasurementReading.types.ts

export const MeasurementReadingSchema = z.object({
	podId: zodEnumFromObjKeys(pods),
	measurementKey: z.string(),
	timestamp: z.string(), // to handle nanoseconds timestamp
	value: z.number(),
});

export type MeasurementReading = z.infer<typeof MeasurementReadingSchema>;

Appendix 6

Excerpt from packages/constants/src/socket/index.ts

export const EVENTS = {
	SUBSCRIBE_TO_MEASUREMENT: 'SubscribeToMeasurement',
	UNSUBSCRIBE_FROM_MEASUREMENT: 'UnsubscribeFromMeasurement',
	SUBSCRIBE_TO_FAULTS: 'SubscribeToFaults',
	UNSUBSCRIBE_FROM_FAULTS: 'UnsubscribeFromFaults',
};

Appendix 7

Excerpt from packages/types/src/openmct/openmct-fault.types.ts

export type OpenMctFault = {
	type: null | string;
	fault: {
		acknowledged: boolean;
		currentValueInfo: {
			value: number;
			rangeCondition: string;
			monitoringResult: string;
		};
		id: string;
		name: string;
		namespace: string;
		seqNum: number;
		severity: string;
		shelved: boolean;
		shortDescription: string;
		triggerTime: string;
		triggerValueInfo: {
			value: number;
			rangeCondition: string;
			monitoringResult: string;
		};
	};
};

export type HistoricalFaults = {
	faultId: string;
	timestamp: number;
	openMctFault: OpenMctFault;
	podId: string;
	measurementKey: string;
}[];

export type OpenMctHistoricalFaults = {
	timestamp: number;
	fault: OpenMctFault;
}[];

Appendix 8

Excerpt from packages/server/src/modules/openmct/faults/Fault.controller.ts

@Controller('openmct/faults')
export class FaultsController {
	constructor(private faultsService: FaultService) {}

	@Post('acknowledge')
	acknowledgeFault(
		@Body() { faultId, comment }: { faultId: string; comment: string },
	) {
		return this.faultsService.acknowledgeFault(faultId, comment);
	}

	@Post('shelve')
	shelveFault(
		@Body()
		{
			faultId,
			shelved,
			shelveDuration,
			comment,
		}: {
			faultId: string;
			shelved: boolean;
			shelveDuration: number;
			comment: string;
		},
	) {
		return this.faultsService.shelveFault(
			faultId,
			shelved,
			shelveDuration,
			comment,
		);
	}
}

Excerpt from packages/server/src/modules/remote-logs/RemoteLogs.controller.ts

@Controller('logs')
export class RemoteLogsController {
	constructor(private remoteLogsService: RemoteLogsService) {}

	@Post()
	logUIMessage(@Body() body: { message: string }) {
		return this.remoteLogsService.logRemoteMessage(body.message);
	}

	@Post(':podId')
	logUIMessageWithPodID(
		@Param('podId') podId: string,
		@Body() body: { message: string },
	) {
		return this.remoteLogsService.logRemoteMessageWithPodID(
			podId,
			body.message,
		);
	}
}

Excerpt from packages/server/src/modules/public-data/PublicData.module.ts

@Module({
	imports: [InfluxModule],
	controllers: [PublicDataController],
	providers: [PublicDataService, HistoricalTelemetryDataService],
})
export class PublicDataModule {}

Clone this wiki locally