add mesh communication from django form

This commit is contained in:
Laura Klünder 2023-10-04 15:42:03 +02:00
parent 2ff4a9a64a
commit 21b75bec86
10 changed files with 191 additions and 21 deletions

View file

@ -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,

View file

@ -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 %}
<form method="POST" style="max-width:400px;">
{% csrf_token %}
{{ form }}
<button type="submit">{% trans 'Send' %}</button>
</form>
{% endblock %}

View file

@ -13,7 +13,7 @@
{{ form.src_nodes }}
</div>
<div class="field">
<button type="submit">Filer</button>
<button type="submit">Filter</button>
</div>
</div>
</form>

View file

@ -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/<str:pk>/', MeshNodeDetailView.as_view(), name='control.mesh_node.detail'),
path('mesh/<str:recipient>/message/<str:msg_type>/', MeshMessageSendView.as_view(), name='control.mesh_message.send'),
path('', ControlPanelIndexView.as_view(), name='control.index'),
]

View file

@ -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)

View file

@ -0,0 +1,2 @@
def get_mesh_comm_group(address):
return 'mesh_comm_%s' % address.replace(':', '-')

View file

@ -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)

View file

@ -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:]

86
src/c3nav/mesh/forms.py Normal file
View file

@ -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()

View file

@ -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):