move mesh control into mesh module and add permissions

This commit is contained in:
Laura Klünder 2023-11-09 15:52:55 +01:00
parent ce8f5f0084
commit 88d6f07eaf
27 changed files with 504 additions and 432 deletions

View file

@ -0,0 +1,19 @@
# Generated by Django 4.2.1 on 2023-11-09 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("control", "0009_django_4_0"),
]
operations = [
migrations.AddField(
model_name="userpermissions",
name="mesh_control",
field=models.BooleanField(
default=False, verbose_name="can access mesh control"
),
),
]

View file

@ -35,6 +35,8 @@ class UserPermissions(models.Model):
limit_choices_to={'access_restriction': None},
verbose_name=_('can review reports belonging to'))
mesh_control = models.BooleanField(default=False, verbose_name=_('can access mesh control'))
api_secret = models.CharField(null=True, blank=True, max_length=64, verbose_name=_('API secret'))
class Meta:

View file

@ -24,7 +24,7 @@
{% if request.user_permissions.manage_map_updates %}
<a href="{% url 'control.map_updates' %}">{% trans 'Map Updates' %}</a> &middot;
{% endif %}
<a href="{% url 'control.mesh.nodes' %}">{% trans 'Mesh' %}</a> &middot;
<a href="{% url 'mesh.nodes' %}">{% trans 'Mesh' %}</a> &middot;
<a href="{% url 'control.users.detail' user=request.user.pk %}">{{ request.user.username }}</a>
</p>
</nav>

View file

