add view to show currently installed firmwares

This commit is contained in:
Laura Klünder 2023-11-06 18:26:19 +01:00
parent e01e718356
commit c872b97fa3
6 changed files with 188 additions and 14 deletions

View file

@ -5,6 +5,9 @@
{% block subcontent %}
<h4>Firmwares</h4>
<a class="button" href="{% url "control.mesh.firmwares.current" %}">
{% trans 'View current firmwares' %}
</a>
{% include 'control/fragment_pagination.html' %}
@ -12,8 +15,7 @@
<tr>
<th>{% trans 'Created' %}</th>
<th>{% trans 'Uploader' %}</th>
<th>{% trans 'Project Name' %}</th>
<th>{% trans 'Version' %}</th>
<th>{% trans 'Firmware' %}</th>
<th>{% trans 'IDF version' %}</th>
<th>{% trans 'Builds' %}</th>
</tr>
@ -21,8 +23,7 @@
<tr>
<td>{{ firmware.created }}</td>
<td>{{ firmware.uploader }}</td>
<td>{{ firmware.project_name }}</td>
<td>{{ firmware.version }}</td>
<td>{{ firmware.project_name }} {{ firmware.version }}</td>
<td>{{ firmware.idf_version }}</td>
<td>
{% for build in firmware.builds.all %}

View file

