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 %}
+
+
+{% 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):