full scancollector implementation with all kinds of nice features

This commit is contained in:
Laura Klünder 2024-03-31 18:49:35 +02:00
parent 7532fc39ed
commit 7fa75e1617
6 changed files with 157 additions and 13 deletions

View file

@ -1,16 +1,19 @@
from django.urls import Resolver404, resolve
from django.utils.translation import gettext_lazy as _
from ninja import Router as APIRouter
from shapely.geometry.geo import mapping
from c3nav.api.auth import APIKeyAuth, auth_permission_responses
from c3nav.api.exceptions import API404
from c3nav.editor.api.base import api_etag_with_update_cache_key
from c3nav.editor.api.geometries import get_level_geometries_result, get_space_geometries_result
from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, GeometryStylesSchema, UpdateCacheKey
from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, GeometryStylesSchema, UpdateCacheKey, \
EditorBeaconsLookup
from c3nav.editor.views.base import editor_etag_func
from c3nav.mapdata.api.base import api_etag
from c3nav.mapdata.models import Source
from c3nav.mapdata.schemas.responses import WithBoundsSchema
from c3nav.mesh.utils import get_nodes_and_ranging_beacons
editor_api_router = APIRouter(tags=["editor"], auth=APIKeyAuth(permissions={"editor_access"}))
@ -145,3 +148,54 @@ def post_view_as_api(request, path: str):
this is a mess. good luck. if you actually want to use this, poke us so we might add better documentation.
"""
raise NotImplementedError
@editor_api_router.get('/beacons-lookup/', summary="get beacon coordinates",
description="get xyz coordinates for all known positioning beacons",
response={200: EditorBeaconsLookup, **auth_permission_responses},
openapi_extra={"security": [{"APIKeyAuth": ["editor_access", "write"]}]})
def beacons_lookup(request):
# todo: update with more details? todo permission?
from c3nav.mesh.messages import MeshMessageType
calculated = get_nodes_and_ranging_beacons()
wifi_beacons = {}
ibeacons = {}
for beacon in calculated.beacons.values():
node = calculated.nodes_for_beacons.get(beacon.id, None)
beacon_data = {
"name": node.name if node else ("Beacon #%d" % beacon.pk),
"point": mapping(beacon.geometry),
}
if beacon.wifi_bssid:
wifi_beacons[beacon.wifi_bssid] = beacon_data
if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None:
ibeacons.setdefault(
str(beacon.ibeacon_uuid), {}
).setdefault(
beacon.ibeacon_major, {}
)[beacon.ibeacon_minor] = beacon_data
for node in calculated.nodes.values():
beacon_data = {
"name": node.name,
"point": None,
}
ibeacon_msg = node.last_messages[MeshMessageType.CONFIG_IBEACON]
if ibeacon_msg:
ibeacons.setdefault(
str(ibeacon_msg.parsed.content.uuid), {}
).setdefault(
ibeacon_msg.parsed.content.major, {}
).setdefault(
ibeacon_msg.parsed.content.minor, beacon_data
)
node_msg = node.last_messages[MeshMessageType.CONFIG_NODE]
if node_msg:
wifi_beacons.setdefault(node.address, beacon_data)
return EditorBeaconsLookup(
wifi_beacons=wifi_beacons,
ibeacons=ibeacons,
).model_dump(mode="json")

View file

@ -1,9 +1,13 @@
from typing import Annotated, Literal, Optional, Union
from uuid import UUID
from annotated_types import Lt
from pydantic import Field as APIField
from pydantic import PositiveInt
from pydantic.types import NonNegativeInt
from pydantic_extra_types.mac_address import MacAddress
from c3nav.api.schema import AnyGeometrySchema, BaseSchema, GeometrySchema, LineSchema
from c3nav.api.schema import AnyGeometrySchema, BaseSchema, GeometrySchema, LineSchema, PointSchema
from c3nav.api.utils import NonEmptyStr
GeometryStylesSchema = Annotated[
@ -100,3 +104,22 @@ UpdateCacheKey = Annotated[
Optional[NonEmptyStr],
APIField(default=None, title="the cache key under which you have cached objects"),
]
class EditorBeacon(BaseSchema):
name: NonEmptyStr
point: Optional[PointSchema]
class EditorBeaconsLookup(BaseSchema):
wifi_beacons: dict[Annotated[MacAddress, APIField(title="WiFi beacon BSSID")], EditorBeacon]
ibeacons: dict[
Annotated[str, APIField(title="iBeacon UUID")], # todo: nice to use UUID but django json encoder fails
dict[
Annotated[NonNegativeInt, Lt(2 ** 16), APIField(title="iBeacon major value")],
dict[
Annotated[NonNegativeInt, Lt(2 ** 16), APIField(title="iBeacon minor value")],
EditorBeacon
]
]
]

View file

@ -284,6 +284,8 @@ editor = {
editor._inform_mobile_client(content.find('[data-user-data]'));
editor._beacon_layer.clearLayers();
var group;
if (content.find('[name=fixed_x]').length) {
$('[name=name]').change(editor._source_name_selected).change();
@ -414,12 +416,17 @@ editor = {
if (data_field.length) {
data_field.hide();
var collector = $($('body .scancollector')[0].outerHTML);
editor.load_scancollector_lookup();
var existing_data = [];
if (data_field.val()) {
existing_data = JSON.parse(data_field.val());
}
if (existing_data.length > 0) {
collector.removeClass('empty').addClass('done').find('.wifi-count').text(existing_data.length);
if (existing_data?.wifi?.length || existing_data?.ibeacon?.length > 0) {
// todo: fix this to work with ibeacons
collector.removeClass('empty').addClass('done');
collector.find('.wifi-count').text(existing_data?.wifi?.length);
collector.find('.ibeacon-count').text(existing_data?.ibeacon?.length);
} else {
data_field.closest('form').addClass('scan-lock');
}
@ -657,6 +664,7 @@ editor = {
_geometries_layer: null,
_line_geometries: [],
_highlight_layer: null,
_beacon_layer: null,
_highlight_type: null,
_editing_id: null,
_editing_layer: null,
@ -775,6 +783,8 @@ editor = {
})
editor.get_sources();
editor._beacon_layer = L.layerGroup().addTo(editor.map);
},
_set_max_bounds: function(bounds) {
bounds = bounds ? L.latLngBounds(editor._max_bounds[0], editor._max_bounds[1]).extend(bounds) : editor._max_bounds;
@ -1313,6 +1323,13 @@ editor = {
.on('click', '.scancollector .reset', editor._scancollector_reset);
window.setInterval(editor._scancollector_wifi_scan_perhaps, 1000);
},
_scancollector_lookup: {},
load_scancollector_lookup: function () {
c3nav_api.get('editor/beacons-lookup')
.then(data => {
editor._scancollector_lookup = data;
})
},
_scancollector_data: {
wifi: [],
ibeacon: [],
@ -1341,16 +1358,17 @@ editor = {
},
_scancollector_reset: function () {
var $collector = $('#sidebar').find('.scancollector');
$collector.removeClass('done').removeClass('running').addClass('empty').find('table tbody').each(function(elem) {elem.html('');});
$collector.removeClass('done').removeClass('running').addClass('empty').find('table tbody').each(function(elem) {elem.innerHTML = "";});
$collector.siblings('[name=data]').val('');
$collector.closest('form').addClass('scan-lock');
editor._beacon_layer.clearLayers();
},
_scancollector_wifi_last_max_last: 0,
_scancollector_wifi_last_result: 0,
_scancollector_wifi_result: function(data) {
var $collector = $('#sidebar').find('.scancollector.running'),
$table = $collector.find('.wifi-table tbody'),
item, i, line, apid, color, max_last = 0, now = Date.now();
item, i, line, apid, color, max_last = 0, now = Date.now(), match;
editor._wifi_scan_waits = false;
if (!data.length) return;
@ -1389,9 +1407,22 @@ editor = {
if (line.length) {
line.removeClass('old').find(':last-child').text(item.rssi).css('color', color);
} else {
match = editor._scancollector_lookup.wifi_beacons?.[item.bssid];
if (match && match.point) {
L.geoJson(match.point, {
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {});
}
}).addTo(editor._beacon_layer);
}
shortened_ssid = item.ssid;
if (shortened_ssid.length > 20) {
shortened_ssid = shortened_ssid.slice(0, 20)+'…';
}
line = $('<tr>').addClass(apid);
line.append($('<td>').text(item.bssid));
line.append($('<td>').text(item.ssid));
line.append($('<td>').text(shortened_ssid));
line.append($('<td>').text(match ? match.name : ''));
line.append($('<td>').text(item.rssi).css('color', color));
$table.append(line);
}
@ -1403,7 +1434,7 @@ editor = {
_scancollector_ibeacon_result: function(data) {
var $collector = $('#sidebar').find('.scancollector.running'),
$table = $collector.find('.ibeacon-table tbody'),
item, i, line, beaconid, color = Date.now();
item, i, line, beaconid, color = Date.now(), match;
if (!data.length) return;
@ -1415,12 +1446,21 @@ editor = {
color = Math.max(0, Math.min(50, item.distance));
color = 'rgb('+String(color*5)+', '+String(200-color*4)+', 0)';
if (line.length) {
line.removeClass('old').find(':last-child').text(item.distance).css('color', color);
line.removeClass('old').find(':last-child').text(Math.round(item.distance*100)/100).css('color', color);
} else {
match = editor._scancollector_lookup.ibeacons?.[item.uuid]?.[item.major]?.[item.minor];
if (match && match.point) {
L.geoJson(match.point, {
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {});
}
}).addTo(editor._beacon_layer);
}
line = $('<tr>').addClass(beaconid);
line.append($('<td>').text(item.major));
line.append($('<td>').text(item.minor));
line.append($('<td>').text(item.distance).css('color', color));
line.append($('<td>').text(match ? match.name : ''));
line.append($('<td>').text(Math.round(item.distance*100)/100).css('color', color));
$table.append(line);
}
}

View file

@ -80,12 +80,12 @@
</em></p>
<p><span class="count-line"><span class="wifi-count">0</span> WiFi scans</span></p>
<table class="wifi-table">
<thead><tr><th>BSSID</th><th>SSID</th><th>RSSI</th></tr></thead>
<thead><tr><th>BSSID</th><th>SSID</th><th>Match</th><th>RSSI</th></tr></thead>
<tbody></tbody>
</table>
<p><span class="count-line"><span class="ibeacon-count">0</span> iBeacon scans</span></p>
<table class="ibeacon-table">
<thead><tr><th>Major</th><th>Minor</th><th>Dist</th></tr></thead>
<thead><tr><th>Major</th><th>Minor</th><th>Match</th><th>Dist</th></tr></thead>
<tbody></tbody>
</table>
</div>

View file

@ -44,6 +44,7 @@ NodesAndBeacons = namedtuple("NodesAndBeacons", ("beacons", "nodes", "nodes_for_
def get_nodes_and_ranging_beacons():
from c3nav.mesh.models import MeshNode
from c3nav.mesh.messages import MeshMessageType
beacons = {beacon.id: beacon for beacon in RangingBeacon.objects.all().select_related("space")}
nodes = {
node.address: node
@ -54,6 +55,31 @@ def get_nodes_and_ranging_beacons():
for node in nodes.values()
if node.ranging_beacon and node.ranging_beacon.id in beacons
}
# todo: throw warnings if duplicates somewhere
for ranging_beacon_id, node in nodes_for_beacons.items():
ranging_beacon = beacons[ranging_beacon_id]
ranging_beacon.save = None
if not ranging_beacon.wifi_bssid:
ranging_beacon.wifi_bssid = node.address
if not ranging_beacon.bluetooth_address:
ranging_beacon.bluetooth_address = node.address[:-2] + hex(int(node.address[-2:], 16)+1)[2:]
ibeacon_msg = node.last_messages[MeshMessageType.CONFIG_IBEACON]
if ibeacon_msg:
if not ranging_beacon.ibeacon_uuid:
ranging_beacon.ibeacon_uuid = ibeacon_msg.parsed.content.uuid
if not ranging_beacon.ibeacon_major:
ranging_beacon.ibeacon_major = ibeacon_msg.parsed.content.major
if not ranging_beacon.ibeacon_uuid:
ranging_beacon.ibeacon_minor = ibeacon_msg.parsed.content.minor
node_msg = node.last_messages[MeshMessageType.CONFIG_NODE]
if node_msg:
if not ranging_beacon.node_number:
ranging_beacon.node_number = node_msg.parsed.content.number
ranging_beacon.node_name = node_msg.parsed.content.name
return NodesAndBeacons(
beacons=beacons,
nodes=nodes,

View file

@ -114,4 +114,5 @@ BeaconsXYZ = dict[
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()