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', )
|
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,
|
||||||
|
|
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 }}
|
{{ 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>
|
||||||
|
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
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 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)
|
||||||
|
|
|
@ -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
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 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):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue