add MeshUplink model to keep better track of uplinks
This commit is contained in:
parent
d69a9c4be0
commit
61673603ea
6 changed files with 260 additions and 52 deletions
|
@ -132,7 +132,8 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{% mesh_node msg.uplink_node %}</td>
|
{% comment %}todo: more details{% endcomment %}
|
||||||
|
<td>{% mesh_node msg.uplink.node %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
<strong>Name:</strong> {% if node.name %}{{ node.name }}{% else %}<em>{% trans '(no name)' %}</em>{% endif %}
|
<strong>Name:</strong> {% if node.name %}{{ node.name }}{% else %}<em>{% trans '(no name)' %}</em>{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Uplink:</strong> {% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.address %}">{{ node.uplink }}</a><br>{% endif %}
|
{% comment %}todo: more details{% endcomment %}
|
||||||
|
<strong>Uplink:</strong> {% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a><br>{% endif %}
|
||||||
|
|
||||||
<strong>Last signin:</strong>
|
<strong>Last signin:</strong>
|
||||||
{{ node.last_signin.date }} {{ node.last_signin.time|date:"H:i:s" }}
|
{{ node.last_signin.date }} {{ node.last_signin.time|date:"H:i:s" }}
|
||||||
|
|
|
@ -66,7 +66,9 @@
|
||||||
{{ timesince }} ago
|
{{ timesince }} ago
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</td>
|
</td>
|
||||||
<td>{% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.address %}">{{ node.uplink }}</a>{% endif %}</td>
|
{% comment %}todo: hide uplink if timed out{% endcomment %}
|
||||||
|
{% comment %}todo: more details{% endcomment %}
|
||||||
|
<td>{% if node.uplink %}<a href="{% url "control.mesh.node.detail" pk=node.uplink.node_id %}">{{ node.uplink.node }}</a>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -2,12 +2,13 @@ import traceback
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from channels.generic.websocket import JsonWebsocketConsumer, WebsocketConsumer
|
from channels.generic.websocket import JsonWebsocketConsumer, WebsocketConsumer
|
||||||
|
from django.db import transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from c3nav.mesh import messages
|
from c3nav.mesh import messages
|
||||||
from c3nav.mesh.messages import (MESH_BROADCAST_ADDRESS, MESH_NONE_ADDRESS, MESH_ROOT_ADDRESS, MeshMessage,
|
from c3nav.mesh.messages import (MESH_BROADCAST_ADDRESS, MESH_NONE_ADDRESS, MESH_ROOT_ADDRESS, MeshMessage,
|
||||||
MeshMessageType)
|
MeshMessageType)
|
||||||
from c3nav.mesh.models import MeshNode, NodeMessage
|
from c3nav.mesh.models import MeshNode, MeshUplink, NodeMessage
|
||||||
from c3nav.mesh.tasks import send_channel_msg
|
from c3nav.mesh.tasks import send_channel_msg
|
||||||
from c3nav.mesh.utils import get_mesh_comm_group
|
from c3nav.mesh.utils import get_mesh_comm_group
|
||||||
|
|
||||||
|
@ -16,15 +17,17 @@ from c3nav.mesh.utils import get_mesh_comm_group
|
||||||
class MeshConsumer(WebsocketConsumer):
|
class MeshConsumer(WebsocketConsumer):
|
||||||
def connect(self):
|
def connect(self):
|
||||||
# todo: auth
|
# todo: auth
|
||||||
self.uplink_node = None
|
self.uplink = None
|
||||||
self.log_text(None, "new mesh websocket connection")
|
self.log_text(None, "new mesh websocket connection")
|
||||||
self.dst_nodes = set()
|
self.dst_nodes = set()
|
||||||
self.open_requests = set()
|
self.open_requests = set()
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
# todo: ping stuff
|
||||||
|
|
||||||
def disconnect(self, close_code):
|
def disconnect(self, close_code):
|
||||||
self.log_text(self.uplink_node, "mesh websocket disconnected")
|
self.log_text(self.uplink.node, "mesh websocket disconnected")
|
||||||
if self.uplink_node is not None:
|
if self.uplink is not None:
|
||||||
# leave broadcast group
|
# leave broadcast group
|
||||||
async_to_sync(self.channel_layer.group_discard)(
|
async_to_sync(self.channel_layer.group_discard)(
|
||||||
get_mesh_comm_group(MESH_BROADCAST_ADDRESS), self.channel_name
|
get_mesh_comm_group(MESH_BROADCAST_ADDRESS), self.channel_name
|
||||||
|
@ -33,6 +36,15 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
# remove all other destinations
|
# remove all other destinations
|
||||||
self.remove_dst_nodes(self.dst_nodes)
|
self.remove_dst_nodes(self.dst_nodes)
|
||||||
|
|
||||||
|
# set end reason (unless we set it to replaced already)
|
||||||
|
MeshUplink.objects.filter(
|
||||||
|
pk=self.uplink.pk,
|
||||||
|
).exclude(
|
||||||
|
end_reason=MeshUplink.EndReason.REPLACED
|
||||||
|
).update(
|
||||||
|
end_reason=MeshUplink.EndReason.CLOSED
|
||||||
|
)
|
||||||
|
|
||||||
def send_msg(self, msg, sender=None, exclude_uplink_address=None):
|
def send_msg(self, msg, sender=None, exclude_uplink_address=None):
|
||||||
# print("sending", msg, MeshMessage.encode(msg).hex(' ', 1))
|
# print("sending", msg, MeshMessage.encode(msg).hex(' ', 1))
|
||||||
# self.log_text(msg.dst, "sending %s" % msg)
|
# self.log_text(msg.dst, "sending %s" % msg)
|
||||||
|
@ -42,7 +54,7 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
"timestamp": timezone.now().strftime("%d.%m.%y %H:%M:%S.%f"),
|
"timestamp": timezone.now().strftime("%d.%m.%y %H:%M:%S.%f"),
|
||||||
"channel": self.channel_name,
|
"channel": self.channel_name,
|
||||||
"sender": sender,
|
"sender": sender,
|
||||||
"uplink": self.uplink_node.address if self.uplink_node else None,
|
"uplink": self.uplink.node.address if self.uplink else None,
|
||||||
"recipient": msg.dst,
|
"recipient": msg.dst,
|
||||||
# "msg": msg.tojson(), # not doing this part for privacy reasons
|
# "msg": msg.tojson(), # not doing this part for privacy reasons
|
||||||
})
|
})
|
||||||
|
@ -62,8 +74,8 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
# message not adressed to us, forward it
|
# message not adressed to us, forward it
|
||||||
print('Received message for forwarding:', msg)
|
print('Received message for forwarding:', msg)
|
||||||
|
|
||||||
if not self.uplink_node:
|
if not self.uplink:
|
||||||
self.log_text(self.uplink_node, "received message not for us before sign in message, ignoring...")
|
self.log_text(None, "received message not for us before sign in message, ignoring...")
|
||||||
print('no sign in yet, ignoring')
|
print('no sign in yet, ignoring')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -73,7 +85,7 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
self.log_text(MESH_ROOT_ADDRESS, "adding ourselves to trace message before forwarding")
|
self.log_text(MESH_ROOT_ADDRESS, "adding ourselves to trace message before forwarding")
|
||||||
msg.trace.append(MESH_ROOT_ADDRESS)
|
msg.trace.append(MESH_ROOT_ADDRESS)
|
||||||
|
|
||||||
msg.send(exclude_uplink_address=self.uplink_node.address)
|
msg.send(exclude_uplink_address=self.uplink.node.address)
|
||||||
|
|
||||||
# don't handle this message unless it's a broadcast message
|
# don't handle this message unless it's a broadcast message
|
||||||
if msg.dst != messages.MESH_BROADCAST_ADDRESS:
|
if msg.dst != messages.MESH_BROADCAST_ADDRESS:
|
||||||
|
@ -88,34 +100,49 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
src_node, created = MeshNode.objects.get_or_create(address=msg.src)
|
src_node, created = MeshNode.objects.get_or_create(address=msg.src)
|
||||||
|
|
||||||
if isinstance(msg, messages.MeshSigninMessage):
|
if isinstance(msg, messages.MeshSigninMessage):
|
||||||
self.uplink_node = src_node
|
with transaction.atomic():
|
||||||
# log message, since we will not log it further down
|
# tatabase fumbling, lock the mesh node database row
|
||||||
self.log_received_message(src_node, msg)
|
locked_node = MeshNode.objects.select_for_update().get(address=msg.src)
|
||||||
|
|
||||||
# inform signed in uplink node about its layer
|
# close other uplinks in the database (they might add their own close reason in a bit)
|
||||||
self.send_msg(messages.MeshLayerAnnounceMessage(
|
locked_node.uplink_sessions.filter(end_reason__isnull=True).update(
|
||||||
src=messages.MESH_ROOT_ADDRESS,
|
end_reason=MeshUplink.EndReason.NEW_TIMEOUT
|
||||||
dst=msg.src,
|
)
|
||||||
layer=messages.NO_LAYER
|
|
||||||
))
|
|
||||||
|
|
||||||
# add signed in uplink node to broadcast group
|
# create our own uplink in the database
|
||||||
async_to_sync(self.channel_layer.group_add)(
|
self.uplink = MeshUplink.objects.create(
|
||||||
get_mesh_comm_group(MESH_BROADCAST_ADDRESS), self.channel_name
|
node=locked_node,
|
||||||
)
|
last_ping=timezone.now(),
|
||||||
|
name=self.channel_name,
|
||||||
|
)
|
||||||
|
|
||||||
# kick out other consumers talking to the same uplink
|
# inform other uplinks to shut down
|
||||||
async_to_sync(self.channel_layer.group_send)(get_mesh_comm_group(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,
|
||||||
})
|
})
|
||||||
|
|
||||||
# add this node as a destination that this uplink handles (duh)
|
# log message, since we will not log it further down
|
||||||
self.add_dst_nodes(nodes=(src_node, ))
|
self.log_received_message(src_node, msg)
|
||||||
|
|
||||||
|
# inform signed in uplink node about its layer
|
||||||
|
self.send_msg(messages.MeshLayerAnnounceMessage(
|
||||||
|
src=messages.MESH_ROOT_ADDRESS,
|
||||||
|
dst=msg.src,
|
||||||
|
layer=messages.NO_LAYER
|
||||||
|
))
|
||||||
|
|
||||||
|
# add signed in uplink node to broadcast group
|
||||||
|
async_to_sync(self.channel_layer.group_add)(
|
||||||
|
get_mesh_comm_group(MESH_BROADCAST_ADDRESS), self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# add this node as a destination that this uplink handles (duh)
|
||||||
|
self.add_dst_nodes(nodes=(src_node, ))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.uplink_node is None:
|
if self.uplink is None:
|
||||||
print('Expected sign-in message, but got a different one!')
|
print('Expected sign-in message, but got a different one!')
|
||||||
self.close()
|
self.close()
|
||||||
return
|
return
|
||||||
|
@ -156,35 +183,38 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
def mesh_uplink_consumer(self, data):
|
def mesh_uplink_consumer(self, data):
|
||||||
# message handler: if we are not the given uplink, leave this group
|
# message handler: if we are not the given uplink, leave this group
|
||||||
if data["name"] != self.channel_name:
|
if data["name"] != self.channel_name:
|
||||||
self.log_text(self.uplink_node, "shutting down, uplink now served by new consumer")
|
self.log_text(self.uplink.node, "shutting down, uplink now served by new consumer")
|
||||||
|
MeshUplink.objects.filter(pk=self.uplink.pk,).update(
|
||||||
|
end_reason=MeshUplink.EndReason.REPLACED
|
||||||
|
)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def mesh_dst_node_uplink(self, data):
|
def mesh_dst_node_uplink(self, data):
|
||||||
# message handler: if we are not the given uplink, leave this group
|
# message handler: if we are not the given uplink, leave this group
|
||||||
if data["uplink"] != self.uplink_node.address:
|
if data["uplink"] != self.uplink.node.address:
|
||||||
self.log_text(data["address"], "node now served by new consumer")
|
self.log_text(data["address"], "node now served by new consumer")
|
||||||
self.remove_dst_nodes((data["address"], ))
|
self.remove_dst_nodes((data["address"], ))
|
||||||
|
|
||||||
def mesh_send(self, data):
|
def mesh_send(self, data):
|
||||||
if self.uplink_node.address == data["exclude_uplink_address"]:
|
if self.uplink.node.address == data["exclude_uplink_address"]:
|
||||||
if data["msg"]["dst"] == MESH_BROADCAST_ADDRESS:
|
if data["msg"]["dst"] == MESH_BROADCAST_ADDRESS:
|
||||||
self.log_text(
|
self.log_text(
|
||||||
self.uplink_node.address, "not forwarding this broadcast message via us since it came from here"
|
self.uplink.node.address, "not forwarding this broadcast message via us since it came from here"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.log_text(
|
self.log_text(
|
||||||
self.uplink_node.address, "we're the route for this message but it came from here so... no"
|
self.uplink.node.address, "we're the route for this message but it came from here so... no"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
self.send_msg(MeshMessage.fromjson(data["msg"]), data["sender"])
|
self.send_msg(MeshMessage.fromjson(data["msg"]), data["sender"])
|
||||||
|
|
||||||
def mesh_send_route_response(self, data):
|
def mesh_send_route_response(self, data):
|
||||||
self.log_text(self.uplink_node.address, "we're the uplink for this address, sending route response...")
|
self.log_text(self.uplink.node.address, "we're the uplink for this address, sending route response...")
|
||||||
messages.MeshRouteResponseMessage(
|
messages.MeshRouteResponseMessage(
|
||||||
src=MESH_ROOT_ADDRESS,
|
src=MESH_ROOT_ADDRESS,
|
||||||
dst=data["dst"],
|
dst=data["dst"],
|
||||||
request_id=data["request_id"],
|
request_id=data["request_id"],
|
||||||
route=self.uplink_node.address,
|
route=self.uplink.node.address,
|
||||||
).send()
|
).send()
|
||||||
async_to_sync(self.channel_layer.send)(data["channel"], {
|
async_to_sync(self.channel_layer.send)(data["channel"], {
|
||||||
"type": "mesh.route_response_sent",
|
"type": "mesh.route_response_sent",
|
||||||
|
@ -213,11 +243,11 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
"type": "mesh.msg_received",
|
"type": "mesh.msg_received",
|
||||||
"timestamp": timezone.now().strftime("%d.%m.%y %H:%M:%S.%f"),
|
"timestamp": timezone.now().strftime("%d.%m.%y %H:%M:%S.%f"),
|
||||||
"channel": self.channel_name,
|
"channel": self.channel_name,
|
||||||
"uplink": self.uplink_node.address if self.uplink_node else None,
|
"uplink": self.uplink.node.address if self.uplink else None,
|
||||||
"msg": as_json,
|
"msg": as_json,
|
||||||
})
|
})
|
||||||
NodeMessage.objects.create(
|
NodeMessage.objects.create(
|
||||||
uplink_node=self.uplink_node,
|
uplink=self.uplink,
|
||||||
src_node=src_node,
|
src_node=src_node,
|
||||||
message_type=msg.msg_type.name,
|
message_type=msg.msg_type.name,
|
||||||
data=as_json,
|
data=as_json,
|
||||||
|
@ -229,11 +259,11 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
"type": "mesh.log_entry",
|
"type": "mesh.log_entry",
|
||||||
"timestamp": timezone.now().strftime("%d.%m.%y %H:%M:%S.%f"),
|
"timestamp": timezone.now().strftime("%d.%m.%y %H:%M:%S.%f"),
|
||||||
"channel": self.channel_name,
|
"channel": self.channel_name,
|
||||||
"uplink": self.uplink_node.address if self.uplink_node else None,
|
"uplink": self.uplink.node.address if self.uplink else None,
|
||||||
"node": address,
|
"node": address,
|
||||||
"text": text,
|
"text": text,
|
||||||
})
|
})
|
||||||
print("MESH %s: [%s] %s" % (self.uplink_node, address, text))
|
print("MESH %s: [%s] %s" % (self.uplink.node, address, text))
|
||||||
|
|
||||||
def add_dst_nodes(self, nodes=None, addresses=None):
|
def add_dst_nodes(self, nodes=None, addresses=None):
|
||||||
nodes = list(nodes) if nodes else []
|
nodes = list(nodes) if nodes else []
|
||||||
|
@ -266,7 +296,7 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
async_to_sync(self.channel_layer.group_send)(group, {
|
async_to_sync(self.channel_layer.group_send)(group, {
|
||||||
"type": "mesh.dst_node_uplink",
|
"type": "mesh.dst_node_uplink",
|
||||||
"node": address,
|
"node": address,
|
||||||
"uplink": self.uplink_node.address
|
"uplink": self.uplink.node.address
|
||||||
})
|
})
|
||||||
|
|
||||||
# tell the node to dump its current information
|
# tell the node to dump its current information
|
||||||
|
@ -279,7 +309,7 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
|
|
||||||
# add the stuff to the db as well
|
# add the stuff to the db as well
|
||||||
MeshNode.objects.filter(address__in=addresses).update(
|
MeshNode.objects.filter(address__in=addresses).update(
|
||||||
uplink_id=self.uplink_node.address,
|
uplink=self.uplink,
|
||||||
last_signin=timezone.now(),
|
last_signin=timezone.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -297,9 +327,7 @@ class MeshConsumer(WebsocketConsumer):
|
||||||
|
|
||||||
# add the stuff to the db as well
|
# add the stuff to the db as well
|
||||||
# todo: shouldn't do this because of race condition?
|
# todo: shouldn't do this because of race condition?
|
||||||
MeshNode.objects.filter(address__in=addresses, uplink_id=self.uplink_node.address).update(
|
MeshNode.objects.filter(address__in=addresses, uplink=self.uplink).update(uplink=None)
|
||||||
uplink_id=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MeshUIConsumer(JsonWebsocketConsumer):
|
class MeshUIConsumer(JsonWebsocketConsumer):
|
||||||
|
|
149
src/c3nav/mesh/migrations/0009_meshuplink.py
Normal file
149
src/c3nav/mesh/migrations/0009_meshuplink.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-11-06 12:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db.models import Max, Min
|
||||||
|
|
||||||
|
|
||||||
|
def forwards_func(apps, schema_editor):
|
||||||
|
MeshNode = apps.get_model("mesh", "MeshNode")
|
||||||
|
MeshUplink = apps.get_model("mesh", "MeshUplink")
|
||||||
|
for node in MeshNode.objects.annotate(first_relayed=Min('relayed_messages__datetime'),
|
||||||
|
last_relayed=Max('relayed_messages__datetime')):
|
||||||
|
if node.first_relayed is None:
|
||||||
|
continue
|
||||||
|
uplink = MeshUplink.objects.create(
|
||||||
|
name="migration_generated",
|
||||||
|
started=node.first_relayed,
|
||||||
|
node=node,
|
||||||
|
last_ping=node.last_relayed,
|
||||||
|
)
|
||||||
|
node.routed_nodes.update(uplink=uplink)
|
||||||
|
node.relayed_messages.update(uplink=uplink)
|
||||||
|
|
||||||
|
|
||||||
|
def backwards_func(apps, schema_editor):
|
||||||
|
MeshUplink = apps.get_model("mesh", "MeshUplink")
|
||||||
|
for uplink in MeshUplink.objects.select_related('node'):
|
||||||
|
uplink.routed_nodes.update(uplink_node=uplink.node)
|
||||||
|
uplink.relayed_messages.update(uplink_node=uplink.node)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("mesh", "0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="nodemessage",
|
||||||
|
name="uplink_node",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="relayed_messages",
|
||||||
|
to="mesh.meshnode",
|
||||||
|
verbose_name="uplink node",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="MeshUplink",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=128, verbose_name="channel name")),
|
||||||
|
(
|
||||||
|
"started",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="started"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_ping",
|
||||||
|
models.DateTimeField(verbose_name="last ping from consumer"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"end_reason",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("closed", "closed"),
|
||||||
|
("replaced", "replaced"),
|
||||||
|
("new_timeout", "new (timeout)"),
|
||||||
|
],
|
||||||
|
max_length=16,
|
||||||
|
null=True,
|
||||||
|
verbose_name="end reason",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"node",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="uplink_sessions",
|
||||||
|
to="mesh.meshnode",
|
||||||
|
verbose_name="node",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="nodemessage",
|
||||||
|
name="uplink",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="relayed_messages",
|
||||||
|
to="mesh.meshuplink",
|
||||||
|
verbose_name="uplink",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="meshuplink",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("end_reason__isnull", True)),
|
||||||
|
fields=("node",),
|
||||||
|
name="only_one_active_uplink",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="meshnode",
|
||||||
|
old_name="uplink",
|
||||||
|
new_name="uplink_node",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="meshnode",
|
||||||
|
name="uplink",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="routed_nodes",
|
||||||
|
to="mesh.meshuplink",
|
||||||
|
verbose_name="uplink",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards_func, backwards_func),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="meshnode",
|
||||||
|
name="uplink_node",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="nodemessage",
|
||||||
|
name="uplink_node",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="nodemessage",
|
||||||
|
name="uplink",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="relayed_messages",
|
||||||
|
to="mesh.meshuplink",
|
||||||
|
verbose_name="uplink",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,6 +5,7 @@ from typing import Any, Mapping, Self
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import NotSupportedError, models
|
from django.db import NotSupportedError, models
|
||||||
|
from django.db.models import Q, UniqueConstraint
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ class MeshNodeQuerySet(models.QuerySet):
|
||||||
for message in NodeMessage.objects.order_by('message_type', 'src_node', '-datetime', '-pk').filter(
|
for message in NodeMessage.objects.order_by('message_type', 'src_node', '-datetime', '-pk').filter(
|
||||||
message_type__in=(t.name for t in self._prefetch_last_messages),
|
message_type__in=(t.name for t in self._prefetch_last_messages),
|
||||||
src_node__in=nodes.keys(),
|
src_node__in=nodes.keys(),
|
||||||
).prefetch_related("uplink_node").distinct('message_type', 'src_node'):
|
).prefetch_related("uplink").distinct('message_type', 'src_node'):
|
||||||
nodes[message.src_node_id].last_messages[message.message_type] = message
|
nodes[message.src_node_id].last_messages[message.message_type] = message
|
||||||
for node in nodes.values():
|
for node in nodes.values():
|
||||||
node.last_messages["any"] = max(node.last_messages.values(), key=attrgetter("datetime"))
|
node.last_messages["any"] = max(node.last_messages.values(), key=attrgetter("datetime"))
|
||||||
|
@ -85,10 +86,13 @@ class LastMessagesByTypeLookup(UserDict):
|
||||||
|
|
||||||
|
|
||||||
class MeshNode(models.Model):
|
class MeshNode(models.Model):
|
||||||
|
"""
|
||||||
|
A nesh node. Any node.
|
||||||
|
"""
|
||||||
address = models.CharField(_('mac address'), max_length=17, primary_key=True)
|
address = models.CharField(_('mac address'), max_length=17, primary_key=True)
|
||||||
name = models.CharField(_('name'), max_length=32, null=True, blank=True)
|
name = models.CharField(_('name'), max_length=32, null=True, blank=True)
|
||||||
first_seen = models.DateTimeField(_('first seen'), auto_now_add=True)
|
first_seen = models.DateTimeField(_('first seen'), auto_now_add=True)
|
||||||
uplink = models.ForeignKey('MeshNode', models.PROTECT, null=True,
|
uplink = models.ForeignKey('MeshUplink', models.PROTECT, null=True,
|
||||||
related_name='routed_nodes', verbose_name=_('uplink'))
|
related_name='routed_nodes', verbose_name=_('uplink'))
|
||||||
last_signin = models.DateTimeField(_('last signin'), null=True)
|
last_signin = models.DateTimeField(_('last signin'), null=True)
|
||||||
objects = models.Manager.from_queryset(MeshNodeQuerySet)()
|
objects = models.Manager.from_queryset(MeshNodeQuerySet)()
|
||||||
|
@ -103,12 +107,35 @@ class MeshNode(models.Model):
|
||||||
return LastMessagesByTypeLookup(self)
|
return LastMessagesByTypeLookup(self)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshUplink(models.Model):
|
||||||
|
"""
|
||||||
|
An uplink session, a direct connection to a node
|
||||||
|
"""
|
||||||
|
|
||||||
|
class EndReason(models.TextChoices):
|
||||||
|
CLOSED = "closed", _("closed")
|
||||||
|
REPLACED = "replaced", _("replaced")
|
||||||
|
NEW_TIMEOUT = "new_timeout", _("new (timeout)")
|
||||||
|
|
||||||
|
name = models.CharField(_('channel name'), max_length=128)
|
||||||
|
started = models.DateTimeField(_('started'), auto_now_add=True)
|
||||||
|
node = models.ForeignKey('MeshNode', models.PROTECT, related_name='uplink_sessions',
|
||||||
|
verbose_name=_('node'))
|
||||||
|
last_ping = models.DateTimeField(_('last ping from consumer'))
|
||||||
|
end_reason = models.CharField(_('end reason'), choices=EndReason.choices, null=True, max_length=16)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = (
|
||||||
|
UniqueConstraint(fields=["node"], condition=Q(end_reason__isnull=True), name='only_one_active_uplink'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NodeMessage(models.Model):
|
class NodeMessage(models.Model):
|
||||||
MESSAGE_TYPES = [(msgtype.name, msgtype.pretty_name) for msgtype in MeshMessageType]
|
MESSAGE_TYPES = [(msgtype.name, msgtype.pretty_name) for msgtype in MeshMessageType]
|
||||||
src_node = models.ForeignKey('MeshNode', models.PROTECT,
|
src_node = models.ForeignKey('MeshNode', models.PROTECT,
|
||||||
related_name='received_messages', verbose_name=_('node'))
|
related_name='received_messages', verbose_name=_('node'))
|
||||||
uplink_node = models.ForeignKey('MeshNode', models.PROTECT,
|
uplink = models.ForeignKey('MeshUplink', models.PROTECT, related_name='relayed_messages',
|
||||||
related_name='relayed_messages', verbose_name=_('uplink node'))
|
verbose_name=_('uplink'))
|
||||||
datetime = models.DateTimeField(_('datetime'), db_index=True, auto_now_add=True)
|
datetime = models.DateTimeField(_('datetime'), db_index=True, auto_now_add=True)
|
||||||
message_type = models.CharField(_('message type'), max_length=24, db_index=True, choices=MESSAGE_TYPES)
|
message_type = models.CharField(_('message type'), max_length=24, db_index=True, choices=MESSAGE_TYPES)
|
||||||
data = models.JSONField(_('message data'))
|
data = models.JSONField(_('message data'))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue