diff --git a/src/c3nav/mapdata/migrations/0105_alter_theme_color_background_and_more.py b/src/c3nav/mapdata/migrations/0105_alter_theme_color_background_and_more.py new file mode 100644 index 00000000..f0fa1976 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0105_alter_theme_color_background_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.0.1 on 2024-03-30 18:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0104_theme_color_css_grid_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='theme', + name='color_background', + field=models.CharField(blank=True, max_length=32, verbose_name='background color'), + ), + migrations.AlterField( + model_name='theme', + name='color_door_fill', + field=models.CharField(blank=True, max_length=32, verbose_name='door fill color'), + ), + migrations.AlterField( + model_name='theme', + name='color_ground_fill', + field=models.CharField(blank=True, max_length=32, verbose_name='ground fill color'), + ), + migrations.AlterField( + model_name='theme', + name='color_obstacles_default_border', + field=models.CharField(blank=True, max_length=32, verbose_name='default border color for obstacles'), + ), + migrations.AlterField( + model_name='theme', + name='color_obstacles_default_fill', + field=models.CharField(blank=True, max_length=32, verbose_name='default fill color for obstacles'), + ), + migrations.AlterField( + model_name='theme', + name='color_wall_border', + field=models.CharField(blank=True, max_length=32, verbose_name='wall border color'), + ), + migrations.AlterField( + model_name='theme', + name='color_wall_fill', + field=models.CharField(blank=True, max_length=32, verbose_name='wall fill color'), + ), + ] diff --git a/src/c3nav/mesh/consumers.py b/src/c3nav/mesh/consumers.py index 586c51c1..ab385fbc 100644 --- a/src/c3nav/mesh/consumers.py +++ b/src/c3nav/mesh/consumers.py @@ -15,6 +15,7 @@ from django.conf import settings from django.db import transaction from django.utils import timezone from django.utils.crypto import constant_time_compare +from pydantic_extra_types.mac_address import MacAddress from c3nav.mesh import messages from c3nav.mesh.cformats import CFormat @@ -196,7 +197,7 @@ class MeshConsumer(AsyncWebsocketConsumer): await self.channel_layer.group_add(MESH_ALL_OTA_GROUP, self.channel_name) # add this node as a destination that this uplink handles (duh) - await self.add_dst_nodes(nodes=(src_node, )) + await self.add_dst_node(src_node, parent=None) self.dst_nodes[msg.src].last_msg[MeshMessageType.MESH_SIGNIN] = msg.content return @@ -215,11 +216,21 @@ class MeshConsumer(AsyncWebsocketConsumer): return node_status.last_msg[msg.content.msg_type] = msg.content - if isinstance(msg.content, messages.MeshAddDestinationsMessage): - result = await self.add_dst_nodes(addresses=msg.content.addresses) + if isinstance(msg.content, messages.MeshAddDestinationMessage): + result = await self.add_dst_node( + node=await MeshNode.objects.aget_or_create(address=msg.content.address), + parent_address=msg.src, + ) if not result: print('disconnecting node that send invalid destinations', msg.content) await self.close() + await self.send_msg(messages.MeshMessage( + src=MESH_ROOT_ADDRESS, + dst=msg.src, + content=messages.MeshSigninConfirmMessage( + address=msg.content.address + ) + )) if isinstance(msg.content, messages.MeshRemoveDestinationsMessage): await self.remove_dst_nodes(addresses=msg.content.addresses) @@ -554,44 +565,25 @@ class MeshConsumer(AsyncWebsocketConsumer): def check_valid_address(address): return not (address.startswith('00:00:00') or address.startswith('ff:ff:ff')) - async def add_dst_nodes(self, nodes=None, addresses=None): - nodes = list(nodes) if nodes else [] - addresses = set(addresses) if addresses else set() + async def add_dst_node(self, node: MeshNode, parent_address: MacAddress | None): + await self.log_text(node.address, "destination added") - if not all(self.check_valid_address(a) for a in addresses): - return False + # add ourselves as uplink + await self._add_destination(node.address, parent_address) - node_addresses = set(node.address for node in nodes) - missing_addresses = addresses - set(node.address for node in nodes) + # if we aren't handling this address yet, write it down + if node.address not in self.dst_nodes: + self.dst_nodes[node.address] = NodeState() - if missing_addresses: - await MeshNode.objects.abulk_create( - [MeshNode(address=address) for address in missing_addresses], - ignore_conflicts=True - ) - - addresses |= node_addresses - addresses |= missing_addresses - - for address in addresses: - await self.log_text(address, "destination added") - - # add ourselves as uplink - await self._add_destination(address) - - # if we aren't handling this address yet, write it down - if address not in self.dst_nodes: - self.dst_nodes[address] = NodeState() - - await self.node_resend_ask(address) - return True + await self.node_resend_ask(node.address) @database_sync_to_async - def _add_destination(self, address): + def _add_destination(self, address, parent_address: MacAddress | None): with transaction.atomic(): node = MeshNode.objects.select_for_update().get(address=address) # update database node.uplink = self.uplink + node.upstream_id = parent_address node.last_signin = timezone.now() node.save() diff --git a/src/c3nav/mesh/messages.py b/src/c3nav/mesh/messages.py index 2143e403..a39f6e3b 100644 --- a/src/c3nav/mesh/messages.py +++ b/src/c3nav/mesh/messages.py @@ -33,12 +33,13 @@ class MeshMessageType(CEnum): MESH_SIGNIN = "MESH_SIGNIN", 0x03 MESH_LAYER_ANNOUNCE = "MESH_LAYER_ANNOUNCE", 0x04 - MESH_ADD_DESTINATIONS = "MESH_ADD_DESTINATIONS", 0x05 + MESH_ADD_DESTINATION = "MESH_ADD_DESTINATION", 0x05 MESH_REMOVE_DESTINATIONS = "MESH_REMOVE_DESTINATIONS", 0x06 MESH_ROUTE_REQUEST = "MESH_ROUTE_REQUEST", 0x07 MESH_ROUTE_RESPONSE = "MESH_ROUTE_RESPONSE", 0x08 MESH_ROUTE_TRACE = "MESH_ROUTE_TRACE", 0x09 MESH_ROUTING_FAILED = "MESH_ROUTING_FAILED", 0x0a + MESH_SIGNIN_CONFIRM = "MESH_SIGNIN_CONFIRM", 0x0b CONFIG_DUMP = "CONFIG_DUMP", 0x10 CONFIG_HARDWARE = "CONFIG_HARDWARE", 0x11 @@ -101,9 +102,9 @@ class MeshLayerAnnounceMessage(discriminator_value(msg_type=MeshMessageType.MESH layer: Annotated[PositiveInt, Lt(2 ** 8), CDoc("mesh layer that the sending node is on")] -class MeshAddDestinationsMessage(discriminator_value(msg_type=MeshMessageType.MESH_ADD_DESTINATIONS), BaseModel): +class MeshAddDestinationMessage(discriminator_value(msg_type=MeshMessageType.MESH_ADD_DESTINATION), BaseModel): """ downstream node announces served destination """ - addresses: Annotated[list[MacAddress], MaxLen(16), VarLen(), CDoc("adresses of the added destinations",)] + address: Annotated[MacAddress, CDoc("address of the added destination",)] class MeshRemoveDestinationsMessage(discriminator_value(msg_type=MeshMessageType.MESH_REMOVE_DESTINATIONS), BaseModel): @@ -134,6 +135,11 @@ class MeshRoutingFailedMessage(discriminator_value(msg_type=MeshMessageType.MESH address: MacAddress +class MeshSigninConfirmMessage(discriminator_value(msg_type=MeshMessageType.MESH_SIGNIN_CONFIRM), BaseModel): + """ Confirm signin from root node """ + address: MacAddress + + class ConfigDumpMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_DUMP), BaseModel): """ request for the node to dump its config """ pass @@ -307,12 +313,13 @@ MeshMessageContent = Annotated[ EchoResponseMessage, MeshSigninMessage, MeshLayerAnnounceMessage, - MeshAddDestinationsMessage, + MeshAddDestinationMessage, MeshRemoveDestinationsMessage, MeshRouteRequestMessage, MeshRouteResponseMessage, MeshRouteTraceMessage, MeshRoutingFailedMessage, + MeshSigninConfirmMessage, ConfigDumpMessage, ConfigHardwareMessage, ConfigBoardMessage, diff --git a/src/c3nav/mesh/migrations/0013_meshnode_upstream_alter_nodemessage_message_type.py b/src/c3nav/mesh/migrations/0013_meshnode_upstream_alter_nodemessage_message_type.py new file mode 100644 index 00000000..14dae416 --- /dev/null +++ b/src/c3nav/mesh/migrations/0013_meshnode_upstream_alter_nodemessage_message_type.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.1 on 2024-03-30 18:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mesh', '0012_otaupdaterecipient_status_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='meshnode', + name='upstream', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='downstream', to='mesh.meshnode', verbose_name='parent node'), + ), + migrations.AlterField( + model_name='nodemessage', + name='message_type', + field=models.CharField(choices=[('NOOP', 'noop'), ('ECHO_REQUEST', 'echo request'), ('ECHO_RESPONSE', 'echo response'), ('MESH_SIGNIN', 'mesh signin'), ('MESH_LAYER_ANNOUNCE', 'mesh layer announce'), ('MESH_ADD_DESTINATION', 'mesh add destination'), ('MESH_REMOVE_DESTINATIONS', 'mesh remove destinations'), ('MESH_ROUTE_REQUEST', 'mesh route request'), ('MESH_ROUTE_RESPONSE', 'mesh route response'), ('MESH_ROUTE_TRACE', 'mesh route trace'), ('MESH_ROUTING_FAILED', 'mesh routing failed'), ('MESH_SIGNIN_CONFIRM', 'mesh signin confirm'), ('CONFIG_DUMP', 'dump config'), ('CONFIG_HARDWARE', 'hardware config'), ('CONFIG_BOARD', 'board config'), ('CONFIG_FIRMWARE', 'firmware config'), ('CONFIG_UPLINK', 'uplink config'), ('CONFIG_POSITION', 'position config'), ('CONFIG_NODE', 'node config'), ('CONFIG_IBEACON', 'ibeacon config'), ('OTA_STATUS', 'ota status'), ('OTA_REQUEST_STATUS', 'ota request status'), ('OTA_START', 'ota start'), ('OTA_URL', 'ota url'), ('OTA_FRAGMENT', 'ota fragment'), ('OTA_REQUEST_FRAGMENTS', 'ota request fragments'), ('OTA_SETTING', 'ota setting'), ('OTA_APPLY', 'ota apply'), ('OTA_ABORT', 'ota abort'), ('LOCATE_REQUEST_RANGE', 'locate request range'), ('LOCATE_RANGE_RESULTS', 'locate range results'), ('LOCATE_RAW_FTM_RESULTS', 'locate raw ftm results'), ('REBOOT', 'reboot'), ('REPORT_ERROR', 'report error')], db_index=True, max_length=24, verbose_name='message type'), + ), + ] diff --git a/src/c3nav/mesh/models.py b/src/c3nav/mesh/models.py index 9991e193..98404fd4 100644 --- a/src/c3nav/mesh/models.py +++ b/src/c3nav/mesh/models.py @@ -260,6 +260,8 @@ class MeshNode(models.Model): first_seen = models.DateTimeField(_('first seen'), auto_now_add=True) uplink = models.ForeignKey('MeshUplink', models.PROTECT, null=True, related_name='routed_nodes', verbose_name=_('uplink')) + upstream = models.ForeignKey('MeshNode', models.SET_NULL, null=True, + related_name='downstream', verbose_name=_('parent node')) last_signin = models.DateTimeField(_('last signin'), null=True) objects = models.Manager.from_queryset(MeshNodeQuerySet)()