@ -0,0 +1,47 @@
{% extends 'control/base.html' %}
{% load i18n mesh_node %}
{% block heading %}{% trans 'Mesh Firmwares' %}{% endblock %}
{% block subcontent %}
<h4>Current Firmwares</h4>
<a class="button" href="{% url "control.mesh.firmwares" %}">
{% trans 'View available firmwares' %}
</a>
<table>
<tr>
<th>{% trans 'Created / First seen' %}</th>
<th>{% trans 'Firmware' %}</th>
<th>{% trans 'Build' %}</th>
<th>{% trans 'IDF version' %}</th>
<th>{% trans 'Installed on' %}</th>
</tr>
{% for firmware, nodes in firmwares %}
<tr>
<td>{{ firmware.created }}</td>
<td>
{% if firmware.build %}
<a href="#">{{ firmware.project_name }} {{ firmware.version }}</a>
{% else %}
{{ firmware.project_name }} {{ firmware.version }}<br>
<small>{{ firmware.sha256_hash }}</small>
{% endif %}
</td>
<td>
{% if firmware.build %}
<a href="#">{{ firmware.build.variant }} ({{ firmware.chip.pretty_name }})</a>
{% else %}
({{ firmware.chip.pretty_name }})
{% endif %}
</td>
<td>{{ firmware.idf_version }}</td>
<td>
{% for node in nodes %}
{% mesh_node node %}
{% endfor %}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -2,12 +2,19 @@ from django import template
from django.urls import reverse
from django.utils.html import format_html
from c3nav.mesh.models import MeshNode
register = template.Library()
@register.simple_tag(takes_context=True)
def mesh_node(context, bssid):
name = context.get("node_names", {}).get(bssid, None)
def mesh_node(context, node: str | MeshNode):
if isinstance(node, str):
bssid = node
name = context.get("node_names", {}).get(node, None)
else:
bssid = node.address
name = node.name
if name:
return format_html(
'<a href="{url}">{bssid}</a> ({name})',

View file

@ -4,8 +4,9 @@ from c3nav.control.views.access import grant_access, grant_access_qr
from c3nav.control.views.announcements import announcement_detail, announcement_list
from c3nav.control.views.base import ControlPanelIndexView
from c3nav.control.views.mapupdates import map_updates
from c3nav.control.views.mesh import (MeshFirmwaresListView, MeshLogView, MeshMessageListView, MeshMessageSendingView,
MeshMessageSendView, MeshNodeDetailView, MeshNodeEditView, MeshNodeListView)
from c3nav.control.views.mesh import (MeshFirmwaresCurrentListView, MeshFirmwaresListView, MeshLogView,
MeshMessageListView, MeshMessageSendingView, MeshMessageSendView,
MeshNodeDetailView, MeshNodeEditView, MeshNodeListView)
from c3nav.control.views.users import UserListView, user_detail
urlpatterns = [
@ -20,6 +21,7 @@ urlpatterns = [
path('mesh/logs/', MeshLogView.as_view(), name='control.mesh.log'),
path('mesh/messages/', MeshMessageListView.as_view(), name='control.mesh.messages'),
path('mesh/firmwares/', MeshFirmwaresListView.as_view(), name='control.mesh.firmwares'),
path('mesh/firmwares/current/', MeshFirmwaresCurrentListView.as_view(), name='control.mesh.firmwares.current'),
path('mesh/<str:pk>/', MeshNodeDetailView.as_view(), name='control.mesh.node.detail'),
path('mesh/<str:pk>/edit/', MeshNodeEditView.as_view(), name='control.mesh.node.edit'),
path('mesh/message/sending/<uuid:uuid>/', MeshMessageSendingView.as_view(), name='control.mesh.sending'),

View file

@ -195,3 +195,23 @@ class MeshFirmwaresListView(ControlPanelMixin, ListView):
ordering = "-created"
context_object_name = "firmwares"
paginate_by = 20
class MeshFirmwaresCurrentListView(ControlPanelMixin, TemplateView):
template_name = "control/mesh_firmwares_current.html"
def get_context_data(self, **kwargs):
nodes = list(MeshNode.objects.all().prefetch_firmwares())
firmwares = {}
for node in nodes:
firmwares.setdefault(node.firmware_desc.get_lookup(), (node.firmware_desc, []))[1].append(node)
firmwares = sorted(firmwares.values(), key=lambda k: k[0].created, reverse=True)
print(firmwares)
return {
**super().get_context_data(),
"firmwares": firmwares,
}

View file

@ -1,7 +1,9 @@
from collections import UserDict
from collections import UserDict, namedtuple
from dataclasses import dataclass
from datetime import datetime
from functools import cached_property
from operator import attrgetter
from typing import Any, Mapping, Self
from typing import Any, Mapping, Optional, Self
from django.contrib.auth import get_user_model
from django.db import NotSupportedError, models
@ -10,20 +12,44 @@ from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from c3nav.mesh.dataformats import BoardType
from c3nav.mesh.messages import ChipType
from c3nav.mesh.messages import ChipType, ConfigFirmwareMessage, ConfigHardwareMessage
from c3nav.mesh.messages import MeshMessage as MeshMessage
from c3nav.mesh.messages import MeshMessageType
FirmwareLookup = namedtuple('FirmwareLookup', ('sha256_hash', 'chip', 'project_name', 'version', 'idf_version'))
@dataclass
class FirmwareDescription:
chip: ChipType
project_name: str
version: str
idf_version: str
sha256_hash: str
build: Optional["FirmwareBuild"] = None
created: datetime | None = None
def get_lookup(self) -> FirmwareLookup:
return FirmwareLookup(
chip=self.chip,
project_name=self.project_name,
version=self.version,
idf_version=self.idf_version,
sha256_hash=self.sha256_hash,
)
class MeshNodeQuerySet(models.QuerySet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._prefetch_last_messages = set()
self._prefetch_last_messages_done = False
self._prefetch_firmwares = False
def _clone(self):
clone = super()._clone()
clone._prefetch_last_messages = self._prefetch_last_messages
clone._prefetch_firmwares = self._prefetch_firmwares
return clone
def prefetch_last_messages(self, *types: MeshMessageType):
@ -33,10 +59,16 @@ class MeshNodeQuerySet(models.QuerySet):
)
return clone
def prefetch_firmwares(self, *types: MeshMessageType):
clone = self.prefetch_last_messages(MeshMessageType.CONFIG_FIRMWARE,
MeshMessageType.CONFIG_HARDWARE)
clone._prefetch_firmwares = True
return clone
def _fetch_all(self):
super()._fetch_all()
if self._prefetch_last_messages and not self._prefetch_last_messages_done:
nodes = {node.pk: node for node in self._result_cache}
nodes: dict[str, MeshNode] = {node.pk: node for node in self._result_cache}
try:
for message in NodeMessage.objects.order_by('message_type', 'src_node', '-datetime', '-pk').filter(
message_type__in=(t.name for t in self._prefetch_last_messages),
@ -45,9 +77,52 @@ class MeshNodeQuerySet(models.QuerySet):
nodes[message.src_node_id].last_messages[message.message_type] = message
for node in nodes.values():
node.last_messages["any"] = max(node.last_messages.values(), key=attrgetter("datetime"))
self._prefetch_last_messages_done = True
except NotSupportedError:
pass
if self._prefetch_firmwares:
# fetch matching firmware builds
firmwares = {
fw_desc.get_lookup(): fw_desc for fw_desc in
(build.get_firmware_description() for build in FirmwareBuild.objects.filter(
sha256_hash__in=set(
node.last_messages[MeshMessageType.CONFIG_FIRMWARE].parsed.app_desc.app_elf_sha256
for node in self._result_cache
)
))
}
# assign firmware descriptions
for node in nodes.values():
firmware_desc = node.get_firmware_description()
node.firmware_desc = firmwares.get(firmware_desc.get_lookup(), firmware_desc)
# get date of first appearance
nodes_to_complete = tuple(
node for node in nodes.values()
if node.firmware_desc.build is None
)
try:
created_lookup = {
msg.parsed.app_desc.app_elf_sha256: msg.datetime
for msg in NodeMessage.objects.filter(
message_type=MeshMessageType.CONFIG_FIRMWARE.name,
data__app_elf_sha256__in=(node.firmware_desc.sha256_hash for node in nodes_to_complete)
).order_by('data__app_elf_sha256', 'datetime').distinct('data__app_elf_sha256')
}
print(created_lookup)
except NotSupportedError:
created_lookup = {
app_elf_sha256: NodeMessage.objects.filter(
message_type=MeshMessageType.CONFIG_FIRMWARE.name,
data__app_elf_sha256=app_elf_sha256
).order_by('datetime').first()
for app_elf_sha256 in {node.firmware_desc.sha256_hash for node in nodes_to_complete}
}
for node in nodes_to_complete:
node.firmware_desc.created = created_lookup[node.firmware_desc.sha256_hash]
class LastMessagesByTypeLookup(UserDict):
def __init__(self, node):
@ -103,9 +178,20 @@ class MeshNode(models.Model):
return self.address
@cached_property
def last_messages(self) -> Mapping[Any, Self]:
def last_messages(self) -> Mapping[Any, "NodeMessage"]:
return LastMessagesByTypeLookup(self)
def get_firmware_description(self) -> FirmwareDescription:
firmware_msg: ConfigFirmwareMessage = self.last_messages[MeshMessageType.CONFIG_FIRMWARE].parsed
hardware_msg: ConfigHardwareMessage = self.last_messages[MeshMessageType.CONFIG_HARDWARE].parsed
return FirmwareDescription(
chip=hardware_msg.chip,
project_name=firmware_msg.app_desc.project_name,
version=firmware_msg.app_desc.version,
idf_version=firmware_msg.app_desc.idf_version,
sha256_hash=firmware_msg.app_desc.app_elf_sha256,
)
class MeshUplink(models.Model):
"""
@ -144,7 +230,7 @@ class NodeMessage(models.Model):
return '(#%d) %s at %s' % (self.pk, self.get_message_type_display(), self.datetime)
@cached_property
def parsed(self) -> dict:
def parsed(self) -> Self:
return MeshMessage.fromjson(self.data)
@ -202,6 +288,17 @@ class FirmwareBuild(models.Model):
'boards': self.boards,
}
def get_firmware_description(self) -> FirmwareDescription:
return FirmwareDescription(
chip=ChipType(self.chip),
project_name=self.version.project_name,
version=self.version.version,
idf_version=self.version.idf_version,
sha256_hash=self.sha256_hash,
created=self.version.created,
build=self,
)
class FirmwareBuildBoard(models.Model):
BOARDS = [(boardtype.name, boardtype.pretty_name) for boardtype in BoardType]