Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions demo/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .demo_sat import DemoSat, CommandCancelledError
from .demo_telemetry import DemoTelemetry
name = "demo"
4 changes: 3 additions & 1 deletion demo/demo_sat.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def __init__(self, name="Space Oddity"):
"display_name": "Update File List",
"description": "Downlinks the latest file list from the spacecraft.",
"tags": ["files", "operations"],
"fields": []
"fields": [
{"name": "show_hidden", "type": "boolean", "default": False }
]
},
"uplink_file": {
"display_name": "Uplink File",
Expand Down
1 change: 1 addition & 0 deletions gateway/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .gateway import Gateway, CommandStatus
7 changes: 3 additions & 4 deletions gateway/gateway.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import logging
import time
from asgiref.sync import async_to_sync
from random import randint
from . import stubs
from asgiref.sync import async_to_sync

from .statuses import CommandStatus
from satellite.satellite import Satellite
from satellite import Satellite, stubs

logger = logging.getLogger(__name__)


class Gateway:
def __init__(self, *args, **kwargs):
# For simplicity, our gateway is going to talk with a fake satellite.
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
websockets
requests
majortom_gateway>=0.0.10
majortom_gateway>=0.1.0
mock>=4.0.3
pytest-asyncio
pytest-only>=1.2.2
pytest-watch>=4.2.0
pytest>=6.2.2
asgiref
asgiref
4 changes: 2 additions & 2 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import asyncio
import argparse
import json
from gateway.gateway import Gateway
from demo.demo_sat import DemoSat
from gateway import Gateway
from demo import DemoSat
from majortom_gateway import GatewayAPI

logger = logging.getLogger(__name__)
Expand Down
3 changes: 3 additions & 0 deletions satellite/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .satellite import Satellite, CommandCancelledError
from .telemetry import FakeTelemetry
from . import stubs
7 changes: 6 additions & 1 deletion satellite/example_commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@
"display_name": "6. Update File List",
"description": "Gets the latest list of downloadable files from the spacecraft.",
"tags": ["files", "operations"],
"fields": []
"fields": [
{"name": "sort_by", "type": "string", "range": ["none", "size", "time", "type"], "default": "none"},
{"name": "filter_regex", "type": "string", "default": ""},
{"name": "exclude_regex", "type": "string", "default": ""},
{"name": "show_hidden", "type": "boolean", "default": false }
]
},
"uplink_file": {
"display_name": "Uplink File",
Expand Down
53 changes: 37 additions & 16 deletions satellite/satellite.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
It takes the place of a simulator, flatsat, engineering model, or real satellite.
'''
import time
from threading import Timer
from gateway import stubs
from gateway.statuses import CommandStatus
from satellite.telemetry import FakeTelemetry
from random import randint
import logging
import asyncio
import majortom_gateway

from random import randint
from threading import Timer
from distutils.util import strtobool

from . import stubs
from .telemetry import FakeTelemetry

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -73,10 +77,11 @@ def process_command(self, bytes, gateway):
duration = command.fields['duration']
mode = command.fields['mode']
if errors:
gateway.fail_command(command.id, errors)
gateway.api.fail_command(command.id, errors)
else:
msg = f"Started Telemetry Beacon in mode: {command.fields['mode']} for {command.fields['duration']} seconds."
gateway.set_command_status(command.id, CommandStatus.COMPLETED, payload=msg)
command_completed = "completed"
gateway.set_command_status(command.id, command_completed, payload=msg)
timeout = time.time() + duration
while time.time() < timeout:
self.check_cancelled(id=command.id)
Expand All @@ -97,6 +102,8 @@ def process_command(self, bytes, gateway):
Certain commands are 'known' to Major Tom, and can appear in more convenient places in the UI because of that.
See the documentation for a full list of such commands.
"""
# Handle potential API returning 'true' or 'false' instead of proper boolean value.
show_hidden = bool(strtobool(command.fields['show_hidden']))
for i in range(1, randint(2, 4)):
self.file_list.append({
"name": f'Payload-Image-{(len(self.file_list)+1):04d}.png',
Expand All @@ -105,16 +112,30 @@ def process_command(self, bytes, gateway):
"metadata": {"type": "image", "lat": (randint(-89, 89) + .0001*randint(0, 9999)), "lng": (randint(-179, 179) + .0001*randint(0, 9999))}
})

if show_hidden:
for i in range(1, randint(1, 3)):
self.file_list.append({
"name": f'.thumb-{(len(self.file_list)+1):04d}.png',
"size": randint(200, 300),
"timestamp": int(time.time() * 1000) + i*10,
"metadata": {"type": "image", "lat": (randint(-89, 89) + .0001*randint(0, 9999)), "lng": (randint(-179, 179) + .0001*randint(0, 9999))}
})

self.check_cancelled(id=command.id)
logger.info("Files: {}".format(self.file_list))
r = Timer(0.1, gateway.update_file_list, kwargs={"system":self.name, "files":self.file_list})
r.start()
time.sleep(10)

binary = stubs.translate_command_to_binary(command)
packetized = stubs.packetize(binary)
encrypted = stubs.encrypt(packetized)

self.check_cancelled(id=command.id)
asyncio.run(gateway.api.update_file_list(system=self.name, files=self.file_list))

self.check_cancelled(id=command.id)
gateway.set_command_status(
command_id=command.id,
status=CommandStatus.COMPLETED,
payload="Updated Remote File List")
asyncio.run(gateway.api.complete_command(
command_id=command.id,
output="Updated Remote File List",
))

elif command.type == "error":
""" Simulates a command erroring out. """
Expand All @@ -124,7 +145,7 @@ def process_command(self, bytes, gateway):

else:
# We'd want to generate an error if the command wasn't found.
logger.warn(f"Satellite does not recognize command {command.type}")
logger.warning(f"Satellite does not recognize command {command.type}")
errors = [f"Command {command.type} not found on Satellite."]
gateway.fail_command(command.id, errors=errors)

Expand All @@ -137,4 +158,4 @@ def validate(self, command):
if type(command.fields['duration']) != type(int()):
return [f"Duration type is invalid. Must be an int. Type: {type(command.fields['duration'])}"]

return None
return None
File renamed without changes.
6 changes: 6 additions & 0 deletions test_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mock>=4.0.3
pytest-asyncio>=0.14.0
pytest-only>=1.2.2
pytest-watch>=4.2.0
pytest>=6.2.2
virtualenv
Empty file added tests/__init__.py
Empty file.
5 changes: 0 additions & 5 deletions tests/satellite/test_sat.py

This file was deleted.

85 changes: 85 additions & 0 deletions tests/test_sat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import sys
import json
import unittest
from unittest.mock import call
import pytest
import asyncio

try:
# Python 3.8+
from unittest.mock import AsyncMock
except ImportError:
# Python 3.6+
from mock import AsyncMock

from majortom_gateway.command import Command
from majortom_gateway import gateway_api
from satellite import Satellite, stubs
from gateway import Gateway

class TypeMatcher:
def __init__(self, expected_type):
self.expected_type = expected_type

def __eq__(self, other):
return isinstance(other, self.expected_type)

@pytest.fixture
def callback_mock():
future = asyncio.Future()
future.set_result(None)
fn = AsyncMock(return_value=future)
return fn


#@pytest.mark.asyncio example test
#async def test_calls_transit_callback(callback_mock):
# gw = GatewayAPI("host", "gateway_token", transit_callback=callback_mock)
# # ToDo: Update this example message
# message = {
# "type": "transit",
# }
#
# res = await gw.handle_message(json.dumps(message))
#
# # The transit callback is given the raw message
# callback_mock.assert_called_once_with(message)


class TestCommandDefinitions(unittest.TestCase):
def setUp(self):
self.gw_api = gateway_api.GatewayAPI("host", "gateway_token",
command_callback=callback_mock, transit_callback=callback_mock)
self.gateway = Gateway(api=self.gw_api)

def test_process_command(self):
system = "Example Flatsat" # Match 'system' identifier with Gateway CommandDef
json_bytes = """{
"type": "update_file_list",
"id": 18,
"system": "Example FlatSat",
"display_name": "Update File List",
"tags": ["files", "operations"],
"fields": [
{"name": "show_hidden", "type": "boolean", "value": "true" }
]
}"""
jl = json.loads(json_bytes)
cmd = Command(jl)

cmd_bytes = stubs.translate_command_to_binary(cmd)
print("CMD: #{cmd_bytes}", cmd_bytes)


with open('satellite/example_commands.json','r') as f:
command_defs = json.loads(f.read())
asyncio.run(self.gw_api.update_command_definitions(
system="Example FlatSat",
definitions=command_defs["definitions"]))

self.gateway.command_callback(cmd, self.gw_api)
#...
# Profit?
resp = ""
#self.gateway.satellite_response(cmd_bytes, resp)
#print("command resp: {}", resp)