diff --git a/src/c3nav/mesh/views/misc.py b/src/c3nav/mesh/views/misc.py index 24a10aee..ebb5ff2e 100644 --- a/src/c3nav/mesh/views/misc.py +++ b/src/c3nav/mesh/views/misc.py @@ -23,5 +23,5 @@ class MeshRangingView(TemplateView): return { "ranging_form": RangingForm(self.request.GET or None), "node_names": get_node_names(), - "nodes_xyz": Locator.load().get_all_xyz(), + "nodes_xyz": Locator.load().get_all_nodes_xyz(), } diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index f32f11d7..45379fda 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -4,6 +4,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from ninja import Field as APIField from ninja import Router as APIRouter +from pydantic_extra_types.mac_address import MacAddress from c3nav.api.auth import auth_responses 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.utils.cache.stats import increment_cache_key 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"]) @@ -98,7 +99,7 @@ def locate_test(request): BeaconsXYZ = dict[ - BSSIDSchema, + MacAddress, Annotated[ tuple[ Annotated[int, APIField(title="X (in cm)")], @@ -107,12 +108,4 @@ BeaconsXYZ = dict[ ], 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() \ No newline at end of file +] \ No newline at end of file diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index b10e4665..4cf5ffd1 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -3,14 +3,19 @@ import pickle from dataclasses import dataclass, field from functools import cached_property, reduce from pprint import pprint +from typing import Annotated from typing import Optional, Self, Sequence, TypeAlias +from uuid import UUID import numpy as np +from annotated_types import Lt 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.geometry.space import RangingBeacon 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.schemas import LocateRequestWifiPeerSchema @@ -19,27 +24,30 @@ try: except ImportError: 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 class LocatorPeer: - bssid: BSSID + identifier: LocatorPeerIdentifier frequencies: set[int] = field(default_factory=set) xyz: Optional[tuple[int, int, int]] = None @dataclass class ScanDataValue: - rssi: int + rssi: Optional[int] = None + ibeacon_range: Optional[float] = None distance: Optional[float] = None @classmethod 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] 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, ) @@ -57,7 +65,7 @@ class LocatorPoint: @dataclass class Locator: 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,)))) spaces: dict[int, "LocatorSpace"] = field(default_factory=dict) @@ -69,13 +77,20 @@ class Locator: return locator def _rebuild(self, router): - for beacon in RangingBeacon.objects.all(): - peer_id = self.get_peer_id(beacon.wifi_bssid, 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), - ) + calculated = get_nodes_and_ranging_beacons() + for beacon in calculated.beacons.values(): + identifiers = [] + if beacon.wifi_bssid: + identifiers.append(beacon.wifi_bssid) + 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)) for space in Space.objects.prefetch_related('beacon_measurements'): @@ -85,7 +100,7 @@ class Locator: LocatorPoint( x=measurement.geometry.x, 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() ) @@ -93,16 +108,16 @@ class Locator: if new_space.points: self.spaces[space.pk] = new_space - def get_peer_id(self, bssid: BSSID, create=False) -> Optional[int]: - peer_id = self.peer_lookup.get(bssid, None) + def get_peer_id(self, identifier: LocatorPeerIdentifier, create=False) -> Optional[int]: + peer_id = self.peer_lookup.get(identifier, None) if peer_id is None and create: - peer = LocatorPeer(bssid=bssid) + peer = LocatorPeer(identifier=identifier) peer_id = len(self.peers) - self.peer_lookup[bssid] = peer_id + self.peer_lookup[identifier] = peer_id self.peers.append(peer) 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 = {} for scan_value in scan_data: 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)) 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: converted = [] - for scan in scans_data: - converted.append(self.convert_scan(scan, create_peers=create_peers)) + for scan in scans_data["wifi"]: + 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()) return { peer_id: ScanDataValue.average( @@ -149,15 +179,16 @@ class Locator: def convert_raw_scan_data(self, raw_scan_data: list[LocateRequestWifiPeerSchema]) -> ScanData: return self.convert_scan(raw_scan_data, create_peers=False) - def get_xyz(self, address: BSSID) -> tuple[int, int, int] | None: - i = self.get_peer_id(address) + def get_xyz(self, identifier: LocatorPeerIdentifier) -> tuple[int, int, int] | None: + i = self.get_peer_id(identifier) if i is None: return None 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 { - 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): diff --git a/src/c3nav/routing/schemas.py b/src/c3nav/routing/schemas.py index 854d6f80..9a8414a8 100644 --- a/src/c3nav/routing/schemas.py +++ b/src/c3nav/routing/schemas.py @@ -4,16 +4,14 @@ from uuid import UUID from annotated_types import Lt from pydantic import Field as APIField 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.utils import NonEmptyStr - -BSSIDSchema = Annotated[str, APIField(pattern=r"^[a-z0-9]{2}(:[a-z0-9]{2}){5}$", title="BSSID")] class LocateRequestWifiPeerSchema(BaseSchema): - bssid: BSSIDSchema = APIField( + bssid: MacAddress = APIField( title="BSSID", description="BSSID of the peer", example="c3:42:13:37:ac:ab",