Merge pull request 'feat: tracker location based on multiple location providers' (#5) from feature/ttn-location-algo into main
Reviewed-on: #5
This commit is contained in:
		| @ -10,7 +10,7 @@ TODO | ||||
| ### Database | ||||
| **Change name of database and credentials as you like!** | ||||
|  | ||||
| - Create new database: `CREATE DATABASE locationhub;` | ||||
| - Create new database: `CREATE DATABASE dev_locationhub;` | ||||
| - Create new user for database: `GRANT ALL PRIVILEGES ON dev_locationhub.* TO 'dbuser'@'localhost' IDENTIFIED BY '1234';` | ||||
| - Import tables: `/usr/bin/mariadb -u dbuser -p1234 dev_locationhub < server/sql/tables.sql` | ||||
|  | ||||
|  | ||||
| @ -7,6 +7,6 @@ DB_PORT="" | ||||
| WIGLE_TOKEN="" # Go to account and generate token "Encoded for use" | ||||
| WIGLE_BASE_URL="https://api.wigle.net" | ||||
| WIGLE_NETWORK_SEARCH="/api/v2/network/search" | ||||
| GET_LOCATION_WIFI_MAX_AGE=1209600000 # 14 Tage in Millisekunden (14 * 24 * 60 * 60 * 1000) | ||||
| GET_LOCATION_WIFI_MAX_AGE=1209600000 # 14 days in milliseconds (14 * 24 * 60 * 60 * 1000) | ||||
| GET_LOCATION_WIFI_MAX=10000           | ||||
| GET_LOCATION_WIFI_PRIMITIVE=true   | ||||
| @ -13,11 +13,11 @@ | ||||
|   "license": "ISC", | ||||
|   "devDependencies": { | ||||
|     "@types/express": "^5.0.0", | ||||
|     "@types/memoizee": "^0.4.11", | ||||
|     "@types/node": "^22.10.2", | ||||
|     "nodemon": "^3.1.9", | ||||
|     "ts-node": "^10.9.2", | ||||
|     "typescript": "^5.7.2", | ||||
|     "@types/memoizee": "^0.4.11" | ||||
|     "typescript": "^5.7.2" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "cors": "^2.8.5", | ||||
|  | ||||
| @ -46,6 +46,8 @@ CREATE TABLE IF NOT EXISTS location ( | ||||
|     wifi_longitude DOUBLE, | ||||
|     gnss_latitude DOUBLE, | ||||
|     gnss_longitude DOUBLE, | ||||
|     ttn_gw_latitude DOUBLE, | ||||
|     ttn_gw_longitude DOUBLE, | ||||
|     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||||
|     FOREIGN KEY (lp_ttn_end_device_uplinks_id) REFERENCES lp_ttn_end_device_uplinks(lp_ttn_end_device_uplinks_id) | ||||
|  | ||||
| @ -28,7 +28,8 @@ router.post( | ||||
|   async (req: Request, res: Response) => { | ||||
|     try { | ||||
|       const message = req.body as TtnMessage; | ||||
|       const { lp_ttn_end_device_uplinks_id } = | ||||
|       // Create uplink record | ||||
|       const { lp_ttn_end_device_uplinks_id, latitude, longitude } = | ||||
|         await lpTtnEndDeviceUplinksService.createUplink({ | ||||
|           device_id: message.end_device_ids.device_id, | ||||
|           application_ids: | ||||
| @ -78,6 +79,7 @@ router.post( | ||||
|           latitude: g.latitude, | ||||
|           longitude: g.longitude, | ||||
|         })), | ||||
|         gnssLocation: { latitude, longitude } | ||||
|       }; | ||||
|  | ||||
|       domainEventEmitter.emit(TtnMessageReceivedEventName, event); | ||||
|  | ||||
| @ -11,4 +11,8 @@ export type TtnMessageReceivedEvent = { | ||||
|     longitude: number; | ||||
|     altitude: number; | ||||
|   }[]; | ||||
|   gnssLocation: { | ||||
|     latitude?: number; | ||||
|     longitude?: number; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| @ -3,11 +3,156 @@ import { | ||||
|   TtnMessageReceivedEvent, | ||||
|   TtnMessageReceivedEventName, | ||||
| } from "../event/ttnMessageReceivedEvent"; | ||||
| import { container } from "tsyringe"; | ||||
| import { LocationService } from "../services/locationService"; | ||||
| import { WifiScanService } from "../services/wifiScanService"; | ||||
| import { getLocationForWifiMemoized } from "../proxy/wigle"; | ||||
|  | ||||
| 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 | ||||
|     virtualLocation.latitude = weightedLatitude / totalWeight; | ||||
|     virtualLocation.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 | ||||
|     const 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); | ||||
|         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, | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     await wifiScanService.createWifiScans(wifiScans); | ||||
|  | ||||
|     const { totalWeight, weightedLatitude, weightedLongitude } = | ||||
|       wifiScans.reduce( | ||||
|         (acc, { latitude, longitude, rssi }) => { | ||||
|           if (latitude && longitude && rssi !== 0) { | ||||
|             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); | ||||
|     // TODO Hendrik 🚀 | ||||
|  | ||||
|     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); | ||||
|       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; | ||||
|  | ||||
|     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) | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| @ -25,5 +25,5 @@ app.use("/api/locations", locationRoutes); | ||||
| app.use("/api/ttn", ttnRoutes); | ||||
|  | ||||
| app.listen(PORT, () => { | ||||
|   console.log(`🚀 Server läuft auf http://localhost:${PORT}`); | ||||
|   console.log(`🚀 Server runs here: http://localhost:${PORT}`); | ||||
| }); | ||||
|  | ||||
| @ -8,6 +8,8 @@ export class Location extends Model { | ||||
|   public wifi_longitude!: number; | ||||
|   public gnss_latitude!: number; | ||||
|   public gnss_longitude!: number; | ||||
|   public ttn_gw_latitude!: number; | ||||
|   public ttn_gw_longitude!: number; | ||||
|   public created_at_utc!: Date; | ||||
|   public updated_at_utc!: Date; | ||||
| } | ||||
| @ -40,6 +42,14 @@ Location.init( | ||||
|       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, | ||||
|       defaultValue: DataTypes.NOW, | ||||
|  | ||||
		Reference in New Issue
	
	Block a user