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 %} {% block subcontent %}
<h4>Firmwares</h4> <h4>Firmwares</h4>
<a class="button" href="{% url "control.mesh.firmwares.current" %}">
{% trans 'View current firmwares' %}
</a>
{% include 'control/fragment_pagination.html' %} {% include 'control/fragment_pagination.html' %}
@ -12,8 +15,7 @@
<tr> <tr>
<th>{% trans 'Created' %}</th> <th>{% trans 'Created' %}</th>
<th>{% trans 'Uploader' %}</th> <th>{% trans 'Uploader' %}</th>
<th>{% trans 'Project Name' %}</th> <th>{% trans 'Firmware' %}</th>
<th>{% trans 'Version' %}</th>
<th>{% trans 'IDF version' %}</th> <th>{% trans 'IDF version' %}</th>
<th>{% trans 'Builds' %}</th> <th>{% trans 'Builds' %}</th>
</tr> </tr>
@ -21,8 +23,7 @@
<tr> <tr>
<td>{{ firmware.created }}</td> <td>{{ firmware.created }}</td>
<td>{{ firmware.uploader }}</td> <td>{{ firmware.uploader }}</td>
<td>{{ firmware.project_name }}</td> <td>{{ firmware.project_name }} {{ firmware.version }}</td>
<td>{{ firmware.version }}</td>
<td>{{ firmware.idf_version }}</td> <td>{{ firmware.idf_version }}</td>
<td> <td>
{% for build in firmware.builds.all %} {% 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.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from c3nav.mesh.models import MeshNode
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def mesh_node(context, bssid): def mesh_node(context, node: str | MeshNode):
name = context.get("node_names", {}).get(bssid, None) if isinstance(node, str):
bssid = node
name = context.get("node_names", {}).get(node, None)
else:
bssid = node.address
name = node.name
if name: if name:
return format_html( return format_html(
'<a href="{url}">{bssid}</a> ({name})', '<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.announcements import announcement_detail, announcement_list
from c3nav.control.views.base import ControlPanelIndexView from c3nav.control.views.base import ControlPanelIndexView
from c3nav.control.views.mapupdates import map_updates from c3nav.control.views.mapupdates import map_updates
from c3nav.control.views.mesh import (MeshFirmwaresListView, MeshLogView, MeshMessageListView, MeshMessageSendingView, from c3nav.control.views.mesh import (MeshFirmwaresCurrentListView, MeshFirmwaresListView, MeshLogView,
MeshMessageSendView, MeshNodeDetailView, MeshNodeEditView, MeshNodeListView) MeshMessageListView, MeshMessageSendingView, MeshMessageSendView,
MeshNodeDetailView, MeshNodeEditView, MeshNodeListView)
from c3nav.control.views.users import UserListView, user_detail from c3nav.control.views.users import UserListView, user_detail
urlpatterns = [ urlpatterns = [
@ -20,6 +21,7 @@ urlpatterns = [
path('mesh/logs/', MeshLogView.as_view(), name='control.mesh.log'), path('mesh/logs/', MeshLogView.as_view(), name='control.mesh.log'),
path('mesh/messages/', MeshMessageListView.as_view(), name='control.mesh.messages'), path('mesh/messages/', MeshMessageListView.as_view(), name='control.mesh.messages'),
path('mesh/firmwares/', MeshFirmwaresListView.as_view(), name='control.mesh.firmwares'), 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>/', MeshNodeDetailView.as_view(), name='control.mesh.node.detail'),
path('mesh/<str:pk>/edit/', MeshNodeEditView.as_view(), name='control.mesh.node.edit'), 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'), 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" ordering = "-created"
context_object_name = "firmwares" context_object_name = "firmwares"
paginate_by = 20 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 functools import cached_property
from operator import attrgetter 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.contrib.auth import get_user_model
from django.db import NotSupportedError, models 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 django.utils.translation import gettext_lazy as _
from c3nav.mesh.dataformats import BoardType 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 MeshMessage as MeshMessage
from c3nav.mesh.messages import MeshMessageType 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): class MeshNodeQuerySet(models.QuerySet):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._prefetch_last_messages = set() self._prefetch_last_messages = set()
self._prefetch_last_messages_done = False self._prefetch_last_messages_done = False
self._prefetch_firmwares = False
def _clone(self): def _clone(self):
clone = super()._clone() clone = super()._clone()
clone._prefetch_last_messages = self._prefetch_last_messages clone._prefetch_last_messages = self._prefetch_last_messages
clone._prefetch_firmwares = self._prefetch_firmwares
return clone return clone
def prefetch_last_messages(self, *types: MeshMessageType): def prefetch_last_messages(self, *types: MeshMessageType):
@ -33,10 +59,16 @@ class MeshNodeQuerySet(models.QuerySet):
) )
return clone 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): def _fetch_all(self):
super()._fetch_all() super()._fetch_all()
if self._prefetch_last_messages and not self._prefetch_last_messages_done: 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: try:
for message in NodeMessage.objects.order_by('message_type', 'src_node', '-datetime', '-pk').filter( 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), 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 nodes[message.src_node_id].last_messages[message.message_type] = message
for node in nodes.values(): for node in nodes.values():
node.last_messages["any"] = max(node.last_messages.values(), key=attrgetter("datetime")) node.last_messages["any"] = max(node.last_messages.values(), key=attrgetter("datetime"))
self._prefetch_last_messages_done = True
except NotSupportedError: except NotSupportedError:
pass 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): class LastMessagesByTypeLookup(UserDict):
def __init__(self, node): def __init__(self, node):
@ -103,9 +178,20 @@ class MeshNode(models.Model):
return self.address return self.address
@cached_property @cached_property
def last_messages(self) -> Mapping[Any, Self]: def last_messages(self) -> Mapping[Any, "NodeMessage"]:
return LastMessagesByTypeLookup(self) 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): 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) return '(#%d) %s at %s' % (self.pk, self.get_message_type_display(), self.datetime)
@cached_property @cached_property
def parsed(self) -> dict: def parsed(self) -> Self:
return MeshMessage.fromjson(self.data) return MeshMessage.fromjson(self.data)
@ -202,6 +288,17 @@ class FirmwareBuild(models.Model):
'boards': self.boards, '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): class FirmwareBuildBoard(models.Model):
BOARDS = [(boardtype.name, boardtype.pretty_name) for boardtype in BoardType] BOARDS = [(boardtype.name, boardtype.pretty_name) for boardtype in BoardType]