From 21b75bec8666709e6e909973f41c7bf69be92160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Wed, 4 Oct 2023 15:42:03 +0200 Subject: [PATCH] add mesh communication from django form --- src/c3nav/control/forms.py | 2 +- .../templates/control/mesh_message_send.html | 23 +++++ .../templates/control/mesh_messages.html | 2 +- src/c3nav/control/urls.py | 3 +- src/c3nav/control/views/mesh.py | 52 ++++++++++- src/c3nav/control/views/utils.py | 2 + src/c3nav/mesh/consumers.py | 26 +++--- src/c3nav/mesh/dataformats.py | 4 +- src/c3nav/mesh/forms.py | 86 +++++++++++++++++++ src/c3nav/mesh/messages.py | 12 ++- 10 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 src/c3nav/control/templates/control/mesh_message_send.html create mode 100644 src/c3nav/control/views/utils.py create mode 100644 src/c3nav/mesh/forms.py diff --git a/src/c3nav/control/forms.py b/src/c3nav/control/forms.py index 4273aafd..1d5b51a1 100644 --- a/src/c3nav/control/forms.py +++ b/src/c3nav/control/forms.py @@ -300,7 +300,7 @@ class MapUpdateForm(ModelForm): fields = ('geometries_changed', ) -class MeshMessageFilerForm(Form): +class MeshMessageFilterForm(Form): message_types = MultipleChoiceField( choices=[(msgtype.value, msgtype.name) for msgtype in MessageType], required=False, diff --git a/src/c3nav/control/templates/control/mesh_message_send.html b/src/c3nav/control/templates/control/mesh_message_send.html new file mode 100644 index 00000000..d16ea909 --- /dev/null +++ b/src/c3nav/control/templates/control/mesh_message_send.html @@ -0,0 +1,23 @@ +{% extends 'control/base.html' %} +{% load i18n %} + +{% block heading %} + {% if form.recipient %} + {% blocktrans trimmed with msg_type=form.msg_type.name recipient=form.get_recipient_display %} + Send {{ msg_type }} message to {{ recipient }} + {% endblocktrans %} + {% else %} + {% blocktrans trimmed with msg_type=form.msg_type %} + Send {{ msg_type }} message + {% endblocktrans %} + {% endif %} +{% endblock %} + +{% block subcontent %} +
+ {% csrf_token %} + {{ form }} + +
+ +{% endblock %} diff --git a/src/c3nav/control/templates/control/mesh_messages.html b/src/c3nav/control/templates/control/mesh_messages.html index 36806408..b9fe070c 100644 --- a/src/c3nav/control/templates/control/mesh_messages.html +++ b/src/c3nav/control/templates/control/mesh_messages.html @@ -13,7 +13,7 @@ {{ form.src_nodes }}
- +
diff --git a/src/c3nav/control/urls.py b/src/c3nav/control/urls.py index d2d31416..626b533f 100644 --- a/src/c3nav/control/urls.py +++ b/src/c3nav/control/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from c3nav.control.views.mesh import MeshNodeListView, MeshMessageListView, MeshNodeDetailView +from c3nav.control.views.mesh import MeshNodeListView, MeshMessageListView, MeshNodeDetailView, MeshMessageSendView from c3nav.control.views.mapupdates import map_updates from c3nav.control.views.announcements import announcement_list, announcement_detail from c3nav.control.views.access import grant_access, grant_access_qr @@ -18,5 +18,6 @@ urlpatterns = [ path('mesh/', MeshNodeListView.as_view(), name='control.mesh_nodes'), path('mesh/messages/', MeshMessageListView.as_view(), name='control.mesh_messages'), path('mesh//', MeshNodeDetailView.as_view(), name='control.mesh_node.detail'), + path('mesh//message//', MeshMessageSendView.as_view(), name='control.mesh_message.send'), path('', ControlPanelIndexView.as_view(), name='control.index'), ] diff --git a/src/c3nav/control/views/mesh.py b/src/c3nav/control/views/mesh.py index 36b70ce2..9d9eb948 100644 --- a/src/c3nav/control/views/mesh.py +++ b/src/c3nav/control/views/mesh.py @@ -1,8 +1,14 @@ +from django.contrib import messages from django.db.models import Max -from django.views.generic import ListView, DetailView +from django.http import Http404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.views.generic import ListView, DetailView, FormView -from c3nav.control.forms import MeshMessageFilerForm +from c3nav.control.forms import MeshMessageFilterForm from c3nav.control.views.base import ControlPanelMixin +from c3nav.mesh.forms import MeshMessageForm +from c3nav.mesh.messages import MessageType from c3nav.mesh.models import MeshNode, NodeMessage @@ -22,6 +28,9 @@ class MeshNodeDetailView(ControlPanelMixin, DetailView): 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 MeshMessageListView(ControlPanelMixin, ListView): model = NodeMessage @@ -33,7 +42,7 @@ class MeshMessageListView(ControlPanelMixin, ListView): def get_queryset(self): qs = super().get_queryset() - self.form = MeshMessageFilerForm(self.request.GET) + 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']) @@ -53,3 +62,40 @@ class MeshMessageListView(ControlPanelMixin, ListView): 'form_data': form_data.urlencode(), }) return ctx + + +class MeshMessageSendView(ControlPanelMixin, FormView): + template_name = "control/mesh_message_send.html" + + def get_form_class(self): + try: + return MeshMessageForm.get_form_for_type(MessageType[self.kwargs['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.kwargs['msg_type'].startswith('CONFIG_'): + try: + node = MeshNode.objects.get(address=self.kwargs['recipient']) + except MeshNode.DoesNotExist: + pass + else: + return node.last_messages[self.kwargs['msg_type']].parsed.tojson() + 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): + form.send() + messages.success(self.request, _('Message sent successfully')) + return super().form_valid(form) diff --git a/src/c3nav/control/views/utils.py b/src/c3nav/control/views/utils.py new file mode 100644 index 00000000..061759b3 --- /dev/null +++ b/src/c3nav/control/views/utils.py @@ -0,0 +1,2 @@ +def get_mesh_comm_group(address): + return 'mesh_comm_%s' % address.replace(':', '-') diff --git a/src/c3nav/mesh/consumers.py b/src/c3nav/mesh/consumers.py index 0c0cb6ee..9863d4d8 100644 --- a/src/c3nav/mesh/consumers.py +++ b/src/c3nav/mesh/consumers.py @@ -4,7 +4,9 @@ from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer from django.utils import timezone +from c3nav.control.views.utils import get_mesh_comm_group from c3nav.mesh import messages +from c3nav.mesh.messages import Message, BROADCAST_ADDRESS from c3nav.mesh.models import MeshNode, NodeMessage @@ -21,7 +23,7 @@ class MeshConsumer(WebsocketConsumer): print('disconnected!') if self.uplink_node is not None: # leave broadcast group - async_to_sync(self.channel_layer.group_add)('mesh_broadcast', self.channel_name) + async_to_sync(self.channel_layer.group_add)(get_mesh_comm_group(BROADCAST_ADDRESS), self.channel_name) # remove all other destinations self.remove_dst_nodes(self.dst_nodes) @@ -64,7 +66,7 @@ class MeshConsumer(WebsocketConsumer): async_to_sync(self.channel_layer.group_add)('mesh_broadcast', self.channel_name) # kick out other consumers talking to the same uplink - async_to_sync(self.channel_layer.group_send)(self.group_name_for_node(msg.src), { + async_to_sync(self.channel_layer.group_send)(get_mesh_comm_group(msg.src), { "type": "mesh.uplink_consumer", "name": self.channel_name, }) @@ -93,6 +95,9 @@ class MeshConsumer(WebsocketConsumer): print('leaving node group...') self.remove_dst_nodes((data["address"], )) + def mesh_send(self, data): + self.send_msg(Message.fromjson(data["msg"])) + def log_received_message(self, src_node: MeshNode, msg: messages.Message): NodeMessage.objects.create( uplink_node=self.uplink_node, @@ -104,7 +109,7 @@ class MeshConsumer(WebsocketConsumer): def add_dst_nodes(self, addresses): for address in addresses: # create group name for this address - group = self.group_name_for_node(address) + group = self.comm_address_group(address) # if we aren't handling this address yet, join the group if address not in self.dst_nodes: @@ -133,9 +138,9 @@ class MeshConsumer(WebsocketConsumer): ) def remove_dst_nodes(self, addresses): - for address in addresses: + for address in tuple(addresses): # create group name for this address - group = self.group_name_for_node(address) + group = self.comm_address_group(address) # leave the group if address in self.dst_nodes: @@ -144,13 +149,10 @@ class MeshConsumer(WebsocketConsumer): # add the stuff to the db as well # todo: can't do this because of race condition - #MeshNode.objects.filter(address__in=addresses, uplink_id=self.uplink_node.address).update( - # uplink_id=self.uplink_node.address, - # last_signin=timezone.now(), - #) - - def group_name_for_node(self, address): - return 'mesh_%s' % address.replace(':', '-') + # MeshNode.objects.filter(address__in=addresses, uplink_id=self.uplink_node.address).update( + # uplink_id=self.uplink_node.address, + # last_signin=timezone.now(), + # ) def remove_route(self, route_address): MeshNode.objects.filter(route_id=route_address).update(route_id=None) diff --git a/src/c3nav/mesh/dataformats.py b/src/c3nav/mesh/dataformats.py index cab3fa9d..60f2000a 100644 --- a/src/c3nav/mesh/dataformats.py +++ b/src/c3nav/mesh/dataformats.py @@ -25,7 +25,7 @@ class FixedStrFormat: self.num = num def encode(self, value): - return struct.pack('%ss' % self.num, value) + return struct.pack('%ss' % self.num, value.encode()) def decode(self, data: bytes): return struct.unpack('%ss' % self.num, data[:self.num])[0].rstrip(bytes((0, ))).decode(), data[self.num:] @@ -33,7 +33,7 @@ class FixedStrFormat: class BoolFormat: def encode(self, value): - return struct.pack('B', (int(value), )) + return struct.pack('B', int(value)) def decode(self, data: bytes): return bool(struct.unpack('B', data[:1])[0]), data[1:] diff --git a/src/c3nav/mesh/forms.py b/src/c3nav/mesh/forms.py new file mode 100644 index 00000000..720252d5 --- /dev/null +++ b/src/c3nav/mesh/forms.py @@ -0,0 +1,86 @@ +from django import forms +from django.http import Http404 +from django.utils.translation import gettext_lazy as _ + +from c3nav.mesh.messages import MessageType, Message, ROOT_ADDRESS +from c3nav.mesh.models import MeshNode + + +class MeshMessageForm(forms.Form): + msg_types = {} + + recipients = forms.MultipleChoiceField(choices=()) + + def __init__(self, *args, recipient=None, initial=None, **kwargs): + self.recipient = recipient + if self.recipient is not None: + initial = { + **(initial or {}), + 'recipients': [self.recipient], + } + super().__init__(*args, initial=initial, **kwargs) + + recipient_root_choices = { + 'ff:ff:ff:ff:ff:ff': _('broadcast') + } + recipient_node_choices = { + node.address: str(node) for node in MeshNode.objects.all() + } + self.recipient_choices = { + **recipient_root_choices, + **recipient_node_choices, + } + if self.recipient is None: + self.fields['recipients'].choices = ( + *recipient_root_choices.items(), + (_('nodes'), tuple(recipient_node_choices.items())) + ) + else: + if self.recipient not in self.recipient_choices: + raise Http404 + self.fields.pop('recipients') + + # noinspection PyMethodOverriding + def __init_subclass__(cls, /, msg=None, **kwargs): + super().__init_subclass__(**kwargs) + if cls.msg_type in MeshMessageForm.msg_types: + raise TypeError('duplicate use of msg %s' % cls.msg_type) + MeshMessageForm.msg_types[cls.msg_type] = cls + + @classmethod + def get_form_for_type(cls, msg_type): + return cls.msg_types[msg_type] + + def get_recipient_display(self): + return self.recipient_choices[self.recipient] + + +class ConfigUplinkMessageForm(MeshMessageForm): + msg_type = MessageType.CONFIG_UPLINK + + enabled = forms.BooleanField(required=False, label=_('enabled')) + ssid = forms.CharField(required=False, label=_('ssid'), max_length=31) + password = forms.CharField(required=False, label=_('password'), max_length=63) + channel = forms.IntegerField(min_value=0, max_value=11, label=_('channel')) + udp = forms.BooleanField(required=False, label=_('udp')) + ssl = forms.BooleanField(required=False, label=_('ssl')) + host = forms.CharField(required=False, label=_('host'), max_length=63) + port = forms.IntegerField(min_value=1, max_value=65535, label=_('port')) + + def send(self): + if not self.is_valid(): + raise Exception('nope') + + msg_data = { + 'msg_id': self.msg_type, + 'src': ROOT_ADDRESS, + **self.cleaned_data, + } + + recipients = [self.recipient] if self.recipient else self.cleaned_data['recipients'] + for recipient in recipients: + print('sending to ', recipient) + Message.fromjson({ + 'dst': recipient, + **msg_data, + }).send() diff --git a/src/c3nav/mesh/messages.py b/src/c3nav/mesh/messages.py index 11126fdb..7d76b312 100644 --- a/src/c3nav/mesh/messages.py +++ b/src/c3nav/mesh/messages.py @@ -2,11 +2,16 @@ from dataclasses import asdict, dataclass, field, fields, is_dataclass from enum import IntEnum, unique from typing import TypeVar +import channels +from asgiref.sync import async_to_sync + +from c3nav.control.views.utils import get_mesh_comm_group from c3nav.mesh.dataformats import (BoolFormat, FixedStrFormat, HexFormat, LedConfig, LedConfigFormat, MacAddressesListFormat, MacAddressFormat, SimpleFormat, VarStrFormat) ROOT_ADDRESS = '00:00:00:00:00:00' PARENT_ADDRESS = '00:00:00:ff:ff:ff' +BROADCAST_ADDRESS = 'ff:ff:ff:ff:ff:ff' NO_LAYER = 0xFF @@ -60,7 +65,6 @@ class Message: @classmethod def decode(cls, data: bytes) -> M: - # print('decode', data.hex(' ')) klass = cls.msg_types[data[12]] values = {} for field_ in fields(klass): @@ -80,6 +84,12 @@ class Message: kwargs[field_.name] = field_.type.fromjson(kwargs[field_.name]) return klass(**kwargs) + def send(self): + async_to_sync(channels.layers.get_channel_layer().group_send)(get_mesh_comm_group(self.dst), { + "type": "mesh.send", + "msg": self.tojson() + }) + @dataclass class EchoRequestMessage(Message, msg_id=MessageType.ECHO_REQUEST):