full scancollector implementation with all kinds of nice features
This commit is contained in:
parent
7532fc39ed
commit
7fa75e1617
6 changed files with 157 additions and 13 deletions
|
@ -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")
|
|
@ -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
|
||||
]
|
||||
]
|
||||
]
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
Loading…
Add table
Add a link
Reference in a new issue