Skip to content

Commit

Permalink
feat: add TTN webhook endpoint and make it work
Browse files Browse the repository at this point in the history
  • Loading branch information
raska-vilem committed Jan 25, 2025
1 parent 1b07a10 commit 40ced4b
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 16 deletions.
2 changes: 2 additions & 0 deletions GAPP/apps/gapp-server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
import { carsController } from './controllers/cars.controller';
import sondehubPlugin from './plugins/sondehub';
import { sondesController } from './controllers/sondes.controller';

interface AppOptions extends FastifyPluginOptions {
influxDbToken: string;
Expand All @@ -31,6 +32,7 @@ export const app = async (fastify: FastifyInstance, opts: AppOptions) => {

// ROUTES
fastify.register(carsController, { prefix: '/cars' });
fastify.register(sondesController, { prefix: '/sondes' });

fastify.get(
'/ping',
Expand Down
23 changes: 23 additions & 0 deletions GAPP/apps/gapp-server/src/controllers/sondes.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
import { B_SondeTelemetry } from '../schemas';
import { ttnPacketDto } from '../utils/ttn-packet-dto';

export const sondesController: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.post(
'/telemetry',
{
schema: {
summary: 'TTN webhook',
description: 'Endpoint for receiving telemetry data from TTN',
body: B_SondeTelemetry,
},
},
async (req, rep) => {
const telemetryPacket = ttnPacketDto(req.body);

req.server.sondehub.addTelemetry(telemetryPacket);

rep.code(200).send('OK');
}
);
};
49 changes: 49 additions & 0 deletions GAPP/apps/gapp-server/src/plugins/mqttClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FastifyPluginCallback, FastifyPluginOptions } from 'fastify';
import mqtt from 'mqtt';
import fp from 'fastify-plugin';
import { Plugins } from './plugins';

interface MqttPluginOptions extends FastifyPluginOptions {
clientId: string;
host: string;
username: string;
password: string;
}

declare module 'fastify' {
interface FastifyInstance {
mqtt: mqtt.MqttClient;
}
}

const mqttPlugin: FastifyPluginCallback<MqttPluginOptions> = (fastify, options, done) => {
const client = mqtt.connect(options.host, {
clientId: options.clientId,
username: options.username,
password: options.password,
});

fastify.decorate('mqttClient', client);
fastify.addHook(
'onClose',
async () =>
new Promise<void>((resolve) => {
fastify.log.info('Closing MQTT client...');
client.end(() => {
fastify.log.info('MQTT client closed');
resolve();
});
})
);

client.on('error', (error: Error) => {
fastify.log.error(error);
});

client.on('connect', () => {
fastify.log.info('Connected to MQTT broker');
done();
});
};

export default fp(mqttPlugin, { name: Plugins.MQTT });
1 change: 1 addition & 0 deletions GAPP/apps/gapp-server/src/plugins/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Plugins {
INFLUXDB = 'influxdb',
SONDEHUB = 'sondehub',
MQTT = 'mqtt',
}
66 changes: 66 additions & 0 deletions GAPP/apps/gapp-server/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,69 @@ export const B_CarStatus = T.Object({
longitude: T.Number(),
altitude: T.Number(),
});

export const B_SondeTelemetry = T.Object({
end_device_ids: T.Object({
device_id: T.String(),
}),
received_at: T.String(),
uplink_message: T.Object({
f_port: T.Number(),
f_cnt: T.Number(),
frm_payload: T.String(),
decoded_payload: T.Object({
alt_m: T.Number(),
alt_okay: T.Number(),
course: T.Number(),
course_ok: T.Number(),
lat: T.Number(),
latlon_age_s: T.Number(),
latlon_ok: T.Number(),
lon: T.Number(),
speed_mps: T.Number(),
speed_ok: T.Number(),
}),
rx_metadata: T.Array(
T.Object({
gateway_ids: T.Object({
gateway_id: T.String(),
eui: T.String(),
}),
timestamp: T.Number(),
rssi: T.Number(),
signal_rssi: T.Optional(T.Number()),
channel_rssi: T.Optional(T.Number()),
snr: T.Number(),
uplink_token: T.String(),
received_at: T.String(),
})
),
settings: T.Object({
data_rate: T.Object({
lora: T.Object({
bandwidth: T.Number(),
spreading_factor: T.Number(),
coding_rate: T.String(),
}),
}),
frequency: T.String(),
timestamp: T.Number(),
}),
received_at: T.String(),
consumed_airtime: T.String(),
locations: T.Object({
'frm-payload': T.Object({
latitude: T.Number(),
longitude: T.Number(),
source: T.String(),
}),
}),
network_ids: T.Object({
net_id: T.String(),
ns_id: T.String(),
tenant_id: T.String(),
cluster_id: T.String(),
cluster_address: T.String(),
}),
}),
});
17 changes: 17 additions & 0 deletions GAPP/apps/gapp-server/src/utils/ttn-packet-dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Static } from '@sinclair/typebox';
import { B_SondeTelemetry } from '../schemas';
import { TelemetryPacket } from '@gapp/sondehub';

export const ttnPacketDto = (ttnPayload: Static<typeof B_SondeTelemetry>): TelemetryPacket => {
return {
time_received: ttnPayload.uplink_message.received_at,
payload_callsign: ttnPayload.end_device_ids.device_id,
datetime: new Date().toISOString(),
lat: ttnPayload.uplink_message.decoded_payload.lat,
lon: ttnPayload.uplink_message.decoded_payload.lon,
alt: ttnPayload.uplink_message.decoded_payload.alt_m,
heading: ttnPayload.uplink_message.decoded_payload.course,
modulation: 'LoRa',
uploader_callsign: 'GAPP-Server',
};
};
3 changes: 2 additions & 1 deletion GAPP/libs/sondehub/src/lib/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface BasePacket {
uploader_antenna?: string;
}

interface TelemetryPacket extends Partial<BasePacket> {
export interface TelemetryPacket extends Partial<BasePacket> {
dev?: string;
time_received?: string;
payload_callsign: string;
Expand All @@ -33,6 +33,7 @@ interface TelemetryPacket extends Partial<BasePacket> {
telemetry_hidden?: boolean;
historical?: boolean;
upload_time?: string;
modulation?: 'APRS' | 'Hours Binary' | 'RTTY' | 'LoRa' | 'WSPR';
}

type StationBasePayload = Partial<Omit<BasePacket, 'uploader_callsign' | 'uploader_position'>> &
Expand Down
Loading

0 comments on commit 40ced4b

Please sign in to comment.