Locator now can deal with ibeacons in theory
This commit is contained in:
parent
3be707f52b
commit
76b8858923
4 changed files with 65 additions and 43 deletions
|
@ -23,5 +23,5 @@ class MeshRangingView(TemplateView):
|
||||||
return {
|
return {
|
||||||
"ranging_form": RangingForm(self.request.GET or None),
|
"ranging_form": RangingForm(self.request.GET or None),
|
||||||
"node_names": get_node_names(),
|
"node_names": get_node_names(),
|
||||||
"nodes_xyz": Locator.load().get_all_xyz(),
|
"nodes_xyz": Locator.load().get_all_nodes_xyz(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from ninja import Field as APIField
|
from ninja import Field as APIField
|
||||||
from ninja import Router as APIRouter
|
from ninja import Router as APIRouter
|
||||||
|
from pydantic_extra_types.mac_address import MacAddress
|
||||||
|
|
||||||
from c3nav.api.auth import auth_responses
|
from c3nav.api.auth import auth_responses
|
||||||
from c3nav.api.schema import BaseSchema
|
from c3nav.api.schema import BaseSchema
|
||||||
|
@ -11,7 +12,7 @@ from c3nav.mapdata.models.access import AccessPermission
|
||||||
from c3nav.mapdata.schemas.models import CustomLocationSchema
|
from c3nav.mapdata.schemas.models import CustomLocationSchema
|
||||||
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
from c3nav.mapdata.utils.cache.stats import increment_cache_key
|
||||||
from c3nav.routing.locator import Locator
|
from c3nav.routing.locator import Locator
|
||||||
from c3nav.routing.schemas import BSSIDSchema, LocateRequestWifiPeerSchema, LocateRequestIBeaconPeerSchema
|
from c3nav.routing.schemas import LocateRequestWifiPeerSchema, LocateRequestIBeaconPeerSchema
|
||||||
|
|
||||||
positioning_api_router = APIRouter(tags=["positioning"])
|
positioning_api_router = APIRouter(tags=["positioning"])
|
||||||
|
|
||||||
|
@ -98,7 +99,7 @@ def locate_test(request):
|
||||||
|
|
||||||
|
|
||||||
BeaconsXYZ = dict[
|
BeaconsXYZ = dict[
|
||||||
BSSIDSchema,
|
MacAddress,
|
||||||
Annotated[
|
Annotated[
|
||||||
tuple[
|
tuple[
|
||||||
Annotated[int, APIField(title="X (in cm)")],
|
Annotated[int, APIField(title="X (in cm)")],
|
||||||
|
@ -107,12 +108,4 @@ BeaconsXYZ = dict[
|
||||||
],
|
],
|
||||||
APIField(title="global XYZ coordinates")
|
APIField(title="global XYZ coordinates")
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@positioning_api_router.get('/beacons-xyz/', summary="get beacon coordinates",
|
|
||||||
description="get xyz coordinates for all known positioning beacons",
|
|
||||||
response={200: BeaconsXYZ, **auth_responses})
|
|
||||||
def beacons_xyz():
|
|
||||||
# todo: update with more details? todo permission?
|
|
||||||
return Locator.load().get_all_xyz()
|
|
|
@ -3,14 +3,19 @@ import pickle
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import cached_property, reduce
|
from functools import cached_property, reduce
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
from typing import Annotated
|
||||||
from typing import Optional, Self, Sequence, TypeAlias
|
from typing import Optional, Self, Sequence, TypeAlias
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from annotated_types import Lt
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from pydantic.types import NonNegativeInt
|
||||||
|
from pydantic_extra_types.mac_address import MacAddress
|
||||||
|
|
||||||
from c3nav.mapdata.models import MapUpdate, Space
|
from c3nav.mapdata.models import MapUpdate, Space
|
||||||
from c3nav.mapdata.models.geometry.space import RangingBeacon
|
|
||||||
from c3nav.mapdata.utils.locations import CustomLocation
|
from c3nav.mapdata.utils.locations import CustomLocation
|
||||||
|
from c3nav.mesh.utils import get_nodes_and_ranging_beacons
|
||||||
from c3nav.routing.router import Router
|
from c3nav.routing.router import Router
|
||||||
from c3nav.routing.schemas import LocateRequestWifiPeerSchema
|
from c3nav.routing.schemas import LocateRequestWifiPeerSchema
|
||||||
|
|
||||||
|
@ -19,27 +24,30 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from threading import local as LocalContext
|
from threading import local as LocalContext
|
||||||
|
|
||||||
BSSID: TypeAlias = str
|
LocatorPeerIdentifier: TypeAlias = MacAddress | tuple[UUID, Annotated[NonNegativeInt, Lt(2 ** 16)], Annotated[NonNegativeInt, Lt(2 ** 16)]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocatorPeer:
|
class LocatorPeer:
|
||||||
bssid: BSSID
|
identifier: LocatorPeerIdentifier
|
||||||
frequencies: set[int] = field(default_factory=set)
|
frequencies: set[int] = field(default_factory=set)
|
||||||
xyz: Optional[tuple[int, int, int]] = None
|
xyz: Optional[tuple[int, int, int]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ScanDataValue:
|
class ScanDataValue:
|
||||||
rssi: int
|
rssi: Optional[int] = None
|
||||||
|
ibeacon_range: Optional[float] = None
|
||||||
distance: Optional[float] = None
|
distance: Optional[float] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def average(cls, items: Sequence[Self]):
|
def average(cls, items: Sequence[Self]):
|
||||||
rssi = [item.rssi for item in items]
|
rssi = [item.rssi for item in items if item.rssi]
|
||||||
|
ibeacon_range = [item.ibeacon_range for item in items if item.ibeacon_range is not None]
|
||||||
distance = [item.distance for item in items if item.distance is not None]
|
distance = [item.distance for item in items if item.distance is not None]
|
||||||
return cls(
|
return cls(
|
||||||
rssi=(sum(rssi)//len(rssi)),
|
rssi=(sum(rssi)//len(rssi)) if rssi else None,
|
||||||
|
ibeacon_range=(sum(ibeacon_range) // len(ibeacon_range)) if ibeacon_range else None,
|
||||||
distance=(sum(distance)/len(distance)) if distance else None,
|
distance=(sum(distance)/len(distance)) if distance else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,7 +65,7 @@ class LocatorPoint:
|
||||||
@dataclass
|
@dataclass
|
||||||
class Locator:
|
class Locator:
|
||||||
peers: list[LocatorPeer] = field(default_factory=list)
|
peers: list[LocatorPeer] = field(default_factory=list)
|
||||||
peer_lookup: dict[BSSID, int] = field(default_factory=dict)
|
peer_lookup: dict[LocatorPeerIdentifier, int] = field(default_factory=dict)
|
||||||
xyz: np.array = field(default_factory=(lambda: np.empty((0,))))
|
xyz: np.array = field(default_factory=(lambda: np.empty((0,))))
|
||||||
spaces: dict[int, "LocatorSpace"] = field(default_factory=dict)
|
spaces: dict[int, "LocatorSpace"] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@ -69,13 +77,20 @@ class Locator:
|
||||||
return locator
|
return locator
|
||||||
|
|
||||||
def _rebuild(self, router):
|
def _rebuild(self, router):
|
||||||
for beacon in RangingBeacon.objects.all():
|
calculated = get_nodes_and_ranging_beacons()
|
||||||
peer_id = self.get_peer_id(beacon.wifi_bssid, create=True)
|
for beacon in calculated.beacons.values():
|
||||||
self.peers[peer_id].xyz = (
|
identifiers = []
|
||||||
int(beacon.geometry.x * 100),
|
if beacon.wifi_bssid:
|
||||||
int(beacon.geometry.y * 100),
|
identifiers.append(beacon.wifi_bssid)
|
||||||
int((router.altitude_for_point(beacon.space_id, beacon.geometry) + float(beacon.altitude)) * 100),
|
if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None:
|
||||||
)
|
identifiers.append((beacon.ibeacon_uuid, beacon.ibeacon_major, beacon.ibeacon_minor))
|
||||||
|
for identifier in identifiers:
|
||||||
|
peer_id = self.get_peer_id(identifier, create=True)
|
||||||
|
self.peers[peer_id].xyz = (
|
||||||
|
int(beacon.geometry.x * 100),
|
||||||
|
int(beacon.geometry.y * 100),
|
||||||
|
int((router.altitude_for_point(beacon.space_id, beacon.geometry) + float(beacon.altitude)) * 100),
|
||||||
|
)
|
||||||
self.xyz = np.array(tuple(peer.xyz for peer in self.peers))
|
self.xyz = np.array(tuple(peer.xyz for peer in self.peers))
|
||||||
|
|
||||||
for space in Space.objects.prefetch_related('beacon_measurements'):
|
for space in Space.objects.prefetch_related('beacon_measurements'):
|
||||||
|
@ -85,7 +100,7 @@ class Locator:
|
||||||
LocatorPoint(
|
LocatorPoint(
|
||||||
x=measurement.geometry.x,
|
x=measurement.geometry.x,
|
||||||
y=measurement.geometry.y,
|
y=measurement.geometry.y,
|
||||||
values=self.convert_scans(measurement.data["wifi"], create_peers=True),
|
values=self.convert_scans(measurement.data, create_peers=True),
|
||||||
)
|
)
|
||||||
for measurement in space.beacon_measurements.all()
|
for measurement in space.beacon_measurements.all()
|
||||||
)
|
)
|
||||||
|
@ -93,16 +108,16 @@ class Locator:
|
||||||
if new_space.points:
|
if new_space.points:
|
||||||
self.spaces[space.pk] = new_space
|
self.spaces[space.pk] = new_space
|
||||||
|
|
||||||
def get_peer_id(self, bssid: BSSID, create=False) -> Optional[int]:
|
def get_peer_id(self, identifier: LocatorPeerIdentifier, create=False) -> Optional[int]:
|
||||||
peer_id = self.peer_lookup.get(bssid, None)
|
peer_id = self.peer_lookup.get(identifier, None)
|
||||||
if peer_id is None and create:
|
if peer_id is None and create:
|
||||||
peer = LocatorPeer(bssid=bssid)
|
peer = LocatorPeer(identifier=identifier)
|
||||||
peer_id = len(self.peers)
|
peer_id = len(self.peers)
|
||||||
self.peer_lookup[bssid] = peer_id
|
self.peer_lookup[identifier] = peer_id
|
||||||
self.peers.append(peer)
|
self.peers.append(peer)
|
||||||
return peer_id
|
return peer_id
|
||||||
|
|
||||||
def convert_scan(self, scan_data, create_peers=False) -> ScanData:
|
def convert_wifi_scan(self, scan_data, create_peers=False) -> ScanData:
|
||||||
result = {}
|
result = {}
|
||||||
for scan_value in scan_data:
|
for scan_value in scan_data:
|
||||||
if settings.WIFI_SSIDS and scan_value['ssid'] not in settings.WIFI_SSIDS:
|
if settings.WIFI_SSIDS and scan_value['ssid'] not in settings.WIFI_SSIDS:
|
||||||
|
@ -112,10 +127,25 @@ class Locator:
|
||||||
result[peer_id] = ScanDataValue(rssi=scan_value["rssi"], distance=scan_value.get("distance", None))
|
result[peer_id] = ScanDataValue(rssi=scan_value["rssi"], distance=scan_value.get("distance", None))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def convert_ibeacon_scan(self, scan_data, create_peers=False) -> ScanData:
|
||||||
|
result = {}
|
||||||
|
for scan_value in scan_data:
|
||||||
|
peer_id = self.get_peer_id(
|
||||||
|
(scan_value['uuid'], scan_value['major'], scan_value['minor']),
|
||||||
|
create=create_peers
|
||||||
|
)
|
||||||
|
if peer_id is not None:
|
||||||
|
result[peer_id] = ScanDataValue(ibeacon_range=scan_value["distance"])
|
||||||
|
return result
|
||||||
|
|
||||||
def convert_scans(self, scans_data, create_peers=False) -> ScanData:
|
def convert_scans(self, scans_data, create_peers=False) -> ScanData:
|
||||||
converted = []
|
converted = []
|
||||||
for scan in scans_data:
|
for scan in scans_data["wifi"]:
|
||||||
converted.append(self.convert_scan(scan, create_peers=create_peers))
|
converted.append(self.convert_wifi_scan(scan, create_peers=create_peers))
|
||||||
|
|
||||||
|
for scan in scans_data["ibeacon"]:
|
||||||
|
converted.append(self.convert_ibeacon_scan(scan, create_peers=create_peers))
|
||||||
|
|
||||||
peer_ids = reduce(operator.or_, (frozenset(values.keys()) for values in converted), frozenset())
|
peer_ids = reduce(operator.or_, (frozenset(values.keys()) for values in converted), frozenset())
|
||||||
return {
|
return {
|
||||||
peer_id: ScanDataValue.average(
|
peer_id: ScanDataValue.average(
|
||||||
|
@ -149,15 +179,16 @@ class Locator:
|
||||||
def convert_raw_scan_data(self, raw_scan_data: list[LocateRequestWifiPeerSchema]) -> ScanData:
|
def convert_raw_scan_data(self, raw_scan_data: list[LocateRequestWifiPeerSchema]) -> ScanData:
|
||||||
return self.convert_scan(raw_scan_data, create_peers=False)
|
return self.convert_scan(raw_scan_data, create_peers=False)
|
||||||
|
|
||||||
def get_xyz(self, address: BSSID) -> tuple[int, int, int] | None:
|
def get_xyz(self, identifier: LocatorPeerIdentifier) -> tuple[int, int, int] | None:
|
||||||
i = self.get_peer_id(address)
|
i = self.get_peer_id(identifier)
|
||||||
if i is None:
|
if i is None:
|
||||||
return None
|
return None
|
||||||
return self.peers[i].xyz
|
return self.peers[i].xyz
|
||||||
|
|
||||||
def get_all_xyz(self) -> dict[BSSID, float]:
|
def get_all_nodes_xyz(self) -> dict[LocatorPeerIdentifier, tuple[float, float, float]]:
|
||||||
return {
|
return {
|
||||||
peer: peer.xyz for peer in self.peers[:len(self.xyz)]
|
peer.identifier: peer.xyz for peer in self.peers[:len(self.xyz)]
|
||||||
|
if isinstance(peer.identifier, MacAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
def locate(self, raw_scan_data: list[LocateRequestWifiPeerSchema], permissions=None):
|
def locate(self, raw_scan_data: list[LocateRequestWifiPeerSchema], permissions=None):
|
||||||
|
|
|
@ -4,16 +4,14 @@ from uuid import UUID
|
||||||
from annotated_types import Lt
|
from annotated_types import Lt
|
||||||
from pydantic import Field as APIField
|
from pydantic import Field as APIField
|
||||||
from pydantic import NegativeInt, PositiveInt
|
from pydantic import NegativeInt, PositiveInt
|
||||||
from pydantic.types import NonNegativeInt, PositiveFloat, NonNegativeFloat
|
from pydantic.types import NonNegativeInt, NonNegativeFloat
|
||||||
|
from pydantic_extra_types.mac_address import MacAddress
|
||||||
|
|
||||||
from c3nav.api.schema import BaseSchema
|
from c3nav.api.schema import BaseSchema
|
||||||
from c3nav.api.utils import NonEmptyStr
|
|
||||||
|
|
||||||
BSSIDSchema = Annotated[str, APIField(pattern=r"^[a-z0-9]{2}(:[a-z0-9]{2}){5}$", title="BSSID")]
|
|
||||||
|
|
||||||
|
|
||||||
class LocateRequestWifiPeerSchema(BaseSchema):
|
class LocateRequestWifiPeerSchema(BaseSchema):
|
||||||
bssid: BSSIDSchema = APIField(
|
bssid: MacAddress = APIField(
|
||||||
title="BSSID",
|
title="BSSID",
|
||||||
description="BSSID of the peer",
|
description="BSSID of the peer",
|
||||||
example="c3:42:13:37:ac:ab",
|
example="c3:42:13:37:ac:ab",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue