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:
commit
fc8e4ca486
@ -10,7 +10,7 @@ TODO
|
|||||||
### Database
|
### Database
|
||||||
**Change name of database and credentials as you like!**
|
**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';`
|
- 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`
|
- 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_TOKEN="" # Go to account and generate token "Encoded for use"
|
||||||
WIGLE_BASE_URL="https://api.wigle.net"
|
WIGLE_BASE_URL="https://api.wigle.net"
|
||||||
WIGLE_NETWORK_SEARCH="/api/v2/network/search"
|
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_MAX=10000
|
||||||
GET_LOCATION_WIFI_PRIMITIVE=true
|
GET_LOCATION_WIFI_PRIMITIVE=true
|
@ -13,11 +13,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/memoizee": "^0.4.11",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2"
|
||||||
"@types/memoizee": "^0.4.11"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
@ -46,6 +46,8 @@ CREATE TABLE IF NOT EXISTS location (
|
|||||||
wifi_longitude DOUBLE,
|
wifi_longitude DOUBLE,
|
||||||
gnss_latitude DOUBLE,
|
gnss_latitude DOUBLE,
|
||||||
gnss_longitude DOUBLE,
|
gnss_longitude DOUBLE,
|
||||||
|
ttn_gw_latitude DOUBLE,
|
||||||
|
ttn_gw_longitude DOUBLE,
|
||||||
created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE 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)
|
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) => {
|
async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const message = req.body as TtnMessage;
|
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({
|
await lpTtnEndDeviceUplinksService.createUplink({
|
||||||
device_id: message.end_device_ids.device_id,
|
device_id: message.end_device_ids.device_id,
|
||||||
application_ids:
|
application_ids:
|
||||||
@ -78,6 +79,7 @@ router.post(
|
|||||||
latitude: g.latitude,
|
latitude: g.latitude,
|
||||||
longitude: g.longitude,
|
longitude: g.longitude,
|
||||||
})),
|
})),
|
||||||
|
gnssLocation: { latitude, longitude }
|
||||||
};
|
};
|
||||||
|
|
||||||
domainEventEmitter.emit(TtnMessageReceivedEventName, event);
|
domainEventEmitter.emit(TtnMessageReceivedEventName, event);
|
||||||
|
@ -11,4 +11,8 @@ export type TtnMessageReceivedEvent = {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
altitude: number;
|
altitude: number;
|
||||||
}[];
|
}[];
|
||||||
|
gnssLocation: {
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -3,11 +3,156 @@ import {
|
|||||||
TtnMessageReceivedEvent,
|
TtnMessageReceivedEvent,
|
||||||
TtnMessageReceivedEventName,
|
TtnMessageReceivedEventName,
|
||||||
} from "../event/ttnMessageReceivedEvent";
|
} 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(
|
domainEventEmitter.on(
|
||||||
TtnMessageReceivedEventName,
|
TtnMessageReceivedEventName,
|
||||||
async (event: TtnMessageReceivedEvent) => {
|
async (event: TtnMessageReceivedEvent) => {
|
||||||
console.log(event);
|
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.use("/api/ttn", ttnRoutes);
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
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 wifi_longitude!: number;
|
||||||
public gnss_latitude!: number;
|
public gnss_latitude!: number;
|
||||||
public gnss_longitude!: number;
|
public gnss_longitude!: number;
|
||||||
|
public ttn_gw_latitude!: number;
|
||||||
|
public ttn_gw_longitude!: number;
|
||||||
public created_at_utc!: Date;
|
public created_at_utc!: Date;
|
||||||
public updated_at_utc!: Date;
|
public updated_at_utc!: Date;
|
||||||
}
|
}
|
||||||
@ -40,6 +42,14 @@ Location.init(
|
|||||||
type: DataTypes.NUMBER,
|
type: DataTypes.NUMBER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
ttn_gw_latitude: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
ttn_gw_longitude: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
created_at_utc: {
|
created_at_utc: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
defaultValue: DataTypes.NOW,
|
defaultValue: DataTypes.NOW,
|
||||||
|
Loading…
Reference in New Issue
Block a user