Compare commits

...

26 Commits

Author SHA256 Message Date
5319b38338 add Wigle API dummy script 2025-01-02 18:03:36 +01:00
283482b361 Merge pull request 'Fix creation of WifiScan' (#8) from bugfix/wifiscan-location into main
Reviewed-on: #8
2025-01-02 16:33:09 +01:00
ad32baa844 refactor wifi location parsing 2025-01-02 16:31:55 +01:00
3f3c47d629 only create valid wifiScans 2025-01-02 15:42:52 +01:00
fc8e4ca486 Merge pull request 'feat: tracker location based on multiple location providers' (#5) from feature/ttn-location-algo into main
Reviewed-on: #5
2025-01-02 14:57:33 +01:00
7e42d3b8c9 fix ttn location parse 2025-01-02 14:56:00 +01:00
64b77c33b5 cleanup and fix gnss location 2025-01-02 14:44:59 +01:00
66b245e6ab Merge branch 'main' into feature/ttn-location-algo 2025-01-02 14:40:14 +01:00
e3aebb041f Merge pull request 'feat: Switch to auth token' (#7) from feat/wigle-api into main
Reviewed-on: #7
2025-01-02 14:24:13 +01:00
755f26a93c switch to auth token 2025-01-02 13:57:27 +01:00
6d20f4e54c cleanup 2025-01-02 11:38:27 +01:00
f341e6039f cleanup event definition 2025-01-02 10:38:06 +01:00
718e093d3d fix location model 2025-01-01 21:56:23 +01:00
6300004ec3 Merge branch 'main' into feature/ttn-location-algo 2025-01-01 21:53:26 +01:00
50721114e3 store location created from all location sources 2025-01-01 21:39:34 +01:00
2ed915601b Merge branch 'main' into feature/ttn-location-algo 2025-01-01 20:25:21 +01:00
dae4403eaf save GNSS location in location element 2025-01-01 19:14:21 +01:00
16d49c9940 fix event sending 2025-01-01 18:40:58 +01:00
68e3121f41 add gnss location to event 2024-12-31 16:57:36 +01:00
4994b8a246 save TTN GW based location into DB 2024-12-31 14:18:47 +01:00
c27763fc11 add TTN Gateway based location to DB model 2024-12-31 14:00:32 +01:00
393eab2b45 calculate virtual location based on TTN GW 2024-12-31 13:19:13 +01:00
097cb44649 Merge branch 'main' into feature/ttn-location-algo 2024-12-31 13:06:15 +01:00
95adba8e9a Merge branch 'feature/ttn-location-algo' of git.mosad.xyz:localhorst/LocationHub into feature/ttn-location-algo 2024-12-30 23:21:43 +01:00
a4a8b6c3c1 feat: added event and event handler 2024-12-30 23:20:10 +01:00
aa3c250c2e basic algo that computes virtual location based on TTN gateway locations 2024-12-30 22:48:47 +01:00
11 changed files with 275 additions and 13 deletions

View File

@ -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`

View File

@ -4,9 +4,9 @@ DB_PASSWORD=""
DB_HOST=""
DB_DIALECT=""
DB_PORT=""
WIGLE_TOKEN=""
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

View File

@ -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",

View File

@ -0,0 +1,88 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Author: Hendrik Schutter, mail@hendrikschutter.com
"""
import requests
import os
import json
import argparse
import random
import http.server
import json
from urllib.parse import urlparse, parse_qs
port = 8000
def generateDummyResponse(netid):
response_payload = {
"success": True,
"totalResults": 1,
"first": 0,
"last": 0,
"resultCount": 1,
"results": [
{
"trilat": random.uniform(-90, 90),
"trilong": random.uniform(-180, 180),
"ssid": "Wifi-Name",
"qos": 0,
"transid": "string",
"firsttime": "2025-01-02T16:48:28.368Z",
"lasttime": "2025-01-02T16:48:28.368Z",
"lastupdt": "2025-01-02T16:48:28.368Z",
"netid": netid,
"name": "string",
"type": "string",
"comment": "string",
"wep": "string",
"bcninterval": 0,
"freenet": "string",
"dhcp": "string",
"paynet": "string",
"userfound": False,
"channel": 0,
"rcois": "string",
"encryption": "none",
"country": "string",
"region": "string",
"road": "string",
"city": "string",
"housenumber": "string",
"postalcode": "string",
}
],
"searchAfter": "string",
"search_after": 0,
}
return response_payload
class SimpleHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
# Parse the URL and query parameters
parsed_url = urlparse(self.path)
if parsed_url.path == "/api/v2/network/search":
query_params = parse_qs(parsed_url.query)
netid = query_params.get("netid", [""])[0]
# Send response headers
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
# Send the JSON response
self.wfile.write(json.dumps(generateDummyResponse(netid)).encode("utf-8"))
else:
# Handle 404 Not Found
self.send_response(404)
self.end_headers()
self.wfile.write(b"Not Found")
def main():
server = http.server.HTTPServer(("127.0.0.1", port), SimpleHTTPRequestHandler)
print(f"Server running on http://127.0.0.1:{port}/api/v2/network/search'...")
server.serve_forever()
if __name__ == "__main__":
main()

View File

@ -18,8 +18,8 @@ CREATE TABLE IF NOT EXISTS wifi_scan (
lp_ttn_end_device_uplinks_id UUID,
mac VARCHAR(255),
rssi NUMERIC,
latitude DOUBLE,
longitude DOUBLE,
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
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)
@ -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)

View File

@ -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);

View File

@ -11,4 +11,8 @@ export type TtnMessageReceivedEvent = {
longitude: number;
altitude: number;
}[];
gnssLocation: {
latitude?: number;
longitude?: number;
}
};

View File

@ -3,11 +3,162 @@ 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";
import { WifiScan } from "../models/wifiScan";
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
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);
// 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)
}
);

View File

@ -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}`);
});

View File

@ -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,

View File

@ -51,12 +51,17 @@ export const getLocationForWifi = async (
method: "GET",
headers: {
"Content-Type": "application/json",
Cookie: `auth=${process.env.WIGLE_TOKEN}`,
Authorization: `Basic ${process.env.WIGLE_TOKEN}`,
},
});
return await response.json();
if (response.ok) {
return await response.json();
}
console.log(response.status);
return undefined;
} catch (error) {
console.error("Fehler beim Aufruf des Services:", error);
console.error("Error during call of API wigle.net:", error);
}
};