diff --git a/demo/__init__.py b/demo/__init__.py index a7ede05..188fc47 100644 --- a/demo/__init__.py +++ b/demo/__init__.py @@ -1 +1,3 @@ +from .demo_sat import DemoSat, CommandCancelledError +from .demo_telemetry import DemoTelemetry name = "demo" diff --git a/demo/demo_sat.py b/demo/demo_sat.py index 97b4d21..f21a7e9 100644 --- a/demo/demo_sat.py +++ b/demo/demo_sat.py @@ -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", diff --git a/gateway/__init__.py b/gateway/__init__.py index e69de29..f3a0ead 100644 --- a/gateway/__init__.py +++ b/gateway/__init__.py @@ -0,0 +1 @@ +from .gateway import Gateway, CommandStatus \ No newline at end of file diff --git a/gateway/gateway.py b/gateway/gateway.py index 8b71b5c..2ffbcd9 100644 --- a/gateway/gateway.py +++ b/gateway/gateway.py @@ -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. diff --git a/requirements.txt b/requirements.txt index caae78e..e3da46d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +asgiref diff --git a/run.py b/run.py index 573b90b..676376c 100644 --- a/run.py +++ b/run.py @@ -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__) diff --git a/satellite/__init__.py b/satellite/__init__.py index e69de29..3ba3a0a 100644 --- a/satellite/__init__.py +++ b/satellite/__init__.py @@ -0,0 +1,3 @@ +from .satellite import Satellite, CommandCancelledError +from .telemetry import FakeTelemetry +from . import stubs diff --git a/satellite/example_commands.json b/satellite/example_commands.json index 30f35d4..4ad942f 100644 --- a/satellite/example_commands.json +++ b/satellite/example_commands.json @@ -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", diff --git a/satellite/satellite.py b/satellite/satellite.py index c76a4be..44d9f76 100644 --- a/satellite/satellite.py +++ b/satellite/satellite.py @@ -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__) @@ -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) @@ -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', @@ -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. """ @@ -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) @@ -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 \ No newline at end of file + return None diff --git a/gateway/stubs.py b/satellite/stubs.py similarity index 100% rename from gateway/stubs.py rename to satellite/stubs.py diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..e2e7817 --- /dev/null +++ b/test_requirements.txt @@ -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 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/satellite/test_sat.py b/tests/satellite/test_sat.py deleted file mode 100644 index d9fa06a..0000000 --- a/tests/satellite/test_sat.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys -from satellite import satellite - -def test_stub(): - assert True \ No newline at end of file diff --git a/tests/test_sat.py b/tests/test_sat.py new file mode 100644 index 0000000..b201e8c --- /dev/null +++ b/tests/test_sat.py @@ -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)