diff --git a/src/c3nav/control/templates/control/mesh_firmwares.html b/src/c3nav/control/templates/control/mesh_firmwares.html index c92dc262..731d6f34 100644 --- a/src/c3nav/control/templates/control/mesh_firmwares.html +++ b/src/c3nav/control/templates/control/mesh_firmwares.html @@ -5,6 +5,9 @@ {% block subcontent %}

Firmwares

+ + {% trans 'View current firmwares' %} + {% include 'control/fragment_pagination.html' %} @@ -12,8 +15,7 @@ {% trans 'Created' %} {% trans 'Uploader' %} - {% trans 'Project Name' %} - {% trans 'Version' %} + {% trans 'Firmware' %} {% trans 'IDF version' %} {% trans 'Builds' %} @@ -21,8 +23,7 @@ {{ firmware.created }} {{ firmware.uploader }} - {{ firmware.project_name }} - {{ firmware.version }} + {{ firmware.project_name }} {{ firmware.version }} {{ firmware.idf_version }} {% for build in firmware.builds.all %} diff --git a/src/c3nav/control/templates/control/mesh_firmwares_current.html b/src/c3nav/control/templates/control/mesh_firmwares_current.html new file mode 100644 index 00000000..db9be23c --- /dev/null +++ b/src/c3nav/control/templates/control/mesh_firmwares_current.html @@ -0,0 +1,47 @@ +{% extends 'control/base.html' %} +{% load i18n mesh_node %} + +{% block heading %}{% trans 'Mesh Firmwares' %}{% endblock %} + +{% block subcontent %} +

Current Firmwares

+ + {% trans 'View available firmwares' %} + + + + + + + + + + + {% for firmware, nodes in firmwares %} + + + + + + + + {% endfor %} +
{% trans 'Created / First seen' %}{% trans 'Firmware' %}{% trans 'Build' %}{% trans 'IDF version' %}{% trans 'Installed on' %}
{{ firmware.created }} + {% if firmware.build %} + {{ firmware.project_name }} {{ firmware.version }} + {% else %} + {{ firmware.project_name }} {{ firmware.version }}
+ {{ firmware.sha256_hash }} + {% endif %} +
+ {% if firmware.build %} + {{ firmware.build.variant }} ({{ firmware.chip.pretty_name }}) + {% else %} + ({{ firmware.chip.pretty_name }}) + {% endif %} + {{ firmware.idf_version }} + {% for node in nodes %} + {% mesh_node node %} + {% endfor %} +
+{% endblock %} diff --git a/src/c3nav/control/templatetags/mesh_node.py b/src/c3nav/control/templatetags/mesh_node.py index c908071b..97fed52a 100644 --- a/src/c3nav/control/templatetags/mesh_node.py +++ b/src/c3nav/control/templatetags/mesh_node.py @@ -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( '{bssid} ({name})', diff --git a/src/c3nav/control/urls.py b/src/c3nav/control/urls.py index b140b567..9e82a48c 100644 --- a/src/c3nav/control/urls.py +++ b/src/c3nav/control/urls.py @@ -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//', MeshNodeDetailView.as_view(), name='control.mesh.node.detail'), path('mesh//edit/', MeshNodeEditView.as_view(), name='control.mesh.node.edit'), path('mesh/message/sending//', MeshMessageSendingView.as_view(), name='control.mesh.sending'), diff --git a/src/c3nav/control/views/mesh.py b/src/c3nav/control/views/mesh.py index 2c3648cc..44f16605 100644 --- a/src/c3nav/control/views/mesh.py +++ b/src/c3nav/control/views/mesh.py @@ -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, + } diff --git a/src/c3nav/mesh/models.py b/src/c3nav/mesh/models.py index cd9f6ffb..1ce84bd5 100644 --- a/src/c3nav/mesh/models.py +++ b/src/c3nav/mesh/models.py @@ -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]