mesh debugging and control

This commit is contained in:
Laura Klünder 2023-10-02 22:02:25 +02:00
parent 4c40597cd3
commit df6efbc8d5
11 changed files with 201 additions and 12 deletions

View file

@ -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'),
)

View file

@ -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> &middot;
<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> &middot;
{% endif %}
@ -12,10 +12,10 @@
{% blocktrans %}Page {{ page_number }} of {{ num_pages }}{% endblocktrans %}
{% endwith %}
{% if page_obj.has_next %}
&middot; <a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.next_page_number }}">
&middot; <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>
&middot; <a href="?{% if request.GET.s %}s={{ request.GET.s | urlencode }}&{% endif %}page={{ page_obj.paginator.num_pages }}">
&middot; <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 %}

View 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 %}

View file

@ -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>

View file

@ -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'),
]

View file

@ -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

View file

@ -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()

View file

@ -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):

View file

@ -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',
),
]

View 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'),
),
]

View file

@ -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'),
]