@ -1,81 +0,0 @@
{% extends 'control/base.html' %}
{% load i18n %}
{% block heading %}{% trans 'Mesh' %}{% endblock %}
{% block subcontent %}
<div class="columns">
<div>
<h4>View messages</h4>
<a class="button" href="{% url "control.mesh.messages" %}">
{% trans 'View received messages' %}
</a>
</div>
<div>
<h4>View firmwares</h4>
<a class="button" href="{% url "control.mesh.firmwares" %}">
{% trans 'View firmwares' %}
</a>
</div>
<div>
<h4>Send messages</h4>
<form method="POST">
{% csrf_token %}
<select name="send_msg_type" style="display: inline-block; width: auto;" required>
<option value="">select type</option>
{% for category, msg_types in send_msg_types %}
<optgroup label="{{ category }}">
{% for value, label in msg_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
<button type="submit">{% trans 'Send message' %}</button>
</form>
</div>
<div>
<h4>Logs</h4>
<a class="button" href="{% url "control.mesh.log" %}">
{% trans 'View log' %}
</a>
</div>
</div>
<h4>Nodes</h4>
<table>
<tr>
<th>{% trans 'Node' %}</th>
<th>{% trans 'Chip' %}</th>
<th>{% trans 'Firmware' %}</th>
<th>{% trans 'Last msg' %}</th>
<th>{% trans 'Last signin' %}</th>
<th>{% trans 'Uplink' %}</th>
</tr>
{% for node in nodes %}
<tr>
<td><a href="{% url "control.mesh.node.detail" pk=node.address %}">{{ node }}</a></td>
<td>
{{ node.last_messages.CONFIG_BOARD.parsed.board_config.board.pretty_name }}
({{ node.last_messages.CONFIG_HARDWARE.parsed.chip.pretty_name }} <small>rev{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_major }}.{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_minor }}</small>)
</td>
<td>
{{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.version }}
<small>(IDF {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.idf_version }})</small>
</td>
<td>
{% blocktrans trimmed with timesince=node.last_msg|timesince %}
{{ timesince }} ago
{% endblocktrans %}
</td>
<td>
{% blocktrans trimmed with timesince=node.last_signin|timesince %}
{{ timesince }} ago
{% endblocktrans %}
</td>
{% comment %}todo: hide uplink if timed out{% endcomment %}
{% comment %}todo: more details{% endcomment %}
<td>{% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a>{% endif %}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -18,12 +18,12 @@ def mesh_node(context, node: str | MeshNode):
if name:
return format_html(
'<a href="{url}">{bssid}</a> ({name})',
url=reverse('control.mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid, name=name
url=reverse('mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid, name=name
)
else:
return format_html(
'<a href="{url}">{bssid}</a>',
url=reverse('control.mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid
url=reverse('mesh.node.detail', kwargs={"pk": bssid}), bssid=bssid
)

View file

@ -4,9 +4,6 @@ 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 (MeshFirmwareBuildDetailView, MeshFirmwareDetailView, MeshFirmwaresCurrentListView,
MeshFirmwaresListView, MeshLogView, MeshMessageListView, MeshMessageSendingView,
MeshMessageSendView, MeshNodeDetailView, MeshNodeEditView, MeshNodeListView)
from c3nav.control.views.users import UserListView, user_detail
urlpatterns = [
@ -17,18 +14,5 @@ urlpatterns = [
path('announcements/', announcement_list, name='control.announcements'),
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/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/firmwares/<int:pk>/', MeshFirmwareDetailView.as_view(), name='control.mesh.firmwares.detail'),
path('mesh/firmwares/builds/<int:pk>/', MeshFirmwareBuildDetailView.as_view(),
name='control.mesh.firmwares.build.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/message/sending/<uuid:uuid>/', MeshMessageSendingView.as_view(), name='control.mesh.sending'),
path('mesh/message/<str:recipient>/<str:msg_type>/', MeshMessageSendView.as_view(), name='control.mesh.send'),
path('mesh/message/<str:msg_type>/', MeshMessageSendView.as_view(), name='control.mesh.send'),
path('', ControlPanelIndexView.as_view(), name='control.index'),
]

View file

@ -1,290 +0,0 @@
from functools import cached_property
from uuid import uuid4
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import Max
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, FormView, ListView, TemplateView, UpdateView
from c3nav.control.forms import MeshMessageFilterForm
from c3nav.control.views.base import ControlPanelMixin
from c3nav.mesh.forms import MeshMessageForm, MeshNodeForm
from c3nav.mesh.messages import MeshMessage, MeshMessageType
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, MeshNode, NodeMessage
from c3nav.mesh.utils import get_node_names, group_msg_type_choices
class MeshNodeListView(ControlPanelMixin, ListView):
model = MeshNode
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')).prefetch_last_messages()
def get_context_data(self, *args, **kwargs):
return {
**super().get_context_data(*args, **kwargs),
"send_msg_types": group_msg_type_choices({msg_type for msg_type in MeshMessageForm.msg_types.keys()})
}
def post(self, request):
return redirect(
reverse("control.mesh.send", kwargs={"msg_type": request.POST.get("send_msg_type", "")})
)
class MeshNodeDetailView(ControlPanelMixin, DetailView):
model = MeshNode
template_name = "control/mesh_node_detail.html"
pk_url_kwargs = "address"
context_object_name = "node"
def get_queryset(self):
return super().get_queryset().annotate(last_msg=Max('received_messages__datetime')).prefetch_last_messages()
class MeshNodeEditView(ControlPanelMixin, SuccessMessageMixin, UpdateView):
model = MeshNode
form_class = MeshNodeForm
template_name = "control/form.html"
success_message = _('Name updated successfully')
def get_context_data(self, **kwargs):
return {
**super().get_context_data(),
'title': _('Editing mesh node: %s') % self.get_object(),
}
def get_success_url(self):
return reverse('control.mesh.node.detail', kwargs={'pk': self.get_object().pk})
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 = MeshMessageFilterForm(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['src_nodes']:
qs = qs.filter(src_node__in=self.form.cleaned_data['src_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({
"node_names": get_node_names(),
'form': self.form,
'form_data': form_data.urlencode(),
})
return ctx
class MeshMessageSendView(ControlPanelMixin, FormView):
template_name = "control/mesh_message_send.html"
@cached_property
def msg_type(self):
return MeshMessageType[self.kwargs['msg_type']]
def get_form_class(self):
try:
return MeshMessageForm.get_form_for_type(self.msg_type)
except KeyError:
raise Http404('unknown message type')
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
'recipient': self.kwargs.get('recipient', None),
}
def get_initial(self):
if 'recipient' in self.kwargs and self.msg_type.name.startswith('CONFIG_'):
try:
node = MeshNode.objects.get(address=self.kwargs['recipient'])
except MeshNode.DoesNotExist:
pass
else:
initial = MeshMessage.get_type(self.msg_type).tojson(
node.last_messages[self.msg_type].parsed
)
while keys := tuple(key for key, value in initial.items() if isinstance(value, dict)):
for key in keys:
subdict = initial.pop(key)
for subkey, value in subdict.items():
initial[key+"_"+subkey.removeprefix(key).lstrip('_')] = value
return initial
if 'address' in self.request.GET and self.msg_type == MeshMessageType.MESH_ROUTE_REQUEST:
return {"address": self.request.GET["address"]}
return {}
def get_success_url(self):
if 'recipient' in self.kwargs and False:
return reverse('control.mesh.node.detail', kwargs={'pk': self.kwargs['recipient']})
else:
return self.request.path
def form_valid(self, form):
if 'noscript' in self.request.POST:
form.send()
messages.success(self.request, _('Message sent successfully(?)'))
super().form_valid(form)
uuid = uuid4()
self.request.session["mesh_msg_%s" % uuid] = {
"success_url": self.get_success_url(),
"recipients": form.get_recipients(),
"msg_data": form.get_msg_data(),
}
return redirect(reverse('control.mesh.sending', kwargs={'uuid': uuid}))
class MeshMessageSendingView(ControlPanelMixin, TemplateView):
template_name = "control/mesh_message_sending.html"
def get_context_data(self, uuid):
try:
data = self.request.session["mesh_msg_%s" % uuid]
except KeyError:
raise Http404
node_names = get_node_names()
return {
**super().get_context_data(),
"node_names": node_names,
"send_uuid": uuid,
**data,
"node_name": node_names.get(data["msg_data"].get("address"), ""),
"recipients": [(address, node_names[address]) for address in data["recipients"]],
"msg_type": MeshMessageType[data["msg_data"]["msg_type"]].pretty_name,
}
class MeshLogView(ControlPanelMixin, TemplateView):
template_name = "control/mesh_logs.html"
def get_context_data(self, **kwargs):
return {
**super().get_context_data(),
"node_names": get_node_names(),
}
class MeshFirmwaresListView(ControlPanelMixin, ListView):
model = FirmwareVersion
template_name = "control/mesh_firmwares.html"
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,
}
class MeshFirmwareDetailView(ControlPanelMixin, DetailView):
model = FirmwareVersion
template_name = "control/mesh_firmware_detail.html"
context_object_name = "firmware"
def get_queryset(self):
return super().get_queryset().prefetch_related('builds', 'builds__firmwarebuildboard_set')
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages(
MeshMessageType.CONFIG_BOARD,
))
builds = self.get_object().builds.all()
build_lookups = set(build.get_firmware_description().get_lookup() for build in builds)
installed_nodes = []
compatible_nodes = []
for node in nodes:
if node.firmware_desc.get_lookup() in build_lookups:
installed_nodes.append(node)
else:
node.compatible_builds = []
for build in builds:
if node.board in build.boards:
node.compatible_builds.append(build)
if node.compatible_builds:
compatible_nodes.append(node)
ctx.update({
'builds': builds,
'installed_nodes': installed_nodes,
'compatible_nodes': compatible_nodes,
})
return ctx
class MeshFirmwareBuildDetailView(ControlPanelMixin, DetailView):
model = FirmwareBuild
template_name = "control/mesh_firmware_build_detail.html"
context_object_name = "build"
def get_queryset(self):
return super().get_queryset().prefetch_related('firmwarebuildboard_set')
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages(
MeshMessageType.CONFIG_BOARD,
))
build_lookup = self.get_object().get_firmware_description().get_lookup()
build_boards = self.get_object().boards
installed_nodes = []
compatible_nodes = []
for node in nodes:
if node.firmware_desc.get_lookup() == build_lookup:
installed_nodes.append(node)
else:
if node.board in build_boards:
compatible_nodes.append(node)
ctx.update({
'installed_nodes': installed_nodes,
'compatible_nodes': compatible_nodes,
})
return ctx

View file

@ -0,0 +1,41 @@
{% extends 'site/base.html' %}
{% load i18n %}
{% load compress %}
{% load static %}
{% block title %}{% trans 'c3nav mesh control' %}{% endblock %}
{% block header_title %}<span id="subheader">{% trans 'mesh control' %}</span>{% endblock %}
{% block header_title_url %}{% url 'mesh.nodes' %}{% endblock %}
{% block content %}
<main class="control"{% block addattributes %}{% endblock %}>
{% include 'site/fragment_messages.html' %}
{% block back_link %}{% if not request.mobileclient %}<a href="{% url 'site.index' %}" class="float-right">&laquo; {% trans 'back to c3nav' %}</a>{% endif %}{% endblock %}
<h2>{% block heading %}{% endblock %}</h2>
{% block menu %}
<nav>
<p>
<a href="{% url 'mesh.nodes' %}">Nodes</a> &middot;
<a href="{% url 'mesh.messages' %}">Messages</a> &middot;
<a href="{% url 'mesh.firmwares' %}">Firmwares</a> &middot;
<a href="{% url 'mesh.logs' %}">Live logs</a> &middot;
</p>
</nav>
<hr>
{% endblock %}
{% block subcontent %}
{% endblock %}
</main>
{% include 'site/fragment_fakemobileclient.html' %}
{% compress js %}
<script type="text/javascript" src="{% static 'jquery/jquery.js' %}"></script>
<script type="text/javascript">
if (window.mobileclient) {
var $body = $('body');
if ($body.is('[data-user-data]')) {
mobileclient.setUserData($body.attr('data-user-data'));
}
}
</script>
{% endcompress %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n mesh_node %}
{% block heading %}{% trans 'Mesh' %}{% endblock %}
@ -62,10 +62,10 @@
</td>
<td>
{% if node.firmware_desc.build %}
<a href="{% url "control.mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
<a href="{% url "mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
</a><br>
Build <a href="{% url "control.mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
Build <a href="{% url "mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
{% else %}
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
{% endif %}

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n mesh_node %}
{% block heading %}{% trans 'Mesh' %}{% endblock %}
@ -26,7 +26,7 @@
{% for build in builds %}
<tr>
<td>
<a href="{% url "control.mesh.firmwares.build.detail" pk=build.pk %}">
<a href="{% url "mesh.firmwares.build.detail" pk=build.pk %}">
{{ build.variant }}
</a>
</td>
@ -82,10 +82,10 @@
</td>
<td>
{% if node.firmware_desc.build %}
<a href="{% url "control.mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
<a href="{% url "mesh.firmwares.detail" pk=node.firmware_desc.build.version.pk %}">
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
</a><br>
Build <a href="{% url "control.mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
Build <a href="{% url "mesh.firmwares.build.detail" pk=node.firmware_desc.build.pk %}">{{ node.firmware_desc.build.variant }}</a>
{% else %}
{{ node.firmware_desc.project_name }} {{ node.firmware_desc.version }}
{% endif %}

View file

@ -1,11 +1,10 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n %}
{% block heading %}{% trans 'Mesh' %}{% endblock %}
{% block heading %}{% trans 'Available firmwares' %}{% endblock %}
{% block subcontent %}
<h4>Firmwares</h4>
<a class="button" href="{% url "control.mesh.firmwares.current" %}">
<a class="button" href="{% url "mesh.firmwares.current" %}">
{% trans 'View current firmwares' %}
</a>
@ -24,14 +23,14 @@
<td>{{ firmware.created }}</td>
<td>{{ firmware.uploader }}</td>
<td>
<a href="{% url "control.mesh.firmwares.detail" pk=firmware.pk %}">
<a href="{% url "mesh.firmwares.detail" pk=firmware.pk %}">
{{ firmware.project_name }} {{ firmware.version }}
</a>
</td>
<td>{{ firmware.idf_version }}</td>
<td>
{% for build in firmware.builds.all %}
<a href="{% url "control.mesh.firmwares.build.detail" pk=build.pk %}">
<a href="{% url "mesh.firmwares.build.detail" pk=build.pk %}">
{{ build.variant }} ({{ build.get_chip_display }})
</a><br>
{% endfor %}

