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.urls import Resolver404, resolve
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from ninja import Router as APIRouter
|
from ninja import Router as APIRouter
|
||||||
|
from shapely.geometry.geo import mapping
|
||||||
|
|
||||||
from c3nav.api.auth import APIKeyAuth, auth_permission_responses
|
from c3nav.api.auth import APIKeyAuth, auth_permission_responses
|
||||||
from c3nav.api.exceptions import API404
|
from c3nav.api.exceptions import API404
|
||||||
from c3nav.editor.api.base import api_etag_with_update_cache_key
|
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.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.editor.views.base import editor_etag_func
|
||||||
from c3nav.mapdata.api.base import api_etag
|
from c3nav.mapdata.api.base import api_etag
|
||||||
from c3nav.mapdata.models import Source
|
from c3nav.mapdata.models import Source
|
||||||
from c3nav.mapdata.schemas.responses import WithBoundsSchema
|
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"}))
|
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.
|
this is a mess. good luck. if you actually want to use this, poke us so we might add better documentation.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
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 typing import Annotated, Literal, Optional, Union
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from annotated_types import Lt
|
||||||
from pydantic import Field as APIField
|
from pydantic import Field as APIField
|
||||||
from pydantic import PositiveInt
|
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
|
from c3nav.api.utils import NonEmptyStr
|
||||||
|
|
||||||
GeometryStylesSchema = Annotated[
|
GeometryStylesSchema = Annotated[
|
||||||
|
@ -100,3 +104,22 @@ UpdateCacheKey = Annotated[
|
||||||
Optional[NonEmptyStr],
|
Optional[NonEmptyStr],
|
||||||
APIField(default=None, title="the cache key under which you have cached objects"),
|
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._inform_mobile_client(content.find('[data-user-data]'));
|
||||||
|
|
||||||
|
editor._beacon_layer.clearLayers();
|
||||||
|
|
||||||
var group;
|
var group;
|
||||||
if (content.find('[name=fixed_x]').length) {
|
if (content.find('[name=fixed_x]').length) {
|
||||||
$('[name=name]').change(editor._source_name_selected).change();
|
$('[name=name]').change(editor._source_name_selected).change();
|
||||||
|
@ -414,12 +416,17 @@ editor = {
|
||||||
if (data_field.length) {
|
if (data_field.length) {
|
||||||
data_field.hide();
|
data_field.hide();
|
||||||
var collector = $($('body .scancollector')[0].outerHTML);
|
var collector = $($('body .scancollector')[0].outerHTML);
|
||||||
|
editor.load_scancollector_lookup();
|
||||||
|
|
||||||
var existing_data = [];
|
var existing_data = [];
|
||||||
if (data_field.val()) {
|
if (data_field.val()) {
|
||||||
existing_data = JSON.parse(data_field.val());
|
existing_data = JSON.parse(data_field.val());
|
||||||
}
|
}
|
||||||
if (existing_data.length > 0) {
|
if (existing_data?.wifi?.length || existing_data?.ibeacon?.length > 0) {
|
||||||
collector.removeClass('empty').addClass('done').find('.wifi-count').text(existing_data.length);
|
// 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 {
|
} else {
|
||||||
data_field.closest('form').addClass('scan-lock');
|
data_field.closest('form').addClass('scan-lock');
|
||||||
}
|
}
|
||||||
|
@ -657,6 +664,7 @@ editor = {
|
||||||
_geometries_layer: null,
|
_geometries_layer: null,
|
||||||
_line_geometries: [],
|
_line_geometries: [],
|
||||||
_highlight_layer: null,
|
_highlight_layer: null,
|
||||||
|
_beacon_layer: null,
|
||||||
_highlight_type: null,
|
_highlight_type: null,
|
||||||
_editing_id: null,
|
_editing_id: null,
|
||||||
_editing_layer: null,
|
_editing_layer: null,
|
||||||
|
@ -775,6 +783,8 @@ editor = {
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.get_sources();
|
editor.get_sources();
|
||||||
|
|
||||||
|
editor._beacon_layer = L.layerGroup().addTo(editor.map);
|
||||||
},
|
},
|
||||||
_set_max_bounds: function(bounds) {
|
_set_max_bounds: function(bounds) {
|
||||||
bounds = bounds ? L.latLngBounds(editor._max_bounds[0], editor._max_bounds[1]).extend(bounds) : editor._max_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);
|
.on('click', '.scancollector .reset', editor._scancollector_reset);
|
||||||
window.setInterval(editor._scancollector_wifi_scan_perhaps, 1000);
|
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: {
|
_scancollector_data: {
|
||||||
wifi: [],
|
wifi: [],
|
||||||
ibeacon: [],
|
ibeacon: [],
|
||||||
|
@ -1341,16 +1358,17 @@ editor = {
|
||||||
},
|
},
|
||||||
_scancollector_reset: function () {
|
_scancollector_reset: function () {
|
||||||
var $collector = $('#sidebar').find('.scancollector');
|
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.siblings('[name=data]').val('');
|
||||||
$collector.closest('form').addClass('scan-lock');
|
$collector.closest('form').addClass('scan-lock');
|
||||||
|
editor._beacon_layer.clearLayers();
|
||||||
},
|
},
|
||||||
_scancollector_wifi_last_max_last: 0,
|
_scancollector_wifi_last_max_last: 0,
|
||||||
_scancollector_wifi_last_result: 0,
|
_scancollector_wifi_last_result: 0,
|
||||||
_scancollector_wifi_result: function(data) {
|
_scancollector_wifi_result: function(data) {
|
||||||
var $collector = $('#sidebar').find('.scancollector.running'),
|
var $collector = $('#sidebar').find('.scancollector.running'),
|
||||||
$table = $collector.find('.wifi-table tbody'),
|
$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;
|
editor._wifi_scan_waits = false;
|
||||||
|
|
||||||
if (!data.length) return;
|
if (!data.length) return;
|
||||||
|
@ -1389,9 +1407,22 @@ editor = {
|
||||||
if (line.length) {
|
if (line.length) {
|
||||||
line.removeClass('old').find(':last-child').text(item.rssi).css('color', color);
|
line.removeClass('old').find(':last-child').text(item.rssi).css('color', color);
|
||||||
} else {
|
} 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 = $('<tr>').addClass(apid);
|
||||||
line.append($('<td>').text(item.bssid));
|
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));
|
line.append($('<td>').text(item.rssi).css('color', color));
|
||||||
$table.append(line);
|
$table.append(line);
|
||||||
}
|
}
|
||||||
|
@ -1403,7 +1434,7 @@ editor = {
|
||||||
_scancollector_ibeacon_result: function(data) {
|
_scancollector_ibeacon_result: function(data) {
|
||||||
var $collector = $('#sidebar').find('.scancollector.running'),
|
var $collector = $('#sidebar').find('.scancollector.running'),
|
||||||
$table = $collector.find('.ibeacon-table tbody'),
|
$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;
|
if (!data.length) return;
|
||||||
|
|
||||||
|
@ -1415,12 +1446,21 @@ editor = {
|
||||||
color = Math.max(0, Math.min(50, item.distance));
|
color = Math.max(0, Math.min(50, item.distance));
|
||||||
color = 'rgb('+String(color*5)+', '+String(200-color*4)+', 0)';
|
color = 'rgb('+String(color*5)+', '+String(200-color*4)+', 0)';
|
||||||
if (line.length) {
|
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 {
|
} 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 = $('<tr>').addClass(beaconid);
|
||||||
line.append($('<td>').text(item.major));
|
line.append($('<td>').text(item.major));
|
||||||
line.append($('<td>').text(item.minor));
|
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);
|
$table.append(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,12 +80,12 @@
|
||||||
</em></p>
|
</em></p>
|
||||||
<p><span class="count-line"><span class="wifi-count">0</span> WiFi scans</span></p>
|
<p><span class="count-line"><span class="wifi-count">0</span> WiFi scans</span></p>
|
||||||
<table class="wifi-table">
|
<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>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
<p><span class="count-line"><span class="ibeacon-count">0</span> iBeacon scans</span></p>
|
<p><span class="count-line"><span class="ibeacon-count">0</span> iBeacon scans</span></p>
|
||||||
<table class="ibeacon-table">
|
<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>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,6 +44,7 @@ NodesAndBeacons = namedtuple("NodesAndBeacons", ("beacons", "nodes", "nodes_for_
|
||||||
|
|
||||||
def get_nodes_and_ranging_beacons():
|
def get_nodes_and_ranging_beacons():
|
||||||
from c3nav.mesh.models import MeshNode
|
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")}
|
beacons = {beacon.id: beacon for beacon in RangingBeacon.objects.all().select_related("space")}
|
||||||
nodes = {
|
nodes = {
|
||||||
node.address: node
|
node.address: node
|
||||||
|
@ -54,6 +55,31 @@ def get_nodes_and_ranging_beacons():
|
||||||
for node in nodes.values()
|
for node in nodes.values()
|
||||||
if node.ranging_beacon and node.ranging_beacon.id in beacons
|
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(
|
return NodesAndBeacons(
|
||||||
beacons=beacons,
|
beacons=beacons,
|
||||||
nodes=nodes,
|
nodes=nodes,
|
||||||
|
|
|
@ -114,4 +114,5 @@ BeaconsXYZ = dict[
|
||||||
description="get xyz coordinates for all known positioning beacons",
|
description="get xyz coordinates for all known positioning beacons",
|
||||||
response={200: BeaconsXYZ, **auth_responses})
|
response={200: BeaconsXYZ, **auth_responses})
|
||||||
def beacons_xyz():
|
def beacons_xyz():
|
||||||
return Locator.load().get_all_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