721 lines
32 KiB
Python
721 lines
32 KiB
Python
"""SatNOGS DB API django rest framework Views"""
|
|
from django.conf import settings
|
|
from django.core.cache import cache
|
|
from django.core.files.base import ContentFile
|
|
from django.db.models import F
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils.timezone import now
|
|
from drf_spectacular.types import OpenApiTypes
|
|
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema, \
|
|
extend_schema_view
|
|
from rest_framework import mixins, status, viewsets
|
|
from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser
|
|
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
|
|
from rest_framework.response import Response
|
|
from rest_framework.serializers import ValidationError
|
|
|
|
from db.api import filters, pagination, serializers
|
|
from db.api.parsers import JSONLDParser
|
|
from db.api.perms import IsAuthenticatedOrOptions, SafeMethodsWithPermission
|
|
from db.api.renderers import BrowserableJSONLDRenderer, JSONLDRenderer
|
|
from db.api.throttling import GetTelemetryAnononymousRateThrottle, GetTelemetryUserRateThrottle, \
|
|
GetTelemetryViolatorThrottle
|
|
from db.base.helpers import gridsquare
|
|
from db.base.models import SATELLITE_STATUS, SERVICE_TYPE, TRANSMITTER_STATUS, TRANSMITTER_TYPE, \
|
|
Artifact, DemodData, LatestTleSet, Mode, Satellite, SatelliteEntry, SatelliteIdentifier, \
|
|
Transmitter
|
|
from db.base.tasks import decode_current_frame, publish_current_frame, update_satellite_name
|
|
|
|
ISS_EXAMPLE = OpenApiExample('25544 (ISS)', value=25544)
|
|
|
|
|
|
@extend_schema_view(
|
|
retrieve=extend_schema(
|
|
description='Retrieve a single RF Mode from SatNOGS DB based on its ID',
|
|
),
|
|
list=extend_schema(description='Retrieve a complete list of RF Modes from SatNOGS DB', )
|
|
)
|
|
class ModeViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
|
|
"""
|
|
Read-only view into the transmitter modulation modes (RF Modes) currently tracked
|
|
in the SatNOGS DB database
|
|
|
|
For more details on individual RF mode types please [see our wiki][moderef].
|
|
|
|
[moderef]: https://wiki.satnogs.org/Category:RF_Modes
|
|
"""
|
|
renderer_classes = [
|
|
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
|
]
|
|
queryset = Mode.objects.all()
|
|
serializer_class = serializers.ModeSerializer
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(
|
|
description='Retrieve a full or filtered list of satellites in SatNOGS DB',
|
|
parameters=[
|
|
# drf-spectacular does not currently recognize the in_orbit filter as a
|
|
# bool, forcing it here. See drf-spectacular#234
|
|
OpenApiParameter(
|
|
name='in_orbit',
|
|
description='Filter by satellites currently in orbit (True) or those that have \
|
|
decayed (False)',
|
|
required=False,
|
|
type=bool
|
|
),
|
|
OpenApiParameter(
|
|
name='status',
|
|
description='Filter by satellite status: ' + ' '.join(SATELLITE_STATUS),
|
|
required=False,
|
|
type=OpenApiTypes.STR
|
|
),
|
|
OpenApiParameter(
|
|
name='norad_cat_id',
|
|
description='Select a satellite by its NORAD-assigned identifier',
|
|
examples=[ISS_EXAMPLE],
|
|
),
|
|
],
|
|
),
|
|
retrieve=extend_schema(
|
|
description='Retrieve details on a single satellite in SatNOGS DB',
|
|
parameters=[
|
|
OpenApiParameter(
|
|
'satellite_identifier__sat_id',
|
|
OpenApiTypes.STR,
|
|
OpenApiParameter.PATH,
|
|
description='Select a satellite by its Satellite Identifier',
|
|
),
|
|
],
|
|
),
|
|
)
|
|
class SatelliteViewSet( # pylint: disable=R0901
|
|
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
|
viewsets.GenericViewSet):
|
|
"""
|
|
View into the Satellite entities in the SatNOGS DB database
|
|
"""
|
|
renderer_classes = [
|
|
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
|
]
|
|
parser_classes = [JSONLDParser]
|
|
queryset = Satellite.objects.filter(
|
|
associated_satellite__isnull=True, satellite_entry__approved=True
|
|
).prefetch_related('associated_with', 'telemetries')
|
|
serializer_class = serializers.SatelliteSerializer
|
|
filterset_class = filters.SatelliteViewFilter
|
|
lookup_field = 'satellite_identifier__sat_id'
|
|
|
|
def get_object(self):
|
|
queryset = self.get_queryset()
|
|
# Apply any filter backends
|
|
queryset = self.filter_queryset(queryset)
|
|
|
|
# In case user uses NORAD ID for getting satellite
|
|
if 'satellite_entry__norad_cat_id' in self.kwargs:
|
|
norad_cat_id = self.kwargs['satellite_entry__norad_cat_id']
|
|
return get_object_or_404(queryset, satellite_entry__norad_cat_id=norad_cat_id)
|
|
|
|
# Getting satellite by using Satellite Identifier
|
|
sat_id = self.kwargs['satellite_identifier__sat_id']
|
|
try:
|
|
return queryset.get(satellite_identifier__sat_id=sat_id)
|
|
except Satellite.DoesNotExist:
|
|
return get_object_or_404(
|
|
queryset, associated_with__satellite_identifier__sat_id=sat_id
|
|
)
|
|
|
|
def create(self, request, *args, **kwargs): # noqa: C901; pylint: disable=R0911,R0912,R0915
|
|
"""
|
|
Creates a satellite suggestion.
|
|
"""
|
|
satellites_data = []
|
|
for satellite_entry in request.data['@graph']:
|
|
if 'satellite' not in satellite_entry:
|
|
data = 'Satellite Entry without "satellite" key'
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if not satellite_entry['satellite']:
|
|
data = 'One or more of the required fields are missing.\n Required fields: \
|
|
name, status, citation'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
satellite = satellite_entry['satellite']
|
|
create_satellite_identifier = False
|
|
|
|
satellite_data = {}
|
|
if "@id" not in satellite:
|
|
data = 'Missing "@id" for one or more entries'
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if 'sat_id' in satellite:
|
|
if isinstance(satellite['sat_id'], list):
|
|
data = 'Multiple values for "http://schema.org/identifier" or multiple \
|
|
entries with the same "@id" and different \
|
|
"http://schema.org/identifier" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
sat_id = satellite['sat_id']
|
|
try:
|
|
satellite_object = Satellite.objects.get(satellite_identifier__sat_id=sat_id)
|
|
satellite_data['satellite_identifier'
|
|
] = satellite_object.satellite_entry.satellite_identifier.pk
|
|
satellite_data['norad_follow_id'
|
|
] = satellite_object.satellite_entry.norad_follow_id
|
|
satellite_data['description'] = satellite_object.satellite_entry.description
|
|
satellite_data['dashboard_url'
|
|
] = satellite_object.satellite_entry.dashboard_url
|
|
satellite_data['image'] = satellite_object.satellite_entry.image
|
|
satellite_data['decayed'] = satellite_object.satellite_entry.decayed
|
|
satellite_data['countries'] = satellite_object.satellite_entry.countries
|
|
satellite_data['website'] = satellite_object.satellite_entry.website
|
|
satellite_data['launched'] = satellite_object.satellite_entry.launched
|
|
satellite_data['deployed'] = satellite_object.satellite_entry.deployed
|
|
satellite_data['operator'] = satellite_object.satellite_entry.operator
|
|
except Satellite.DoesNotExist:
|
|
try:
|
|
satellite_identifier = SatelliteIdentifier.objects.get(sat_id=sat_id)
|
|
satellite_data['satellite_identifier'] = satellite_identifier.pk
|
|
except SatelliteIdentifier.DoesNotExist:
|
|
satellite_identifier = SatelliteIdentifier.objects.create(sat_id=sat_id)
|
|
satellite_data['satellite_identifier'] = satellite_identifier.pk
|
|
|
|
else:
|
|
create_satellite_identifier = True
|
|
|
|
if isinstance(satellite['status'], list):
|
|
data = 'Multiple values for "https://schema.space/metasat/status" or multiple \
|
|
entries with the same "@id" and different \
|
|
"https://schema.space/metasat/status" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if isinstance(satellite['name'], list):
|
|
data = 'Multiple values for "https://schema.space/metasat/name" or multiple \
|
|
entries with the same "@id" and different \
|
|
"https://schema.space/metasat/name" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if isinstance(satellite['citation'], list):
|
|
data = 'Multiple values for "https://schema.org/citation" or multiple \
|
|
entries with the same "@id" and different \
|
|
"https://schema.org/citation" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
satellite_data['name'] = satellite['name']
|
|
satellite_data['status'] = satellite['status']
|
|
satellite_data['citation'] = satellite['citation']
|
|
satellite_data['created_by'] = request.user.pk
|
|
|
|
if 'norad_cat_id' in satellite:
|
|
if isinstance(satellite['norad_cat_id'], list):
|
|
data = 'Multiple values for "https://schema.space/metasat/noradID" or \
|
|
multiple entries with the same "@id" and different \
|
|
"https://schema.space/metasat/noradId" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
satellite_data['norad_cat_id'] = satellite['norad_cat_id']
|
|
|
|
if 'names' in satellite:
|
|
if isinstance(satellite['norad_cat_id'], list):
|
|
satellite_data['names'] = '\r\n'.join(satellite['names'])
|
|
else:
|
|
satellite_data['names'] = satellite['names']
|
|
|
|
if create_satellite_identifier:
|
|
satellite_identifier = SatelliteIdentifier.objects.create()
|
|
satellite_data['satellite_identifier'] = satellite_identifier.pk
|
|
|
|
satellites_data.append(satellite_data)
|
|
|
|
serializer = serializers.SatelliteEntrySerializer(
|
|
data=satellites_data, many=True, allow_empty=True
|
|
)
|
|
if serializer.is_valid():
|
|
serializer.save()
|
|
else:
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
return Response(status=status.HTTP_201_CREATED)
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name='satellite__norad_cat_id',
|
|
description='NORAD ID of a satellite to filter telemetry data for',
|
|
examples=[ISS_EXAMPLE],
|
|
),
|
|
OpenApiParameter(
|
|
name='status',
|
|
description='Filter by transmitter status: ' + ' '.join(TRANSMITTER_STATUS),
|
|
required=False,
|
|
type=OpenApiTypes.STR,
|
|
examples=[OpenApiExample('active', value='\'active\'')]
|
|
),
|
|
OpenApiParameter(
|
|
name='service',
|
|
description='Filter by transmitter service: ' + ' '.join(SERVICE_TYPE),
|
|
required=False,
|
|
type=OpenApiTypes.STR,
|
|
examples=[OpenApiExample('Amateur', value='\'Amateur\'')]
|
|
),
|
|
OpenApiParameter(
|
|
name='type',
|
|
description='Filter by transmitter type: ' + ' '.join(TRANSMITTER_TYPE),
|
|
required=False,
|
|
type=OpenApiTypes.STR,
|
|
examples=[OpenApiExample('Transmitter', value='\'Transmitter\'')]
|
|
),
|
|
],
|
|
),
|
|
)
|
|
class TransmitterViewSet( # pylint: disable=R0901
|
|
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
|
viewsets.GenericViewSet):
|
|
"""
|
|
View into the Transmitter entities in the SatNOGS DB database.
|
|
Transmitters are inclusive of Transceivers and Transponders
|
|
"""
|
|
renderer_classes = [
|
|
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
|
]
|
|
parser_classes = [JSONLDParser]
|
|
queryset = Transmitter.objects.filter(
|
|
satellite__satellite_entry__approved=True, satellite__associated_satellite__isnull=True
|
|
).exclude(status='invalid')
|
|
serializer_class = serializers.TransmitterSerializer
|
|
filterset_class = filters.TransmitterViewFilter
|
|
lookup_field = 'uuid'
|
|
|
|
def create(self, request, *args, **kwargs): # noqa: C901; pylint: disable=R0911,R0912,R0915
|
|
"""
|
|
Creates a transmitter suggestion.
|
|
"""
|
|
transmitters_data = []
|
|
for transmitter_entry in request.data['@graph']:
|
|
if 'transmitter' not in transmitter_entry:
|
|
data = 'Transmitter Entry without "transmitter" key'
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if not transmitter_entry['transmitter']:
|
|
data = 'One or more of the required fields are missing.\n Required fields: \
|
|
description, status, citation, service, satellite'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
transmitter = transmitter_entry['transmitter']
|
|
|
|
transmitter_data = {}
|
|
if "@id" not in transmitter:
|
|
data = 'Missing "@id" for one or more entries'
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if 'uuid' in transmitter:
|
|
if isinstance(transmitter['uuid'], list):
|
|
data = 'Multiple values for "http://schema.org/identifier" or multiple \
|
|
entries with the same "@id" and different \
|
|
"http://schema.org/identifier" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
transmitter_uuid = transmitter['uuid']
|
|
if Transmitter.objects.filter(uuid=transmitter_uuid).exists():
|
|
transmitter_data['uuid'] = transmitter_uuid
|
|
|
|
transmitter_data['description'] = transmitter['description']
|
|
transmitter_data['status'] = transmitter['status']
|
|
transmitter_data['citation'] = transmitter['citation']
|
|
transmitter_data['service'] = transmitter['service']
|
|
transmitter_data['created_by'] = request.user.pk
|
|
|
|
try:
|
|
if transmitter['satellite']:
|
|
if isinstance(transmitter['satellite'], list):
|
|
data = 'Multiple values for "https://schema.space/metasat/satellite" \
|
|
or multiple entries with the same "@id" and different \
|
|
"https://schema.space/metasat/satellite" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
transmitter_data['satellite'] = Satellite.objects.get(
|
|
satellite_entry__norad_cat_id=transmitter['satellite']['norad_cat_id']
|
|
).pk
|
|
else:
|
|
data = 'Missing NORAD ID value for Satellite'
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
except Satellite.DoesNotExist:
|
|
data = 'Unknown NORAD ID: {}'.format(transmitter['satellite']['norad_cat_id'])
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if 'baud' in transmitter:
|
|
transmitter_data['baud'] = transmitter['baud']
|
|
|
|
if 'invert' in transmitter:
|
|
transmitter_data['invert'] = transmitter['invert']
|
|
|
|
if 'uplink' not in transmitter and 'downlink' not in transmitter:
|
|
data = 'Missing "https://schema.space/metasat/uplink" or \
|
|
"https://schema.space/metasat/downlink"'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if 'uplink' in transmitter:
|
|
if isinstance(transmitter['uplink'], list):
|
|
data = 'Multiple values for "https://schema.space/metasat/uplink" or multiple \
|
|
entries with the same "@id" and different \
|
|
"https://schema.space/metasat/uplink" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if 'frequency' not in transmitter['uplink']:
|
|
data = 'Missing "https://schema.space/metasat/frequency" from \
|
|
"https://schema.space/metasat/uplink" value'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if isinstance(transmitter['uplink']['frequency'], int):
|
|
transmitter_data['type'] = 'Transceiver'
|
|
transmitter_data['uplink_low'] = transmitter['uplink']['frequency']
|
|
else:
|
|
transmitter_data['type'] = 'Transponder'
|
|
if 'minimum' not in transmitter['uplink'][
|
|
'frequency'] or 'maximum' not in transmitter['uplink']['frequency']:
|
|
data = 'Missing "https://schema.org/minimum" or \
|
|
"https://schema.org/maximum" from \
|
|
"https://schema.space/metasat/frequency" value of \
|
|
"https://schema.space/metasat/uplink"'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
transmitter_data['uplink_low'] = transmitter['uplink']['frequency']['minimum']
|
|
transmitter_data['uplink_high'] = transmitter['uplink']['frequency']['maximum']
|
|
if 'mode' in transmitter['uplink']:
|
|
try:
|
|
transmitter_data['uplink_mode'] = Mode.objects.get(
|
|
name=transmitter['uplink']['mode']
|
|
).pk
|
|
except Mode.DoesNotExist:
|
|
data = 'Unknown Mode: {}'.format(transmitter['uplink']['mode'])
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if 'drift' in transmitter['uplink']:
|
|
transmitter_data['uplink_drift'] = transmitter['uplink']['drift']
|
|
else:
|
|
transmitter_data['type'] = 'Transmitter'
|
|
|
|
if 'downlink' in transmitter:
|
|
if isinstance(transmitter['downlink'], list):
|
|
data = 'Multiple values for "https://schema.space/metasat/downlink" or \
|
|
multiple entries with the same "@id" and different \
|
|
"https://schema.space/metasat/downlink" values'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if 'frequency' not in transmitter['downlink']:
|
|
data = 'Missing "https://schema.space/metasat/frequency" from \
|
|
"https://schema.space/metasat/downlink" value'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if isinstance(transmitter['downlink']['frequency'],
|
|
int) and not transmitter_data['type'] == 'Transponder':
|
|
transmitter_data['downlink_low'] = transmitter['downlink']['frequency']
|
|
elif transmitter_data['type'] == 'Transponder':
|
|
if 'minimum' not in transmitter['downlink'][
|
|
'frequency'] or 'maximum' not in transmitter['downlink']['frequency']:
|
|
data = 'Missing "https://schema.org/minimum" or \
|
|
"https://schema.org/maximum" from \
|
|
"https://schema.space/metasat/frequency" value of \
|
|
"https://schema.space/metasat/downlink"'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
transmitter_data['downlink_low'] = transmitter['downlink']['frequency'][
|
|
'minimum']
|
|
transmitter_data['downlink_high'] = transmitter['downlink']['frequency'][
|
|
'maximum']
|
|
else:
|
|
data = 'Expected integer for "https://schema.space/metasat/frequency" value \
|
|
of "https://schema.space/metasat/downlink"'
|
|
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if 'mode' in transmitter['downlink']:
|
|
try:
|
|
transmitter_data['downlink_mode'] = Mode.objects.get(
|
|
name=transmitter['downlink']['mode']
|
|
).pk
|
|
except Mode.DoesNotExist:
|
|
data = 'Unknown Mode: {}'.format(transmitter['downlink']['mode'])
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
if 'drift' in transmitter['downlink']:
|
|
transmitter_data['downlink_drift'] = transmitter['downlink']['drift']
|
|
transmitters_data.append(transmitter_data)
|
|
|
|
serializer = serializers.TransmitterEntrySerializer(
|
|
data=transmitters_data, many=True, allow_empty=True
|
|
)
|
|
if serializer.is_valid():
|
|
serializer.save()
|
|
else:
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
return Response(status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class LatestTleSetViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
|
|
"""
|
|
Read-only view into the most recent two-line elements (TLE) in the SatNOGS DB
|
|
database
|
|
"""
|
|
renderer_classes = [JSONRenderer, BrowsableAPIRenderer]
|
|
queryset = LatestTleSet.objects.all().select_related('satellite').exclude(
|
|
latest_distributable__isnull=True
|
|
).annotate(
|
|
tle0=F('latest_distributable__tle0'),
|
|
tle1=F('latest_distributable__tle1'),
|
|
tle2=F('latest_distributable__tle2'),
|
|
tle_source=F('latest_distributable__tle_source'),
|
|
updated=F('latest_distributable__updated')
|
|
)
|
|
serializer_class = serializers.LatestTleSetSerializer
|
|
filterset_class = filters.LatestTleSetViewFilter
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Returns latest TLE queryset depending on user permissions
|
|
"""
|
|
if self.request.user.has_perm('base.access_all_tles'):
|
|
return LatestTleSet.objects.all().select_related('satellite').exclude(
|
|
latest__isnull=True
|
|
).annotate(
|
|
tle0=F('latest__tle0'),
|
|
tle1=F('latest__tle1'),
|
|
tle2=F('latest__tle2'),
|
|
tle_source=F('latest__tle_source'),
|
|
updated=F('latest__updated')
|
|
)
|
|
return self.queryset
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name='app_source',
|
|
description='The submission source for the telemetry frames: manual (a manual \
|
|
upload/entry), network (SatNOGS Network observations), or sids \
|
|
(legacy API submission)',
|
|
),
|
|
OpenApiParameter(
|
|
name='observer',
|
|
description='(string) name of the observer (submitter) to retrieve telemetry data \
|
|
from'
|
|
),
|
|
OpenApiParameter(
|
|
name='satellite',
|
|
description='NORAD ID of a satellite to filter telemetry data for',
|
|
examples=[ISS_EXAMPLE],
|
|
),
|
|
OpenApiParameter(name='transmitter', description='Not currently in use'),
|
|
],
|
|
),
|
|
)
|
|
class TelemetryViewSet( # pylint: disable=R0901,R0912,R0915
|
|
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
|
viewsets.GenericViewSet):
|
|
"""
|
|
View into the Telemetry objects in the SatNOGS DB database. Currently,
|
|
this table is inclusive of all data collected from satellite downlink
|
|
observations
|
|
"""
|
|
renderer_classes = [
|
|
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
|
]
|
|
queryset = DemodData.objects.all().select_related('satellite', 'transmitter')
|
|
serializer_class = serializers.TelemetrySerializer
|
|
filterset_class = filters.TelemetryViewFilter
|
|
permission_classes = [SafeMethodsWithPermission]
|
|
throttle_classes = [
|
|
GetTelemetryAnononymousRateThrottle, GetTelemetryUserRateThrottle,
|
|
GetTelemetryViolatorThrottle
|
|
]
|
|
parser_classes = (FormParser, MultiPartParser, FileUploadParser)
|
|
pagination_class = pagination.LinkedHeaderPageNumberPagination
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
"""
|
|
Lists data from satellite if they are filtered by NORAD ID or Satellite ID. Also logs the
|
|
requests if it is set to do so.
|
|
"""
|
|
satellite = request.query_params.get('satellite', None)
|
|
sat_id = request.query_params.get('sat_id', None)
|
|
|
|
if not (satellite or sat_id):
|
|
data = {
|
|
'detail': (
|
|
'For getting data please use either satellite(NORAD ID) filter or'
|
|
'sat_id(Satellite ID) filter'
|
|
),
|
|
'results': None
|
|
}
|
|
response = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
response.exception = True
|
|
return response
|
|
|
|
if settings.LOG_TELEMETRY_REQUESTS:
|
|
user_id = str(request.user.id)
|
|
remote_address = str(request.META.get("REMOTE_ADDR"))
|
|
x_forwarded_for = str(request.META.get("HTTP_X_FORWARDED_FOR"))
|
|
timestamp = now().isoformat()
|
|
request_data = user_id + ';' + remote_address + ';' + x_forwarded_for + ';' + timestamp
|
|
cache.set(
|
|
'telemetry_log_' + user_id + '_' + timestamp, request_data,
|
|
settings.TELEMETRY_LOGS_TIME_TO_LIVE
|
|
)
|
|
return super().list(request, *args, **kwargs)
|
|
|
|
@extend_schema(
|
|
responses={'201': None}, # None
|
|
)
|
|
def create(self, request, *args, **kwargs):
|
|
"""
|
|
Creates a frame of telemetry data from a satellite observation.
|
|
"""
|
|
# pylint: disable=R0914
|
|
data = {}
|
|
|
|
norad_id = request.data.get('noradID')
|
|
|
|
try:
|
|
if norad_id:
|
|
satellite = Satellite.objects.get(satellite_entry__norad_cat_id=norad_id)
|
|
else:
|
|
raise ValueError
|
|
except Satellite.DoesNotExist:
|
|
satellite_identifier = SatelliteIdentifier.objects.create()
|
|
satellite_entry = SatelliteEntry.objects.create(
|
|
norad_cat_id=norad_id,
|
|
name='New Satellite',
|
|
satellite_identifier=satellite_identifier,
|
|
created=now()
|
|
)
|
|
satellite = Satellite.objects.create(
|
|
satellite_identifier=satellite_identifier, satellite_entry=satellite_entry
|
|
)
|
|
update_satellite_name.delay(int(norad_id))
|
|
except ValueError:
|
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
data['satellite'] = satellite.id
|
|
data['station'] = request.data.get('source')
|
|
data['timestamp'] = request.data.get('timestamp')
|
|
if request.data.get('version'):
|
|
data['version'] = request.data.get('version')
|
|
observation_id = ''
|
|
if request.data.get('observation_id'):
|
|
observation_id = request.data.get('observation_id')
|
|
data['observation_id'] = observation_id
|
|
station_id = ''
|
|
if request.data.get('station_id'):
|
|
station_id = request.data.get('station_id')
|
|
data['station_id'] = station_id
|
|
|
|
# Convert coordinates to omit N-S and W-E designators
|
|
lat = request.data.get('latitude')
|
|
lng = request.data.get('longitude')
|
|
try:
|
|
if any(x.isalpha() for x in lat):
|
|
data['lat'] = (-float(lat[:-1]) if ('S' in lat) else float(lat[:-1]))
|
|
else:
|
|
data['lat'] = float(lat)
|
|
if any(x.isalpha() for x in lng):
|
|
data['lng'] = (-float(lng[:-1]) if ('W' in lng) else float(lng[:-1]))
|
|
else:
|
|
data['lng'] = float(lng)
|
|
except ValueError:
|
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Network or SiDS submission?
|
|
if request.data.get('satnogs_network'):
|
|
data['app_source'] = 'network'
|
|
else:
|
|
data['app_source'] = 'sids'
|
|
|
|
# Create file out of frame string
|
|
frame = ContentFile(request.data.get('frame'), name='sids')
|
|
data['payload_frame'] = frame
|
|
# Create observer
|
|
qth = gridsquare(data['lat'], data['lng'])
|
|
observer = '{0}-{1}'.format(data['station'], qth)
|
|
data['observer'] = observer
|
|
|
|
serializer = serializers.SidsSerializer(data=data)
|
|
serializer.is_valid(raise_exception=True)
|
|
self.perform_create(serializer)
|
|
|
|
# Run task to decode the current frame
|
|
decode_current_frame.delay(satellite.satellite_identifier.sat_id, serializer.instance.pk)
|
|
|
|
# Run task to publish the current frame via ZeroMQ
|
|
if settings.ZEROMQ_ENABLE:
|
|
publish_current_frame.delay(
|
|
request.data.get('timestamp'), request.data.get('frame'), observer, {
|
|
'norad_id': norad_id,
|
|
'observation_id': observation_id,
|
|
'station_id': station_id
|
|
}
|
|
)
|
|
|
|
return Response(status=status.HTTP_201_CREATED)
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(
|
|
parameters=[
|
|
OpenApiParameter(
|
|
'network_obs_id',
|
|
OpenApiTypes.INT64,
|
|
required=False,
|
|
description='Given a SatNOGS Network observation ID, this will return any \
|
|
artifacts files associated with the observation.'
|
|
),
|
|
],
|
|
),
|
|
retrieve=extend_schema(
|
|
parameters=[
|
|
OpenApiParameter(
|
|
'id',
|
|
OpenApiTypes.URI,
|
|
OpenApiParameter.PATH,
|
|
description='The ID for the requested artifact entry in DB'
|
|
),
|
|
],
|
|
),
|
|
)
|
|
class ArtifactViewSet( # pylint: disable=R0901
|
|
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
|
viewsets.GenericViewSet):
|
|
"""
|
|
Artifacts are file-formatted objects collected from a satellite observation.
|
|
"""
|
|
queryset = Artifact.objects.all()
|
|
filterset_class = filters.ArtifactViewFilter
|
|
permission_classes = [IsAuthenticatedOrOptions]
|
|
parser_classes = (FormParser, MultiPartParser)
|
|
pagination_class = pagination.LinkedHeaderPageNumberPagination
|
|
|
|
def get_serializer_class(self):
|
|
"""Returns the right serializer depending on http method that is used"""
|
|
if self.action == 'create':
|
|
return serializers.NewArtifactSerializer
|
|
return serializers.ArtifactSerializer
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
"""
|
|
Creates observation artifact from an [HDF5 formatted file][hdf5ref]
|
|
* Requires session or key authentication to create an artifact
|
|
|
|
[hdf5ref]: https://en.wikipedia.org/wiki/Hierarchical_Data_Format
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
try:
|
|
if serializer.is_valid():
|
|
data = serializer.save()
|
|
http_response = {}
|
|
http_response['id'] = data.id
|
|
response = Response(http_response, status=status.HTTP_200_OK)
|
|
else:
|
|
data = serializer.errors
|
|
response = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
except (ValidationError, ValueError, OSError) as error:
|
|
response = Response(str(error), status=status.HTTP_400_BAD_REQUEST)
|
|
return response
|