View file

@ -1,11 +1,10 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n mesh_node %}
{% block heading %}{% trans 'Mesh' %}{% endblock %}
{% block heading %}{% trans 'Current firmwares' %}{% endblock %}
{% block subcontent %}
<h4>Current Firmwares</h4>
<a class="button" href="{% url "control.mesh.firmwares" %}">
<a class="button" href="{% url "mesh.firmwares" %}">
{% trans 'View available firmwares' %}
</a>
@ -22,7 +21,7 @@
<td>{{ firmware.created }}</td>
<td>
{% if firmware.build %}
<a href="{% url "control.mesh.firmwares.detail" pk=firmware.pk %}">{{ firmware.project_name }} {{ firmware.version }}</a>
<a href="{% url "mesh.firmwares.detail" pk=firmware.pk %}">{{ firmware.project_name }} {{ firmware.version }}</a>
{% else %}
{{ firmware.project_name }} {{ firmware.version }}<br>
<small>{{ firmware.sha256_hash }}</small>
@ -30,7 +29,7 @@
</td>
<td>
{% if firmware.build %}
<a href="{% url "control.mesh.firmwares.build.detail" pk=firmware.build.pk %}">
<a href="{% url "mesh.firmwares.build.detail" pk=firmware.build.pk %}">
{{ firmware.build.variant }} ({{ firmware.chip.pretty_name }})
</a>
{% else %}

View file

@ -38,7 +38,7 @@ function connect() {
if (data.uplink) {
cell.append(document.createElement("br"));
link_tag = document.createElement("a");
link_tag.href = "/control/mesh/" + data.uplink;
link_tag.href = "/mesh/" + data.uplink;
link_tag.innerText = data.uplink;
if (node_names[data.uplink]) {
link_tag.innerText += " ("+node_names[data.uplink]+")";
@ -49,7 +49,7 @@ function connect() {
cell = document.createElement("td");
link_tag = document.createElement("a");
link_tag.href = "/control/mesh/" + data.node;
link_tag.href = "/mesh/" + data.node;
link_tag.innerText = data.node;
if (node_names[data.node]) {
link_tag.innerText += " ("+node_names[data.node]+")";
@ -76,7 +76,7 @@ function connect() {
line.appendChild(text)
link_tag = document.createElement("a");
link_tag.href = "/control/mesh/" + data.uplink;
link_tag.href = "/mesh/" + data.uplink;
link_tag.innerText = data.uplink;
if (node_names[data.uplink]) {
link_tag.innerText += "("+node_names[data.uplink]+")";
@ -96,7 +96,7 @@ function connect() {
cell = document.createElement("td");
link_tag = document.createElement("a");
link_tag.href = "/control/mesh/" + data.msg.src;
link_tag.href = "/mesh/" + data.msg.src;
link_tag.innerText = data.msg.src;
if (node_names[data.msg.src]) {
link_tag.innerText += " ("+node_names[data.msg.src]+")";
@ -109,7 +109,7 @@ function connect() {
} else {
cell = document.createElement("td");
link_tag = document.createElement("a");
link_tag.href = "/control/mesh/" + data.msg.route;
link_tag.href = "/mesh/" + data.msg.route;
link_tag.innerText = data.msg.route;
if (node_names[data.msg.route]) {
link_tag.innerText += " ("+node_names[data.msg.route]+")";
@ -125,7 +125,7 @@ function connect() {
cell = document.createElement("td");
link_tag = document.createElement("a");
link_tag.href = "/control/mesh/" + data.msg.trace[i];
link_tag.href = "/mesh/" + data.msg.trace[i];
link_tag.innerText = data.msg.trace[i];
if (node_names[data.msg.trace[i]]) {
link_tag.innerText += " ("+node_names[data.msg.trace[i]]+")";
@ -145,4 +145,4 @@ function connect() {
connect();
</script>
</script>

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n %}
{% block heading %}{% trans 'Mesh logs' %}{% endblock %}
@ -15,5 +15,5 @@
</thead>
<tbody id="mesh-logs"></tbody>
</table>
{% include "control/fragment_mesh_websocket.html" %}
{% include "mesh/fragment_mesh_websocket.html" %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n %}
{% block heading %}

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n %}
{% block heading %}
@ -10,7 +10,7 @@
{% block subcontent %}
<p><a class="button" href="{{ success_url }}">Go back</a></p>
{% if msg_type == "MESH_ROUTE_REQUEST" %}
<p>Route to <a href="{% url "control.mesh.node.detail" pk=msg_data.address %}">{{ msg_data.address }} {% if node_name %} ({{ node_name }}){% endif %}</a></p>
<p>Route to <a href="{% url "mesh.node.detail" pk=msg_data.address %}">{{ msg_data.address }} {% if node_name %} ({{ node_name }}){% endif %}</a></p>
{% endif %}
<div class="columns">
<div>
@ -23,7 +23,7 @@
{% for address, name in recipients %}
<tr>
<td>
{% if address != "ff:ff:ff:ff:ff:ff" %}<a href="{% url "control.mesh.node.detail" pk=address %}">{% endif %}
{% if address != "ff:ff:ff:ff:ff:ff" %}<a href="{% url "mesh.node.detail" pk=address %}">{% endif %}
{{ address }}{% if name %} ({{ name }}){% endif %}
{% if address != "ff:ff:ff:ff:ff:ff" %}</a>{% endif %}
</td>
@ -69,6 +69,6 @@
</div>
{% endif %}
</div>
{% include "control/fragment_mesh_websocket.html" %}
{% include "mesh/fragment_mesh_websocket.html" %}
{% endblock %}

View file

@ -1,10 +1,30 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n mesh_node %}
{% block heading %}{% trans 'Mesh messages' %}{% endblock %}
{% block subcontent %}
<div class="columns">
<div>
<h4>Send messages</h4>
<form method="POST">
{% csrf_token %}
<select name="send_msg_type" style="display: inline-block; width: auto;" required>
<option value="">select type</option>
{% for category, msg_types in send_msg_types %}
<optgroup label="{{ category }}">
{% for value, label in msg_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
<button type="submit">{% trans 'Send message' %}</button>
</form>
</div>
</div>
<form>
<h4>Filter</h4>
<div class="fields">
<div class="field">
{{ form.message_types }}

View file

@ -1,4 +1,4 @@
{% extends 'control/base.html' %}
{% extends 'mesh/base.html' %}
{% load i18n %}
{% block heading %}{% trans 'Mesh Node' %} {{ node }}{% endblock %}
@ -13,7 +13,7 @@
</p>
<p>
{% comment %}todo: more details{% endcomment %}
<strong>Uplink:</strong> {% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a><br>{% endif %}
<strong>Uplink:</strong> {% if node.uplink %}<a href="{% url "mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a><br>{% endif %}
<strong>Last signin:</strong>
{{ node.last_signin.date }} {{ node.last_signin.time|date:"H:i:s" }}
@ -30,13 +30,13 @@
<br>
</p>
<p>
<a class="button" href="{% url "control.mesh.node.edit" pk=node.pk %}">
<a class="button" href="{% url "mesh.node.edit" pk=node.pk %}">
{% trans 'Edit' %}
</a>
<a class="button" href="{% url "control.mesh.messages" %}?src_nodes={{ node.address }}">
<a class="button" href="{% url "mesh.messages" %}?src_nodes={{ node.address }}">
{% trans 'View messages' %}
</a>
<a class="button" href="{% url "control.mesh.send" msg_type="MESH_ROUTE_REQUEST" %}?address={{ node.address }}" >
<a class="button" href="{% url "mesh.send" msg_type="MESH_ROUTE_REQUEST" %}?address={{ node.address }}" >
{% trans 'Find route' %}
</a>
</p>
@ -58,7 +58,7 @@
</p>
<p>
<a class="button" href="{% url "control.mesh.send" recipient=node.address msg_type="CONFIG_BOARD" %}">
<a class="button" href="{% url "mesh.send" recipient=node.address msg_type="CONFIG_BOARD" %}">
{% trans 'Chaange board settings' %}
</a>
</p>
@ -83,7 +83,7 @@
<strong>SSL:</strong> {{ node.last_messages.CONFIG_UPLINK.parsed.ssl }}<br>
</p>
<p>
<a class="button" href="{% url "control.mesh.send" recipient=node.address msg_type="CONFIG_UPLINK" %}">
<a class="button" href="{% url "mesh.send" recipient=node.address msg_type="CONFIG_UPLINK" %}">
{% trans 'Change' %}
</a>
</p>
@ -93,7 +93,7 @@
<strong>X=</strong>{{ node.last_messages.CONFIG_POSITION.parsed.x_pos }}, <strong>Y=</strong>{{ node.last_messages.CONFIG_POSITION.parsed.y_pos }}, <strong>Z=</strong>{{ node.last_messages.CONFIG_POSITION.parsed.z_pos }}
</p>
<p>
<a class="button" href="{% url "control.mesh.send" recipient=node.address msg_type="CONFIG_POSITION" %}">
<a class="button" href="{% url "mesh.send" recipient=node.address msg_type="CONFIG_POSITION" %}">
{% trans 'Change' %}
</a>
</p>

View file

@ -0,0 +1,43 @@
{% extends 'mesh/base.html' %}
{% load i18n %}
{% block heading %}{% trans 'Mesh Nodes' %}{% endblock %}
{% block subcontent %}
<table>
<tr>
<th>{% trans 'Node' %}</th>
<th>{% trans 'Chip' %}</th>
<th>{% trans 'Firmware' %}</th>
<th>{% trans 'Last msg' %}</th>
<th>{% trans 'Last signin' %}</th>
<th>{% trans 'Uplink' %}</th>
</tr>
{% for node in nodes %}
<tr>
<td><a href="{% url "mesh.node.detail" pk=node.address %}">{{ node }}</a></td>
<td>
{{ node.last_messages.CONFIG_BOARD.parsed.board_config.board.pretty_name }}
({{ node.last_messages.CONFIG_HARDWARE.parsed.chip.pretty_name }} <small>rev{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_major }}.{{ node.last_messages.CONFIG_HARDWARE.parsed.revision_minor }}</small>)
</td>
<td>
{{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.version }}
<small>(IDF {{ node.last_messages.CONFIG_FIRMWARE.parsed.app_desc.idf_version }})</small>
</td>
<td>
{% blocktrans trimmed with timesince=node.last_msg|timesince %}
{{ timesince }} ago
{% endblocktrans %}
</td>
<td>
{% blocktrans trimmed with timesince=node.last_signin|timesince %}
{{ timesince }} ago
{% endblocktrans %}
</td>
{% comment %}todo: hide uplink if timed out{% endcomment %}
{% comment %}todo: more details{% endcomment %}
<td>{% if node.uplink %}<a href="{% url "mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a>{% endif %}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,6 +1,26 @@
from django.urls import path
from c3nav.mesh.consumers import MeshConsumer, MeshUIConsumer
from c3nav.mesh.views.firmware import (FirmwareBuildDetailView, FirmwareDetailView, FirmwaresCurrentListView,
FirmwaresListView)
from c3nav.mesh.views.messages import MeshMessageListView, MeshMessageSendingView, MeshMessageSendView
from c3nav.mesh.views.misc import MeshLogView
from c3nav.mesh.views.nodes import NodeDetailView, NodeEditView, NodeListView
urlpatterns = [
path('', NodeListView.as_view(), name='mesh.nodes'),
path('logs/', MeshLogView.as_view(), name='mesh.logs'),
path('messages/', MeshMessageListView.as_view(), name='mesh.messages'),
path('firmwares/', FirmwaresListView.as_view(), name='mesh.firmwares'),
path('firmwares/current/', FirmwaresCurrentListView.as_view(), name='mesh.firmwares.current'),
path('firmwares/<int:pk>/', FirmwareDetailView.as_view(), name='mesh.firmwares.detail'),
path('firmwares/builds/<int:pk>/', FirmwareBuildDetailView.as_view(), name='mesh.firmwares.build.detail'),
path('<str:pk>/', NodeDetailView.as_view(), name='mesh.node.detail'),
path('<str:pk>/edit/', NodeEditView.as_view(), name='mesh.node.edit'),
path('message/sending/<uuid:uuid>/', MeshMessageSendingView.as_view(), name='mesh.sending'),
path('message/<str:recipient>/<str:msg_type>/', MeshMessageSendView.as_view(), name='mesh.send'),
path('message/<str:msg_type>/', MeshMessageSendView.as_view(), name='mesh.send'),
]
websocket_urlpatterns = [
path('ws', MeshConsumer.as_asgi()),

View file

View file

@ -0,0 +1,13 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
class MeshControlMixin(UserPassesTestMixin, LoginRequiredMixin):
login_url = 'site.login'
user_permission = None
def test_func(self):
if not self.request.user_permissions.mesh_control:
return False
if not self.user_permission:
return True
return getattr(self.request.user_permissions, self.user_permission)

View file

@ -0,0 +1,106 @@
from django.views.generic import DetailView, ListView, TemplateView
from c3nav.mesh.messages import MeshMessageType
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, MeshNode
from c3nav.mesh.views.base import MeshControlMixin
class FirmwaresListView(MeshControlMixin, ListView):
model = FirmwareVersion
template_name = "mesh/firmwares.html"
ordering = "-created"
context_object_name = "firmwares"
paginate_by = 20
class FirmwaresCurrentListView(MeshControlMixin, TemplateView):
template_name = "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,
}
class FirmwareDetailView(MeshControlMixin, DetailView):
model = FirmwareVersion
template_name = "mesh/firmware_detail.html"
context_object_name = "firmware"
def get_queryset(self):
return super().get_queryset().prefetch_related('builds', 'builds__firmwarebuildboard_set')
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages(
MeshMessageType.CONFIG_BOARD,
))
builds = self.get_object().builds.all()
build_lookups = set(build.get_firmware_description().get_lookup() for build in builds)
installed_nodes = []
compatible_nodes = []
for node in nodes:
if node.firmware_desc.get_lookup() in build_lookups:
installed_nodes.append(node)
else:
node.compatible_builds = []
for build in builds:
if node.board in build.boards:
node.compatible_builds.append(build)
if node.compatible_builds:
compatible_nodes.append(node)
ctx.update({
'builds': builds,
'installed_nodes': installed_nodes,
'compatible_nodes': compatible_nodes,
})
return ctx
class FirmwareBuildDetailView(MeshControlMixin, DetailView):
model = FirmwareBuild
template_name = "mesh/firmware_build_detail.html"
context_object_name = "build"
def get_queryset(self):
return super().get_queryset().prefetch_related('firmwarebuildboard_set')
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
nodes = list(MeshNode.objects.all().prefetch_firmwares().prefetch_last_messages(
MeshMessageType.CONFIG_BOARD,
))
build_lookup = self.get_object().get_firmware_description().get_lookup()
build_boards = self.get_object().boards
installed_nodes = []
compatible_nodes = []
for node in nodes:
if node.firmware_desc.get_lookup() == build_lookup:
installed_nodes.append(node)
else:
if node.board in build_boards:
compatible_nodes.append(node)
ctx.update({
'installed_nodes': installed_nodes,
'compatible_nodes': compatible_nodes,
})
return ctx

View file

@ -0,0 +1,131 @@
from functools import cached_property
from uuid import uuid4
from django.contrib import messages
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, ListView, TemplateView
from c3nav.control.forms import MeshMessageFilterForm
from c3nav.mesh.forms import MeshMessageForm
from c3nav.mesh.messages import MeshMessage, MeshMessageType
from c3nav.mesh.models import MeshNode, NodeMessage
from c3nav.mesh.utils import get_node_names, group_msg_type_choices
from c3nav.mesh.views.base import MeshControlMixin
class MeshMessageListView(MeshControlMixin, ListView):
model = NodeMessage
template_name = "mesh/mesh_messages.html"
ordering = "-datetime"
paginate_by = 20
context_object_name = "mesh_messages"
def get_queryset(self):
qs = super().get_queryset()
self.form = MeshMessageFilterForm(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['src_nodes']:
qs = qs.filter(src_node__in=self.form.cleaned_data['src_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({
"node_names": get_node_names(),
"send_msg_types": group_msg_type_choices({msg_type for msg_type in MeshMessageForm.msg_types.keys()}),
'form': self.form,
'form_data': form_data.urlencode(),
})
return ctx
class MeshMessageSendView(MeshControlMixin, FormView):
template_name = "mesh/mesh_message_send.html"
@cached_property
def msg_type(self):
return MeshMessageType[self.kwargs['msg_type']]
def get_form_class(self):
try:
return MeshMessageForm.get_form_for_type(self.msg_type)
except KeyError:
raise Http404('unknown message type')
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
'recipient': self.kwargs.get('recipient', None),
}
def get_initial(self):
if 'recipient' in self.kwargs and self.msg_type.name.startswith('CONFIG_'):
try:
node = MeshNode.objects.get(address=self.kwargs['recipient'])
except MeshNode.DoesNotExist:
pass
else:
initial = MeshMessage.get_type(self.msg_type).tojson(
node.last_messages[self.msg_type].parsed
)
while keys := tuple(key for key, value in initial.items() if isinstance(value, dict)):
for key in keys:
subdict = initial.pop(key)
for subkey, value in subdict.items():
initial[key+"_"+subkey.removeprefix(key).lstrip('_')] = value
return initial
if 'address' in self.request.GET and self.msg_type == MeshMessageType.MESH_ROUTE_REQUEST:
return {"address": self.request.GET["address"]}
return {}
def get_success_url(self):
if 'recipient' in self.kwargs and False:
return reverse('mesh.node.detail', kwargs={'pk': self.kwargs['recipient']})
else:
return self.request.path
def form_valid(self, form):
if 'noscript' in self.request.POST:
form.send()
messages.success(self.request, _('Message sent successfully(?)'))
super().form_valid(form)
uuid = uuid4()
self.request.session["mesh_msg_%s" % uuid] = {
"success_url": self.get_success_url(),
"recipients": form.get_recipients(),
"msg_data": form.get_msg_data(),
}
return redirect(reverse('mesh.sending', kwargs={'uuid': uuid}))
class MeshMessageSendingView(MeshControlMixin, TemplateView):
template_name = "mesh/mesh_message_sending.html"
def get_context_data(self, uuid):
try:
data = self.request.session["mesh_msg_%s" % uuid]
except KeyError:
raise Http404
node_names = get_node_names()
return {
**super().get_context_data(),
"node_names": node_names,
"send_uuid": uuid,
**data,
"node_name": node_names.get(data["msg_data"].get("address"), ""),
"recipients": [(address, node_names[address]) for address in data["recipients"]],
"msg_type": MeshMessageType[data["msg_data"]["msg_type"]].pretty_name,
}

View file

@ -0,0 +1,14 @@
from django.views.generic import TemplateView
from c3nav.mesh.utils import get_node_names
from c3nav.mesh.views.base import MeshControlMixin
class MeshLogView(MeshControlMixin, TemplateView):
template_name = "mesh/mesh_logs.html"
def get_context_data(self, **kwargs):
return {
**super().get_context_data(),
"node_names": get_node_names(),
}

View file

@ -0,0 +1,51 @@
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import Max
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, UpdateView
from c3nav.mesh.forms import MeshNodeForm
from c3nav.mesh.models import MeshNode
from c3nav.mesh.views.base import MeshControlMixin
class NodeListView(MeshControlMixin, ListView):
model = MeshNode
template_name = "mesh/nodes.html"
ordering = "address"
context_object_name = "nodes"
def get_queryset(self):
return super().get_queryset().annotate(last_msg=Max('received_messages__datetime')).prefetch_last_messages()
def post(self, request):
return redirect(
reverse("control.mesh.send", kwargs={"msg_type": request.POST.get("send_msg_type", "")})
)
class NodeDetailView(MeshControlMixin, DetailView):
model = MeshNode
template_name = "mesh/node_detail.html"
pk_url_kwargs = "address"
context_object_name = "node"
def get_queryset(self):
return super().get_queryset().annotate(last_msg=Max('received_messages__datetime')).prefetch_last_messages()
class NodeEditView(MeshControlMixin, SuccessMessageMixin, UpdateView):
model = MeshNode
form_class = MeshNodeForm
template_name = "control/form.html"
success_message = _('Name updated successfully')
def get_context_data(self, **kwargs):
return {
**super().get_context_data(),
'title': _('Editing mesh node: %s') % self.get_object(),
}
def get_success_url(self):
return reverse('mesh.node.detail', kwargs={'pk': self.get_object().pk})

View file

@ -19,6 +19,7 @@ urlpatterns = [
path('map/', include(c3nav.mapdata.urls)),
path('admin/', admin.site.urls),
path('control/', include(c3nav.control.urls)),
path('mesh/', include(c3nav.mesh.urls)),
path('locales/', include('django.conf.urls.i18n')),
path('', include(c3nav.site.urls)),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)