mesh debugging and control
This commit is contained in:
parent
4c40597cd3
commit
df6efbc8d5
11 changed files with 201 additions and 12 deletions
|
@ -9,7 +9,8 @@ from itertools import chain
|
|||
import pytz
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Prefetch
|
||||
from django.forms import ChoiceField, Form, IntegerField, ModelForm, Select
|
||||
from django.forms import ChoiceField, Form, IntegerField, ModelForm, Select, MultipleChoiceField, \
|
||||
ModelMultipleChoiceField
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext_lazy
|
||||
|
@ -19,6 +20,8 @@ from c3nav.mapdata.forms import I18nModelFormMixin
|
|||
from c3nav.mapdata.models import MapUpdate, Space
|
||||
from c3nav.mapdata.models.access import (AccessPermission, AccessPermissionToken, AccessPermissionTokenItem,
|
||||
AccessRestriction, AccessRestrictionGroup)
|
||||
from c3nav.mesh.messages import MessageType
|
||||
from c3nav.mesh.models import MeshNode
|
||||
from c3nav.site.models import Announcement
|
||||
|
||||
|
||||
|
@ -295,3 +298,16 @@ class MapUpdateForm(ModelForm):
|
|||
class Meta:
|
||||
model = MapUpdate
|
||||
fields = ('geometries_changed', )
|
||||
|
||||
|
||||
class MeshMessageFilerForm(Form):
|
||||
message_types = MultipleChoiceField(
|
||||
choices=[(msgtype.value, msgtype.name) for msgtype in MessageType],
|
||||
required=False,
|
||||
label=_('message types'),
|
||||
)
|
||||
nodes = ModelMultipleChoiceField(
|
||||
queryset=MeshNode.objects.all(),
|
||||
required=False,
|
||||
label=_('nodes'),
|
||||
)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{% load i18n %}
|
||||
<p>
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page=1">
|
||||
<a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page=1{% if form_data %}&{{ form_data }}{% endif %}">
|
||||
« {% trans 'first' %}
|
||||
</a> ·
|
||||
<a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.previous_page_number }}">
|
||||
<a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.previous_page_number }}{% if form_data %}&{{ form_data }}{% endif %}">
|
||||
‹ {% trans 'previous' %}
|
||||
</a> ·
|
||||
{% endif %}
|
||||
|
@ -12,10 +12,10 @@
|
|||
{% blocktrans %}Page {{ page_number }} of {{ num_pages }}{% endblocktrans %}
|
||||
{% endwith %}
|
||||
{% if page_obj.has_next %}
|
||||
· <a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.next_page_number }}">
|
||||
· <a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.next_page_number }}{% if form_data %}&{{ form_data }}{% endif %}">
|
||||
{% trans 'next' %} ›
|
||||
</a>
|
||||
· <a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.paginator.num_pages }}">
|
||||
· <a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.paginator.num_pages }}{% if form_data %}&{{ form_data }}{% endif %}">
|
||||
{% trans 'last' %} »
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
50
src/c3nav/control/templates/control/mesh_messages.html
Normal file
50
src/c3nav/control/templates/control/mesh_messages.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% extends 'control/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block heading %}{% trans 'Mesh messages' %}{% endblock %}
|
||||
|
||||
{% block subcontent %}
|
||||
<form>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
{{ form.message_types }}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{ form.nodes }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<button type="submit">Filer</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% include 'control/fragment_pagination.html' %}
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Time' %}</th>
|
||||
<th>{% trans 'Node' %}</th>
|
||||
<th>{% trans 'Type' %}</th>
|
||||
<th>{% trans 'Data' %}</th>
|
||||
</tr>
|
||||
{% for msg in mesh_messages %}
|
||||
<tr>
|
||||
<td>{{ msg.datetime }}</td>
|
||||
<td>{{ msg.node }}</td>
|
||||
<td>{{ msg.get_message_type_display }}</td>
|
||||
<td>
|
||||
|
||||
{% for key, value in msg.data.items %}
|
||||
{% if key != "src" and key != "dst" and key != "msg_id" %}
|
||||
<div class="mesh-msg-data mesh-msg-type-{{ key }}">
|
||||
<strong>{{ key }}</strong>: {{ value }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% include 'control/fragment_pagination.html' %}
|
||||
{% endblock %}
|
|
@ -8,7 +8,9 @@
|
|||
<tr>
|
||||
<th>{% trans 'Address' %}</th>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Last seen' %}</th>
|
||||
<th>{% trans 'Chip' %}</th>
|
||||
<th>{% trans 'Firmware' %}</th>
|
||||
<th>{% trans 'Last msg' %}</th>
|
||||
<th>{% trans 'Parent' %}</th>
|
||||
<th>{% trans 'Route' %}</th>
|
||||
</tr>
|
||||
|
@ -16,7 +18,13 @@
|
|||
<tr>
|
||||
<td>{{ node.address }}</td>
|
||||
<td>{{ node.name }}</td>
|
||||
<td>{{ node.last_seen }}</td>
|
||||
<td>
|
||||
{{ node.firmware.get_chip_display }}
|
||||
</td>
|
||||
<td>
|
||||
{{ node.firmware.version }} (IDF {{ node.firmware.idf_version }})
|
||||
</td>
|
||||
<td>{{ node.last_msg }}</td>
|
||||
<td>{{ node.parent }}</td>
|
||||
<td>{{ node.route }}</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.urls import path
|
||||
|
||||
from c3nav.control.views.mesh import MeshNodeListView
|
||||
from c3nav.control.views.mesh import MeshNodeListView, MeshMessageListView
|
||||
from c3nav.control.views.mapupdates import map_updates
|
||||
from c3nav.control.views.announcements import announcement_list, announcement_detail
|
||||
from c3nav.control.views.access import grant_access, grant_access_qr
|
||||
|
@ -16,5 +16,6 @@ urlpatterns = [
|
|||
path('announcements/<int:annoucement>/', announcement_detail, name='control.announcements.detail'),
|
||||
path('mapupdates/', map_updates, name='control.map_updates'),
|
||||
path('mesh/', MeshNodeListView.as_view(), name='control.mesh_nodes'),
|
||||
path('mesh/messages/', MeshMessageListView.as_view(), name='control.mesh_messages'),
|
||||
path('', ControlPanelIndexView.as_view(), name='control.index'),
|
||||
]
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
from django.db.models import Max
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
from c3nav.control.forms import MeshMessageFilerForm
|
||||
from c3nav.control.views.base import ControlPanelMixin
|
||||
from c3nav.mesh.models import MeshNode
|
||||
from c3nav.mesh.models import MeshNode, NodeMessage
|
||||
|
||||
|
||||
class MeshNodeListView(ControlPanelMixin, ListView):
|
||||
|
@ -9,3 +12,39 @@ class MeshNodeListView(ControlPanelMixin, ListView):
|
|||
template_name = "control/mesh_nodes.html"
|
||||
ordering = "address"
|
||||
context_object_name = "nodes"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().annotate(last_msg=Max('received_messages__datetime'))
|
||||
|
||||
|
||||
class MeshMessageListView(ControlPanelMixin, ListView):
|
||||
model = NodeMessage
|
||||
template_name = "control/mesh_messages.html"
|
||||
ordering = "-datetime"
|
||||
paginate_by = 20
|
||||
context_object_name = "mesh_messages"
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
|
||||
self.form = MeshMessageFilerForm(self.request.GET)
|
||||
if self.form.is_valid():
|
||||
if self.form.cleaned_data['message_types']:
|
||||
qs = qs.filter(message_type__in=self.form.cleaned_data['message_types'])
|
||||
if self.form.cleaned_data['nodes']:
|
||||
qs = qs.filter(node__in=self.form.cleaned_data['nodes'])
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
ctx = super().get_context_data(*args, **kwargs)
|
||||
|
||||
form_data = self.request.GET.copy()
|
||||
form_data.pop('page', None)
|
||||
|
||||
ctx.update({
|
||||
'form': self.form,
|
||||
'form_data': form_data.urlencode(),
|
||||
})
|
||||
return ctx
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from channels.db import database_sync_to_async
|
|||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
from c3nav.mesh import messages
|
||||
from c3nav.mesh.models import MeshNode, NodeMessage
|
||||
from c3nav.mesh.models import MeshNode, NodeMessage, Firmware
|
||||
|
||||
|
||||
class MeshConsumer(AsyncWebsocketConsumer):
|
||||
|
@ -69,6 +69,16 @@ class MeshConsumer(AsyncWebsocketConsumer):
|
|||
|
||||
await self.log_received_message(msg)
|
||||
|
||||
if isinstance(msg, messages.ConfigFirmwareMessage):
|
||||
await self._handle_config_firmware_msg(msg)
|
||||
return
|
||||
|
||||
@database_sync_to_async
|
||||
def _handle_config_firmware_msg(self, msg):
|
||||
self.firmware, created = Firmware.objects.get_or_create(**msg.to_model_data())
|
||||
self.node.firmware = self.firmware
|
||||
self.node.save()
|
||||
|
||||
@database_sync_to_async
|
||||
def get_node(self, address):
|
||||
return MeshNode.objects.get_or_create(address=address)
|
||||
|
@ -100,3 +110,8 @@ class MeshConsumer(AsyncWebsocketConsumer):
|
|||
@database_sync_to_async
|
||||
def remove_route_to_nodes(self, route_address, node_addresses):
|
||||
MeshNode.objects.filter(address__in=node_addresses, route_id=route_address).update(route_id=None)
|
||||
|
||||
@database_sync_to_async
|
||||
def set_node_firmware(self, firmware):
|
||||
self.node.firmware = firmware
|
||||
self.node.save()
|
|
@ -112,6 +112,8 @@ class ConfigDumpMessage(Message, msg_id=MessageType.CONFIG_DUMP):
|
|||
|
||||
@dataclass
|
||||
class ConfigFirmwareMessage(Message, msg_id=MessageType.CONFIG_FIRMWARE):
|
||||
chip: int = field(metadata={'format': SimpleFormat('H')})
|
||||
revision: int = field(metadata={'format': SimpleFormat('2B')})
|
||||
magic_word: int = field(metadata={'format': SimpleFormat('I')}, repr=False)
|
||||
secure_version: int = field(metadata={'format': SimpleFormat('I')})
|
||||
reserv1: list[int] = field(metadata={'format': SimpleFormat('2I')}, repr=False)
|
||||
|
@ -123,6 +125,15 @@ class ConfigFirmwareMessage(Message, msg_id=MessageType.CONFIG_FIRMWARE):
|
|||
app_elf_sha256: str = field(metadata={'format': HexFormat(32)})
|
||||
reserv2: list[int] = field(metadata={'format': SimpleFormat('20I')}, repr=False)
|
||||
|
||||
def to_model_data(self):
|
||||
return {
|
||||
'chip': self.chip,
|
||||
'project_name': self.project_name,
|
||||
'version': self.version,
|
||||
'idf_version': self.idf_version,
|
||||
'sha256_hash': self.app_elf_sha256,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigPositionMessage(Message, msg_id=MessageType.CONFIG_POSITION):
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.2.1 on 2023-10-02 19:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mesh', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='firmware',
|
||||
unique_together={('chip', 'project_name', 'version', 'idf_version', 'sha256_hash')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='meshnode',
|
||||
name='firmware',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='installed_on', to='mesh.firmware', verbose_name='firmware'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='firmware',
|
||||
name='compile_time',
|
||||
),
|
||||
]
|
18
src/c3nav/mesh/migrations/0003_meshnode_name.py
Normal file
18
src/c3nav/mesh/migrations/0003_meshnode_name.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.1 on 2023-10-02 19:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mesh', '0002_alter_firmware_unique_together_meshnode_firmware_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='meshnode',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='name'),
|
||||
),
|
||||
]
|
|
@ -11,13 +11,18 @@ class ChipID(models.IntegerChoices):
|
|||
|
||||
class MeshNode(models.Model):
|
||||
address = models.CharField(_('mac address'), max_length=17, primary_key=True)
|
||||
name = models.CharField(_('name'), max_length=32, null=True, blank=True)
|
||||
first_seen = models.DateTimeField(_('first seen'), auto_now_add=True)
|
||||
parent_node = models.ForeignKey('MeshNode', models.PROTECT, null=True,
|
||||
related_name='child_nodes', verbose_name=_('parent node'))
|
||||
route = models.ForeignKey('MeshNode', models.PROTECT, null=True,
|
||||
related_name='routed_nodes', verbose_name=_('route'))
|
||||
firmware = models.ForeignKey('Firmware', models.PROTECT, null=True,
|
||||
related_name='installed_on', verbose_name=_('firmware'))
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return '%s (%s)' % (self.address, self.name)
|
||||
return self.address
|
||||
|
||||
|
||||
|
@ -38,11 +43,10 @@ class Firmware(models.Model):
|
|||
project_name = models.CharField(_('project name'), max_length=32)
|
||||
version = models.CharField(_('firmware version'), max_length=32)
|
||||
idf_version = models.CharField(_('IDF version'), max_length=32)
|
||||
compile_time = models.DateTimeField(_('compile time'))
|
||||
sha256_hash = models.CharField(_('SHA256 hash'), unique=True, max_length=64)
|
||||
binary = models.FileField(_('firmware file'), null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
('chip', 'project_name', 'version', 'idf_version', 'compile_time', 'sha256_hash'),
|
||||
('chip', 'project_name', 'version', 'idf_version', 'sha256_hash'),
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue