add mesh communication from django form
This commit is contained in:
parent
2ff4a9a64a
commit
21b75bec86
10 changed files with 191 additions and 21 deletions
|
@ -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,
|
||||
|
|
23
src/c3nav/control/templates/control/mesh_message_send.html
Normal file
23
src/c3nav/control/templates/control/mesh_message_send.html
Normal 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 %}
|
|
@ -13,7 +13,7 @@
|
|||
{{ form.src_nodes }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<button type="submit">Filer</button>
|
||||
<button type="submit">Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
2
src/c3nav/control/views/utils.py
Normal file
2
src/c3nav/control/views/utils.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
def get_mesh_comm_group(address):
|
||||
return 'mesh_comm_%s' % address.replace(':', '-')
|
|
@ -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)
|
||||
|
|
|
@ -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
86
src/c3nav/mesh/forms.py
Normal 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()
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue