add view to show currently installed firmwares
This commit is contained in:
parent
e01e718356
commit
c872b97fa3
6 changed files with 188 additions and 14 deletions
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
@ -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})',
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue