feat: reworked ttn webhook endpoint logic
This commit is contained in:
		| @ -7,8 +7,6 @@ CREATE TABLE IF NOT EXISTS lp_ttn_end_device_uplinks ( | ||||
|     dev_addr VARCHAR(255), | ||||
|     received_at_utc DATE, | ||||
|     battery NUMERIC, | ||||
|     latitude DOUBLE, | ||||
|     longitude DOUBLE, | ||||
|     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
| @ -1,12 +1,8 @@ | ||||
| import express, { Request, Response } from "express"; | ||||
| import { container } from "tsyringe"; | ||||
| import { domainEventEmitter } from "../config/eventEmitter"; | ||||
| import { | ||||
|   TtnMessageReceivedEvent, | ||||
|   TtnMessageReceivedEventName, | ||||
| } from "../event/ttnMessageReceivedEvent"; | ||||
| import { validateData } from "../middleware/validationMiddleware"; | ||||
| import { TtnMessage } from "../models/ttnMessage"; | ||||
| import { LocationService } from "../services/locationService"; | ||||
| import { LpTtnEndDeviceUplinksService } from "../services/lpTtnEndDeviceUplinksService"; | ||||
| import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService"; | ||||
| import { WifiScanService } from "../services/wifiScanService"; | ||||
| @ -20,6 +16,8 @@ const ttnGatewayReceptionService = container.resolve( | ||||
| ); | ||||
| const wifiScanService = container.resolve(WifiScanService); | ||||
|  | ||||
| const locationService = container.resolve(LocationService); | ||||
|  | ||||
| const router = express.Router(); | ||||
|  | ||||
| router.post( | ||||
| @ -28,8 +26,8 @@ router.post( | ||||
|   async (req: Request, res: Response) => { | ||||
|     try { | ||||
|       const message = req.body as TtnMessage; | ||||
|       // Create uplink record | ||||
|       const { lp_ttn_end_device_uplinks_id, latitude, longitude } = | ||||
|  | ||||
|       const { lp_ttn_end_device_uplinks_id } = | ||||
|         await lpTtnEndDeviceUplinksService.createUplink({ | ||||
|           device_id: message.end_device_ids.device_id, | ||||
|           application_ids: | ||||
| @ -41,14 +39,17 @@ router.post( | ||||
|           battery: message.uplink_message.decoded_payload?.messages[0].find( | ||||
|             (e) => e.type === "Battery" | ||||
|           )?.measurementValue, | ||||
|           latitude: message.uplink_message.decoded_payload?.messages[0].find( | ||||
|             (e) => e.type === "Latitude" | ||||
|           )?.measurementValue, | ||||
|           longitude: message.uplink_message.decoded_payload?.messages[0].find( | ||||
|             (e) => e.type === "Longitude" | ||||
|           )?.measurementValue, | ||||
|         }); | ||||
|  | ||||
|       const gnnsLocation = { | ||||
|         latitude: message.uplink_message.decoded_payload?.messages[0].find( | ||||
|           (e) => e.type === "Latitude" | ||||
|         )?.measurementValue, | ||||
|         longitude: message.uplink_message.decoded_payload?.messages[0].find( | ||||
|           (e) => e.type === "Longitude" | ||||
|         )?.measurementValue, | ||||
|       }; | ||||
|  | ||||
|       const wifiScans = | ||||
|         message.uplink_message.decoded_payload?.messages[0] | ||||
|           .find((e) => e.type === "Wi-Fi Scan") | ||||
| @ -58,39 +59,50 @@ router.post( | ||||
|             rssi: w.rssi, | ||||
|           })) ?? []; | ||||
|  | ||||
|       console.log(wifiScans); | ||||
|  | ||||
|       const ttnGatewayReceptions = message.uplink_message.rx_metadata.map( | ||||
|         (g) => ({ | ||||
|           lp_ttn_end_device_uplinks_id, | ||||
|           gateway_id: g.gateway_ids.gateway_id, | ||||
|           eui: g.gateway_ids.eui, | ||||
|           rssi: g.rssi, | ||||
|           latitude: g.location.latitude, | ||||
|           longitude: g.location.longitude, | ||||
|           altitude: g.location.altitude, | ||||
|           latitude: g.location?.latitude, | ||||
|           longitude: g.location?.longitude, | ||||
|           altitude: g.location?.altitude, | ||||
|         }) | ||||
|       ); | ||||
|  | ||||
|       const event: TtnMessageReceivedEvent = { | ||||
|         lp_ttn_end_device_uplinks_id, | ||||
|         wifis: wifiScans.map((w) => ({ mac: w.mac, rssi: w.rssi })), | ||||
|         ttnGateways: ttnGatewayReceptions.map((g) => ({ | ||||
|           rssi: g.rssi, | ||||
|           altitude: g.altitude, | ||||
|           latitude: g.latitude, | ||||
|           longitude: g.longitude, | ||||
|         })), | ||||
|         gnssLocation: { latitude, longitude } | ||||
|       const createDatabaseEntries = async () => { | ||||
|         const [wifiResults, gatewayResults] = await Promise.all([ | ||||
|           wifiScanService.createWifiScans(wifiScans), | ||||
|           ttnGatewayReceptionService.filterAndInsertGatewayReception( | ||||
|             ttnGatewayReceptions | ||||
|           ), | ||||
|         ]); | ||||
|  | ||||
|         locationService.createLocationFromTriangulation({ | ||||
|           lp_ttn_end_device_uplinks_id, | ||||
|           wifi: wifiResults.map(({ latitude, longitude, rssi }) => ({ | ||||
|             latitude, | ||||
|             longitude, | ||||
|             rssi, | ||||
|           })), | ||||
|           ttn_gw: gatewayResults.map(({ latitude, longitude, rssi }) => ({ | ||||
|             latitude, | ||||
|             longitude, | ||||
|             rssi, | ||||
|           })), | ||||
|           gnss: | ||||
|             gnnsLocation.latitude && gnnsLocation.longitude | ||||
|               ? { | ||||
|                   latitude: gnnsLocation.latitude, | ||||
|                   longitude: gnnsLocation.longitude, | ||||
|                 } | ||||
|               : undefined, | ||||
|         }); | ||||
|       }; | ||||
|  | ||||
|       domainEventEmitter.emit(TtnMessageReceivedEventName, event); | ||||
|  | ||||
|       await Promise.all([ | ||||
|         wifiScanService.createWifiScans(wifiScans), | ||||
|         ttnGatewayReceptionService.createGatewayReceptions( | ||||
|           ttnGatewayReceptions | ||||
|         ), | ||||
|       ]); | ||||
|  | ||||
|       createDatabaseEntries().then(); | ||||
|       res.status(200); | ||||
|     } catch (error) { | ||||
|       console.log(error); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import express, { Request, Response } from "express"; | ||||
| import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService"; | ||||
| import { container } from "tsyringe"; | ||||
| import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService"; | ||||
|  | ||||
| const ttnGatewayReceptionService = container.resolve( | ||||
|   TtnGatewayReceptionService | ||||
| @ -35,7 +35,7 @@ router.get("/:id", async (req: Request, res: Response) => { | ||||
| router.post("/", async (req: Request, res: Response) => { | ||||
|   try { | ||||
|     const newGatewayReception = | ||||
|       await ttnGatewayReceptionService.createGatewayReception(req.body); | ||||
|       await ttnGatewayReceptionService.createTtnGatewayReception(req.body); | ||||
|     res.status(201).json(newGatewayReception); | ||||
|   } catch (error) { | ||||
|     res.status(500).json({ error: "Error creating gateway reception" }); | ||||
| @ -46,7 +46,10 @@ router.put("/:id", async (req: Request, res: Response) => { | ||||
|   try { | ||||
|     const { id } = req.params; | ||||
|     const updatedGatewayReception = | ||||
|       await ttnGatewayReceptionService.updateGatewayReception(id, req.body); | ||||
|       await ttnGatewayReceptionService.updateGatewayReception({ | ||||
|         ...req.body, | ||||
|         ttn_gateway_reception_id: id, | ||||
|       }); | ||||
|     if (!updatedGatewayReception) { | ||||
|       res.status(404).json({ error: "Gateway reception not found" }); | ||||
|       return; | ||||
|  | ||||
| @ -40,7 +40,10 @@ router.post("/", async (req: Request, res: Response) => { | ||||
| router.put("/:id", async (req: Request, res: Response) => { | ||||
|   try { | ||||
|     const { id } = req.params; | ||||
|     const updatedWifiScan = await wifiScanService.updateWifiScan(id, req.body); | ||||
|     const updatedWifiScan = await wifiScanService.updateWifiScan({ | ||||
|       ...req.body, | ||||
|       wifi_scan_id: id, | ||||
|     }); | ||||
|     if (!updatedWifiScan) { | ||||
|       res.status(404).json({ error: "Wifi scan not found" }); | ||||
|       return; | ||||
|  | ||||
| @ -1,18 +0,0 @@ | ||||
| export const TtnMessageReceivedEventName = "TtnMessageReceived"; | ||||
| export type TtnMessageReceivedEvent = { | ||||
|   lp_ttn_end_device_uplinks_id: string; | ||||
|   wifis: { | ||||
|     mac: string; | ||||
|     rssi: number; | ||||
|   }[]; | ||||
|   ttnGateways: { | ||||
|     rssi: number; | ||||
|     latitude: number; | ||||
|     longitude: number; | ||||
|     altitude: number; | ||||
|   }[]; | ||||
|   gnssLocation: { | ||||
|     latitude?: number; | ||||
|     longitude?: number; | ||||
|   } | ||||
| }; | ||||
| @ -1,184 +0,0 @@ | ||||
| import { container } from "tsyringe"; | ||||
| import { domainEventEmitter } from "../config/eventEmitter"; | ||||
| import { | ||||
|   TtnMessageReceivedEvent, | ||||
|   TtnMessageReceivedEventName, | ||||
| } from "../event/ttnMessageReceivedEvent"; | ||||
| import { getLocationForWifiMemoized } from "../proxy/wigle"; | ||||
| import { LocationService } from "../services/locationService"; | ||||
| import { WifiScanService } from "../services/wifiScanService"; | ||||
|  | ||||
| const locationService = container.resolve(LocationService); | ||||
| const wifiScanService = container.resolve(WifiScanService); | ||||
|  | ||||
| const CalculateTtnGatewayLocation = (event: TtnMessageReceivedEvent) => { | ||||
|   // Get location based on TTN Gateways | ||||
|   const virtualLocation = { | ||||
|     latitude: undefined as number | undefined, | ||||
|     longitude: undefined as number | undefined, | ||||
|   }; | ||||
|  | ||||
|   if (!event.ttnGateways || event.ttnGateways.length === 0) { | ||||
|     console.log("No TTN Gateway location received!"); | ||||
|   } else { | ||||
|     let totalWeight = 0; | ||||
|     let weightedLatitude = 0; | ||||
|     let weightedLongitude = 0; | ||||
|  | ||||
|     event.ttnGateways.forEach((gw) => { | ||||
|       const weight = 1 / Math.abs(gw.rssi); // Higher RSSI (closer to 0) gives more weight | ||||
|       totalWeight += weight; | ||||
|       weightedLatitude += gw.latitude * weight; | ||||
|       weightedLongitude += gw.longitude * weight; | ||||
|     }); | ||||
|  | ||||
|     // Calculate the weighted average to get the virtual location | ||||
|     const virtualLocation = { | ||||
|       latitude: weightedLatitude / totalWeight, | ||||
|       longitude: weightedLongitude / totalWeight, | ||||
|     }; | ||||
|  | ||||
|     console.log( | ||||
|       "Tracker location based on TTN Gateway location:", | ||||
|       virtualLocation | ||||
|     ); | ||||
|   } | ||||
|   return { | ||||
|     ttn_latitude: virtualLocation.latitude, | ||||
|     ttn_longitude: virtualLocation.longitude, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| const CalculateWifiLocation = async (event: TtnMessageReceivedEvent) => { | ||||
|   // Get location based on WiFi Scans | ||||
|   const virtualLocation = { | ||||
|     latitude: undefined as number | undefined, | ||||
|     longitude: undefined as number | undefined, | ||||
|   }; | ||||
|  | ||||
|   if (!event.wifis || event.wifis.length === 0) { | ||||
|     console.log("No WiFi scans received!"); | ||||
|   } else { | ||||
|     // Process Wi-Fi data to compute weighted location | ||||
|     let wifiScans = await Promise.all( | ||||
|       event.wifis.map(async (wifi) => { | ||||
|         // Create new WiFi Scan entry if wigle.net reported location | ||||
|         const apiResponse = await getLocationForWifiMemoized(wifi.mac);; | ||||
|         // Only return valid data wifiScans (location for MAC was found) | ||||
|         if ((apiResponse?.success == true) && (apiResponse.totalResults > 0)) { | ||||
|           return { | ||||
|             lp_ttn_end_device_uplinks_id: event.lp_ttn_end_device_uplinks_id, | ||||
|             mac: wifi.mac, | ||||
|             rssi: wifi.rssi, | ||||
|             latitude: apiResponse?.results[0].trilat, | ||||
|             longitude: apiResponse?.results[0].trilong, | ||||
|           } | ||||
|         } | ||||
|         return undefined; | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     const wifiScansFiltered = wifiScans.filter(w => (w !== undefined)); | ||||
|  | ||||
|     // Store valid wifiScans into DB | ||||
|     const locatedWifiScans = await wifiScanService.createWifiScans(wifiScansFiltered); | ||||
|  | ||||
|     if (locatedWifiScans.length !== 0) { | ||||
|       const { totalWeight, weightedLatitude, weightedLongitude } = | ||||
|         locatedWifiScans.reduce( | ||||
|           (acc, { latitude, longitude, rssi }) => { | ||||
|             const weight = 1 / Math.abs(rssi); | ||||
|             acc.totalWeight += weight; | ||||
|             acc.weightedLatitude += latitude * weight; | ||||
|             acc.weightedLongitude += longitude * weight; | ||||
|             return acc; | ||||
|           }, | ||||
|           { | ||||
|             totalWeight: 0, | ||||
|             weightedLatitude: 0, | ||||
|             weightedLongitude: 0, | ||||
|           } | ||||
|         ); | ||||
|  | ||||
|       // Calculate the weighted average to get the virtual location | ||||
|       virtualLocation.latitude = weightedLatitude / totalWeight; | ||||
|       virtualLocation.longitude = weightedLongitude / totalWeight; | ||||
|  | ||||
|     console.log( | ||||
|       "Tracker location based on WiFi Scan location:", | ||||
|       virtualLocation | ||||
|     ); | ||||
|   } | ||||
|   return { | ||||
|     wifi_latitude: virtualLocation.latitude, | ||||
|     wifi_longitude: virtualLocation.longitude, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| const CalculateGnssLocation = (event: TtnMessageReceivedEvent) => { | ||||
|   // Get location based on reported GNSS | ||||
|   if ( | ||||
|     event.gnssLocation.latitude === undefined || | ||||
|     event.gnssLocation.longitude === undefined | ||||
|   ) { | ||||
|     console.log("No valid GNSS location received!"); | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     gnss_latitude: event.gnssLocation.latitude, | ||||
|     gnss_longitude: event.gnssLocation.longitude, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| domainEventEmitter.on( | ||||
|   TtnMessageReceivedEventName, | ||||
|   async (event: TtnMessageReceivedEvent) => { | ||||
|     console.log(event); | ||||
|  | ||||
|     var wifi_based_latitude: number | undefined = undefined; | ||||
|     var wifi_based_longitude: number | undefined = undefined; | ||||
|     var gnss_based_latitude: number | undefined = undefined; | ||||
|     var gnss_based_longitude: number | undefined = undefined; | ||||
|     var ttn_gw_based_latitude: number | undefined = undefined; | ||||
|     var ttn_gw_based_longitude: number | undefined = undefined; | ||||
|  | ||||
|     if (event.ttnGateways && event.ttnGateways.length > 0) { | ||||
|       const virtualLocation = CalculateTtnGatewayLocation(event); | ||||
|       console.log(virtualLocation); | ||||
|       ttn_gw_based_latitude = virtualLocation.ttn_latitude; | ||||
|       ttn_gw_based_longitude = virtualLocation.ttn_longitude; | ||||
|     } | ||||
|  | ||||
|     if (event.wifis && event.wifis.length > 0) { | ||||
|       const virtualLocation = await CalculateWifiLocation(event); | ||||
|       wifi_based_latitude = virtualLocation.wifi_latitude; | ||||
|       wifi_based_longitude = virtualLocation.wifi_longitude; | ||||
|     } | ||||
|  | ||||
|     const virtualLocation = CalculateGnssLocation(event); | ||||
|     gnss_based_latitude = virtualLocation.gnss_latitude; | ||||
|     gnss_based_longitude = virtualLocation.gnss_longitude; | ||||
|  | ||||
|     console.log({ | ||||
|       lp_ttn_end_device_uplinks_id: event.lp_ttn_end_device_uplinks_id, | ||||
|       ttn_gw_latitude: ttn_gw_based_latitude, | ||||
|       ttn_gw_longitude: ttn_gw_based_longitude, | ||||
|       gnss_latitude: gnss_based_latitude, | ||||
|       gnss_longitude: gnss_based_longitude, | ||||
|       wifi_latitude: wifi_based_latitude, | ||||
|       wifi_longitude: wifi_based_longitude, | ||||
|     }); | ||||
|  | ||||
|     const newLocation = await locationService.createLocation({ | ||||
|       lp_ttn_end_device_uplinks_id: event.lp_ttn_end_device_uplinks_id, | ||||
|       ttn_gw_latitude: ttn_gw_based_latitude, | ||||
|       ttn_gw_longitude: ttn_gw_based_longitude, | ||||
|       gnss_latitude: gnss_based_latitude, | ||||
|       gnss_longitude: gnss_based_longitude, | ||||
|       wifi_latitude: wifi_based_latitude, | ||||
|       wifi_longitude: wifi_based_longitude, | ||||
|     }); | ||||
|  | ||||
|     console.log(newLocation); | ||||
|   } | ||||
| ); | ||||
| @ -1,7 +1,6 @@ | ||||
| import dotenv from "dotenv"; | ||||
| import express from "express"; | ||||
| import "reflect-metadata"; | ||||
| import "./eventHandler/ttnMessageReceivedEventHandler"; | ||||
| const cors = require("cors"); | ||||
|  | ||||
| import locationRoutes from "./controller/locationController"; | ||||
|  | ||||
| @ -20,35 +20,27 @@ Location.init( | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     lp_ttn_end_device_uplinks_id: { | ||||
|       type: DataTypes.UUID, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     wifi_latitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     wifi_longitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     gnss_latitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     gnss_longitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     ttn_gw_latitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     ttn_gw_longitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     created_at_utc: { | ||||
|       type: DataTypes.DATE, | ||||
|  | ||||
| @ -10,8 +10,6 @@ export class LpTtnEndDeviceUplinks extends Model { | ||||
|   public dev_addr!: string; | ||||
|   public received_at_utc!: Date; | ||||
|   public battery!: number; | ||||
|   public latitude!: number; | ||||
|   public longitude!: number; | ||||
|   public created_at_utc!: Date; | ||||
|   public updated_at_utc!: Date; | ||||
| } | ||||
| @ -30,35 +28,21 @@ LpTtnEndDeviceUplinks.init( | ||||
|     }, | ||||
|     application_ids: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     dev_eui: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     join_eui: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     dev_addr: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     received_at_utc: { | ||||
|       type: DataTypes.DATE, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     battery: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     latitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     longitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     created_at_utc: { | ||||
|       type: DataTypes.DATE, | ||||
|  | ||||
| @ -32,23 +32,18 @@ TtnGatewayReception.init( | ||||
|     }, | ||||
|     eui: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     rssi: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     latitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     longitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     altitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     created_at_utc: { | ||||
|       type: DataTypes.DATE, | ||||
|  | ||||
| @ -6,14 +6,14 @@ export interface TtnMessage { | ||||
|     }; | ||||
|     dev_eui: string; | ||||
|     join_eui: string; | ||||
|     dev_addr: string; | ||||
|     dev_addr?: string; | ||||
|   }; | ||||
|   correlation_ids: string[]; | ||||
|   received_at: string; | ||||
|   uplink_message: { | ||||
|     session_key_id: string; | ||||
|     session_key_id?: string; | ||||
|     f_port?: number; | ||||
|     f_cnt: number; | ||||
|     f_cnt?: number; | ||||
|     frm_payload?: string; | ||||
|     decoded_payload?: { | ||||
|       err: number; | ||||
| @ -22,8 +22,8 @@ export interface TtnMessage { | ||||
|           { | ||||
|             measurementId: "4200"; | ||||
|             measurementValue: any[]; | ||||
|             motionId: number; | ||||
|             timestamp: number; | ||||
|             motionId?: number; | ||||
|             timestamp?: number; | ||||
|             type: "Event Status"; | ||||
|           }, | ||||
|           { | ||||
| @ -32,29 +32,29 @@ export interface TtnMessage { | ||||
|               mac: string; | ||||
|               rssi: number; | ||||
|             }[]; | ||||
|             motionId: number; | ||||
|             timestamp: number; | ||||
|             motionId?: number; | ||||
|             timestamp?: number; | ||||
|             type: "Wi-Fi Scan"; | ||||
|           }, | ||||
|           { | ||||
|             measurementId: "3000"; | ||||
|             measurementValue: number; | ||||
|             motionId: number; | ||||
|             timestamp: number; | ||||
|             motionId?: number; | ||||
|             timestamp?: number; | ||||
|             type: "Battery"; | ||||
|           }, | ||||
|           { | ||||
|             measurementId: "4197"; | ||||
|             measurementValue: number; | ||||
|             motionId: number; | ||||
|             timestamp: number; | ||||
|             motionId?: number; | ||||
|             timestamp?: number; | ||||
|             type: "Longitude"; | ||||
|           }, | ||||
|           { | ||||
|             measurementId: "4198"; | ||||
|             measurementValue: number; | ||||
|             motionId: number; | ||||
|             timestamp: number; | ||||
|             motionId?: number; | ||||
|             timestamp?: number; | ||||
|             type: "Latitude"; | ||||
|           } | ||||
|         ] | ||||
| @ -67,44 +67,44 @@ export interface TtnMessage { | ||||
|         gateway_id: string; | ||||
|         eui?: string; | ||||
|       }; | ||||
|       time: string; | ||||
|       time?: string; | ||||
|       timestamp?: number; | ||||
|       rssi: number; | ||||
|       channel_rssi: number; | ||||
|       snr: number; | ||||
|       location: { | ||||
|       snr?: number; | ||||
|       location?: { | ||||
|         latitude: number; | ||||
|         longitude: number; | ||||
|         altitude: number; | ||||
|         altitude?: number; | ||||
|         source?: string; | ||||
|       }; | ||||
|       uplink_token: string; | ||||
|       uplink_token?: string; | ||||
|       channel_index?: number; | ||||
|       received_at: string; | ||||
|       received_at?: string; | ||||
|     }[]; | ||||
|     settings: { | ||||
|       data_rate: { | ||||
|         lora: { | ||||
|           bandwidth: number; | ||||
|           spreading_factor: number; | ||||
|           coding_rate: string; | ||||
|           coding_rate?: string; | ||||
|         }; | ||||
|       }; | ||||
|       frequency: string; | ||||
|       timestamp?: number; | ||||
|       time?: Date; | ||||
|     }; | ||||
|     received_at: Date; | ||||
|     received_at?: Date; | ||||
|     confirmed?: boolean; | ||||
|     consumed_airtime: string; | ||||
|     version_ids: { | ||||
|     consumed_airtime?: string; | ||||
|     version_ids?: { | ||||
|       brand_id: string; | ||||
|       model_id: string; | ||||
|       hardware_version: string; | ||||
|       firmware_version: string; | ||||
|       band_id: string; | ||||
|     }; | ||||
|     network_ids: { | ||||
|     network_ids?: { | ||||
|       net_id: string; | ||||
|       ns_id: string; | ||||
|       tenant_id: string; | ||||
|  | ||||
| @ -30,15 +30,15 @@ WifiScan.init( | ||||
|     }, | ||||
|     rssi: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     latitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     longitude: { | ||||
|       type: DataTypes.NUMBER, | ||||
|       allowNull: true, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     created_at_utc: { | ||||
|       type: DataTypes.DATE, | ||||
|  | ||||
| @ -47,6 +47,7 @@ export const getLocationForWifi = async ( | ||||
|   try { | ||||
|     const url = `${process.env.WIGLE_BASE_URL!}${process.env | ||||
|       .WIGLE_NETWORK_SEARCH!}?netid=${encodeURIComponent(mac)}`; | ||||
|  | ||||
|     const response = await fetch(url, { | ||||
|       method: "GET", | ||||
|       headers: { | ||||
| @ -54,12 +55,12 @@ export const getLocationForWifi = async ( | ||||
|         Authorization: `Basic ${process.env.WIGLE_TOKEN}`, | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     if (response.ok) { | ||||
|       return await response.json(); | ||||
|     } | ||||
|     console.log(response.status); | ||||
|     return undefined; | ||||
|  | ||||
|   } catch (error) { | ||||
|     console.error("Error during call of API wigle.net:", error); | ||||
|   } | ||||
|  | ||||
| @ -2,6 +2,39 @@ import { inject, injectable } from "tsyringe"; | ||||
| import { Location } from "../models/location"; | ||||
| import { LocationRepository } from "../repositories/locationRepository"; | ||||
|  | ||||
| interface CreateLocationParams { | ||||
|   lp_ttn_end_device_uplinks_id: string; | ||||
|   wifi?: Coordinates; | ||||
|   gnss?: Coordinates; | ||||
|   ttn_gw?: Coordinates; | ||||
| } | ||||
|  | ||||
| interface CreateLocationTriangulationParams { | ||||
|   lp_ttn_end_device_uplinks_id: string; | ||||
|   wifi: LocationSignal[]; | ||||
|   ttn_gw: LocationSignal[]; | ||||
|   gnss?: Coordinates; | ||||
| } | ||||
|  | ||||
| interface LocationSignal extends Coordinates { | ||||
|   rssi: number; | ||||
| } | ||||
|  | ||||
| interface Coordinates { | ||||
|   latitude: number; | ||||
|   longitude: number; | ||||
| } | ||||
|  | ||||
| interface UpdateTtnGatewayReceptionParams { | ||||
|   ttn_gateway_reception_id: string; | ||||
|   gateway_id?: string; | ||||
|   eui?: string; | ||||
|   rssi?: number; | ||||
|   latitude?: number; | ||||
|   longitude?: number; | ||||
|   altitude?: number; | ||||
| } | ||||
|  | ||||
| @injectable() | ||||
| export class LocationService { | ||||
|   constructor( | ||||
| @ -17,8 +50,30 @@ export class LocationService { | ||||
|     return this.repository.findById(id); | ||||
|   } | ||||
|  | ||||
|   public async createLocation(data: Partial<Location>) { | ||||
|     return this.repository.create(data); | ||||
|   public async createLocation(data: CreateLocationParams) { | ||||
|     return this.repository.create({ | ||||
|       lp_ttn_end_device_uplinks_id: data.lp_ttn_end_device_uplinks_id, | ||||
|       wifi_latitude: data.wifi?.latitude, | ||||
|       wifi_longitude: data.wifi?.longitude, | ||||
|       ttn_gw_latitude: data.ttn_gw?.latitude, | ||||
|       ttn_gw_longitude: data.ttn_gw?.longitude, | ||||
|       gnss_latitude: data.gnss?.latitude, | ||||
|       gnss_longitude: data.gnss?.longitude, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   public async createLocationFromTriangulation( | ||||
|     data: CreateLocationTriangulationParams | ||||
|   ) { | ||||
|     const wifi_location = this.calculateVirtualLocation(data.wifi); | ||||
|     const gateway_location = this.calculateVirtualLocation(data.ttn_gw); | ||||
|  | ||||
|     return this.createLocation({ | ||||
|       lp_ttn_end_device_uplinks_id: data.lp_ttn_end_device_uplinks_id, | ||||
|       wifi: wifi_location, | ||||
|       ttn_gw: gateway_location, | ||||
|       gnss: data.gnss, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   public async updateLocation(id: string, data: Partial<Location>) { | ||||
| @ -28,4 +83,25 @@ export class LocationService { | ||||
|   public async deleteLocation(id: string) { | ||||
|     return this.repository.delete(id); | ||||
|   } | ||||
|  | ||||
|   private calculateVirtualLocation(locations: LocationSignal[]) { | ||||
|     if (locations.length === 0) return undefined; | ||||
|  | ||||
|     const { totalWeight, weightedLatitude, weightedLongitude } = | ||||
|       locations.reduce( | ||||
|         (acc, { latitude, longitude, rssi }) => { | ||||
|           const weight = 1 / Math.abs(rssi); | ||||
|           acc.totalWeight += weight; | ||||
|           acc.weightedLatitude += latitude * weight; | ||||
|           acc.weightedLongitude += longitude * weight; | ||||
|           return acc; | ||||
|         }, | ||||
|         { totalWeight: 0, weightedLatitude: 0, weightedLongitude: 0 } | ||||
|       ); | ||||
|  | ||||
|     return { | ||||
|       latitude: weightedLatitude / totalWeight, | ||||
|       longitude: weightedLongitude / totalWeight, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,26 @@ | ||||
| import { inject, injectable } from "tsyringe"; | ||||
| import { TtnGatewayReception } from "../models/ttnGatewayReception"; | ||||
| import { TtnGatewayReceptionRepository } from "../repositories/ttnGatewayReceptionRepository"; | ||||
|  | ||||
| interface CreateTtnGatewayReceptionParams { | ||||
|   lp_ttn_end_device_uplinks_id: string; | ||||
|   gateway_id: string; | ||||
|   eui?: string; | ||||
|   rssi?: number; | ||||
|   latitude?: number; | ||||
|   longitude?: number; | ||||
|   altitude?: number; | ||||
| } | ||||
|  | ||||
| interface UpdateTtnGatewayReceptionParams { | ||||
|   ttn_gateway_reception_id: string; | ||||
|   gateway_id?: string; | ||||
|   eui?: string; | ||||
|   rssi?: number; | ||||
|   latitude?: number; | ||||
|   longitude?: number; | ||||
|   altitude?: number; | ||||
| } | ||||
|  | ||||
| @injectable() | ||||
| export class TtnGatewayReceptionService { | ||||
|   constructor( | ||||
| @ -17,19 +36,24 @@ export class TtnGatewayReceptionService { | ||||
|     return this.repository.findById(id); | ||||
|   } | ||||
|  | ||||
|   public async createGatewayReception(data: Partial<TtnGatewayReception>) { | ||||
|     return this.repository.create(data); | ||||
|   } | ||||
|  | ||||
|   public async createGatewayReceptions(data: Partial<TtnGatewayReception>[]) { | ||||
|     return this.repository.createMany(data); | ||||
|   } | ||||
|  | ||||
|   public async updateGatewayReception( | ||||
|     id: string, | ||||
|     data: Partial<TtnGatewayReception> | ||||
|   public async createTtnGatewayReception( | ||||
|     data: CreateTtnGatewayReceptionParams | ||||
|   ) { | ||||
|     return this.repository.update(id, data); | ||||
|     if (data.latitude !== undefined && data.longitude !== undefined) | ||||
|       return this.repository.create(data); | ||||
|   } | ||||
|  | ||||
|   public async filterAndInsertGatewayReception( | ||||
|     data: CreateTtnGatewayReceptionParams[] | ||||
|   ) { | ||||
|     const result = await Promise.all( | ||||
|       data.map(async (gateway) => await this.createTtnGatewayReception(gateway)) | ||||
|     ); | ||||
|     return result.filter((gateway) => gateway !== undefined); | ||||
|   } | ||||
|  | ||||
|   public async updateGatewayReception(data: UpdateTtnGatewayReceptionParams) { | ||||
|     return this.repository.update(data.ttn_gateway_reception_id, data); | ||||
|   } | ||||
|  | ||||
|   public async deleteGatewayReception(id: string) { | ||||
|  | ||||
| @ -1,7 +1,21 @@ | ||||
| import { inject, injectable } from "tsyringe"; | ||||
| import { WifiScan } from "../models/wifiScan"; | ||||
| import { getLocationForWifiMemoized } from "../proxy/wigle"; | ||||
| import { WifiScanRepository } from "../repositories/wifiScanRepository"; | ||||
|  | ||||
| interface CreateWifiScanParams { | ||||
|   lp_ttn_end_device_uplinks_id: string; | ||||
|   mac: string; | ||||
|   rssi: number; | ||||
| } | ||||
|  | ||||
| interface UpdateWifiScanParams { | ||||
|   wifi_scan_id: string; | ||||
|   mac?: string; | ||||
|   rssi?: number; | ||||
|   latitude?: number; | ||||
|   longitude?: number; | ||||
| } | ||||
|  | ||||
| @injectable() | ||||
| export class WifiScanService { | ||||
|   constructor( | ||||
| @ -16,16 +30,28 @@ export class WifiScanService { | ||||
|     return this.repository.findById(id); | ||||
|   } | ||||
|  | ||||
|   public async createWifiScan(data: Partial<WifiScan>) { | ||||
|     return this.repository.create(data); | ||||
|   public async createWifiScan(data: CreateWifiScanParams) { | ||||
|     const apiResponse = await getLocationForWifiMemoized(data.mac); | ||||
|  | ||||
|     if (apiResponse !== undefined && apiResponse.results.length > 0) | ||||
|       return this.repository.create({ | ||||
|         ...data, | ||||
|         latitude: apiResponse.results[0].trilat, | ||||
|         longitude: apiResponse.results[0].trilong, | ||||
|       }); | ||||
|   } | ||||
|  | ||||
|   public async createWifiScans(data: Partial<WifiScan>[]) { | ||||
|     return this.repository.createMany(data); | ||||
|   public async createWifiScans(data: CreateWifiScanParams[]) { | ||||
|     let wifiScans = await Promise.all( | ||||
|       data.map(async (wifi) => { | ||||
|         return await this.createWifiScan(wifi); | ||||
|       }) | ||||
|     ); | ||||
|     return wifiScans.filter((wifi) => wifi !== undefined); | ||||
|   } | ||||
|  | ||||
|   public async updateWifiScan(id: string, data: Partial<WifiScan>) { | ||||
|     return this.repository.update(id, data); | ||||
|   public async updateWifiScan(data: UpdateWifiScanParams) { | ||||
|     return this.repository.update(data.wifi_scan_id, data); | ||||
|   } | ||||
|  | ||||
|   public async deleteWifiScan(id: string) { | ||||
|  | ||||
| @ -8,14 +8,14 @@ export const ttnMessageValidator = z.object({ | ||||
|     }), | ||||
|     dev_eui: z.string(), | ||||
|     join_eui: z.string(), | ||||
|     dev_addr: z.string(), | ||||
|     dev_addr: z.string().optional(), | ||||
|   }), | ||||
|   correlation_ids: z.array(z.string()), | ||||
|   received_at: z.string(), | ||||
|   uplink_message: z.object({ | ||||
|     session_key_id: z.string(), | ||||
|     session_key_id: z.string().optional(), | ||||
|     f_port: z.number().optional(), | ||||
|     f_cnt: z.number(), | ||||
|     f_cnt: z.number().optional(), | ||||
|     frm_payload: z.string().optional(), | ||||
|     decoded_payload: z | ||||
|       .object({ | ||||
| @ -25,8 +25,8 @@ export const ttnMessageValidator = z.object({ | ||||
|             z.object({ | ||||
|               measurementId: z.string(), | ||||
|               measurementValue: z.union([z.array(z.any()), z.number()]), | ||||
|               motionId: z.number(), | ||||
|               timestamp: z.number(), | ||||
|               motionId: z.number().optional(), | ||||
|               timestamp: z.number().optional(), | ||||
|               type: z.string(), | ||||
|             }) | ||||
|           ) | ||||
| @ -41,20 +41,22 @@ export const ttnMessageValidator = z.object({ | ||||
|           gateway_id: z.string(), | ||||
|           eui: z.string().optional(), | ||||
|         }), | ||||
|         time: z.string(), | ||||
|         time: z.string().optional(), | ||||
|         timestamp: z.number().optional(), | ||||
|         rssi: z.number(), | ||||
|         channel_rssi: z.number(), | ||||
|         snr: z.number(), | ||||
|         location: z.object({ | ||||
|           latitude: z.number(), | ||||
|           longitude: z.number(), | ||||
|           altitude: z.number(), | ||||
|           source: z.string().optional(), | ||||
|         }), | ||||
|         uplink_token: z.string(), | ||||
|         snr: z.number().optional(), | ||||
|         location: z | ||||
|           .object({ | ||||
|             latitude: z.number(), | ||||
|             longitude: z.number(), | ||||
|             altitude: z.number().optional(), | ||||
|             source: z.string().optional(), | ||||
|           }) | ||||
|           .optional(), | ||||
|         uplink_token: z.string().optional(), | ||||
|         channel_index: z.number().optional(), | ||||
|         received_at: z.string(), | ||||
|         received_at: z.string().optional(), | ||||
|       }) | ||||
|     ), | ||||
|     settings: z.object({ | ||||
| @ -62,29 +64,33 @@ export const ttnMessageValidator = z.object({ | ||||
|         lora: z.object({ | ||||
|           bandwidth: z.number(), | ||||
|           spreading_factor: z.number(), | ||||
|           coding_rate: z.string(), | ||||
|           coding_rate: z.string().optional(), | ||||
|         }), | ||||
|       }), | ||||
|       frequency: z.string(), | ||||
|       timestamp: z.number().optional(), | ||||
|       time: z.string().optional(), | ||||
|     }), | ||||
|     received_at: z.string(), | ||||
|     received_at: z.string().optional(), | ||||
|     confirmed: z.boolean().optional(), | ||||
|     consumed_airtime: z.string(), | ||||
|     version_ids: z.object({ | ||||
|       brand_id: z.string(), | ||||
|       model_id: z.string(), | ||||
|       hardware_version: z.string(), | ||||
|       firmware_version: z.string(), | ||||
|       band_id: z.string(), | ||||
|     }), | ||||
|     network_ids: z.object({ | ||||
|       net_id: z.string(), | ||||
|       ns_id: z.string(), | ||||
|       tenant_id: z.string(), | ||||
|       cluster_id: z.string(), | ||||
|       cluster_address: z.string(), | ||||
|     }), | ||||
|     consumed_airtime: z.string().optional(), | ||||
|     version_ids: z | ||||
|       .object({ | ||||
|         brand_id: z.string(), | ||||
|         model_id: z.string(), | ||||
|         hardware_version: z.string(), | ||||
|         firmware_version: z.string(), | ||||
|         band_id: z.string(), | ||||
|       }) | ||||
|       .optional(), | ||||
|     network_ids: z | ||||
|       .object({ | ||||
|         net_id: z.string(), | ||||
|         ns_id: z.string(), | ||||
|         tenant_id: z.string(), | ||||
|         cluster_id: z.string(), | ||||
|         cluster_address: z.string(), | ||||
|       }) | ||||
|       .optional(), | ||||
|   }), | ||||
| }); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user