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

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