From 9e9e41fb3fb7acc6c42722b795c17c71b43b4351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 5 Nov 2023 18:47:20 +0100 Subject: [PATCH] store firmwares and add firmware api stub --- src/c3nav/api/urls.py | 3 + ...warebuildboard_firmwareversion_and_more.py | 190 ++++++++++++++++++ src/c3nav/mesh/models.py | 66 +++++- src/c3nav/settings.py | 2 + src/c3nav/urls.py | 3 +- 5 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py diff --git a/src/c3nav/api/urls.py b/src/c3nav/api/urls.py index bf5e1e2c..fa4469fe 100644 --- a/src/c3nav/api/urls.py +++ b/src/c3nav/api/urls.py @@ -17,6 +17,7 @@ from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionV LocationViewSet, MapViewSet, ObstacleViewSet, POIViewSet, RampViewSet, SourceViewSet, SpaceViewSet, StairViewSet, UpdatesViewSet) from c3nav.mapdata.utils.user import can_access_editor +from c3nav.mesh.api import FirmwareViewSet from c3nav.routing.api import RoutingViewSet router = SimpleRouter() @@ -53,6 +54,8 @@ router.register(r'editor', EditorViewSet, basename='editor') router.register(r'changesets', ChangeSetViewSet) router.register(r'session', SessionViewSet, basename='session') +router.register(r'firmwares', FirmwareViewSet, basename='firmware') + class APIRoot(GenericAPIView): """ diff --git a/src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py b/src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py new file mode 100644 index 00000000..a8c4d10d --- /dev/null +++ b/src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py @@ -0,0 +1,190 @@ +# Generated by Django 4.2.1 on 2023-11-05 17:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("mesh", "0007_nodemessage_message_type_new"), + ] + + operations = [ + migrations.CreateModel( + name="FirmwareBuild", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "variant", + models.CharField(max_length=64, verbose_name="variant name"), + ), + ( + "chip", + models.SmallIntegerField( + choices=[(2, "ESP32-S2"), (5, "ESP32-C3")], + db_index=True, + verbose_name="chip", + ), + ), + ( + "sha256_hash", + models.CharField( + max_length=64, unique=True, verbose_name="SHA256 hash" + ), + ), + ( + "binary", + models.FileField( + null=True, upload_to="", verbose_name="firmware file" + ), + ), + ], + ), + migrations.CreateModel( + name="FirmwareBuildBoard", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "board", + models.CharField( + choices=[ + ("CUSTOM", "CUSTOM"), + ("ESP32_C3_DEVKIT_M_1", "ESP32-C3-DevKitM-1"), + ("ESP32_C3_32S", "ESP32-C3-32S"), + ("C3NAV_UWB_BOARD", "c3nav UWB board"), + ("C3NAV_LOCATION_PCB_REV_0_1", "c3nav location PCB rev0.1"), + ("C3NAV_LOCATION_PCB_REV_0_2", "c3nav location PCB rev0.2"), + ], + db_index=True, + max_length=32, + verbose_name="board", + ), + ), + ( + "build", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="mesh.firmwarebuild", + ), + ), + ], + options={ + "unique_together": {("build", "board")}, + }, + ), + migrations.CreateModel( + name="FirmwareVersion", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "project_name", + models.CharField(max_length=32, verbose_name="project name"), + ), + ( + "version", + models.CharField( + max_length=32, unique=True, verbose_name="firmware version" + ), + ), + ( + "idf_version", + models.CharField(max_length=32, verbose_name="IDF version"), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, verbose_name="creation/upload date" + ), + ), + ( + "uploader", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.DeleteModel( + name="Firmware", + ), + 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_DESTINATIONS", "mesh add destinations"), + ("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"), + ("CONFIG_DUMP", "dump config"), + ("CONFIG_HARDWARE", "hardware config"), + ("CONFIG_BOARD", "board config"), + ("CONFIG_FIRMWARE", "firmware config"), + ("CONFIG_UPLINK", "uplink config"), + ("CONFIG_POSITION", "position 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_FRAGMENT", "ota request fragment"), + ("OTA_APPLY", "ota apply"), + ("OTA_REBOOT", "ota reboot"), + ("LOCATE_REQUEST_RANGE", "locate request range"), + ("LOCATE_RANGE_RESULTS", "locate range results"), + ("LOCATE_RAW_FTM_RESULTS", "locate raw ftm results"), + ], + db_index=True, + max_length=24, + verbose_name="message type", + ), + ), + migrations.AddField( + model_name="firmwarebuild", + name="version", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="builds", + to="mesh.firmwareversion", + ), + ), + migrations.AlterUniqueTogether( + name="firmwarebuild", + unique_together={("version", "variant")}, + ), + ] diff --git a/src/c3nav/mesh/models.py b/src/c3nav/mesh/models.py index cf0d5f71..56b3791c 100644 --- a/src/c3nav/mesh/models.py +++ b/src/c3nav/mesh/models.py @@ -3,9 +3,12 @@ from functools import cached_property from operator import attrgetter from typing import Any, Mapping, Self +from django.contrib.auth import get_user_model from django.db import NotSupportedError, models +from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ +from c3nav.mesh.dataformats import BoardType from c3nav.mesh.messages import ChipType from c3nav.mesh.messages import MeshMessage as MeshMessage from c3nav.mesh.messages import MeshMessageType @@ -118,16 +121,67 @@ class NodeMessage(models.Model): return MeshMessage.fromjson(self.data) -class Firmware(models.Model): - CHIPS = [(msgtype.value, msgtype.name.replace('_', '-')) for msgtype in ChipType] - chip = models.SmallIntegerField(_('chip'), db_index=True, choices=CHIPS) +class FirmwareVersion(models.Model): project_name = models.CharField(_('project name'), max_length=32) - version = models.CharField(_('firmware version'), max_length=32) + version = models.CharField(_('firmware version'), max_length=32, unique=True) idf_version = models.CharField(_('IDF version'), max_length=32) + uploader = models.ForeignKey(get_user_model(), null=True, on_delete=models.SET_NULL) + created = models.DateTimeField(_('creation/upload date'), auto_now_add=True) + + def serialize(self): + return { + 'project_name': self.project_name, + 'version': self.version, + 'idf_version': self.idf_version, + 'created': self.created.isoformat(), + 'builds': { + build.variant: build.serialize() + for build in self.builds.all().prefetch_related("firmwarebuildboard_set") + } + } + + +def firmware_upload_path(instance, filename): + # file will be uploaded to MEDIA_ROOT/user_/ + version = slugify(instance.version.version) + variant = slugify(instance.variant) + return f"firmware/{version}/{variant}/{filename}" + + +class FirmwareBuild(models.Model): + CHIPS = [(chiptype.value, chiptype.pretty_name) for chiptype in ChipType] + + version = models.ForeignKey(FirmwareVersion, related_name='builds', on_delete=models.CASCADE) + variant = models.CharField(_('variant name'), max_length=64) + chip = models.SmallIntegerField(_('chip'), db_index=True, choices=CHIPS) sha256_hash = models.CharField(_('SHA256 hash'), unique=True, max_length=64) - binary = models.FileField(_('firmware file'), null=True) + project_description = models.JSONField + binary = models.FileField(_('firmware file'), null=True, upload_to=firmware_upload_path) class Meta: unique_together = [ - ('chip', 'project_name', 'version', 'idf_version', 'sha256_hash'), + ('version', 'variant'), + ] + + @property + def boards(self): + return [board.board for board in self.firmwarebuildboard_set.all()] + + def serialize(self): + return { + 'chip': ChipType(self.chip).name, + 'sha256_hash': self.sha256_hash, + 'url': self.binary.url, + 'boards': self.boards, + } + + +class FirmwareBuildBoard(models.Model): + BOARDS = [(boardtype.name, boardtype.pretty_name) for boardtype in BoardType] + build = models.ForeignKey(FirmwareBuild, on_delete=models.CASCADE) + board = models.CharField(_('board'), max_length=32, db_index=True, choices=BOARDS) + + class Meta: + unique_together = [ + ('build', 'board'), ] diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index b65c1e74..c66f5473 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -43,6 +43,8 @@ TILES_ROOT = os.path.join(DATA_DIR, 'tiles') CACHE_ROOT = os.path.join(DATA_DIR, 'cache') STATS_ROOT = os.path.join(DATA_DIR, 'stats') +MEDIA_URL = '/media/' + if not os.path.exists(DATA_DIR): os.mkdir(DATA_DIR) if not os.path.exists(LOG_DIR): diff --git a/src/c3nav/urls.py b/src/c3nav/urls.py index cf1e0271..75482dfa 100644 --- a/src/c3nav/urls.py +++ b/src/c3nav/urls.py @@ -2,6 +2,7 @@ from contextlib import suppress from channels.routing import URLRouter from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path @@ -20,7 +21,7 @@ urlpatterns = [ path('control/', include(c3nav.control.urls)), path('locales/', include('django.conf.urls.i18n')), path('', include(c3nav.site.urls)), -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) websocket_urlpatterns = [ path('mesh/', URLRouter(c3nav.mesh.urls.websocket_urlpatterns)),