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', ) fields = ('geometries_changed', )
class MeshMessageFilerForm(Form): class MeshMessageFilterForm(Form):
message_types = MultipleChoiceField( message_types = MultipleChoiceField(
choices=[(msgtype.value, msgtype.name) for msgtype in MessageType], choices=[(msgtype.value, msgtype.name) for msgtype in MessageType],
required=False, 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 }} {{ form.src_nodes }}
</div> </div>
<div class="field"> <div class="field">
<button type="submit">Filer</button> <button type="submit">Filter</button>
</div> </div>
</div> </div>
</form> </form>

View file

@ -1,6 +1,6 @@
from django.urls import path 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.mapupdates import map_updates
from c3nav.control.views.announcements import announcement_list, announcement_detail from c3nav.control.views.announcements import announcement_list, announcement_detail
from c3nav.control.views.access import grant_access, grant_access_qr 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/', MeshNodeListView.as_view(), name='control.mesh_nodes'),
path('mesh/messages/', MeshMessageListView.as_view(), name='control.mesh_messages'), 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: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'), 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.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.control.views.base import ControlPanelMixin
from c3nav.mesh.forms import MeshMessageForm
from c3nav.mesh.messages import MessageType
from c3nav.mesh.models import MeshNode, NodeMessage from c3nav.mesh.models import MeshNode, NodeMessage
@ -22,6 +28,9 @@ class MeshNodeDetailView(ControlPanelMixin, DetailView):
pk_url_kwargs = "address" pk_url_kwargs = "address"
context_object_name = "node" 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): class MeshMessageListView(ControlPanelMixin, ListView):
model = NodeMessage model = NodeMessage
@ -33,7 +42,7 @@ class MeshMessageListView(ControlPanelMixin, ListView):
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
self.form = MeshMessageFilerForm(self.request.GET) self.form = MeshMessageFilterForm(self.request.GET)
if self.form.is_valid(): if self.form.is_valid():
if self.form.cleaned_data['message_types']: if self.form.cleaned_data['message_types']:
qs = qs.filter(message_type__in=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(), 'form_data': form_data.urlencode(),
}) })
return ctx 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 channels.generic.websocket import WebsocketConsumer
from django.utils import timezone from django.utils import timezone
from c3nav.control.views.utils import get_mesh_comm_group
from c3nav.mesh import messages from c3nav.mesh import messages
from c3nav.mesh.messages import Message, BROADCAST_ADDRESS
from c3nav.mesh.models import MeshNode, NodeMessage from c3nav.mesh.models import MeshNode, NodeMessage
@ -21,7 +23,7 @@ class MeshConsumer(WebsocketConsumer):
print('disconnected!') print('disconnected!')
if self.uplink_node is not None: if self.uplink_node is not None:
# leave broadcast group # 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 # remove all other destinations
self.remove_dst_nodes(self.dst_nodes) 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) async_to_sync(self.channel_layer.group_add)('mesh_broadcast', self.channel_name)
# kick out other consumers talking to the same uplink # 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", "type": "mesh.uplink_consumer",
"name": self.channel_name, "name": self.channel_name,
}) })
@ -93,6 +95,9 @@ class MeshConsumer(WebsocketConsumer):
print('leaving node group...') print('leaving node group...')
self.remove_dst_nodes((data["address"], )) 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): def log_received_message(self, src_node: MeshNode, msg: messages.Message):
NodeMessage.objects.create( NodeMessage.objects.create(
uplink_node=self.uplink_node, uplink_node=self.uplink_node,
@ -104,7 +109,7 @@ class MeshConsumer(WebsocketConsumer):
def add_dst_nodes(self, addresses): def add_dst_nodes(self, addresses):
for address in addresses: for address in addresses:
# create group name for this address # 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 we aren't handling this address yet, join the group
if address not in self.dst_nodes: if address not in self.dst_nodes:
@ -133,9 +138,9 @@ class MeshConsumer(WebsocketConsumer):
) )
def remove_dst_nodes(self, addresses): def remove_dst_nodes(self, addresses):
for address in addresses: for address in tuple(addresses):
# create group name for this address # create group name for this address
group = self.group_name_for_node(address) group = self.comm_address_group(address)
# leave the group # leave the group
if address in self.dst_nodes: if address in self.dst_nodes:
@ -149,8 +154,5 @@ class MeshConsumer(WebsocketConsumer):
# last_signin=timezone.now(), # last_signin=timezone.now(),
# ) # )
def group_name_for_node(self, address):
return 'mesh_%s' % address.replace(':', '-')
def remove_route(self, route_address): def remove_route(self, route_address):
MeshNode.objects.filter(route_id=route_address).update(route_id=None) MeshNode.objects.filter(route_id=route_address).update(route_id=None)

View file

@ -25,7 +25,7 @@ class FixedStrFormat:
self.num = num self.num = num
def encode(self, value): def encode(self, value):
return struct.pack('%ss' % self.num, value) return struct.pack('%ss' % self.num, value.encode())
def decode(self, data: bytes): def decode(self, data: bytes):
return struct.unpack('%ss' % self.num, data[:self.num])[0].rstrip(bytes((0, ))).decode(), data[self.num:] 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: class BoolFormat:
def encode(self, value): def encode(self, value):
return struct.pack('B', (int(value), )) return struct.pack('B', int(value))
def decode(self, data: bytes): def decode(self, data: bytes):
return bool(struct.unpack('B', data[:1])[0]), data[1:] 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 enum import IntEnum, unique
from typing import TypeVar 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, from c3nav.mesh.dataformats import (BoolFormat, FixedStrFormat, HexFormat, LedConfig, LedConfigFormat,
MacAddressesListFormat, MacAddressFormat, SimpleFormat, VarStrFormat) MacAddressesListFormat, MacAddressFormat, SimpleFormat, VarStrFormat)
ROOT_ADDRESS = '00:00:00:00:00:00' ROOT_ADDRESS = '00:00:00:00:00:00'
PARENT_ADDRESS = '00:00:00:ff:ff:ff' PARENT_ADDRESS = '00:00:00:ff:ff:ff'
BROADCAST_ADDRESS = 'ff:ff:ff:ff:ff:ff'
NO_LAYER = 0xFF NO_LAYER = 0xFF
@ -60,7 +65,6 @@ class Message:
@classmethod @classmethod
def decode(cls, data: bytes) -> M: def decode(cls, data: bytes) -> M:
# print('decode', data.hex(' '))
klass = cls.msg_types[data[12]] klass = cls.msg_types[data[12]]
values = {} values = {}
for field_ in fields(klass): for field_ in fields(klass):
@ -80,6 +84,12 @@ class Message:
kwargs[field_.name] = field_.type.fromjson(kwargs[field_.name]) kwargs[field_.name] = field_.type.fromjson(kwargs[field_.name])
return klass(**kwargs) 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 @dataclass
class EchoRequestMessage(Message, msg_id=MessageType.ECHO_REQUEST): class EchoRequestMessage(Message, msg_id=MessageType.ECHO_REQUEST):